@tanstack/ai-sandbox-cloudflare 0.2.4 → 0.3.1

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.
Files changed (47) hide show
  1. package/dist/esm/agent.d.ts +4 -0
  2. package/dist/esm/agent.js +9 -21
  3. package/dist/esm/chat-coordinator.js +144 -132
  4. package/dist/esm/chat-coordinator.js.map +1 -1
  5. package/dist/esm/container-coordinator.js +251 -247
  6. package/dist/esm/container-coordinator.js.map +1 -1
  7. package/dist/esm/coordinator.d.ts +3 -2
  8. package/dist/esm/coordinator.js +204 -184
  9. package/dist/esm/coordinator.js.map +1 -1
  10. package/dist/esm/durability.d.ts +32 -0
  11. package/dist/esm/durability.js +104 -0
  12. package/dist/esm/durability.js.map +1 -0
  13. package/dist/esm/factory.js +98 -59
  14. package/dist/esm/factory.js.map +1 -1
  15. package/dist/esm/handle.js +205 -203
  16. package/dist/esm/handle.js.map +1 -1
  17. package/dist/esm/index.js +2 -8
  18. package/dist/esm/preview-tool.d.ts +7 -1
  19. package/dist/esm/preview-tool.js +75 -32
  20. package/dist/esm/preview-tool.js.map +1 -1
  21. package/dist/esm/protocol.js +61 -50
  22. package/dist/esm/protocol.js.map +1 -1
  23. package/dist/esm/provider.js +43 -62
  24. package/dist/esm/provider.js.map +1 -1
  25. package/dist/esm/public-host.js +84 -39
  26. package/dist/esm/public-host.js.map +1 -1
  27. package/dist/esm/run-log-do.d.ts +19 -5
  28. package/dist/esm/run-log-do.js +196 -121
  29. package/dist/esm/run-log-do.js.map +1 -1
  30. package/dist/esm/run-log.d.ts +127 -0
  31. package/dist/esm/run-log.js +198 -0
  32. package/dist/esm/run-log.js.map +1 -0
  33. package/dist/esm/runner.js +146 -95
  34. package/dist/esm/runner.js.map +1 -1
  35. package/dist/esm/web-crypto.js +27 -16
  36. package/dist/esm/web-crypto.js.map +1 -1
  37. package/dist/esm/worker.js +84 -72
  38. package/dist/esm/worker.js.map +1 -1
  39. package/package.json +9 -9
  40. package/src/agent.ts +26 -0
  41. package/src/coordinator.ts +36 -14
  42. package/src/durability.ts +164 -0
  43. package/src/handle.ts +5 -0
  44. package/src/run-log-do.ts +85 -20
  45. package/src/run-log.ts +352 -0
  46. package/dist/esm/agent.js.map +0 -1
  47. package/dist/esm/index.js.map +0 -1
@@ -28,3 +28,7 @@ export { createSandboxAgentWorker } from './worker.js';
28
28
  export type { ResolveCoordinator } from './worker.js';
29
29
  export { DurableObjectRunEventLog } from './run-log-do.js';
30
30
  export { timingSafeBearerEqualWeb } from './web-crypto.js';
31
+ export { InMemoryRunEventLog, migrateStoredRunRecord } from './run-log.js';
32
+ export type { RunEventLog, RunEvent, RunEventLogReadOptions, RunLogRecord, RunRecordPatch, } from './run-log.js';
33
+ export { runLogStore, runLogStream } from './durability.js';
34
+ export type { RunLogStreamInit } from './durability.js';
package/dist/esm/agent.js CHANGED
@@ -1,25 +1,13 @@
1
- import { createCloudflareSandboxAgent } from "./factory.js";
1
+ import { runLogStore, runLogStream } from "./durability.js";
2
+ import { InMemoryRunEventLog, migrateStoredRunRecord } from "./run-log.js";
3
+ import { DurableObjectRunEventLog } from "./run-log-do.js";
4
+ import { resolveBridgeOrigin, resolvePreviewHost } from "./public-host.js";
2
5
  import { SandboxCoordinator } from "./coordinator.js";
3
- import { PREVIEW_GUIDANCE, exposePreviewTool } from "./preview-tool.js";
6
+ import { timingSafeBearerEqualWeb } from "./web-crypto.js";
4
7
  import { ChatSandboxCoordinator } from "./chat-coordinator.js";
5
8
  import { ContainerSandboxCoordinator } from "./container-coordinator.js";
6
- import { parseContainerRunRequest } from "./protocol.js";
7
9
  import { createSandboxAgentWorker } from "./worker.js";
