@tanstack/ai-sandbox-cloudflare 0.1.0

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 (61) hide show
  1. package/dist/esm/agent.d.ts +30 -0
  2. package/dist/esm/agent.js +25 -0
  3. package/dist/esm/agent.js.map +1 -0
  4. package/dist/esm/chat-coordinator.d.ts +75 -0
  5. package/dist/esm/chat-coordinator.js +135 -0
  6. package/dist/esm/chat-coordinator.js.map +1 -0
  7. package/dist/esm/container-coordinator.d.ts +114 -0
  8. package/dist/esm/container-coordinator.js +256 -0
  9. package/dist/esm/container-coordinator.js.map +1 -0
  10. package/dist/esm/coordinator.d.ts +68 -0
  11. package/dist/esm/coordinator.js +188 -0
  12. package/dist/esm/coordinator.js.map +1 -0
  13. package/dist/esm/factory.d.ts +80 -0
  14. package/dist/esm/factory.js +69 -0
  15. package/dist/esm/factory.js.map +1 -0
  16. package/dist/esm/handle.d.ts +23 -0
  17. package/dist/esm/handle.js +208 -0
  18. package/dist/esm/handle.js.map +1 -0
  19. package/dist/esm/index.d.ts +4 -0
  20. package/dist/esm/index.js +10 -0
  21. package/dist/esm/index.js.map +1 -0
  22. package/dist/esm/preview-tool.d.ts +31 -0
  23. package/dist/esm/preview-tool.js +37 -0
  24. package/dist/esm/preview-tool.js.map +1 -0
  25. package/dist/esm/protocol.d.ts +42 -0
  26. package/dist/esm/protocol.js +64 -0
  27. package/dist/esm/protocol.js.map +1 -0
  28. package/dist/esm/provider.d.ts +31 -0
  29. package/dist/esm/provider.js +65 -0
  30. package/dist/esm/provider.js.map +1 -0
  31. package/dist/esm/public-host.d.ts +67 -0
  32. package/dist/esm/public-host.js +49 -0
  33. package/dist/esm/public-host.js.map +1 -0
  34. package/dist/esm/run-log-do.d.ts +25 -0
  35. package/dist/esm/run-log-do.js +122 -0
  36. package/dist/esm/run-log-do.js.map +1 -0
  37. package/dist/esm/runner.d.ts +32 -0
  38. package/dist/esm/runner.js +107 -0
  39. package/dist/esm/runner.js.map +1 -0
  40. package/dist/esm/web-crypto.d.ts +11 -0
  41. package/dist/esm/web-crypto.js +18 -0
  42. package/dist/esm/web-crypto.js.map +1 -0
  43. package/dist/esm/worker.d.ts +8 -0
  44. package/dist/esm/worker.js +83 -0
  45. package/dist/esm/worker.js.map +1 -0
  46. package/package.json +74 -0
  47. package/src/agent.ts +66 -0
  48. package/src/chat-coordinator.ts +253 -0
  49. package/src/container-coordinator.ts +437 -0
  50. package/src/coordinator.ts +338 -0
  51. package/src/factory.ts +225 -0
  52. package/src/handle.ts +292 -0
  53. package/src/index.ts +5 -0
  54. package/src/preview-tool.ts +110 -0
  55. package/src/protocol.ts +171 -0
  56. package/src/provider.ts +111 -0
  57. package/src/public-host.ts +121 -0
  58. package/src/run-log-do.ts +171 -0
  59. package/src/runner.ts +226 -0
  60. package/src/web-crypto.ts +31 -0
  61. package/src/worker.ts +173 -0
