Unified · Split
| @@ -2,7 +2,6 @@ import { Result } from "better-result"; · 1 unmodified line | |||
| 2 | import { api } from "@executor/database/convex/_generated/api"; | 2 | import { api } from "@executor/database/convex/_generated/api"; |
| 3 | import { ConvexClient, ConvexHttpClient } from "convex/browser"; | 3 | import { ConvexClient, ConvexHttpClient } from "convex/browser"; |
| 4 | import type { | 4 | import type { |
| 5 | BridgeEntrypointContext, | ||
| 6 | BridgeProps, | 5 | BridgeProps, |
| 7 | ToolCallResult, | 6 | ToolCallResult, |
| 8 | WorkerEntrypointExports, | 7 | WorkerEntrypointExports, |
| @@ -10,9 +9,15 @@ import type { · 1 unmodified line | |||
| 10 | 9 | ||
| 11 | const APPROVAL_SUBSCRIPTION_TIMEOUT_MS = 10 * 60 * 1000; | 10 | const APPROVAL_SUBSCRIPTION_TIMEOUT_MS = 10 * 60 * 1000; |
| 12 | 11 | ||
| 12 | function asObject(value: unknown): Record<string, unknown> { | ||
| 13 | if (value && typeof value === "object" && !Array.isArray(value)) { | ||
| 14 | return value as Record<string, unknown>; | ||
| 15 | } | ||
| 16 | return {}; | ||
| 17 | } | ||
| 18 | |||
| 13 | function isBridgeProps(value: unknown): value is BridgeProps { | 19 | function isBridgeProps(value: unknown): value is BridgeProps { |
| 14 | if (!value || typeof value !== "object") return false; | 20 | const props = asObject(value); |
| 15 | const props = value as Partial<BridgeProps>; | ||
| 16 | return ( | 21 | return ( |
| 17 | typeof props.callbackConvexUrl === "string" | 22 | typeof props.callbackConvexUrl === "string" |
| 18 | && typeof props.callbackInternalSecret === "string" | 23 | && typeof props.callbackInternalSecret === "string" |
| @@ -20,33 +25,46 @@ function isBridgeProps(value: unknown): value is BridgeProps { · 1 unmodified line | |||
| 20 | ); | 25 | ); |
| 21 | } | 26 | } |
| 22 | 27 | ||
| 28 | function isToolCallResult(value: unknown): value is ToolCallResult { | ||
| 29 | const record = asObject(value); | ||
| 30 | if (record.ok === true) { | ||
| 31 | return true; | ||
| 32 | } | ||
| 33 | |||
| 34 | if (record.ok === false) { | ||
| 35 | return record.kind === "pending" || record.kind === "denied" || record.kind === "failed"; | ||
| 36 | } | ||
| 37 | |||
| 38 | return false; | ||
| 39 | } | ||
| 40 | |||
| 23 | export function getBridgePropsFromContext(ctx: unknown): BridgeProps { | 41 | export function getBridgePropsFromContext(ctx: unknown): BridgeProps { |
| 24 | if (!ctx || typeof ctx !== "object") { | 42 | const context = asObject(ctx); |
| 43 | if (Object.keys(context).length === 0) { | ||
| 25 | throw new Error("WorkerEntrypoint context is unavailable"); | 44 | throw new Error("WorkerEntrypoint context is unavailable"); |
| 26 | } | 45 | } |
| 27 | 46 | ||
| 28 | const maybeContext = ctx as Partial<BridgeEntrypointContext>; | 47 | if (!isBridgeProps(context.props)) { |
| 29 | if (!isBridgeProps(maybeContext.props)) { | ||
| 30 | throw new Error("ToolBridge props are missing or invalid"); | 48 | throw new Error("ToolBridge props are missing or invalid"); |
| 31 | } | 49 | } |
| 32 | 50 | ||
| 33 | return maybeContext.props; | 51 | return context.props; |
| 34 | } | 52 | } |
| 35 | 53 | ||
| 36 | export function getEntrypointExports(ctx: ExecutionContext): WorkerEntrypointExports { | 54 | export function getEntrypointExports(ctx: ExecutionContext): WorkerEntrypointExports { |
| 37 | const maybeCtx = ctx as ExecutionContext & { exports?: unknown }; | 55 | const context = asObject(ctx); |
| 38 | const exportsValue = maybeCtx.exports; | 56 | const exportsValue = context.exports; |
| 39 | 57 | ||
| 40 | if (!exportsValue || typeof exportsValue !== "object") { | 58 | if (!exportsValue || typeof exportsValue !== "object") { |
| 41 | throw new Error("Execution context exports are unavailable"); | 59 | throw new Error("Execution context exports are unavailable"); |
| 42 | } | 60 | } |
| 43 | 61 | ||
| 44 | const maybeExports = exportsValue as Partial<WorkerEntrypointExports>; | 62 | const exportsObject = asObject(exportsValue); |
| 45 | if (typeof maybeExports.ToolBridge !== "function") { | 63 | if (typeof exportsObject.ToolBridge !== "function") { |
| 46 | throw new Error("Execution context ToolBridge export is unavailable"); | 64 | throw new Error("Execution context ToolBridge export is unavailable"); |
| 47 | } | 65 | } |
| 48 | 66 | ||
| 49 | return { ToolBridge: maybeExports.ToolBridge }; | 67 | return { ToolBridge: exportsObject.ToolBridge as WorkerEntrypointExports["ToolBridge"] }; |
| 50 | } | 68 | } |
| 51 | 69 | ||
| 52 | function createConvexClient(callbackConvexUrl: string): ConvexHttpClient { | 70 | function createConvexClient(callbackConvexUrl: string): ConvexHttpClient { |
| @@ -136,7 +154,11 @@ export async function callToolWithBridge( · 83 unmodified lines | |||
| 136 | return { ok: false, kind: "failed", error: `Tool callback failed: ${message}` }; | 154 | return { ok: false, kind: "failed", error: `Tool callback failed: ${message}` }; |
| 137 | } | 155 | } |
| 138 | 156 | ||
| 139 | const result = response.value as ToolCallResult; | 157 | const result = response.value; |
| 158 | if (!isToolCallResult(result)) { | ||
| 159 | return { ok: false, kind: "failed", error: "Tool callback returned invalid result payload" }; | ||
| 160 | } | ||
| 161 | |||
| 140 | if (!result.ok && result.kind === "pending") { | 162 | if (!result.ok && result.kind === "pending") { |
| 141 | if (!result.approvalId) { | 163 | if (!result.approvalId) { |
| 142 | return { ok: false, kind: "failed", error: "Approval pending without approvalId" }; | 164 | return { ok: false, kind: "failed", error: "Approval pending without approvalId" }; |