8
- import { DurableObjectRunEventLog } from "./run-log-do.js";
9
- import { timingSafeBearerEqualWeb } from "./web-crypto.js";
10
- import { resolveBridgeOrigin, resolvePreviewHost } from "./public-host.js";
11
- export {
12
- ChatSandboxCoordinator,
13
- ContainerSandboxCoordinator,
14
- DurableObjectRunEventLog,
15
- PREVIEW_GUIDANCE,
16
- SandboxCoordinator,
17
- createCloudflareSandboxAgent,
18
- createSandboxAgentWorker,
19
- exposePreviewTool,
20
- parseContainerRunRequest,
21
- resolveBridgeOrigin,
22
- resolvePreviewHost,
23
- timingSafeBearerEqualWeb
24
- };
25
- //# sourceMappingURL=agent.js.map
10
+ import { createCloudflareSandboxAgent } from "./factory.js";
11
+ import { PREVIEW_GUIDANCE, exposePreviewTool } from "./preview-tool.js";
12
+ import { parseContainerRunRequest } from "./protocol.js";
13
+ export { ChatSandboxCoordinator, ContainerSandboxCoordinator, DurableObjectRunEventLog, InMemoryRunEventLog, PREVIEW_GUIDANCE, SandboxCoordinator, createCloudflareSandboxAgent, createSandboxAgentWorker, exposePreviewTool, migrateStoredRunRecord, parseContainerRunRequest, resolveBridgeOrigin, resolvePreviewHost, runLogStore, runLogStream, timingSafeBearerEqualWeb };
@@ -1,135 +1,147 @@
1
- import { chat, defineChatMiddleware } from "@tanstack/ai";
2
- import { withSandbox, ToolBridgeProvisionerCapability, createToolBridgeCore, handleBridgeJsonRpc } from "@tanstack/ai-sandbox";
1
+ import { resolveBridgeOrigin } from "./public-host.js";
3
2
  import { SandboxCoordinator } from "./coordinator.js";
4
3
  import { timingSafeBearerEqualWeb } from "./web-crypto.js";
