@tanstack/ai-sandbox-cloudflare 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,7 +37,7 @@ var WATCHDOG_MS = 3e4;
37
37
  * tailing clients stop waiting forever. Generous so a legitimately slow agent
38
38
  * step (a long tool call that emits no chunks) is not killed prematurely.
39
39
  */
40
- var WATCHDOG_STALL_MS = 5 * 6e4;
40
+ var WATCHDOG_STALL_MS = 3e5;
41
41
  function isSocketAttachment(value) {
42
42
  return value !== null && typeof value === "object" && "runId" in value && typeof value.runId === "string" && "lastSeq" in value && typeof value.lastSeq === "number";
43
43
  }
@@ -1 +1 @@
1
- {"version":3,"file":"coordinator.js","names":[],"sources":["../../src/coordinator.ts"],"sourcesContent":["/**\n * `SandboxCoordinator` — the abstract Durable Object base for the serverless/\n * edge agent run model. It owns everything the two concrete models share:\n *\n * - a durable, resumable run-log ({@link DurableObjectRunEventLog});\n * - `startRun`: open the run, kick off the model's chunk stream WITHOUT blocking\n * the trigger, start piping it into the log via {@link RunController}, register\n * the resulting `done` promise with `ctx.waitUntil` (keeping the instance alive\n * until the run is terminal rather than letting it hibernate mid-run), and arm\n * a watchdog alarm;\n * - `status` (poll fallback) + a hibernatable WebSocket tail with a resumable\n * cursor (replay after `lastSeq`, then live-tail, reconnect-safe);\n * - routing for `GET /runs/:id` and `GET /runs/:id/stream`, delegating any other\n * path to {@link handleRoute} (which a subclass overrides for e.g. `/_bridge`\n * or `/tool-exec`).\n *\n * Subclasses implement {@link buildRunStream} — the ONE difference between the\n * models: run `chat()` in the DO ({@link ChatSandboxCoordinator}) or drive an\n * in-container runner ({@link ContainerSandboxCoordinator}).\n *\n * NOTE: Workers-runtime code — compiles against `@cloudflare/workers-types`; not\n * runtime-verified in this repo.\n */\nimport { DurableObject } from 'cloudflare:workers'\nimport { EventType, isTerminalRunStatus } from '@tanstack/ai'\n// The PORTABLE run driver: this coordinator is a platform binding of core's\n// `RunController`, not a driver of its own. The DO run log backs both of the\n// driver's seams through the adapters in './durability' — `runLogStore` for\n// the lifecycle record, `runLogStream` for the per-run event log — and the\n// vocabulary is core's throughout (historical `done`/`error` records are\n// migrated on read; see './run-log').\nimport { RunController } from '@tanstack/ai-sandbox'\nimport { runLogStore, runLogStream } from './durability'\nimport { DurableObjectRunEventLog } from './run-log-do'\nimport type { ModelMessage, StreamChunk } from '@tanstack/ai'\nimport type { RunLogRecord } from './run-log'\n\n/** Re-arm window for the liveness watchdog while a run is in flight (ms). */\nconst WATCHDOG_MS = 30_000\n\n/**\n * How long a non-terminal run may go without ANY new event before the watchdog\n * presumes the orchestrator driving it is dead (eviction that lost the\n * `waitUntil` promise, an uncaught fault, a hung container) and fails the run so\n * tailing clients stop waiting forever. Generous so a legitimately slow agent\n * step (a long tool call that emits no chunks) is not killed prematurely.\n */\nconst WATCHDOG_STALL_MS = 5 * 60_000\n\n/** What the Worker hands the coordinator to start a run. */\nexport interface StartRunInput {\n runId: string\n threadId: string\n messages: Array<ModelMessage>\n /**\n * The host the `POST /runs` trigger request arrived on, captured by the Worker\n * (`new URL(request.url).host`). Used to derive the container's callback hosts\n * when `PUBLIC_HOSTNAME` / `PREVIEW_HOSTNAME` are not set — see\n * {@link resolveBridgeOrigin} / {@link resolvePreviewHost} for the rules (and the\n * Cloudflare-specific reason request-derivation is safe to trust).\n */\n publicHost?: string\n /**\n * Free-form per-run input forwarded verbatim from the trigger to the app's\n * `adapter` / `sandbox` / `tools` resolvers (it reaches them through `config`\n * unchanged; it is NOT persisted to the run-log). Use it to carry browser-chosen\n * run options the base trigger has no field for — e.g. which harness to run, or a\n * model id. The package never inspects it; the app validates whatever it reads.\n */\n metadata?: Record<string, unknown>\n}\n\n// Host resolvers live in their own (Workers-free) module so they stay pure and\n// unit-testable; re-exported here because the coordinators build their callback\n// URLs with them. `resolveBridgeOrigin` = container→Worker (/_bridge, /tool-exec);\n// `resolvePreviewHost` = browser→container previews. See their docstrings.\nexport { resolveBridgeOrigin, resolvePreviewHost } from './public-host'\n\n/** Cursor stashed on each hibernatable WebSocket so it survives eviction. */\ninterface SocketAttachment {\n runId: string\n lastSeq: number\n}\n\nfunction isSocketAttachment(value: unknown): value is SocketAttachment {\n return (\n value !== null &&\n typeof value === 'object' &&\n 'runId' in value &&\n typeof value.runId === 'string' &&\n 'lastSeq' in value &&\n typeof value.lastSeq === 'number'\n )\n}\n\nexport abstract class SandboxCoordinator<\n TEnv = unknown,\n> extends DurableObject<TEnv> {\n protected readonly log: DurableObjectRunEventLog\n protected readonly controller: RunController\n\n /**\n * Sockets with a live {@link pump} loop. Guards against a second concurrent\n * pump on the same socket: `acceptStream` starts one, and `webSocketMessage`\n * would start another on any inbound client message while the first is still\n * running — double-delivering events and racing the persisted cursor.\n */\n private readonly pumping = new WeakSet<WebSocket>()\n\n constructor(ctx: DurableObjectState, env: TEnv) {\n super(ctx, env)\n this.log = new DurableObjectRunEventLog(ctx.storage)\n this.controller = new RunController({\n runs: runLogStore(this.log),\n durability: (runId) => runLogStream(this.log, { runId }),\n })\n }\n\n // ===========================================================================\n // Subclass seam\n // ===========================================================================\n\n /**\n * Produce the run's `StreamChunk` stream. The ONE model-specific method:\n * `ChatSandboxCoordinator` runs `chat()` here; `ContainerSandboxCoordinator`\n * drives the in-container runner. Lazily consumed by the run driver, so any\n * setup (mint a token, start a container) can happen at the top.\n */\n protected abstract buildRunStream(\n input: StartRunInput,\n ): AsyncIterable<StreamChunk> | Promise<AsyncIterable<StreamChunk>>\n\n /** Extra fetch routes a subclass serves (e.g. `/_bridge`, `/tool-exec`). */\n protected handleRoute(\n _request: Request,\n _parts: Array<string>,\n ): Promise<Response> | Response {\n return new Response('not found', { status: 404 })\n }\n\n /** Called once a run reaches a terminal status (override to clean up state). */\n protected onRunSettled(_runId: string): void {}\n\n protected jsonResponse(body: unknown, status = 200): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': 'application/json' },\n })\n }\n\n // ===========================================================================\n // Trigger (called by the Worker; returns immediately)\n // ===========================================================================\n\n async startRun(input: StartRunInput): Promise<{ runId: string }> {\n const existing = await this.log.get(input.runId)\n if (existing) return { runId: input.runId } // idempotent re-trigger\n\n // Open the run BEFORE building the stream. `pipeToRunLog`'s never-rejects\n // guarantee only covers failures AFTER the stream is handed to it — a throw\n // while BUILDING the stream (config(), chat() validation, mint a token)\n // would otherwise leave no record and no terminal event, so a tailing client\n // would never see the failure. Opening here (idempotent with pipeToRunLog's\n // own open) lets us record it.\n await this.log.open({ runId: input.runId, threadId: input.threadId })\n let stream: AsyncIterable<StreamChunk>\n try {\n stream = await this.buildRunStream(input)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n await this.log.append(input.runId, {\n type: EventType.RUN_ERROR,\n message,\n })\n await this.log.finish(input.runId, 'failed', { message })\n this.onRunSettled(input.runId)\n return { runId: input.runId }\n }\n\n const { done } = this.controller.start({\n runId: input.runId,\n threadId: input.threadId,\n stream,\n })\n // Keep the instance alive until the run is terminal. `pipeToRunLog` never\n // rejects (failures land in the log), but this must not DEPEND on that:\n // `.finally` adopts a rejection, which would hand `waitUntil` a rejected\n // promise. Two-argument `then` settles fulfilled either way while still\n // running the settle hook.\n const settle = (): void => this.onRunSettled(input.runId)\n this.ctx.waitUntil(done.then(settle, settle))\n await this.ctx.storage.setAlarm(Date.now() + WATCHDOG_MS)\n return { runId: input.runId }\n }\n\n async status(runId: string): Promise<RunLogRecord | null> {\n // Straight off the log (not `controller.status`) so the answer keeps the\n // log-level fields (`lastSeq`) a reconnecting client resumes from.\n return this.log.get(runId)\n }\n\n // ===========================================================================\n // HTTP surface\n // ===========================================================================\n\n override async fetch(request: Request): Promise<Response> {\n const url = new URL(request.url)\n const parts = url.pathname.split('/').filter(Boolean)\n\n if (parts[0] === 'runs' && typeof parts[1] === 'string') {\n if (parts[2] === 'stream') return this.acceptStream(parts[1], request)\n if (parts.length === 2 && request.method === 'GET') {\n const record = await this.status(parts[1])\n return record\n ? this.jsonResponse(record)\n : this.jsonResponse({ error: 'unknown run' }, 404)\n }\n }\n return this.handleRoute(request, parts)\n }\n\n // ===========================================================================\n // WebSocket streaming with hibernation + resumable cursor\n // ===========================================================================\n\n private async acceptStream(\n runId: string,\n request: Request,\n ): Promise<Response> {\n if (request.headers.get('upgrade') !== 'websocket') {\n return new Response('expected websocket upgrade', { status: 426 })\n }\n const record = await this.log.get(runId)\n if (!record) return new Response('unknown run', { status: 404 })\n\n const url = new URL(request.url)\n const lastSeqParam = url.searchParams.get('lastSeq')\n const lastSeq =\n lastSeqParam !== null ? Number.parseInt(lastSeqParam, 10) : -1\n if (Number.isNaN(lastSeq)) {\n return new Response('lastSeq must be an integer', { status: 400 })\n }\n\n const pair = new WebSocketPair()\n const [client, server] = [pair[0], pair[1]]\n server.serializeAttachment({ runId, lastSeq } satisfies SocketAttachment)\n this.ctx.acceptWebSocket(server)\n this.pump(server, runId, lastSeq)\n\n return new Response(null, { status: 101, webSocket: client })\n }\n\n /**\n * Replay-then-tail loop for one socket. Each delivered event advances the\n * socket's persisted cursor so a mid-stream reconnect resumes exactly once.\n * No-ops if a pump is already running for this socket (see {@link pumping}).\n */\n private pump(socket: WebSocket, runId: string, fromSeq: number): void {\n if (this.pumping.has(socket)) return\n this.pumping.add(socket)\n const done = (async () => {\n try {\n // The tail reads the log directly by seq — the client wire protocol\n // (`?lastSeq`, `{seq, chunk}` frames) is seq-based, and `log.read` is\n // the seq-cursor surface. Core's `controller.attach` serves consumers\n // that speak opaque `StreamDurability` offsets instead.\n for await (const event of this.log.read(runId, { fromSeq })) {\n socket.send(JSON.stringify(event))\n socket.serializeAttachment({\n runId,\n lastSeq: event.seq,\n } satisfies SocketAttachment)\n }\n const record = await this.log.get(runId)\n if (socket.readyState === WebSocket.OPEN) {\n socket.send(JSON.stringify({ type: 'status', record }))\n socket.close(1000, 'run complete')\n }\n } catch (error) {\n // A tail loop throwing means a run-log read failed — an operator needs\n // the full error, but the client only gets a truncated close reason.\n const message = error instanceof Error ? error.message : String(error)\n console.error(\n `[sandbox-coordinator] tail failed for run ${runId}:`,\n error,\n )\n if (socket.readyState === WebSocket.OPEN) {\n socket.close(1011, message.slice(0, 120))\n }\n } finally {\n this.pumping.delete(socket)\n }\n })()\n this.ctx.waitUntil(done)\n }\n\n override webSocketMessage(\n ws: WebSocket,\n _message: string | ArrayBuffer,\n ): void {\n // Only meaningful as a post-hibernation resume nudge: restart the tail from\n // the persisted cursor IF no pump is live (the guard in `pump` enforces the\n // \"resume exactly once\" invariant when the original pump is still running).\n const attachment: unknown = ws.deserializeAttachment()\n if (isSocketAttachment(attachment)) {\n this.pump(ws, attachment.runId, attachment.lastSeq)\n }\n }\n\n override webSocketClose(\n _ws: WebSocket,\n _code: number,\n _reason: string,\n ): void {\n // Nothing to clean up: the run-log is durable and independent of any socket.\n }\n\n // ===========================================================================\n // Watchdog alarm — keeps a run observable across hibernation\n // ===========================================================================\n\n override async alarm(): Promise<void> {\n try {\n // Through the log (not a raw `rec:` list) so legacy records are migrated\n // on the way out — the storage layout is the log's private concern.\n const runs = await this.log.list()\n const now = Date.now()\n let active = false\n for (const record of runs) {\n if (isTerminalRunStatus(record.status)) continue\n if (now - record.updatedAt > WATCHDOG_STALL_MS) {\n // No progress for too long — the driver is presumed dead. Fail the run\n // so tailing clients stop waiting forever (the whole point of the\n // watchdog; without this a stuck run sits at `running` indefinitely).\n await this.failStalledRun(record.runId)\n } else {\n active = true\n }\n }\n if (active) await this.ctx.storage.setAlarm(Date.now() + WATCHDOG_MS)\n } catch (error) {\n // Never let the watchdog die silently: a transient storage error must not\n // permanently disable liveness detection. Re-arm and try again next tick.\n console.error('[sandbox-coordinator] watchdog alarm failed:', error)\n await this.ctx.storage.setAlarm(Date.now() + WATCHDOG_MS)\n }\n }\n\n /** Mark a stalled (orchestrator-presumed-dead) run as a terminal error. */\n private async failStalledRun(runId: string): Promise<void> {\n const message = 'run watchdog: no progress; orchestrator presumed dead'\n try {\n await this.log.append(runId, { type: EventType.RUN_ERROR, message })\n } catch {\n // The run may have just reached terminal concurrently; finish is idempotent.\n }\n await this.log.finish(runId, 'failed', { message })\n this.onRunSettled(runId)\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"coordinator.js","names":[],"sources":["../../src/coordinator.ts"],"sourcesContent":["/**\n * `SandboxCoordinator` — the abstract Durable Object base for the serverless/\n * edge agent run model. It owns everything the two concrete models share:\n *\n * - a durable, resumable run-log ({@link DurableObjectRunEventLog});\n * - `startRun`: open the run, kick off the model's chunk stream WITHOUT blocking\n * the trigger, start piping it into the log via {@link RunController}, register\n * the resulting `done` promise with `ctx.waitUntil` (keeping the instance alive\n * until the run is terminal rather than letting it hibernate mid-run), and arm\n * a watchdog alarm;\n * - `status` (poll fallback) + a hibernatable WebSocket tail with a resumable\n * cursor (replay after `lastSeq`, then live-tail, reconnect-safe);\n * - routing for `GET /runs/:id` and `GET /runs/:id/stream`, delegating any other\n * path to {@link handleRoute} (which a subclass overrides for e.g. `/_bridge`\n * or `/tool-exec`).\n *\n * Subclasses implement {@link buildRunStream} — the ONE difference between the\n * models: run `chat()` in the DO ({@link ChatSandboxCoordinator}) or drive an\n * in-container runner ({@link ContainerSandboxCoordinator}).\n *\n * NOTE: Workers-runtime code — compiles against `@cloudflare/workers-types`; not\n * runtime-verified in this repo.\n */\nimport { DurableObject } from 'cloudflare:workers'\nimport { EventType, isTerminalRunStatus } from '@tanstack/ai'\n// The PORTABLE run driver: this coordinator is a platform binding of core's\n// `RunController`, not a driver of its own. The DO run log backs both of the\n// driver's seams through the adapters in './durability' — `runLogStore` for\n// the lifecycle record, `runLogStream` for the per-run event log — and the\n// vocabulary is core's throughout (historical `done`/`error` records are\n// migrated on read; see './run-log').\nimport { RunController } from '@tanstack/ai-sandbox'\nimport { runLogStore, runLogStream } from './durability'\nimport { DurableObjectRunEventLog } from './run-log-do'\nimport type { ModelMessage, StreamChunk } from '@tanstack/ai'\nimport type { RunLogRecord } from './run-log'\n\n/** Re-arm window for the liveness watchdog while a run is in flight (ms). */\nconst WATCHDOG_MS = 30_000\n\n/**\n * How long a non-terminal run may go without ANY new event before the watchdog\n * presumes the orchestrator driving it is dead (eviction that lost the\n * `waitUntil` promise, an uncaught fault, a hung container) and fails the run so\n * tailing clients stop waiting forever. Generous so a legitimately slow agent\n * step (a long tool call that emits no chunks) is not killed prematurely.\n */\nconst WATCHDOG_STALL_MS = 5 * 60_000\n\n/** What the Worker hands the coordinator to start a run. */\nexport interface StartRunInput {\n runId: string\n threadId: string\n messages: Array<ModelMessage>\n /**\n * The host the `POST /runs` trigger request arrived on, captured by the Worker\n * (`new URL(request.url).host`). Used to derive the container's callback hosts\n * when `PUBLIC_HOSTNAME` / `PREVIEW_HOSTNAME` are not set — see\n * {@link resolveBridgeOrigin} / {@link resolvePreviewHost} for the rules (and the\n * Cloudflare-specific reason request-derivation is safe to trust).\n */\n publicHost?: string\n /**\n * Free-form per-run input forwarded verbatim from the trigger to the app's\n * `adapter` / `sandbox` / `tools` resolvers (it reaches them through `config`\n * unchanged; it is NOT persisted to the run-log). Use it to carry browser-chosen\n * run options the base trigger has no field for — e.g. which harness to run, or a\n * model id. The package never inspects it; the app validates whatever it reads.\n */\n metadata?: Record<string, unknown>\n}\n\n// Host resolvers live in their own (Workers-free) module so they stay pure and\n// unit-testable; re-exported here because the coordinators build their callback\n// URLs with them. `resolveBridgeOrigin` = container→Worker (/_bridge, /tool-exec);\n// `resolvePreviewHost` = browser→container previews. See their docstrings.\nexport { resolveBridgeOrigin, resolvePreviewHost } from './public-host'\n\n/** Cursor stashed on each hibernatable WebSocket so it survives eviction. */\ninterface SocketAttachment {\n runId: string\n lastSeq: number\n}\n\nfunction isSocketAttachment(value: unknown): value is SocketAttachment {\n return (\n value !== null &&\n typeof value === 'object' &&\n 'runId' in value &&\n typeof value.runId === 'string' &&\n 'lastSeq' in value &&\n typeof value.lastSeq === 'number'\n )\n}\n\nexport abstract class SandboxCoordinator<\n TEnv = unknown,\n> extends DurableObject<TEnv> {\n protected readonly log: DurableObjectRunEventLog\n protected readonly controller: RunController\n\n /**\n * Sockets with a live {@link pump} loop. Guards against a second concurrent\n * pump on the same socket: `acceptStream` starts one, and `webSocketMessage`\n * would start another on any inbound client message while the first is still\n * running — double-delivering events and racing the persisted cursor.\n */\n private readonly pumping = new WeakSet<WebSocket>()\n\n constructor(ctx: DurableObjectState, env: TEnv) {\n super(ctx, env)\n this.log = new DurableObjectRunEventLog(ctx.storage)\n this.controller = new RunController({\n runs: runLogStore(this.log),\n durability: (runId) => runLogStream(this.log, { runId }),\n })\n }\n\n // ===========================================================================\n // Subclass seam\n // ===========================================================================\n\n /**\n * Produce the run's `StreamChunk` stream. The ONE model-specific method:\n * `ChatSandboxCoordinator` runs `chat()` here; `ContainerSandboxCoordinator`\n * drives the in-container runner. Lazily consumed by the run driver, so any\n * setup (mint a token, start a container) can happen at the top.\n */\n protected abstract buildRunStream(\n input: StartRunInput,\n ): AsyncIterable<StreamChunk> | Promise<AsyncIterable<StreamChunk>>\n\n /** Extra fetch routes a subclass serves (e.g. `/_bridge`, `/tool-exec`). */\n protected handleRoute(\n _request: Request,\n _parts: Array<string>,\n ): Promise<Response> | Response {\n return new Response('not found', { status: 404 })\n }\n\n /** Called once a run reaches a terminal status (override to clean up state). */\n protected onRunSettled(_runId: string): void {}\n\n protected jsonResponse(body: unknown, status = 200): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': 'application/json' },\n })\n }\n\n // ===========================================================================\n // Trigger (called by the Worker; returns immediately)\n // ===========================================================================\n\n async startRun(input: StartRunInput): Promise<{ runId: string }> {\n const existing = await this.log.get(input.runId)\n if (existing) return { runId: input.runId } // idempotent re-trigger\n\n // Open the run BEFORE building the stream. `pipeToRunLog`'s never-rejects\n // guarantee only covers failures AFTER the stream is handed to it — a throw\n // while BUILDING the stream (config(), chat() validation, mint a token)\n // would otherwise leave no record and no terminal event, so a tailing client\n // would never see the failure. Opening here (idempotent with pipeToRunLog's\n // own open) lets us record it.\n await this.log.open({ runId: input.runId, threadId: input.threadId })\n let stream: AsyncIterable<StreamChunk>\n try {\n stream = await this.buildRunStream(input)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n await this.log.append(input.runId, {\n type: EventType.RUN_ERROR,\n message,\n })\n await this.log.finish(input.runId, 'failed', { message })\n this.onRunSettled(input.runId)\n return { runId: input.runId }\n }\n\n const { done } = this.controller.start({\n runId: input.runId,\n threadId: input.threadId,\n stream,\n })\n // Keep the instance alive until the run is terminal. `pipeToRunLog` never\n // rejects (failures land in the log), but this must not DEPEND on that:\n // `.finally` adopts a rejection, which would hand `waitUntil` a rejected\n // promise. Two-argument `then` settles fulfilled either way while still\n // running the settle hook.\n const settle = (): void => this.onRunSettled(input.runId)\n this.ctx.waitUntil(done.then(settle, settle))\n await this.ctx.storage.setAlarm(Date.now() + WATCHDOG_MS)\n return { runId: input.runId }\n }\n\n async status(runId: string): Promise<RunLogRecord | null> {\n // Straight off the log (not `controller.status`) so the answer keeps the\n // log-level fields (`lastSeq`) a reconnecting client resumes from.\n return this.log.get(runId)\n }\n\n // ===========================================================================\n // HTTP surface\n // ===========================================================================\n\n override async fetch(request: Request): Promise<Response> {\n const url = new URL(request.url)\n const parts = url.pathname.split('/').filter(Boolean)\n\n if (parts[0] === 'runs' && typeof parts[1] === 'string') {\n if (parts[2] === 'stream') return this.acceptStream(parts[1], request)\n if (parts.length === 2 && request.method === 'GET') {\n const record = await this.status(parts[1])\n return record\n ? this.jsonResponse(record)\n : this.jsonResponse({ error: 'unknown run' }, 404)\n }\n }\n return this.handleRoute(request, parts)\n }\n\n // ===========================================================================\n // WebSocket streaming with hibernation + resumable cursor\n // ===========================================================================\n\n private async acceptStream(\n runId: string,\n request: Request,\n ): Promise<Response> {\n if (request.headers.get('upgrade') !== 'websocket') {\n return new Response('expected websocket upgrade', { status: 426 })\n }\n const record = await this.log.get(runId)\n if (!record) return new Response('unknown run', { status: 404 })\n\n const url = new URL(request.url)\n const lastSeqParam = url.searchParams.get('lastSeq')\n const lastSeq =\n lastSeqParam !== null ? Number.parseInt(lastSeqParam, 10) : -1\n if (Number.isNaN(lastSeq)) {\n return new Response('lastSeq must be an integer', { status: 400 })\n }\n\n const pair = new WebSocketPair()\n const [client, server] = [pair[0], pair[1]]\n server.serializeAttachment({ runId, lastSeq } satisfies SocketAttachment)\n this.ctx.acceptWebSocket(server)\n this.pump(server, runId, lastSeq)\n\n return new Response(null, { status: 101, webSocket: client })\n }\n\n /**\n * Replay-then-tail loop for one socket. Each delivered event advances the\n * socket's persisted cursor so a mid-stream reconnect resumes exactly once.\n * No-ops if a pump is already running for this socket (see {@link pumping}).\n */\n private pump(socket: WebSocket, runId: string, fromSeq: number): void {\n if (this.pumping.has(socket)) return\n this.pumping.add(socket)\n const done = (async () => {\n try {\n // The tail reads the log directly by seq — the client wire protocol\n // (`?lastSeq`, `{seq, chunk}` frames) is seq-based, and `log.read` is\n // the seq-cursor surface. Core's `controller.attach` serves consumers\n // that speak opaque `StreamDurability` offsets instead.\n for await (const event of this.log.read(runId, { fromSeq })) {\n socket.send(JSON.stringify(event))\n socket.serializeAttachment({\n runId,\n lastSeq: event.seq,\n } satisfies SocketAttachment)\n }\n const record = await this.log.get(runId)\n if (socket.readyState === WebSocket.OPEN) {\n socket.send(JSON.stringify({ type: 'status', record }))\n socket.close(1000, 'run complete')\n }\n } catch (error) {\n // A tail loop throwing means a run-log read failed — an operator needs\n // the full error, but the client only gets a truncated close reason.\n const message = error instanceof Error ? error.message : String(error)\n console.error(\n `[sandbox-coordinator] tail failed for run ${runId}:`,\n error,\n )\n if (socket.readyState === WebSocket.OPEN) {\n socket.close(1011, message.slice(0, 120))\n }\n } finally {\n this.pumping.delete(socket)\n }\n })()\n this.ctx.waitUntil(done)\n }\n\n override webSocketMessage(\n ws: WebSocket,\n _message: string | ArrayBuffer,\n ): void {\n // Only meaningful as a post-hibernation resume nudge: restart the tail from\n // the persisted cursor IF no pump is live (the guard in `pump` enforces the\n // \"resume exactly once\" invariant when the original pump is still running).\n const attachment: unknown = ws.deserializeAttachment()\n if (isSocketAttachment(attachment)) {\n this.pump(ws, attachment.runId, attachment.lastSeq)\n }\n }\n\n override webSocketClose(\n _ws: WebSocket,\n _code: number,\n _reason: string,\n ): void {\n // Nothing to clean up: the run-log is durable and independent of any socket.\n }\n\n // ===========================================================================\n // Watchdog alarm — keeps a run observable across hibernation\n // ===========================================================================\n\n override async alarm(): Promise<void> {\n try {\n // Through the log (not a raw `rec:` list) so legacy records are migrated\n // on the way out — the storage layout is the log's private concern.\n const runs = await this.log.list()\n const now = Date.now()\n let active = false\n for (const record of runs) {\n if (isTerminalRunStatus(record.status)) continue\n if (now - record.updatedAt > WATCHDOG_STALL_MS) {\n // No progress for too long — the driver is presumed dead. Fail the run\n // so tailing clients stop waiting forever (the whole point of the\n // watchdog; without this a stuck run sits at `running` indefinitely).\n await this.failStalledRun(record.runId)\n } else {\n active = true\n }\n }\n if (active) await this.ctx.storage.setAlarm(Date.now() + WATCHDOG_MS)\n } catch (error) {\n // Never let the watchdog die silently: a transient storage error must not\n // permanently disable liveness detection. Re-arm and try again next tick.\n console.error('[sandbox-coordinator] watchdog alarm failed:', error)\n await this.ctx.storage.setAlarm(Date.now() + WATCHDOG_MS)\n }\n }\n\n /** Mark a stalled (orchestrator-presumed-dead) run as a terminal error. */\n private async failStalledRun(runId: string): Promise<void> {\n const message = 'run watchdog: no progress; orchestrator presumed dead'\n try {\n await this.log.append(runId, { type: EventType.RUN_ERROR, message })\n } catch {\n // The run may have just reached terminal concurrently; finish is idempotent.\n }\n await this.log.finish(runId, 'failed', { message })\n this.onRunSettled(runId)\n }\n}\n"],"mappings":"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"}
@@ -105,7 +105,8 @@ function runAgent(request, resolveAdapter) {
105
105
  }
106
106
  /** Stream the agent's chunks to the response as NDJSON, one object per line. */
107
107
  async function handleRun(req, res, resolveAdapter) {
108
- const request = parseContainerRunRequest(JSON.parse(await readBody(req)));
108
+ const parsed = JSON.parse(await readBody(req));
109
+ const request = parseContainerRunRequest(parsed);
109
110
  res.writeHead(200, {
110
111
  "content-type": "application/x-ndjson",
111
112
  "cache-control": "no-cache"
@@ -1 +1 @@
1
- {"version":3,"file":"runner.js","names":[],"sources":["../../src/runner.ts"],"sourcesContent":["/**\n * `runInContainerHarness` — the IN-CONTAINER harness runner, shipped from the\n * package so a co-located app's container program is a single function call.\n *\n * This is the heart of the CO-LOCATED model: the agent harness loop AND its MCP\n * tool-bridge run HERE, on the container's own localhost. The Durable Object\n * outside never calls `chat()`; it POSTs `/run` to this server and reads the\n * NDJSON stream back.\n *\n * DO ── POST /run {messages, harness, model, workspace, toolDescriptors,\n * toolExecUrl, toolExecToken} ──▶ THIS\n * THIS ── NDJSON stream of StreamChunk ──────────────────────────────────▶ DO\n *\n * It is a tiny `node:http` server (NODE/container side — NOT Workers; it uses\n * `localProcessSandbox`). On `POST /run` it validates the {@link\n * ContainerRunRequest}, builds `chat()` with the in-container `local-process`\n * sandbox and the adapter the CALLER resolves, and streams each {@link\n * StreamChunk} back as NDJSON (one JSON object per line).\n *\n * Why the MCP bridge is genuinely in-container: the in-container sandbox is\n * `localProcessSandbox()` — the container IS the host — so the harness adapter\n * serves its tool-bridge over the container's own `localhost` and feeds the\n * prompt over NATIVE writable stdin (no file-redirect; the bridge URL/token\n * never leave the container). The MCP protocol never crosses the network.\n *\n * The ONE thing that still crosses back to the DO is host-tool EXECUTION: each\n * tool rebuilt by {@link remoteToolStubs} delegates its `execute()` to {@link\n * httpRemoteToolExecutor}, which POSTs `{ name, args }` (bearer-gated) to the\n * DO's `toolExecUrl`:\n *\n * agent → in-container MCP bridge → stub.execute → httpRemoteToolExecutor → DO\n *\n * The app supplies only `resolveAdapter` — which `*Text` adapter to build for a\n * given `{ harness, model }`. The server + `chat()` wiring lives here, so the\n * package doesn't depend on every adapter package.\n *\n * NOTE: container-side Node code — compiles against the real TanStack AI types;\n * not runtime-verified in this repo (no container build in CI).\n */\nimport { createServer } from 'node:http'\nimport { EventType, chat } from '@tanstack/ai'\nimport {\n createSecrets,\n defineSandbox,\n defineWorkspace,\n httpRemoteToolExecutor,\n remoteToolStubs,\n withSandbox,\n} from '@tanstack/ai-sandbox'\nimport { localProcessSandbox } from '@tanstack/ai-sandbox-local-process'\nimport { parseContainerRunRequest } from './protocol'\nimport type { IncomingMessage, Server, ServerResponse } from 'node:http'\nimport type { AnyTextAdapter, StreamChunk } from '@tanstack/ai'\nimport type { WorkspaceDefinition } from '@tanstack/ai-sandbox'\nimport type { ContainerRunRequest, HarnessId } from './protocol'\n\n/** The `{ harness, model }` the caller maps to a concrete `*Text` adapter. */\nexport interface ResolveAdapterInput {\n harness: HarnessId\n model: string\n}\n\n/** Options for {@link runInContainerHarness}. */\nexport interface RunInContainerHarnessOptions {\n /**\n * Build the text adapter `chat()` runs for one request's `{ harness, model }`.\n * The app supplies this so the package doesn't depend on every adapter package\n * — e.g. `({ model }) => claudeCodeText(model)`.\n */\n resolveAdapter: (input: ResolveAdapterInput) => AnyTextAdapter\n /** Port to listen on. Defaults to `RUNNER_PORT` env, then `8080`. */\n port?: number\n}\n\n/** What {@link runInContainerHarness} returns: the listening `node:http` server. */\nexport interface ContainerHarnessServer {\n /** The underlying `node:http` server (already `listen()`ing). */\n server: Server\n /** The port it is listening on. */\n port: number\n}\n\n/** Read a request body fully into a string (small JSON payloads only). */\nfunction readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n let body = ''\n req.setEncoding('utf8')\n req.on('data', (chunk: string) => {\n body += chunk\n })\n req.on('end', () => resolve(body))\n req.on('error', reject)\n })\n}\n\n/**\n * Rebuild the request's workspace with a real `createSecrets`, pulling each\n * referenced secret's VALUE from the container env. Secret values never cross\n * the `POST /run` boundary (`createSecrets` stores them under a non-enumerable\n * symbol, so serializing the workspace carries only the names) — the DO injects\n * them into the container env via `sandbox.setEnvVars`, and we reconstitute them\n * here. A referenced secret missing from the env is a hard error, never a silent\n * keyless run.\n */\nfunction reconstituteWorkspace(\n workspace: WorkspaceDefinition,\n): WorkspaceDefinition {\n if (workspace.secrets === undefined) return workspace\n const names = Object.keys(workspace.secrets)\n if (names.length === 0) return workspace\n const values: Record<string, string> = {}\n for (const name of names) {\n const value = process.env[name]\n if (value === undefined || value === '') {\n throw new Error(\n `runInContainerHarness: secret \"${name}\" is not set in the container env`,\n )\n }\n values[name] = value\n }\n return defineWorkspace({ ...workspace, secrets: createSecrets(values) })\n}\n\n/**\n * Build the `chat()` stream that runs the harness on THIS container via the\n * `local-process` sandbox. The agent's `chat()` tools are stubs that delegate\n * back to the DO; everything else (the harness loop, the MCP bridge, stdin)\n * stays on localhost.\n */\nfunction runAgent(\n request: ContainerRunRequest,\n resolveAdapter: (input: ResolveAdapterInput) => AnyTextAdapter,\n): AsyncIterable<StreamChunk> {\n const sandbox = defineSandbox({\n // The container IS the host: no isolation, just run on its own filesystem.\n id: 'colocated-in-container',\n provider: localProcessSandbox(),\n // Honor the app's workspace (source / setup / skills / …), with the secrets\n // re-resolved from the container env.\n workspace: reconstituteWorkspace(request.workspace),\n })\n\n // `stream: true` (no outputSchema) makes chat() return AsyncIterable<StreamChunk>.\n return chat({\n threadId: request.threadId,\n adapter: resolveAdapter({\n harness: request.harness,\n model: request.model,\n }),\n messages: request.messages,\n stream: true,\n // Rebuild the DO's host tools as stubs whose execute() POSTs back to the DO.\n // The adapter bridges them over the in-container localhost MCP transport.\n tools: remoteToolStubs(\n request.toolDescriptors,\n httpRemoteToolExecutor(request.toolExecUrl, request.toolExecToken),\n ),\n // Provide the in-container local-process sandbox handle the adapter needs.\n middleware: [withSandbox(sandbox)],\n })\n}\n\n/** Stream the agent's chunks to the response as NDJSON, one object per line. */\nasync function handleRun(\n req: IncomingMessage,\n res: ServerResponse,\n resolveAdapter: (input: ResolveAdapterInput) => AnyTextAdapter,\n): Promise<void> {\n const parsed: unknown = JSON.parse(await readBody(req))\n const request = parseContainerRunRequest(parsed)\n res.writeHead(200, {\n 'content-type': 'application/x-ndjson',\n 'cache-control': 'no-cache',\n })\n // The DO appends each line to its durable run-log; here we are the producer,\n // so we surface a mid-stream failure as a terminal RUN_ERROR line the DO will\n // append + finish on, never a silently truncated stream.\n try {\n for await (const chunk of runAgent(request, resolveAdapter)) {\n res.write(`${JSON.stringify(chunk)}\\n`)\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n res.write(`${JSON.stringify({ type: EventType.RUN_ERROR, message })}\\n`)\n } finally {\n res.end()\n }\n}\n\n/**\n * Start the in-container harness runner: a `node:http` server with `GET /health`\n * and `POST /run`. Call this as the container's program; the app supplies only\n * `resolveAdapter`.\n */\nexport function runInContainerHarness(\n options: RunInContainerHarnessOptions,\n): ContainerHarnessServer {\n const port =\n options.port ?? Number.parseInt(process.env.RUNNER_PORT ?? '8080', 10)\n\n const server = createServer((req, res) => {\n if (req.method === 'POST' && req.url === '/run') {\n handleRun(req, res, options.resolveAdapter).catch((error: unknown) => {\n // A failure BEFORE we start streaming (e.g. a malformed body) is a 400 —\n // surfaced, never swallowed.\n const message = error instanceof Error ? error.message : String(error)\n if (!res.headersSent) {\n res.writeHead(400, { 'content-type': 'text/plain' })\n }\n res.end(message)\n })\n return\n }\n if (req.method === 'GET' && req.url === '/health') {\n res.writeHead(200).end('ok')\n return\n }\n res.writeHead(404).end('not found')\n })\n\n server.listen(port, () => {\n console.log(`[container-runner] listening on :${port}`)\n })\n\n return { server, port }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmFA,SAAS,SAAS,KAAuC;CACvD,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,IAAI,OAAO;EACX,IAAI,YAAY,MAAM;EACtB,IAAI,GAAG,SAAS,UAAkB;GAChC,QAAQ;EACV,CAAC;EACD,IAAI,GAAG,aAAa,QAAQ,IAAI,CAAC;EACjC,IAAI,GAAG,SAAS,MAAM;CACxB,CAAC;AACH;;;;;;;;;;AAWA,SAAS,sBACP,WACqB;CACrB,IAAI,UAAU,YAAY,KAAA,GAAW,OAAO;CAC5C,MAAM,QAAQ,OAAO,KAAK,UAAU,OAAO;CAC3C,IAAI,MAAM,WAAW,GAAG,OAAO;CAC/B,MAAM,SAAiC,CAAC;CACxC,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,QAAQ,QAAQ,IAAI;EAC1B,IAAI,UAAU,KAAA,KAAa,UAAU,IACnC,MAAM,IAAI,MACR,kCAAkC,KAAK,kCACzC;EAEF,OAAO,QAAQ;CACjB;CACA,OAAO,gBAAgB;EAAE,GAAG;EAAW,SAAS,cAAc,MAAM;CAAE,CAAC;AACzE;;;;;;;AAQA,SAAS,SACP,SACA,gBAC4B;CAC5B,MAAM,UAAU,cAAc;EAE5B,IAAI;EACJ,UAAU,oBAAoB;EAG9B,WAAW,sBAAsB,QAAQ,SAAS;CACpD,CAAC;CAGD,OAAO,KAAK;EACV,UAAU,QAAQ;EAClB,SAAS,eAAe;GACtB,SAAS,QAAQ;GACjB,OAAO,QAAQ;EACjB,CAAC;EACD,UAAU,QAAQ;EAClB,QAAQ;EAGR,OAAO,gBACL,QAAQ,iBACR,uBAAuB,QAAQ,aAAa,QAAQ,aAAa,CACnE;EAEA,YAAY,CAAC,YAAY,OAAO,CAAC;CACnC,CAAC;AACH;;AAGA,eAAe,UACb,KACA,KACA,gBACe;CAEf,MAAM,UAAU,yBADQ,KAAK,MAAM,MAAM,SAAS,GAAG,CACZ,CAAM;CAC/C,IAAI,UAAU,KAAK;EACjB,gBAAgB;EAChB,iBAAiB;CACnB,CAAC;CAID,IAAI;EACF,WAAW,MAAM,SAAS,SAAS,SAAS,cAAc,GACxD,IAAI,MAAM,GAAG,KAAK,UAAU,KAAK,EAAE,GAAG;CAE1C,SAAS,OAAO;EACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EACrE,IAAI,MAAM,GAAG,KAAK,UAAU;GAAE,MAAM,UAAU;GAAW;EAAQ,CAAC,EAAE,GAAG;CACzE,UAAU;EACR,IAAI,IAAI;CACV;AACF;;;;;;AAOA,SAAgB,sBACd,SACwB;CACxB,MAAM,OACJ,QAAQ,QAAQ,OAAO,SAAS,QAAQ,IAAI,eAAe,QAAQ,EAAE;CAEvE,MAAM,SAAS,cAAc,KAAK,QAAQ;EACxC,IAAI,IAAI,WAAW,UAAU,IAAI,QAAQ,QAAQ;GAC/C,UAAU,KAAK,KAAK,QAAQ,cAAc,CAAC,CAAC,OAAO,UAAmB;IAGpE,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;IACrE,IAAI,CAAC,IAAI,aACP,IAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;IAErD,IAAI,IAAI,OAAO;GACjB,CAAC;GACD;EACF;EACA,IAAI,IAAI,WAAW,SAAS,IAAI,QAAQ,WAAW;GACjD,IAAI,UAAU,GAAG,CAAC,CAAC,IAAI,IAAI;GAC3B;EACF;EACA,IAAI,UAAU,GAAG,CAAC,CAAC,IAAI,WAAW;CACpC,CAAC;CAED,OAAO,OAAO,YAAY;EACxB,QAAQ,IAAI,oCAAoC,MAAM;CACxD,CAAC;CAED,OAAO;EAAE;EAAQ;CAAK;AACxB"}
1
+ {"version":3,"file":"runner.js","names":[],"sources":["../../src/runner.ts"],"sourcesContent":["/**\n * `runInContainerHarness` — the IN-CONTAINER harness runner, shipped from the\n * package so a co-located app's container program is a single function call.\n *\n * This is the heart of the CO-LOCATED model: the agent harness loop AND its MCP\n * tool-bridge run HERE, on the container's own localhost. The Durable Object\n * outside never calls `chat()`; it POSTs `/run` to this server and reads the\n * NDJSON stream back.\n *\n * DO ── POST /run {messages, harness, model, workspace, toolDescriptors,\n * toolExecUrl, toolExecToken} ──▶ THIS\n * THIS ── NDJSON stream of StreamChunk ──────────────────────────────────▶ DO\n *\n * It is a tiny `node:http` server (NODE/container side — NOT Workers; it uses\n * `localProcessSandbox`). On `POST /run` it validates the {@link\n * ContainerRunRequest}, builds `chat()` with the in-container `local-process`\n * sandbox and the adapter the CALLER resolves, and streams each {@link\n * StreamChunk} back as NDJSON (one JSON object per line).\n *\n * Why the MCP bridge is genuinely in-container: the in-container sandbox is\n * `localProcessSandbox()` — the container IS the host — so the harness adapter\n * serves its tool-bridge over the container's own `localhost` and feeds the\n * prompt over NATIVE writable stdin (no file-redirect; the bridge URL/token\n * never leave the container). The MCP protocol never crosses the network.\n *\n * The ONE thing that still crosses back to the DO is host-tool EXECUTION: each\n * tool rebuilt by {@link remoteToolStubs} delegates its `execute()` to {@link\n * httpRemoteToolExecutor}, which POSTs `{ name, args }` (bearer-gated) to the\n * DO's `toolExecUrl`:\n *\n * agent → in-container MCP bridge → stub.execute → httpRemoteToolExecutor → DO\n *\n * The app supplies only `resolveAdapter` — which `*Text` adapter to build for a\n * given `{ harness, model }`. The server + `chat()` wiring lives here, so the\n * package doesn't depend on every adapter package.\n *\n * NOTE: container-side Node code — compiles against the real TanStack AI types;\n * not runtime-verified in this repo (no container build in CI).\n */\nimport { createServer } from 'node:http'\nimport { EventType, chat } from '@tanstack/ai'\nimport {\n createSecrets,\n defineSandbox,\n defineWorkspace,\n httpRemoteToolExecutor,\n remoteToolStubs,\n withSandbox,\n} from '@tanstack/ai-sandbox'\nimport { localProcessSandbox } from '@tanstack/ai-sandbox-local-process'\nimport { parseContainerRunRequest } from './protocol'\nimport type { IncomingMessage, Server, ServerResponse } from 'node:http'\nimport type { AnyTextAdapter, StreamChunk } from '@tanstack/ai'\nimport type { WorkspaceDefinition } from '@tanstack/ai-sandbox'\nimport type { ContainerRunRequest, HarnessId } from './protocol'\n\n/** The `{ harness, model }` the caller maps to a concrete `*Text` adapter. */\nexport interface ResolveAdapterInput {\n harness: HarnessId\n model: string\n}\n\n/** Options for {@link runInContainerHarness}. */\nexport interface RunInContainerHarnessOptions {\n /**\n * Build the text adapter `chat()` runs for one request's `{ harness, model }`.\n * The app supplies this so the package doesn't depend on every adapter package\n * — e.g. `({ model }) => claudeCodeText(model)`.\n */\n resolveAdapter: (input: ResolveAdapterInput) => AnyTextAdapter\n /** Port to listen on. Defaults to `RUNNER_PORT` env, then `8080`. */\n port?: number\n}\n\n/** What {@link runInContainerHarness} returns: the listening `node:http` server. */\nexport interface ContainerHarnessServer {\n /** The underlying `node:http` server (already `listen()`ing). */\n server: Server\n /** The port it is listening on. */\n port: number\n}\n\n/** Read a request body fully into a string (small JSON payloads only). */\nfunction readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n let body = ''\n req.setEncoding('utf8')\n req.on('data', (chunk: string) => {\n body += chunk\n })\n req.on('end', () => resolve(body))\n req.on('error', reject)\n })\n}\n\n/**\n * Rebuild the request's workspace with a real `createSecrets`, pulling each\n * referenced secret's VALUE from the container env. Secret values never cross\n * the `POST /run` boundary (`createSecrets` stores them under a non-enumerable\n * symbol, so serializing the workspace carries only the names) — the DO injects\n * them into the container env via `sandbox.setEnvVars`, and we reconstitute them\n * here. A referenced secret missing from the env is a hard error, never a silent\n * keyless run.\n */\nfunction reconstituteWorkspace(\n workspace: WorkspaceDefinition,\n): WorkspaceDefinition {\n if (workspace.secrets === undefined) return workspace\n const names = Object.keys(workspace.secrets)\n if (names.length === 0) return workspace\n const values: Record<string, string> = {}\n for (const name of names) {\n const value = process.env[name]\n if (value === undefined || value === '') {\n throw new Error(\n `runInContainerHarness: secret \"${name}\" is not set in the container env`,\n )\n }\n values[name] = value\n }\n return defineWorkspace({ ...workspace, secrets: createSecrets(values) })\n}\n\n/**\n * Build the `chat()` stream that runs the harness on THIS container via the\n * `local-process` sandbox. The agent's `chat()` tools are stubs that delegate\n * back to the DO; everything else (the harness loop, the MCP bridge, stdin)\n * stays on localhost.\n */\nfunction runAgent(\n request: ContainerRunRequest,\n resolveAdapter: (input: ResolveAdapterInput) => AnyTextAdapter,\n): AsyncIterable<StreamChunk> {\n const sandbox = defineSandbox({\n // The container IS the host: no isolation, just run on its own filesystem.\n id: 'colocated-in-container',\n provider: localProcessSandbox(),\n // Honor the app's workspace (source / setup / skills / …), with the secrets\n // re-resolved from the container env.\n workspace: reconstituteWorkspace(request.workspace),\n })\n\n // `stream: true` (no outputSchema) makes chat() return AsyncIterable<StreamChunk>.\n return chat({\n threadId: request.threadId,\n adapter: resolveAdapter({\n harness: request.harness,\n model: request.model,\n }),\n messages: request.messages,\n stream: true,\n // Rebuild the DO's host tools as stubs whose execute() POSTs back to the DO.\n // The adapter bridges them over the in-container localhost MCP transport.\n tools: remoteToolStubs(\n request.toolDescriptors,\n httpRemoteToolExecutor(request.toolExecUrl, request.toolExecToken),\n ),\n // Provide the in-container local-process sandbox handle the adapter needs.\n middleware: [withSandbox(sandbox)],\n })\n}\n\n/** Stream the agent's chunks to the response as NDJSON, one object per line. */\nasync function handleRun(\n req: IncomingMessage,\n res: ServerResponse,\n resolveAdapter: (input: ResolveAdapterInput) => AnyTextAdapter,\n): Promise<void> {\n const parsed: unknown = JSON.parse(await readBody(req))\n const request = parseContainerRunRequest(parsed)\n res.writeHead(200, {\n 'content-type': 'application/x-ndjson',\n 'cache-control': 'no-cache',\n })\n // The DO appends each line to its durable run-log; here we are the producer,\n // so we surface a mid-stream failure as a terminal RUN_ERROR line the DO will\n // append + finish on, never a silently truncated stream.\n try {\n for await (const chunk of runAgent(request, resolveAdapter)) {\n res.write(`${JSON.stringify(chunk)}\\n`)\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n res.write(`${JSON.stringify({ type: EventType.RUN_ERROR, message })}\\n`)\n } finally {\n res.end()\n }\n}\n\n/**\n * Start the in-container harness runner: a `node:http` server with `GET /health`\n * and `POST /run`. Call this as the container's program; the app supplies only\n * `resolveAdapter`.\n */\nexport function runInContainerHarness(\n options: RunInContainerHarnessOptions,\n): ContainerHarnessServer {\n const port =\n options.port ?? Number.parseInt(process.env.RUNNER_PORT ?? '8080', 10)\n\n const server = createServer((req, res) => {\n if (req.method === 'POST' && req.url === '/run') {\n handleRun(req, res, options.resolveAdapter).catch((error: unknown) => {\n // A failure BEFORE we start streaming (e.g. a malformed body) is a 400 —\n // surfaced, never swallowed.\n const message = error instanceof Error ? error.message : String(error)\n if (!res.headersSent) {\n res.writeHead(400, { 'content-type': 'text/plain' })\n }\n res.end(message)\n })\n return\n }\n if (req.method === 'GET' && req.url === '/health') {\n res.writeHead(200).end('ok')\n return\n }\n res.writeHead(404).end('not found')\n })\n\n server.listen(port, () => {\n console.log(`[container-runner] listening on :${port}`)\n })\n\n return { server, port }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmFA,SAAS,SAAS,KAAuC;CACvD,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,IAAI,OAAO;EACX,IAAI,YAAY,MAAM;EACtB,IAAI,GAAG,SAAS,UAAkB;GAChC,QAAQ;EACV,CAAC;EACD,IAAI,GAAG,aAAa,QAAQ,IAAI,CAAC;EACjC,IAAI,GAAG,SAAS,MAAM;CACxB,CAAC;AACH;;;;;;;;;;AAWA,SAAS,sBACP,WACqB;CACrB,IAAI,UAAU,YAAY,KAAA,GAAW,OAAO;CAC5C,MAAM,QAAQ,OAAO,KAAK,UAAU,OAAO;CAC3C,IAAI,MAAM,WAAW,GAAG,OAAO;CAC/B,MAAM,SAAiC,CAAC;CACxC,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,QAAQ,QAAQ,IAAI;EAC1B,IAAI,UAAU,KAAA,KAAa,UAAU,IACnC,MAAM,IAAI,MACR,kCAAkC,KAAK,kCACzC;EAEF,OAAO,QAAQ;CACjB;CACA,OAAO,gBAAgB;EAAE,GAAG;EAAW,SAAS,cAAc,MAAM;CAAE,CAAC;AACzE;;;;;;;AAQA,SAAS,SACP,SACA,gBAC4B;CAC5B,MAAM,UAAU,cAAc;EAE5B,IAAI;EACJ,UAAU,oBAAoB;EAG9B,WAAW,sBAAsB,QAAQ,SAAS;CACpD,CAAC;CAGD,OAAO,KAAK;EACV,UAAU,QAAQ;EAClB,SAAS,eAAe;GACtB,SAAS,QAAQ;GACjB,OAAO,QAAQ;EACjB,CAAC;EACD,UAAU,QAAQ;EAClB,QAAQ;EAGR,OAAO,gBACL,QAAQ,iBACR,uBAAuB,QAAQ,aAAa,QAAQ,aAAa,CACnE;EAEA,YAAY,CAAC,YAAY,OAAO,CAAC;CACnC,CAAC;AACH;;AAGA,eAAe,UACb,KACA,KACA,gBACe;CACf,MAAM,SAAkB,KAAK,MAAM,MAAM,SAAS,GAAG,CAAC;CACtD,MAAM,UAAU,yBAAyB,MAAM;CAC/C,IAAI,UAAU,KAAK;EACjB,gBAAgB;EAChB,iBAAiB;CACnB,CAAC;CAID,IAAI;EACF,WAAW,MAAM,SAAS,SAAS,SAAS,cAAc,GACxD,IAAI,MAAM,GAAG,KAAK,UAAU,KAAK,EAAE,GAAG;CAE1C,SAAS,OAAO;EACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EACrE,IAAI,MAAM,GAAG,KAAK,UAAU;GAAE,MAAM,UAAU;GAAW;EAAQ,CAAC,EAAE,GAAG;CACzE,UAAU;EACR,IAAI,IAAI;CACV;AACF;;;;;;AAOA,SAAgB,sBACd,SACwB;CACxB,MAAM,OACJ,QAAQ,QAAQ,OAAO,SAAS,QAAQ,IAAI,eAAe,QAAQ,EAAE;CAEvE,MAAM,SAAS,cAAc,KAAK,QAAQ;EACxC,IAAI,IAAI,WAAW,UAAU,IAAI,QAAQ,QAAQ;GAC/C,UAAU,KAAK,KAAK,QAAQ,cAAc,CAAC,CAAC,OAAO,UAAmB;IAGpE,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;IACrE,IAAI,CAAC,IAAI,aACP,IAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;IAErD,IAAI,IAAI,OAAO;GACjB,CAAC;GACD;EACF;EACA,IAAI,IAAI,WAAW,SAAS,IAAI,QAAQ,WAAW;GACjD,IAAI,UAAU,GAAG,CAAC,CAAC,IAAI,IAAI;GAC3B;EACF;EACA,IAAI,UAAU,GAAG,CAAC,CAAC,IAAI,WAAW;CACpC,CAAC;CAED,OAAO,OAAO,YAAY;EACxB,QAAQ,IAAI,oCAAoC,MAAM;CACxD,CAAC;CAED,OAAO;EAAE;EAAQ;CAAK;AACxB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-sandbox-cloudflare",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Cloudflare sandbox provider for TanStack AI — run harness adapters inside Cloudflare Containers (edge) through the uniform SandboxHandle.",
5
5
  "author": "",
6
6
  "license": "MIT",
@@ -45,17 +45,17 @@
45
45
  },
46
46
  "peerDependencies": {
47
47
  "zod": "^4.0.0",
48
- "@tanstack/ai": "^0.43.0",
49
- "@tanstack/ai-sandbox": "^0.3.0",
48
+ "@tanstack/ai": "^0.45.0",
49
+ "@tanstack/ai-sandbox": "^0.3.3",
50
50
  "@tanstack/ai-sandbox-local-process": "^0.2.1"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@cloudflare/workers-types": "^4.20260317.1",
54
54
  "@types/node": "^24.10.1",
55
- "@vitest/coverage-v8": "4.0.14",
55
+ "@vitest/coverage-v8": "4.1.10",
56
56
  "zod": "^4.2.0",
57
- "@tanstack/ai": "0.43.0",
58
- "@tanstack/ai-sandbox": "0.3.0",
57
+ "@tanstack/ai": "0.45.0",
58
+ "@tanstack/ai-sandbox": "0.3.3",
59
59
  "@tanstack/ai-sandbox-local-process": "0.2.1"
60
60
  },
61
61
  "scripts": {