package/src/worker.ts ADDED
@@ -0,0 +1,173 @@
1
+ /**
2
+ * `createSandboxAgentWorker` — the STATELESS trigger + router Worker for the
3
+ * serverless/edge agent run model, factored so an app never writes it by hand.
4
+ *
5
+ * It never drives a run; it forwards to the owning {@link SandboxCoordinator}
6
+ * Durable Object and returns immediately:
7
+ *
8
+ * POST /runs → coordinator.startRun(...) → `202 { runId }` (the
9
+ * Worker invocation ENDS here; it does NOT wait for
10
+ * the agent run — that is the whole point).
11
+ * * (with ?threadId) → forward to coordinator.fetch(request), which the
12
+ * base handles for `/runs/:id`, `/runs/:id/stream`,
13
+ * and a subclass handles for `/_bridge`/`/tool-exec`.
14
+ *
15
+ * The coordinator that owns a thread's runs is resolved by the caller-supplied
16
+ * `resolveCoordinator(env, threadId)` — usually a DO addressed by `threadId`, so
17
+ * every event for a conversation lands in one coordinator and the sandbox is
18
+ * reused per thread. {@link createCloudflareSandboxAgent} supplies a resolver
19
+ * that uses the `RUN_COORDINATOR` binding.
20
+ *
21
+ * NOTE: Workers-runtime code — compiles against the real Cloudflare + TanStack
22
+ * AI types; not runtime-verified in this repo (no Workers runtime here).
23
+ */
24
+ import { proxyToSandbox } from '@cloudflare/sandbox'
25
+ import type { SandboxCoordinator, StartRunInput } from './coordinator'
26
+ import type { ModelMessage } from '@tanstack/ai'
27
+ import type { Sandbox } from '@cloudflare/sandbox'
28
+
29
+ /** Resolve the coordinator DO that owns a thread's runs. */
30
+ export type ResolveCoordinator<TEnv> = (
31
+ env: TEnv,
32
+ threadId: string,
33
+ ) => DurableObjectStub<SandboxCoordinator<TEnv>>
34
+
35
+ /** Body of `POST /runs`. */
36
+ interface CreateRunBody {
37
+ threadId: string
38
+ messages: Array<ModelMessage>
39
+ /** Forwarded verbatim to the app's resolvers — see {@link StartRunInput.metadata}. */
40
+ metadata?: Record<string, unknown>
41
+ }
42
+
43
+ /** Narrow the parsed JSON body without casting (project rule: no `as`). */
44
+ function parseCreateRunBody(value: unknown): CreateRunBody {
45
+ if (value === null || typeof value !== 'object') {
46
+ throw new Error('body must be a JSON object')
47
+ }
48
+ if (
49
+ !('threadId' in value) ||
50
+ typeof value.threadId !== 'string' ||
51
+ value.threadId === ''
52
+ ) {
53
+ throw new Error('body.threadId must be a non-empty string')
54
+ }
55
+ if (
56
+ !('messages' in value) ||
57
+ !Array.isArray(value.messages) ||
58
+ value.messages.length === 0
59
+ ) {
60
+ throw new Error('body.messages must be a non-empty array')
61
+ }
62
+ // The chat engine validates message shape; we only assert it is an array of
63
+ // objects here so the request fails fast with a clear 400 on garbage input.
64
+ for (const message of value.messages) {
65
+ if (message === null || typeof message !== 'object') {
66
+ throw new Error('each message must be an object')
67
+ }
68
+ }
69
+ // Optional free-form pass-through (app-validated). Must be an object if present.
70
+ let metadata: Record<string, unknown> | undefined
71
+ if ('metadata' in value && value.metadata !== undefined) {
72
+ if (!isRecord(value.metadata)) {
73
+ throw new Error('body.metadata must be an object')
74
+ }
75
+ metadata = value.metadata
76
+ }
77
+ return { threadId: value.threadId, messages: value.messages, metadata }
78
+ }
79
+
80
+ /** A JSON object — narrows `unknown` to `Record<string, unknown>` cast-free. */
81
+ function isRecord(value: unknown): value is Record<string, unknown> {
82
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
83
+ }
84
+
85
+ /** A Worker env that carries the Sandbox DO namespace `proxyToSandbox` needs. */
86
+ interface SandboxBindingEnv {
87
+ Sandbox: DurableObjectNamespace<Sandbox>
88
+ }
89
+
90
+ /** Narrow an env to one with a Sandbox binding (so previews can be proxied). */
91
+ function hasSandboxBinding<TEnv>(env: TEnv): env is TEnv & SandboxBindingEnv {
92
+ return (
93
+ env !== null &&
94
+ typeof env === 'object' &&
95
+ 'Sandbox' in env &&
96
+ env.Sandbox !== undefined
97
+ )
98
+ }
99
+
100
+ function jsonResponse(body: unknown, status = 200): Response {
101
+ return new Response(JSON.stringify(body), {
102
+ status,
103
+ headers: { 'content-type': 'application/json' },
104
+ })
105
+ }
106
+
107
+ /**
108
+ * Build the Worker fetch handler. `resolveCoordinator` maps `(env, threadId)` to
109
+ * the DO stub that owns that thread's runs.
110
+ */
111
+ export function createSandboxAgentWorker<TEnv>(
112
+ resolveCoordinator: ResolveCoordinator<TEnv>,
113
+ ): ExportedHandler<TEnv> {
114
+ return {
115
+ async fetch(request: Request, env: TEnv): Promise<Response> {
116
+ // Preview-port traffic for exposed sandbox ports is routed by hostname; let
117
+ // the sandbox runtime claim those requests before our app routes run. Only
118
+ // possible when the env actually carries the Sandbox binding.
119
+ if (hasSandboxBinding(env)) {
120
+ const proxied = await proxyToSandbox(request, env)
121
+ if (proxied) return proxied
122
+ }
123
+
124
+ const url = new URL(request.url)
125
+ const parts = url.pathname.split('/').filter(Boolean)
126
+
127
+ // POST /runs — trigger a run, return 202 immediately.
128
+ if (
129
+ request.method === 'POST' &&
130
+ parts.length === 1 &&
131
+ parts[0] === 'runs'
132
+ ) {
133
+ let body: CreateRunBody
134
+ try {
135
+ body = parseCreateRunBody(await request.json())
136
+ } catch (error) {
137
+ const message = error instanceof Error ? error.message : String(error)
138
+ return jsonResponse({ error: message }, 400)
139
+ }
140
+ const runId = crypto.randomUUID()
141
+ const input: StartRunInput = {
142
+ runId,
143
+ threadId: body.threadId,
144
+ messages: body.messages,
145
+ // The host this request arrived on. Coordinators derive the container's
146
+ // callback hosts from it when `PUBLIC_HOSTNAME`/`PREVIEW_HOSTNAME` are
147
+ // unset. On Cloudflare this is safe to trust — the edge only routes
148
+ // hostnames you own to your Worker. See `resolveBridgeOrigin` /
149
+ // `resolvePreviewHost`.
150
+ publicHost: url.host,
151
+ // Forwarded verbatim to the app's resolvers (e.g. the chosen harness).
152
+ metadata: body.metadata,
153
+ }
154
+ // RPC into the coordinator. `startRun` registers the run and returns
155
+ // immediately under `ctx.waitUntil`; we do NOT await the agent loop.
156
+ await resolveCoordinator(env, body.threadId).startRun(input)
157
+ return jsonResponse({ runId }, 202)
158
+ }
159
+
160
+ // Everything else for a run needs the owning coordinator, addressed by the
161
+ // `threadId` query the Worker carries so it never reads run state itself.
162
+ // The base coordinator routes `/runs/:id` + `/runs/:id/stream`, and a
163
+ // subclass routes `/_bridge/:runId` (DO-drives) or `/tool-exec/:runId`
164
+ // (co-located) — all reachable through one forward.
165
+ const threadId = url.searchParams.get('threadId')
166
+ if (threadId !== null) {
167
+ return resolveCoordinator(env, threadId).fetch(request)
168
+ }
169
+
170
+ return jsonResponse({ error: 'threadId query param required' }, 400)
171
+ },
172
+ }
173
+ }