5
- import { resolveBridgeOrigin } from "./public-host.js";
6
- class ChatSandboxCoordinator extends SandboxCoordinator {
7
- /**
8
- * Live per-run bridges, keyed by runId. In-memory by design: a bridge is only
9
- * reachable while its run is in flight, and `ctx.waitUntil(done)` keeps THIS
10
- * instance alive (un-hibernated) for the run's whole lifetime — so the agent's
11
- * MCP calls always hit the instance that provisioned the bridge. A request for
12
- * a run with no live bridge (finished, or never started here) is a hard 404,
13
- * not a silent re-provision.
14
- */
15
- bridges = /* @__PURE__ */ new Map();
16
- // ===========================================================================
17
- // The one per-model seam: run chat() in the DO
18
- // ===========================================================================
19
- /**
20
- * Run `chat()` IN the DO, streaming its `StreamChunk`s. `stream: true` (with no
21
- * outputSchema) makes chat() return an `AsyncIterable<StreamChunk>` directly —
22
- * no cast needed for the run driver. Both middlewares run `setup` before
23
- * streaming begins: our middleware provides the DO-backed bridge provisioner,
24
- * and `withSandbox` provides the sandbox handle the harness adapter needs.
25
- */
26
- buildRunStream(input) {
27
- const { adapter, sandbox, tools, systemPrompts } = this.config(input);
28
- const sessionId = input.metadata?.sessionId;
29
- const modelOptions = typeof sessionId === "string" && sessionId !== "" ? { sessionId } : void 0;
30
- return chat({
31
- threadId: input.threadId,
32
- adapter,
33
- messages: input.messages,
34
- stream: true,
35
- ...tools !== void 0 ? { tools } : {},
36
- ...systemPrompts !== void 0 ? { systemPrompts } : {},
37
- ...modelOptions !== void 0 ? { modelOptions } : {},
38
- middleware: [
39
- this.bridgeProvisionerMiddleware(input),
40
- withSandbox(sandbox)
41
- ]
42
- });
43
- }
44
- /** Drop the per-run bridge once the run is terminal (override from base). */
45
- onRunSettled(runId) {
46
- this.bridges.delete(runId);
47
- }
48
- // ===========================================================================
49
- // The DO-backed tool-bridge provisioner + endpoint
50
- // ===========================================================================
51
- /**
52
- * A tiny middleware that PROVIDES our DO-backed {@link ToolBridgeProvisioner}.
53
- * The harness adapter reads it via `getOptional` and falls back to the
54
- * `node:http` host transport when absent — here we override that so the bridge
55
- * is served from this DO's `fetch` handler instead of a TCP listener.
56
- */
57
- bridgeProvisionerMiddleware(input) {
58
- const provisioner = this.makeBridgeProvisioner(input);
59
- return defineChatMiddleware({
60
- name: "do-tool-bridge-provisioner",
61
- provides: [ToolBridgeProvisionerCapability],
62
- setup: (ctx) => {
63
- ctx.provide(ToolBridgeProvisionerCapability, provisioner);
64
- }
65
- });
66
- }
67
- /**
68
- * Stand up the per-run bridge: register the tool core + a fresh bearer token
69
- * on this DO, and hand back a URL the SANDBOX can reach — the Worker's public
70
- * hostname routed to `/_bridge/:runId`. The `threadId` query lets the Worker
71
- * route the agent's MCP calls back to THIS coordinator. No raw socket is opened.
72
- */
73
- makeBridgeProvisioner(input) {
74
- const env = this.env;
75
- const bridges = this.bridges;
76
- const { runId, threadId } = input;
77
- const origin = resolveBridgeOrigin(env, input);
78
- return {
79
- provision(tools, options) {
80
- const token = crypto.randomUUID() + crypto.randomUUID().replace(/-/g, "");
81
- const core = createToolBridgeCore(tools, {
82
- ...options.context !== void 0 ? { context: options.context } : {},
83
- ...options.signal !== void 0 ? { signal: options.signal } : {},
84
- ...options.permission !== void 0 ? { permission: options.permission } : {}
85
- });
86
- bridges.set(runId, { token, core });
87
- return Promise.resolve({
88
- name: "tanstack",
89
- url: `${origin}/_bridge/${runId}?threadId=${encodeURIComponent(threadId)}`,
90
- token,
91
- close: () => {
92
- bridges.delete(runId);
93
- return Promise.resolve();
94
- }
95
- });
96
- }
97
- };
98
- }
99
- /** Serve `/_bridge/:runId` (the in-sandbox agent's MCP calls) from the base fetch. */
100
- handleRoute(request, parts) {
101
- if (parts[0] === "_bridge" && typeof parts[1] === "string") {
102
- return this.serveBridge(parts[1], request);
103
- }
104
- return super.handleRoute(request, parts);
105
- }
106
- /** Serve one MCP JSON-RPC request for a run after a constant-time token check. */
107
- async serveBridge(runId, request) {
108
- const bridge = this.bridges.get(runId);
109
- if (!bridge)
110
- return new Response("no active bridge for run", { status: 404 });
111
- if (!timingSafeBearerEqualWeb(
112
- request.headers.get("authorization") ?? void 0,
113
- bridge.token
114
- )) {
115
- return new Response("unauthorized", { status: 401 });
116
- }
117
- let message;
118
- try {
119
- message = await request.json();
120
- } catch {
121
- return this.jsonResponse({
122
- jsonrpc: "2.0",
123
- id: null,
124
- error: { code: -32700, message: "Parse error" }
125
- });
126
- }
127
- const reply = await handleBridgeJsonRpc(bridge.core, message);
128
- if (reply === null) return new Response(null, { status: 202 });
129
- return this.jsonResponse(reply);
130
- }
131
- }
132
- export {
133
- ChatSandboxCoordinator
4
+ import { ToolBridgeProvisionerCapability, createToolBridgeCore, handleBridgeJsonRpc, withSandbox } from "@tanstack/ai-sandbox";
5
+ import { chat, defineChatMiddleware } from "@tanstack/ai";
6
+ //#region src/chat-coordinator.ts
7
+ /**
8
+ * `ChatSandboxCoordinator` — the concrete {@link SandboxCoordinator} for the
9
+ * DO-DRIVES model: the Durable Object runs `chat()` ITSELF and hosts the MCP
10
+ * tool-bridge from its own `fetch` handler.
11
+ *
12
+ * Worker (stateless trigger)
13
+ * → ChatSandboxCoordinator (this DO: runs chat(), owns the sandbox + log)
14
+ * → Cloudflare Sandbox (the container the agent executes in)
15
+ *
16
+ * It implements the one per-model seam, {@link buildRunStream}, by running
17
+ * `chat()` in the DO with two middlewares: our DO-backed tool-bridge provisioner
18
+ * (so the bridge is served from this DO instead of a `node:http` listener) and
19
+ * `withSandbox(...)` (the handle the harness adapter needs). The per-run config —
20
+ * which adapter, which sandbox, which chat()-tools — is the subclass's
21
+ * {@link config} method; everything else (run-log, streaming tail, watchdog) is
22
+ * inherited from the base.
23
+ *
24
+ * The MCP tool-bridge lives at `/_bridge/:runId`, gated by a per-run bearer
25
+ * token, served from {@link handleRoute}. The in-sandbox agent reaches it via
26
+ * the Worker's public hostname.
27
+ *
28
+ * NOTE: Workers-runtime code — compiles against the real Cloudflare + TanStack
29
+ * AI types; not runtime-verified in this repo (no Workers runtime here).
30
+ */
31
+ var ChatSandboxCoordinator = class extends SandboxCoordinator {
32
+ /**
33
+ * Live per-run bridges, keyed by runId. In-memory by design: a bridge is only
34
+ * reachable while its run is in flight, and `ctx.waitUntil(done)` keeps THIS
35
+ * instance alive (un-hibernated) for the run's whole lifetime — so the agent's
36
+ * MCP calls always hit the instance that provisioned the bridge. A request for
37
+ * a run with no live bridge (finished, or never started here) is a hard 404,
38
+ * not a silent re-provision.
39
+ */
40
+ bridges = /* @__PURE__ */ new Map();
41
+ /**
42
+ * Run `chat()` IN the DO, streaming its `StreamChunk`s. `stream: true` (with no
43
+ * outputSchema) makes chat() return an `AsyncIterable<StreamChunk>` directly —
44
+ * no cast needed for the run driver. Both middlewares run `setup` before
45
+ * streaming begins: our middleware provides the DO-backed bridge provisioner,
46
+ * and `withSandbox` provides the sandbox handle the harness adapter needs.
47
+ */
48
+ buildRunStream(input) {
49
+ const { adapter, sandbox, tools, systemPrompts } = this.config(input);
50
+ const sessionId = input.metadata?.sessionId;
51
+ const modelOptions = typeof sessionId === "string" && sessionId !== "" ? { sessionId } : void 0;
52
+ return chat({
53
+ threadId: input.threadId,
54
+ adapter,
55
+ messages: input.messages,
56
+ stream: true,
57
+ ...tools !== void 0 ? { tools } : {},
58
+ ...systemPrompts !== void 0 ? { systemPrompts } : {},
59
+ ...modelOptions !== void 0 ? { modelOptions } : {},
60
+ middleware: [this.bridgeProvisionerMiddleware(input), withSandbox(sandbox)]
61
+ });
62
+ }
63
+ /** Drop the per-run bridge once the run is terminal (override from base). */
64
+ onRunSettled(runId) {
65
+ this.bridges.delete(runId);
66
+ }
67
+ /**
68
+ * A tiny middleware that PROVIDES our DO-backed {@link ToolBridgeProvisioner}.
69
+ * The harness adapter reads it via `getOptional` and falls back to the
70
+ * `node:http` host transport when absent — here we override that so the bridge
71
+ * is served from this DO's `fetch` handler instead of a TCP listener.
72
+ */
73
+ bridgeProvisionerMiddleware(input) {
74
+ const provisioner = this.makeBridgeProvisioner(input);
75
+ return defineChatMiddleware({
76
+ name: "do-tool-bridge-provisioner",
77
+ provides: [ToolBridgeProvisionerCapability],
78
+ setup: (ctx) => {
79
+ ctx.provide(ToolBridgeProvisionerCapability, provisioner);
80
+ }
81
+ });
82
+ }
83
+ /**
84
+ * Stand up the per-run bridge: register the tool core + a fresh bearer token
85
+ * on this DO, and hand back a URL the SANDBOX can reach — the Worker's public
86
+ * hostname routed to `/_bridge/:runId`. The `threadId` query lets the Worker
87
+ * route the agent's MCP calls back to THIS coordinator. No raw socket is opened.
88
+ */
89
+ makeBridgeProvisioner(input) {
90
+ const env = this.env;
91
+ const bridges = this.bridges;
92
+ const { runId, threadId } = input;
93
+ const origin = resolveBridgeOrigin(env, input);
94
+ return { provision(tools, options) {
95
+ const token = crypto.randomUUID() + crypto.randomUUID().replace(/-/g, "");
96
+ const core = createToolBridgeCore(tools, {
97
+ ...options.context !== void 0 ? { context: options.context } : {},
98
+ ...options.signal !== void 0 ? { signal: options.signal } : {},
99
+ ...options.permission !== void 0 ? { permission: options.permission } : {}
100
+ });
101
+ bridges.set(runId, {
102
+ token,
103
+ core
104
+ });
105
+ return Promise.resolve({
106
+ name: "tanstack",
107
+ url: `${origin}/_bridge/${runId}?threadId=${encodeURIComponent(threadId)}`,
108
+ token,
109
+ close: () => {
110
+ bridges.delete(runId);
111
+ return Promise.resolve();
112
+ }
113
+ });
114
+ } };
115
+ }
116
+ /** Serve `/_bridge/:runId` (the in-sandbox agent's MCP calls) from the base fetch. */
117
+ handleRoute(request, parts) {
118
+ if (parts[0] === "_bridge" && typeof parts[1] === "string") return this.serveBridge(parts[1], request);
119
+ return super.handleRoute(request, parts);
120
+ }
121
+ /** Serve one MCP JSON-RPC request for a run after a constant-time token check. */
122
+ async serveBridge(runId, request) {
123
+ const bridge = this.bridges.get(runId);
124
+ if (!bridge) return new Response("no active bridge for run", { status: 404 });
125
+ if (!timingSafeBearerEqualWeb(request.headers.get("authorization") ?? void 0, bridge.token)) return new Response("unauthorized", { status: 401 });
126
+ let message;
127
+ try {
128
+ message = await request.json();
129
+ } catch {
130
+ return this.jsonResponse({
131
+ jsonrpc: "2.0",
132
+ id: null,
133
+ error: {
134
+ code: -32700,
135
+ message: "Parse error"
136
+ }
137
+ });
138
+ }
139
+ const reply = await handleBridgeJsonRpc(bridge.core, message);
140
+ if (reply === null) return new Response(null, { status: 202 });
141
+ return this.jsonResponse(reply);
142
+ }
134
143
  };
135
- //# sourceMappingURL=chat-coordinator.js.map
144
+ //#endregion
145
+ export { ChatSandboxCoordinator };
146
+
147
+ //# sourceMappingURL=chat-coordinator.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"chat-coordinator.js","sources":["../../src/chat-coordinator.ts"],"sourcesContent":["/**\n * `ChatSandboxCoordinator` — the concrete {@link SandboxCoordinator} for the\n * DO-DRIVES model: the Durable Object runs `chat()` ITSELF and hosts the MCP\n * tool-bridge from its own `fetch` handler.\n *\n * Worker (stateless trigger)\n * → ChatSandboxCoordinator (this DO: runs chat(), owns the sandbox + log)\n * → Cloudflare Sandbox (the container the agent executes in)\n *\n * It implements the one per-model seam, {@link buildRunStream}, by running\n * `chat()` in the DO with two middlewares: our DO-backed tool-bridge provisioner\n * (so the bridge is served from this DO instead of a `node:http` listener) and\n * `withSandbox(...)` (the handle the harness adapter needs). The per-run config —\n * which adapter, which sandbox, which chat()-tools — is the subclass's\n * {@link config} method; everything else (run-log, streaming tail, watchdog) is\n * inherited from the base.\n *\n * The MCP tool-bridge lives at `/_bridge/:runId`, gated by a per-run bearer\n * token, served from {@link handleRoute}. The in-sandbox agent reaches it via\n * the Worker's public hostname.\n *\n * NOTE: Workers-runtime code — compiles against the real Cloudflare + TanStack\n * AI types; not runtime-verified in this repo (no Workers runtime here).\n */\nimport { chat, defineChatMiddleware } from '@tanstack/ai'\nimport {\n ToolBridgeProvisionerCapability,\n createToolBridgeCore,\n handleBridgeJsonRpc,\n withSandbox,\n} from '@tanstack/ai-sandbox'\nimport { SandboxCoordinator, resolveBridgeOrigin } from './coordinator'\nimport { timingSafeBearerEqualWeb } from './web-crypto'\nimport type { StartRunInput } from './coordinator'\nimport type {\n AnyTextAdapter,\n AnyTool,\n StreamChunk,\n SystemPrompt,\n} from '@tanstack/ai'\nimport type {\n ProvisionedBridge,\n SandboxDefinition,\n ToolBridgeCore,\n ToolBridgeProvisioner,\n} from '@tanstack/ai-sandbox'\n\n/**\n * The Env bindings a {@link ChatSandboxCoordinator} requires. The bridge origin the\n * SANDBOX calls back on needs a hostname; `PUBLIC_HOSTNAME` is OPTIONAL — when\n * unset, the coordinator derives it from the trigger request (locally →\n * `host.docker.internal`; safe on Cloudflare). See {@link resolveBridgeOrigin}.\n */\nexport interface ChatCoordinatorEnv {\n /**\n * Hostname the CONTAINER uses to reach the Worker's tool-bridge (`/_bridge`).\n * Optional: unset → derived from each trigger request (deployed: the request\n * host; local dev: `host.docker.internal`). Set it only to override — e.g. a\n * stable named-tunnel host. See {@link resolveBridgeOrigin}. (Browser-facing\n * preview URLs use a separate `PREVIEW_HOSTNAME`; see {@link resolvePreviewHost}.)\n */\n PUBLIC_HOSTNAME?: string\n}\n\n/** What {@link ChatSandboxCoordinator.config} returns for one run. */\nexport interface ChatRunConfig {\n /** The harness/text adapter `chat()` runs (e.g. `claudeCodeText('sonnet')`). */\n adapter: AnyTextAdapter\n /** The sandbox the agent executes in, projected by `withSandbox`. */\n sandbox: SandboxDefinition\n /** chat()-provided server tools bridged into the harness over MCP. */\n tools?: Array<AnyTool>\n /** Base system prompts prepended to the run's `chat()` (e.g. `[PREVIEW_GUIDANCE]`). */\n systemPrompts?: Array<SystemPrompt>\n}\n\n/** Per-run bridge state so `/_bridge/:runId` can authenticate + serve. */\ninterface BridgeState {\n token: string\n core: ToolBridgeCore\n}\n\nexport abstract class ChatSandboxCoordinator<\n TEnv extends ChatCoordinatorEnv = ChatCoordinatorEnv,\n> extends SandboxCoordinator<TEnv> {\n /**\n * Live per-run bridges, keyed by runId. In-memory by design: a bridge is only\n * reachable while its run is in flight, and `ctx.waitUntil(done)` keeps THIS\n * instance alive (un-hibernated) for the run's whole lifetime — so the agent's\n * MCP calls always hit the instance that provisioned the bridge. A request for\n * a run with no live bridge (finished, or never started here) is a hard 404,\n * not a silent re-provision.\n */\n private readonly bridges = new Map<string, BridgeState>()\n\n // ===========================================================================\n // Subclass seam: the per-run configuration\n // ===========================================================================\n\n /**\n * Resolve the adapter, sandbox, and chat()-tools for one run. Implemented by\n * the app subclass (or supplied by {@link createCloudflareSandboxAgent}); this\n * is the only model-specific input the DO-drives coordinator needs.\n */\n protected abstract config(input: StartRunInput): ChatRunConfig\n\n // ===========================================================================\n // The one per-model seam: run chat() in the DO\n // ===========================================================================\n\n /**\n * Run `chat()` IN the DO, streaming its `StreamChunk`s. `stream: true` (with no\n * outputSchema) makes chat() return an `AsyncIterable<StreamChunk>` directly —\n * no cast needed for the run driver. Both middlewares run `setup` before\n * streaming begins: our middleware provides the DO-backed bridge provisioner,\n * and `withSandbox` provides the sandbox handle the harness adapter needs.\n */\n protected override buildRunStream(\n input: StartRunInput,\n ): AsyncIterable<StreamChunk> {\n const { adapter, sandbox, tools, systemPrompts } = this.config(input)\n const sessionId = input.metadata?.sessionId\n const modelOptions =\n typeof sessionId === 'string' && sessionId !== ''\n ? { sessionId }\n : undefined\n return chat({\n threadId: input.threadId,\n adapter,\n messages: input.messages,\n stream: true,\n ...(tools !== undefined ? { tools } : {}),\n ...(systemPrompts !== undefined ? { systemPrompts } : {}),\n ...(modelOptions !== undefined ? { modelOptions } : {}),\n middleware: [\n this.bridgeProvisionerMiddleware(input),\n withSandbox(sandbox),\n ],\n })\n }\n\n /** Drop the per-run bridge once the run is terminal (override from base). */\n protected override onRunSettled(runId: string): void {\n this.bridges.delete(runId)\n }\n\n // ===========================================================================\n // The DO-backed tool-bridge provisioner + endpoint\n // ===========================================================================\n\n /**\n * A tiny middleware that PROVIDES our DO-backed {@link ToolBridgeProvisioner}.\n * The harness adapter reads it via `getOptional` and falls back to the\n * `node:http` host transport when absent — here we override that so the bridge\n * is served from this DO's `fetch` handler instead of a TCP listener.\n */\n private bridgeProvisionerMiddleware(input: StartRunInput) {\n const provisioner = this.makeBridgeProvisioner(input)\n return defineChatMiddleware({\n name: 'do-tool-bridge-provisioner',\n provides: [ToolBridgeProvisionerCapability],\n setup: (ctx) => {\n ctx.provide(ToolBridgeProvisionerCapability, provisioner)\n },\n })\n }\n\n /**\n * Stand up the per-run bridge: register the tool core + a fresh bearer token\n * on this DO, and hand back a URL the SANDBOX can reach — the Worker's public\n * hostname routed to `/_bridge/:runId`. The `threadId` query lets the Worker\n * route the agent's MCP calls back to THIS coordinator. No raw socket is opened.\n */\n private makeBridgeProvisioner(input: StartRunInput): ToolBridgeProvisioner {\n const env = this.env\n const bridges = this.bridges\n const { runId, threadId } = input\n // Container→Worker origin: `PUBLIC_HOSTNAME` if set, else derived from the\n // trigger request (locally → host.docker.internal). The bearer token rides\n // this URL. See `resolveBridgeOrigin`.\n const origin = resolveBridgeOrigin(env, input)\n return {\n provision(tools, options): Promise<ProvisionedBridge> {\n const token =\n crypto.randomUUID() + crypto.randomUUID().replace(/-/g, '')\n const core = createToolBridgeCore(tools, {\n ...(options.context !== undefined\n ? { context: options.context }\n : {}),\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n ...(options.permission !== undefined\n ? { permission: options.permission }\n : {}),\n })\n bridges.set(runId, { token, core })\n return Promise.resolve({\n name: 'tanstack',\n url: `${origin}/_bridge/${runId}?threadId=${encodeURIComponent(threadId)}`,\n token,\n close: () => {\n bridges.delete(runId)\n return Promise.resolve()\n },\n })\n },\n }\n }\n\n /** Serve `/_bridge/:runId` (the in-sandbox agent's MCP calls) from the base fetch. */\n protected override handleRoute(\n request: Request,\n parts: Array<string>,\n ): Promise<Response> | Response {\n if (parts[0] === '_bridge' && typeof parts[1] === 'string') {\n return this.serveBridge(parts[1], request)\n }\n return super.handleRoute(request, parts)\n }\n\n /** Serve one MCP JSON-RPC request for a run after a constant-time token check. */\n private async serveBridge(\n runId: string,\n request: Request,\n ): Promise<Response> {\n const bridge = this.bridges.get(runId)\n if (!bridge)\n return new Response('no active bridge for run', { status: 404 })\n if (\n !timingSafeBearerEqualWeb(\n request.headers.get('authorization') ?? undefined,\n bridge.token,\n )\n ) {\n return new Response('unauthorized', { status: 401 })\n }\n let message: unknown\n try {\n message = await request.json()\n } catch {\n // A malformed body must still produce a valid JSON-RPC error so the agent's\n // MCP client can react, rather than an opaque DO 500 that can wedge the run.\n return this.jsonResponse({\n jsonrpc: '2.0',\n id: null,\n error: { code: -32700, message: 'Parse error' },\n })\n }\n const reply = await handleBridgeJsonRpc(bridge.core, message)\n // A notification (no id) yields null → MCP expects an empty 202 ack.\n if (reply === null) return new Response(null, { status: 202 })\n return this.jsonResponse(reply)\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"chat-coordinator.js","names":[],"sources":["../../src/chat-coordinator.ts"],"sourcesContent":["/**\n * `ChatSandboxCoordinator` — the concrete {@link SandboxCoordinator} for the\n * DO-DRIVES model: the Durable Object runs `chat()` ITSELF and hosts the MCP\n * tool-bridge from its own `fetch` handler.\n *\n * Worker (stateless trigger)\n * → ChatSandboxCoordinator (this DO: runs chat(), owns the sandbox + log)\n * → Cloudflare Sandbox (the container the agent executes in)\n *\n * It implements the one per-model seam, {@link buildRunStream}, by running\n * `chat()` in the DO with two middlewares: our DO-backed tool-bridge provisioner\n * (so the bridge is served from this DO instead of a `node:http` listener) and\n * `withSandbox(...)` (the handle the harness adapter needs). The per-run config —\n * which adapter, which sandbox, which chat()-tools — is the subclass's\n * {@link config} method; everything else (run-log, streaming tail, watchdog) is\n * inherited from the base.\n *\n * The MCP tool-bridge lives at `/_bridge/:runId`, gated by a per-run bearer\n * token, served from {@link handleRoute}. The in-sandbox agent reaches it via\n * the Worker's public hostname.\n *\n * NOTE: Workers-runtime code — compiles against the real Cloudflare + TanStack\n * AI types; not runtime-verified in this repo (no Workers runtime here).\n */\nimport { chat, defineChatMiddleware } from '@tanstack/ai'\nimport {\n ToolBridgeProvisionerCapability,\n createToolBridgeCore,\n handleBridgeJsonRpc,\n withSandbox,\n} from '@tanstack/ai-sandbox'\nimport { SandboxCoordinator, resolveBridgeOrigin } from './coordinator'\nimport { timingSafeBearerEqualWeb } from './web-crypto'\nimport type { StartRunInput } from './coordinator'\nimport type {\n AnyTextAdapter,\n AnyTool,\n StreamChunk,\n SystemPrompt,\n} from '@tanstack/ai'\nimport type {\n ProvisionedBridge,\n SandboxDefinition,\n ToolBridgeCore,\n ToolBridgeProvisioner,\n} from '@tanstack/ai-sandbox'\n\n/**\n * The Env bindings a {@link ChatSandboxCoordinator} requires. The bridge origin the\n * SANDBOX calls back on needs a hostname; `PUBLIC_HOSTNAME` is OPTIONAL — when\n * unset, the coordinator derives it from the trigger request (locally →\n * `host.docker.internal`; safe on Cloudflare). See {@link resolveBridgeOrigin}.\n */\nexport interface ChatCoordinatorEnv {\n /**\n * Hostname the CONTAINER uses to reach the Worker's tool-bridge (`/_bridge`).\n * Optional: unset → derived from each trigger request (deployed: the request\n * host; local dev: `host.docker.internal`). Set it only to override — e.g. a\n * stable named-tunnel host. See {@link resolveBridgeOrigin}. (Browser-facing\n * preview URLs use a separate `PREVIEW_HOSTNAME`; see {@link resolvePreviewHost}.)\n */\n PUBLIC_HOSTNAME?: string\n}\n\n/** What {@link ChatSandboxCoordinator.config} returns for one run. */\nexport interface ChatRunConfig {\n /** The harness/text adapter `chat()` runs (e.g. `claudeCodeText('sonnet')`). */\n adapter: AnyTextAdapter\n /** The sandbox the agent executes in, projected by `withSandbox`. */\n sandbox: SandboxDefinition\n /** chat()-provided server tools bridged into the harness over MCP. */\n tools?: Array<AnyTool>\n /** Base system prompts prepended to the run's `chat()` (e.g. `[PREVIEW_GUIDANCE]`). */\n systemPrompts?: Array<SystemPrompt>\n}\n\n/** Per-run bridge state so `/_bridge/:runId` can authenticate + serve. */\ninterface BridgeState {\n token: string\n core: ToolBridgeCore\n}\n\nexport abstract class ChatSandboxCoordinator<\n TEnv extends ChatCoordinatorEnv = ChatCoordinatorEnv,\n> extends SandboxCoordinator<TEnv> {\n /**\n * Live per-run bridges, keyed by runId. In-memory by design: a bridge is only\n * reachable while its run is in flight, and `ctx.waitUntil(done)` keeps THIS\n * instance alive (un-hibernated) for the run's whole lifetime — so the agent's\n * MCP calls always hit the instance that provisioned the bridge. A request for\n * a run with no live bridge (finished, or never started here) is a hard 404,\n * not a silent re-provision.\n */\n private readonly bridges = new Map<string, BridgeState>()\n\n // ===========================================================================\n // Subclass seam: the per-run configuration\n // ===========================================================================\n\n /**\n * Resolve the adapter, sandbox, and chat()-tools for one run. Implemented by\n * the app subclass (or supplied by {@link createCloudflareSandboxAgent}); this\n * is the only model-specific input the DO-drives coordinator needs.\n */\n protected abstract config(input: StartRunInput): ChatRunConfig\n\n // ===========================================================================\n // The one per-model seam: run chat() in the DO\n // ===========================================================================\n\n /**\n * Run `chat()` IN the DO, streaming its `StreamChunk`s. `stream: true` (with no\n * outputSchema) makes chat() return an `AsyncIterable<StreamChunk>` directly —\n * no cast needed for the run driver. Both middlewares run `setup` before\n * streaming begins: our middleware provides the DO-backed bridge provisioner,\n * and `withSandbox` provides the sandbox handle the harness adapter needs.\n */\n protected override buildRunStream(\n input: StartRunInput,\n ): AsyncIterable<StreamChunk> {\n const { adapter, sandbox, tools, systemPrompts } = this.config(input)\n const sessionId = input.metadata?.sessionId\n const modelOptions =\n typeof sessionId === 'string' && sessionId !== ''\n ? { sessionId }\n : undefined\n return chat({\n threadId: input.threadId,\n adapter,\n messages: input.messages,\n stream: true,\n ...(tools !== undefined ? { tools } : {}),\n ...(systemPrompts !== undefined ? { systemPrompts } : {}),\n ...(modelOptions !== undefined ? { modelOptions } : {}),\n middleware: [\n this.bridgeProvisionerMiddleware(input),\n withSandbox(sandbox),\n ],\n })\n }\n\n /** Drop the per-run bridge once the run is terminal (override from base). */\n protected override onRunSettled(runId: string): void {\n this.bridges.delete(runId)\n }\n\n // ===========================================================================\n // The DO-backed tool-bridge provisioner + endpoint\n // ===========================================================================\n\n /**\n * A tiny middleware that PROVIDES our DO-backed {@link ToolBridgeProvisioner}.\n * The harness adapter reads it via `getOptional` and falls back to the\n * `node:http` host transport when absent — here we override that so the bridge\n * is served from this DO's `fetch` handler instead of a TCP listener.\n */\n private bridgeProvisionerMiddleware(input: StartRunInput) {\n const provisioner = this.makeBridgeProvisioner(input)\n return defineChatMiddleware({\n name: 'do-tool-bridge-provisioner',\n provides: [ToolBridgeProvisionerCapability],\n setup: (ctx) => {\n ctx.provide(ToolBridgeProvisionerCapability, provisioner)\n },\n })\n }\n\n /**\n * Stand up the per-run bridge: register the tool core + a fresh bearer token\n * on this DO, and hand back a URL the SANDBOX can reach — the Worker's public\n * hostname routed to `/_bridge/:runId`. The `threadId` query lets the Worker\n * route the agent's MCP calls back to THIS coordinator. No raw socket is opened.\n */\n private makeBridgeProvisioner(input: StartRunInput): ToolBridgeProvisioner {\n const env = this.env\n const bridges = this.bridges\n const { runId, threadId } = input\n // Container→Worker origin: `PUBLIC_HOSTNAME` if set, else derived from the\n // trigger request (locally → host.docker.internal). The bearer token rides\n // this URL. See `resolveBridgeOrigin`.\n const origin = resolveBridgeOrigin(env, input)\n return {\n provision(tools, options): Promise<ProvisionedBridge> {\n const token =\n crypto.randomUUID() + crypto.randomUUID().replace(/-/g, '')\n const core = createToolBridgeCore(tools, {\n ...(options.context !== undefined\n ? { context: options.context }\n : {}),\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n ...(options.permission !== undefined\n ? { permission: options.permission }\n : {}),\n })\n bridges.set(runId, { token, core })\n return Promise.resolve({\n name: 'tanstack',\n url: `${origin}/_bridge/${runId}?threadId=${encodeURIComponent(threadId)}`,\n token,\n close: () => {\n bridges.delete(runId)\n return Promise.resolve()\n },\n })\n },\n }\n }\n\n /** Serve `/_bridge/:runId` (the in-sandbox agent's MCP calls) from the base fetch. */\n protected override handleRoute(\n request: Request,\n parts: Array<string>,\n ): Promise<Response> | Response {\n if (parts[0] === '_bridge' && typeof parts[1] === 'string') {\n return this.serveBridge(parts[1], request)\n }\n return super.handleRoute(request, parts)\n }\n\n /** Serve one MCP JSON-RPC request for a run after a constant-time token check. */\n private async serveBridge(\n runId: string,\n request: Request,\n ): Promise<Response> {\n const bridge = this.bridges.get(runId)\n if (!bridge)\n return new Response('no active bridge for run', { status: 404 })\n if (\n !timingSafeBearerEqualWeb(\n request.headers.get('authorization') ?? undefined,\n bridge.token,\n )\n ) {\n return new Response('unauthorized', { status: 401 })\n }\n let message: unknown\n try {\n message = await request.json()\n } catch {\n // A malformed body must still produce a valid JSON-RPC error so the agent's\n // MCP client can react, rather than an opaque DO 500 that can wedge the run.\n return this.jsonResponse({\n jsonrpc: '2.0',\n id: null,\n error: { code: -32700, message: 'Parse error' },\n })\n }\n const reply = await handleBridgeJsonRpc(bridge.core, message)\n // A notification (no id) yields null → MCP expects an empty 202 ack.\n if (reply === null) return new Response(null, { status: 202 })\n return this.jsonResponse(reply)\n }\n}\n"],"mappings":"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"}