@tanstack/ai-sandbox 0.2.4 → 0.3.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 (158) hide show
  1. package/dist/esm/agents-file.js +53 -34
  2. package/dist/esm/agents-file.js.map +1 -1
  3. package/dist/esm/align.d.ts +121 -0
  4. package/dist/esm/align.js +197 -0
  5. package/dist/esm/align.js.map +1 -0
  6. package/dist/esm/approvals.js +63 -29
  7. package/dist/esm/approvals.js.map +1 -1
  8. package/dist/esm/attach-preflight.d.ts +85 -0
  9. package/dist/esm/attach-preflight.js +189 -0
  10. package/dist/esm/attach-preflight.js.map +1 -0
  11. package/dist/esm/bootstrap.js +103 -117
  12. package/dist/esm/bootstrap.js.map +1 -1
  13. package/dist/esm/bridge-events.js +96 -71
  14. package/dist/esm/bridge-events.js.map +1 -1
  15. package/dist/esm/capabilities.d.ts +0 -5
  16. package/dist/esm/capabilities.js +32 -28
  17. package/dist/esm/capabilities.js.map +1 -1
  18. package/dist/esm/chunk-identity.d.ts +52 -0
  19. package/dist/esm/chunk-identity.js +102 -0
  20. package/dist/esm/chunk-identity.js.map +1 -0
  21. package/dist/esm/claim.d.ts +187 -0
  22. package/dist/esm/claim.js +349 -0
  23. package/dist/esm/claim.js.map +1 -0
  24. package/dist/esm/contracts.d.ts +13 -0
  25. package/dist/esm/driver.d.ts +83 -0
  26. package/dist/esm/driver.js +138 -0
  27. package/dist/esm/driver.js.map +1 -0
  28. package/dist/esm/durability.d.ts +263 -0
  29. package/dist/esm/durability.js +230 -0
  30. package/dist/esm/durability.js.map +1 -0
  31. package/dist/esm/errors.js +28 -24
  32. package/dist/esm/errors.js.map +1 -1
  33. package/dist/esm/file-diff.js +151 -135
  34. package/dist/esm/file-diff.js.map +1 -1
  35. package/dist/esm/git-exec.js +51 -62
  36. package/dist/esm/git-exec.js.map +1 -1
  37. package/dist/esm/harness-cwd.js +24 -19
  38. package/dist/esm/harness-cwd.js.map +1 -1
  39. package/dist/esm/index.d.ts +30 -8
  40. package/dist/esm/index.js +23 -91
  41. package/dist/esm/instance-store.d.ts +88 -0
  42. package/dist/esm/instance-store.js +67 -0
  43. package/dist/esm/instance-store.js.map +1 -0
  44. package/dist/esm/journal-bytes.d.ts +67 -0
  45. package/dist/esm/journal-bytes.js +110 -0
  46. package/dist/esm/journal-bytes.js.map +1 -0
  47. package/dist/esm/journal-reader.d.ts +66 -0
  48. package/dist/esm/journal-reader.js +228 -0
  49. package/dist/esm/journal-reader.js.map +1 -0
  50. package/dist/esm/journal-sweep.d.ts +113 -0
  51. package/dist/esm/journal-sweep.js +309 -0
  52. package/dist/esm/journal-sweep.js.map +1 -0
  53. package/dist/esm/journal.d.ts +542 -0
  54. package/dist/esm/journal.js +679 -0
  55. package/dist/esm/journal.js.map +1 -0
  56. package/dist/esm/key.js +36 -33
  57. package/dist/esm/key.js.map +1 -1
  58. package/dist/esm/middleware.d.ts +50 -2
  59. package/dist/esm/middleware.js +335 -208
  60. package/dist/esm/middleware.js.map +1 -1
  61. package/dist/esm/ngrok.js +75 -49
  62. package/dist/esm/ngrok.js.map +1 -1
  63. package/dist/esm/policy.js +43 -34
  64. package/dist/esm/policy.js.map +1 -1
  65. package/dist/esm/projection.js +16 -8
  66. package/dist/esm/projection.js.map +1 -1
  67. package/dist/esm/reap.d.ts +238 -0
  68. package/dist/esm/reap.js +355 -0
  69. package/dist/esm/reap.js.map +1 -0
  70. package/dist/esm/reclaim.d.ts +84 -0
  71. package/dist/esm/reclaim.js +106 -0
  72. package/dist/esm/reclaim.js.map +1 -0
  73. package/dist/esm/remote-tools.js +73 -62
  74. package/dist/esm/remote-tools.js.map +1 -1
  75. package/dist/esm/run.d.ts +93 -25
  76. package/dist/esm/run.js +274 -79
  77. package/dist/esm/run.js.map +1 -1
  78. package/dist/esm/runner.d.ts +119 -2
  79. package/dist/esm/runner.js +270 -51
  80. package/dist/esm/runner.js.map +1 -1
  81. package/dist/esm/sandbox.d.ts +3 -2
  82. package/dist/esm/sandbox.js +139 -123
  83. package/dist/esm/sandbox.js.map +1 -1
  84. package/dist/esm/secrets.js +39 -47
  85. package/dist/esm/secrets.js.map +1 -1
  86. package/dist/esm/setup-plan.js +22 -14
  87. package/dist/esm/setup-plan.js.map +1 -1
  88. package/dist/esm/shell.d.ts +8 -0
  89. package/dist/esm/shell.js +197 -158
  90. package/dist/esm/shell.js.map +1 -1
  91. package/dist/esm/testkit/conformance.d.ts +16 -0
  92. package/dist/esm/testkit/conformance.js +97 -0
  93. package/dist/esm/testkit/conformance.js.map +1 -0
  94. package/dist/esm/testkit/durable-run-fields-conformance.d.ts +4 -0
  95. package/dist/esm/testkit/durable-run-fields-conformance.js +95 -0
  96. package/dist/esm/testkit/durable-run-fields-conformance.js.map +1 -0
  97. package/dist/esm/testkit/journal-conformance.d.ts +51 -0
  98. package/dist/esm/testkit/journal-conformance.js +378 -0
  99. package/dist/esm/testkit/journal-conformance.js.map +1 -0
  100. package/dist/esm/testkit/reaper-conformance.d.ts +37 -0
  101. package/dist/esm/testkit/reaper-conformance.js +847 -0
  102. package/dist/esm/testkit/reaper-conformance.js.map +1 -0
  103. package/dist/esm/testkit/shell-spawn.d.ts +2 -0
  104. package/dist/esm/testkit/shell-spawn.js +60 -0
  105. package/dist/esm/testkit/shell-spawn.js.map +1 -0
  106. package/dist/esm/testkit/takeover-conformance.d.ts +24 -0
  107. package/dist/esm/testkit/takeover-conformance.js +685 -0
  108. package/dist/esm/testkit/takeover-conformance.js.map +1 -0
  109. package/dist/esm/tool-bridge.js +227 -180
  110. package/dist/esm/tool-bridge.js.map +1 -1
  111. package/dist/esm/tool-history.d.ts +62 -0
  112. package/dist/esm/tool-history.js +171 -0
  113. package/dist/esm/tool-history.js.map +1 -0
  114. package/dist/esm/watch.js +310 -236
  115. package/dist/esm/watch.js.map +1 -1
  116. package/dist/esm/workspace.d.ts +1 -1
  117. package/dist/esm/workspace.js +49 -28
  118. package/dist/esm/workspace.js.map +1 -1
  119. package/package.json +16 -6
  120. package/skills/ai-sandbox/SKILL.md +658 -20
  121. package/src/align.ts +297 -0
  122. package/src/attach-preflight.ts +292 -0
  123. package/src/capabilities.ts +4 -13
  124. package/src/chunk-identity.ts +154 -0
  125. package/src/claim.ts +479 -0
  126. package/src/contracts.ts +13 -0
  127. package/src/driver.ts +205 -0
  128. package/src/durability.ts +380 -0
  129. package/src/index.ts +212 -27
  130. package/src/instance-store.ts +122 -0
  131. package/src/journal-bytes.ts +136 -0
  132. package/src/journal-reader.ts +359 -0
  133. package/src/journal-sweep.ts +406 -0
  134. package/src/journal.ts +875 -0
  135. package/src/middleware.ts +470 -30
  136. package/src/reap.ts +723 -0
  137. package/src/reclaim.ts +191 -0
  138. package/src/run.ts +365 -75
  139. package/src/runner.ts +347 -3
  140. package/src/sandbox.ts +38 -8
  141. package/src/shell.ts +106 -38
  142. package/src/testkit/conformance.ts +117 -0
  143. package/src/testkit/durable-run-fields-conformance.ts +147 -0
  144. package/src/testkit/journal-conformance.ts +676 -0
  145. package/src/testkit/reaper-conformance.ts +1201 -0
  146. package/src/testkit/shell-spawn.ts +67 -0
  147. package/src/testkit/takeover-conformance.ts +1040 -0
  148. package/src/tool-history.ts +245 -0
  149. package/src/workspace.ts +1 -1
  150. package/dist/esm/index.js.map +0 -1
  151. package/dist/esm/run-log.d.ts +0 -81
  152. package/dist/esm/run-log.js +0 -107
  153. package/dist/esm/run-log.js.map +0 -1
  154. package/dist/esm/store.d.ts +0 -53
  155. package/dist/esm/store.js +0 -34
  156. package/dist/esm/store.js.map +0 -1
  157. package/src/run-log.ts +0 -224
  158. package/src/store.ts +0 -83
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Turn a harness's PASSTHROUGH tool-call chunks into transcript messages, so a
3
+ * finished run's tool cards survive a reload.
4
+ *
5
+ * Why this is needed at all: a harness executes its tools INSIDE the sandbox, so
6
+ * `chat()` only relays its `TOOL_CALL_*` chunks — it never writes an assistant
7
+ * message for them (`addAssistantToolCallMessage` is gated on the engine having
8
+ * executed the tool itself). Chat persistence stores `ctx.messages`, so the whole
9
+ * tool history existed only in the delivery log. Replaying that log is what makes
10
+ * "switch away and come back" show everything; a FINISHED thread has no run to
11
+ * rejoin, hydrates from the message store instead, and so came back as nothing but
12
+ * the prompt and the final answer.
13
+ *
14
+ * Recording the calls as ordinary `toolCalls` + `role: 'tool'` messages needs no new
15
+ * wire format and no client change: `modelMessagesToUIMessages` already merges a tool
16
+ * result into the call it belongs to and marks the part complete, and
17
+ * `reconstructChat` already runs that converter.
18
+ */
19
+ import { EventType } from '@tanstack/ai'
20
+ import type { ModelMessage, StreamChunk } from '@tanstack/ai'
21
+
22
+ /**
23
+ * Metadata key set on every tool call recorded here.
24
+ *
25
+ * INTERNAL, and deliberately not exported: an app asks {@link isSandboxToolCall}
26
+ * instead of knowing the key. Renaming it is a storage-visible change, because it ends
27
+ * up inside stored `toolCalls[].metadata`, so the recorder test pins the literal.
28
+ */
29
+ const SANDBOX_OBSERVED = 'sandboxObserved'
30
+
31
+ /**
32
+ * What the recorder writes into. `ChatMiddlewareContext.messages` is a
33
+ * `ReadonlyArray`, so the transcript grows by REPLACING the array — the same way the
34
+ * engine itself syncs `middlewareCtx.messages`.
35
+ */
36
+ interface TranscriptTarget {
37
+ messages: ReadonlyArray<ModelMessage>
38
+ }
39
+
40
+ interface OpenCall {
41
+ name: string
42
+ /** Accumulated `TOOL_CALL_ARGS` deltas; superseded by `input` when the adapter sends it. */
43
+ args: string
44
+ }
45
+
46
+ export interface ToolHistoryRecorder {
47
+ /** Feed every chunk. Observes only — never transforms or drops. */
48
+ observe: (chunk: StreamChunk, target: TranscriptTarget) => void
49
+ /**
50
+ * Re-append anything missing from the transcript.
51
+ *
52
+ * The engine reassigns `middlewareCtx.messages` from its own array whenever it
53
+ * syncs config (once per agent iteration), which discards writes made during the
54
+ * previous iteration's stream. Reconciling at each iteration boundary and again at
55
+ * finish makes the result independent of that, and independent of where this
56
+ * middleware sits relative to persistence in the middleware array.
57
+ */
58
+ reconcile: (target: TranscriptTarget) => void
59
+ }
60
+
61
+ /**
62
+ * True when this tool call was executed by the HARNESS inside the sandbox, and
63
+ * recorded into the transcript for display, rather than executed by the agent loop.
64
+ *
65
+ * Use it to decide what your own `MessageStore` keeps — these calls are display
66
+ * history, so dropping or capping them is safe (they are already stripped from the
67
+ * request to the model on the next turn). Also works on a `tool-call` UI part, whose
68
+ * `metadata` is copied straight from the model message.
69
+ *
70
+ * `metadata` is `unknown` on both, so the key can only be read behind a typeof/`in`
71
+ * check; this mirrors the core `isProviderExecutedToolCall` convention.
72
+ *
73
+ * ```ts
74
+ * import { isSandboxToolCall } from '@tanstack/ai-sandbox'
75
+ *
76
+ * const kept = messages.filter(
77
+ * (message) => !message.toolCalls?.every(isSandboxToolCall),
78
+ * )
79
+ * ```
80
+ */
81
+ export function isSandboxToolCall(
82
+ toolCall: { metadata?: unknown } | null | undefined,
83
+ ): boolean {
84
+ const metadata = toolCall?.metadata
85
+ return (
86
+ typeof metadata === 'object' &&
87
+ metadata !== null &&
88
+ SANDBOX_OBSERVED in metadata &&
89
+ metadata[SANDBOX_OBSERVED] === true
90
+ )
91
+ }
92
+
93
+ /** Does the transcript already carry this tool call, from any source? */
94
+ function hasCall(messages: ReadonlyArray<ModelMessage>, id: string): boolean {
95
+ return messages.some((message) =>
96
+ message.toolCalls?.some((call) => call.id === id),
97
+ )
98
+ }
99
+
100
+ /** Does the transcript already carry this tool result? */
101
+ function hasResult(messages: ReadonlyArray<ModelMessage>, id: string): boolean {
102
+ return messages.some(
103
+ (message) => message.role === 'tool' && message.toolCallId === id,
104
+ )
105
+ }
106
+
107
+ function callMessage(id: string, name: string, args: string): ModelMessage {
108
+ return {
109
+ role: 'assistant',
110
+ content: null,
111
+ toolCalls: [
112
+ {
113
+ id,
114
+ type: 'function',
115
+ function: { name, arguments: args },
116
+ metadata: { [SANDBOX_OBSERVED]: true },
117
+ },
118
+ ],
119
+ }
120
+ }
121
+
122
+ function resultMessage(id: string, content: string): ModelMessage {
123
+ return { role: 'tool', toolCallId: id, content }
124
+ }
125
+
126
+ export function createToolHistoryRecorder(): ToolHistoryRecorder {
127
+ const open = new Map<string, OpenCall>()
128
+ /** Completed calls in the order they ran — the order `reconcile` restores. */
129
+ const recorded: Array<{ id: string; name: string; args: string }> = []
130
+ const results = new Map<string, string>()
131
+
132
+ function appendCall(
133
+ target: TranscriptTarget,
134
+ id: string,
135
+ name: string,
136
+ args: string,
137
+ ): void {
138
+ // An id already present is either the engine's own (it executed the tool itself)
139
+ // or a chunk seen before — a journal replay on takeover re-emits the whole
140
+ // stream. Either way a second write would duplicate the card.
141
+ if (hasCall(target.messages, id)) return
142
+ target.messages = [...target.messages, callMessage(id, name, args)]
143
+ }
144
+
145
+ function appendResult(
146
+ target: TranscriptTarget,
147
+ id: string,
148
+ content: string,
149
+ ): void {
150
+ if (hasResult(target.messages, id)) return
151
+ target.messages = [...target.messages, resultMessage(id, content)]
152
+ }
153
+
154
+ const recorder: ToolHistoryRecorder = {
155
+ // An if/else chain rather than a `switch`: only four of the ~20 chunk types are
156
+ // interesting here, and a `switch` on `chunk.type` has to enumerate all of them
157
+ // to satisfy the exhaustiveness lint.
158
+ observe(chunk, target) {
159
+ if (chunk.type === EventType.TOOL_CALL_START) {
160
+ // `toolCallName` is the AG-UI field; `toolName` is its deprecated alias, and
161
+ // that alias is what several harness adapters still emit.
162
+ const name = chunk.toolCallName ?? chunk.toolName
163
+ if (!name) return
164
+ open.set(chunk.toolCallId, { name, args: '' })
165
+ return
166
+ }
167
+ if (chunk.type === EventType.TOOL_CALL_ARGS) {
168
+ const call = open.get(chunk.toolCallId)
169
+ if (!call) return
170
+ // `args` is the accumulated-so-far field. Prefer it over stitching deltas:
171
+ // an adapter that sends both would otherwise double the arguments.
172
+ call.args = chunk.args ?? call.args + chunk.delta
173
+ return
174
+ }
175
+ if (chunk.type === EventType.TOOL_CALL_END) {
176
+ const call = open.get(chunk.toolCallId)
177
+ if (!call) return
178
+ open.delete(chunk.toolCallId)
179
+ // `input` is the final PARSED input, so it beats the streamed string, which
180
+ // can be a truncated fragment if the arguments stream was cut short.
181
+ const args =
182
+ chunk.input !== undefined ? JSON.stringify(chunk.input) : call.args
183
+ recorded.push({ id: chunk.toolCallId, name: call.name, args })
184
+ appendCall(target, chunk.toolCallId, call.name, args)
185
+ return
186
+ }
187
+ if (chunk.type === EventType.TOOL_CALL_RESULT) {
188
+ // AG-UI types `content` as a string; anything else is not a result we can
189
+ // store as a `role: 'tool'` message.
190
+ if (typeof chunk.content !== 'string') return
191
+ results.set(chunk.toolCallId, chunk.content)
192
+ appendResult(target, chunk.toolCallId, chunk.content)
193
+ }
194
+ },
195
+
196
+ reconcile(target) {
197
+ for (const { id, name, args } of recorded) {
198
+ appendCall(target, id, name, args)
199
+ const result = results.get(id)
200
+ // The result goes straight after its own call, so a restored transcript reads
201
+ // in the order the tools actually ran.
202
+ if (result !== undefined) appendResult(target, id, result)
203
+ }
204
+ },
205
+ }
206
+ return recorder
207
+ }
208
+
209
+ /**
210
+ * Drop recorded harness tool calls from a list of messages bound for the model.
211
+ *
212
+ * A stored transcript becomes the history for the NEXT turn. These calls name tools
213
+ * the provider was never given, and one triage-sized run is hundreds of kilobytes of
214
+ * tool output — so replaying them is wasteful at best and rejected at worst. They stay
215
+ * in `ctx.messages` (which is what gets stored and rendered); only the request to the
216
+ * model loses them.
217
+ *
218
+ * An assistant message is dropped only when EVERY call on it is observed, so a mixed
219
+ * message — one engine tool call plus one harness tool call — is left alone rather than
220
+ * silently losing the engine's half.
221
+ */
222
+ export function stripObservedToolCalls(
223
+ messages: ReadonlyArray<ModelMessage>,
224
+ ): Array<ModelMessage> {
225
+ const dropped = new Set<string>()
226
+ const kept: Array<ModelMessage> = []
227
+ for (const message of messages) {
228
+ const calls = message.toolCalls
229
+ if (calls && calls.length > 0 && calls.every(isSandboxToolCall)) {
230
+ for (const call of calls) dropped.add(call.id)
231
+ continue
232
+ }
233
+ // Orphaning a result is worse than keeping it: a provider rejects a tool result
234
+ // whose call is not in the history.
235
+ if (
236
+ message.role === 'tool' &&
237
+ message.toolCallId !== undefined &&
238
+ dropped.has(message.toolCallId)
239
+ ) {
240
+ continue
241
+ }
242
+ kept.push(message)
243
+ }
244
+ return kept
245
+ }
package/src/workspace.ts CHANGED
@@ -137,7 +137,7 @@ export interface WorkspaceDefinition {
137
137
  /**
138
138
  * Typed secret references. The underlying values are injected into the
139
139
  * sandbox env at create/resume — NEVER written to snapshots, the
140
- * SandboxStore, or the event log.
140
+ * SandboxInstanceStore, or the event log.
141
141
  */
142
142
  secrets?: Secrets
143
143
  /** Workspace root inside the sandbox. Defaults to `/workspace`. */
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;"}
@@ -1,81 +0,0 @@
1
- import { StreamChunk } from '@tanstack/ai';
2
- /** A terminal run status: no further events will be appended. */
3
- export type TerminalRunStatus = 'done' | 'error' | 'aborted';
4
- /** Lifecycle status of a run. `done`/`error`/`aborted` are terminal. */
5
- export type RunStatus = 'running' | TerminalRunStatus;
6
- /** Whether a run status is terminal (no further events will be appended). */
7
- export declare function isTerminalRunStatus(status: RunStatus): boolean;
8
- export interface RunError {
9
- message: string;
10
- code?: string;
11
- }
12
- /** Durable bookkeeping for a single run. */
13
- export interface RunRecord {
14
- runId: string;
15
- threadId?: string;
16
- status: RunStatus;
17
- /** Seq of the last appended event, or `-1` when no events yet. */
18
- lastSeq: number;
19
- error?: RunError;
20
- createdAt: number;
21
- updatedAt: number;
22
- }
23
- /** One persisted event: a chunk plus its monotonic, gap-free sequence number. */
24
- export interface RunEvent {
25
- seq: number;
26
- chunk: StreamChunk;
27
- }
28
- export interface RunEventLogReadOptions {
29
- /**
30
- * Exclusive cursor: only events with `seq > fromSeq` are yielded. Pass the
31
- * client's last-seen `seq` to resume; omit (or `-1`) to replay from the start.
32
- */
33
- fromSeq?: number;
34
- /** Stop tailing when this fires (e.g. the client disconnected). */
35
- signal?: AbortSignal;
36
- }
37
- /**
38
- * Append-only, `seq`-indexed log of a run's stream, with resumable reads.
39
- *
40
- * Contract:
41
- * - `append` assigns the next `seq` (0, 1, 2, …) and returns it.
42
- * - `read` yields the backlog after `fromSeq` in order, then live-tails new
43
- * events, and RETURNS once the run is terminal and the cursor has caught up.
44
- * - All methods reject for an unknown `runId` except `get`, which resolves null.
45
- */
46
- export interface RunEventLog {
47
- /** Idempotently create (or return) the run record. */
48
- open: (input: {
49
- runId: string;
50
- threadId?: string;
51
- }) => Promise<RunRecord>;
52
- /** Append one chunk; resolves with its assigned `seq`. */
53
- append: (runId: string, chunk: StreamChunk) => Promise<number>;
54
- /** Move the run to a terminal status. Idempotent for the same status. */
55
- finish: (runId: string, status: TerminalRunStatus, error?: RunError) => Promise<void>;
56
- /** Current record, or null if the run is unknown. */
57
- get: (runId: string) => Promise<RunRecord | null>;
58
- /** Replay-then-tail events with `seq > fromSeq` until the run is terminal. */
59
- read: (runId: string, options?: RunEventLogReadOptions) => AsyncIterable<RunEvent>;
60
- }
61
- /**
62
- * Single-process {@link RunEventLog}. Backs `read`'s live-tail with an internal
63
- * waiter set: `append`/`finish` wake every blocked reader. Suitable for a
64
- * long-running Node host, tests, and as the reference implementation a durable
65
- * backend mirrors.
66
- */
67
- export declare class InMemoryRunEventLog implements RunEventLog {
68
- private readonly runs;
69
- private now;
70
- private require;
71
- private wake;
72
- open(input: {
73
- runId: string;
74
- threadId?: string;
75
- }): Promise<RunRecord>;
76
- append(runId: string, chunk: StreamChunk): Promise<number>;
77
- finish(runId: string, status: TerminalRunStatus, error?: RunError): Promise<void>;
78
- get(runId: string): Promise<RunRecord | null>;
79
- read(runId: string, options?: RunEventLogReadOptions): AsyncIterable<RunEvent>;
80
- private waitForChange;
81
- }
@@ -1,107 +0,0 @@
1
- const TERMINAL = /* @__PURE__ */ new Set([
2
- "done",
3
- "error",
4
- "aborted"
5
- ]);
6
- function isTerminalRunStatus(status) {
7
- return TERMINAL.has(status);
8
- }
9
- class InMemoryRunEventLog {
10
- runs = /* @__PURE__ */ new Map();
11
- now() {
12
- return Date.now();
13
- }
14
- require(runId) {
15
- const state = this.runs.get(runId);
16
- if (!state) throw new Error(`run-log: unknown runId "${runId}"`);
17
- return state;
18
- }
19
- wake(state) {
20
- const waiters = [...state.waiters];
21
- state.waiters.clear();
22
- for (const resolve of waiters) resolve();
23
- }
24
- // Mutators return a Promise without `async` so contract violations REJECT
25
- // (rather than throwing synchronously from a Promise-typed method — a
26
- // `.catch()` footgun) without an `await`-less async body.
27
- open(input) {
28
- const existing = this.runs.get(input.runId);
29
- if (existing) return Promise.resolve({ ...existing.record });
30
- const now = this.now();
31
- const record = {
32
- runId: input.runId,
33
- ...input.threadId !== void 0 ? { threadId: input.threadId } : {},
34
- status: "running",
35
- lastSeq: -1,
36
- createdAt: now,
37
- updatedAt: now
38
- };
39
- this.runs.set(input.runId, { record, chunks: [], waiters: /* @__PURE__ */ new Set() });
40
- return Promise.resolve({ ...record });
41
- }
42
- append(runId, chunk) {
43
- const state = this.runs.get(runId);
44
- if (!state) {
45
- return Promise.reject(new Error(`run-log: unknown runId "${runId}"`));
46
- }
47
- if (isTerminalRunStatus(state.record.status)) {
48
- return Promise.reject(
49
- new Error(
50
- `run-log: cannot append to terminal run "${runId}" (status=${state.record.status})`
51
- )
52
- );
53
- }
54
- const seq = state.record.lastSeq + 1;
55
- state.chunks.push(chunk);
56
- state.record.lastSeq = seq;
57
- state.record.updatedAt = this.now();
58
- this.wake(state);
59
- return Promise.resolve(seq);
60
- }
61
- finish(runId, status, error) {
62
- const state = this.runs.get(runId);
63
- if (!state) {
64
- return Promise.reject(new Error(`run-log: unknown runId "${runId}"`));
65
- }
66
- if (isTerminalRunStatus(state.record.status)) return Promise.resolve();
67
- state.record.status = status;
68
- if (error !== void 0) state.record.error = error;
69
- state.record.updatedAt = this.now();
70
- this.wake(state);
71
- return Promise.resolve();
72
- }
73
- get(runId) {
74
- const state = this.runs.get(runId);
75
- return Promise.resolve(state ? { ...state.record } : null);
76
- }
77
- async *read(runId, options) {
78
- const state = this.require(runId);
79
- const signal = options?.signal;
80
- let cursor = options?.fromSeq ?? -1;
81
- while (!signal?.aborted) {
82
- while (cursor < state.record.lastSeq) {
83
- cursor += 1;
84
- const chunk = state.chunks[cursor];
85
- if (chunk !== void 0) yield { seq: cursor, chunk };
86
- }
87
- if (isTerminalRunStatus(state.record.status)) return;
88
- await this.waitForChange(state, signal);
89
- }
90
- }
91
- waitForChange(state, signal) {
92
- return new Promise((resolve) => {
93
- const wake = () => {
94
- state.waiters.delete(wake);
95
- if (signal) signal.removeEventListener("abort", wake);
96
- resolve();
97
- };
98
- state.waiters.add(wake);
99
- if (signal) signal.addEventListener("abort", wake, { once: true });
100
- });
101
- }
102
- }
103
- export {
104
- InMemoryRunEventLog,
105
- isTerminalRunStatus
106
- };
107
- //# sourceMappingURL=run-log.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"run-log.js","sources":["../../src/run-log.ts"],"sourcesContent":["/**\n * Resumable run event-log — the primitive that lets a trigger (e.g. a\n * Cloudflare Worker) start an agent run and return immediately while a durable\n * orchestrator (e.g. a Durable Object) drives the run and persists every\n * emitted {@link StreamChunk} under a monotonic `seq`.\n *\n * Clients tail the log from a cursor (`fromSeq`), so a dropped connection, a new\n * browser tab, or an orchestrator that hibernated between chunks all reconnect\n * cleanly: replay everything after the client's last-seen `seq`, then live-tail\n * until the run reaches a terminal status. The *run* never depends on any single\n * connection staying open — that is what makes the serverless/edge model work.\n *\n * This module is transport- and storage-agnostic. {@link InMemoryRunEventLog} is\n * the default (single-process / tests); a durable backend (DO storage, KV, SQL)\n * implements the same {@link RunEventLog} interface — see the Cloudflare example.\n */\nimport type { StreamChunk } from '@tanstack/ai'\n\n/** A terminal run status: no further events will be appended. */\nexport type TerminalRunStatus = 'done' | 'error' | 'aborted'\n\n/** Lifecycle status of a run. `done`/`error`/`aborted` are terminal. */\nexport type RunStatus = 'running' | TerminalRunStatus\n\nconst TERMINAL: ReadonlySet<RunStatus> = new Set<RunStatus>([\n 'done',\n 'error',\n 'aborted',\n])\n\n/** Whether a run status is terminal (no further events will be appended). */\nexport function isTerminalRunStatus(status: RunStatus): boolean {\n return TERMINAL.has(status)\n}\n\nexport interface RunError {\n message: string\n code?: string\n}\n\n/** Durable bookkeeping for a single run. */\nexport interface RunRecord {\n runId: string\n threadId?: string\n status: RunStatus\n /** Seq of the last appended event, or `-1` when no events yet. */\n lastSeq: number\n error?: RunError\n createdAt: number\n updatedAt: number\n}\n\n/** One persisted event: a chunk plus its monotonic, gap-free sequence number. */\nexport interface RunEvent {\n seq: number\n chunk: StreamChunk\n}\n\nexport interface RunEventLogReadOptions {\n /**\n * Exclusive cursor: only events with `seq > fromSeq` are yielded. Pass the\n * client's last-seen `seq` to resume; omit (or `-1`) to replay from the start.\n */\n fromSeq?: number\n /** Stop tailing when this fires (e.g. the client disconnected). */\n signal?: AbortSignal\n}\n\n/**\n * Append-only, `seq`-indexed log of a run's stream, with resumable reads.\n *\n * Contract:\n * - `append` assigns the next `seq` (0, 1, 2, …) and returns it.\n * - `read` yields the backlog after `fromSeq` in order, then live-tails new\n * events, and RETURNS once the run is terminal and the cursor has caught up.\n * - All methods reject for an unknown `runId` except `get`, which resolves null.\n */\nexport interface RunEventLog {\n /** Idempotently create (or return) the run record. */\n open: (input: { runId: string; threadId?: string }) => Promise<RunRecord>\n /** Append one chunk; resolves with its assigned `seq`. */\n append: (runId: string, chunk: StreamChunk) => Promise<number>\n /** Move the run to a terminal status. Idempotent for the same status. */\n finish: (\n runId: string,\n status: TerminalRunStatus,\n error?: RunError,\n ) => Promise<void>\n /** Current record, or null if the run is unknown. */\n get: (runId: string) => Promise<RunRecord | null>\n /** Replay-then-tail events with `seq > fromSeq` until the run is terminal. */\n read: (\n runId: string,\n options?: RunEventLogReadOptions,\n ) => AsyncIterable<RunEvent>\n}\n\n/** Per-run state for the in-memory log. */\ninterface RunState {\n record: RunRecord\n chunks: Array<StreamChunk>\n /** Resolved (and cleared) whenever an event is appended or status changes. */\n waiters: Set<() => void>\n}\n\n/**\n * Single-process {@link RunEventLog}. Backs `read`'s live-tail with an internal\n * waiter set: `append`/`finish` wake every blocked reader. Suitable for a\n * long-running Node host, tests, and as the reference implementation a durable\n * backend mirrors.\n */\nexport class InMemoryRunEventLog implements RunEventLog {\n private readonly runs = new Map<string, RunState>()\n\n private now(): number {\n return Date.now()\n }\n\n private require(runId: string): RunState {\n const state = this.runs.get(runId)\n if (!state) throw new Error(`run-log: unknown runId \"${runId}\"`)\n return state\n }\n\n private wake(state: RunState): void {\n const waiters = [...state.waiters]\n state.waiters.clear()\n for (const resolve of waiters) resolve()\n }\n\n // Mutators return a Promise without `async` so contract violations REJECT\n // (rather than throwing synchronously from a Promise-typed method — a\n // `.catch()` footgun) without an `await`-less async body.\n open(input: { runId: string; threadId?: string }): Promise<RunRecord> {\n const existing = this.runs.get(input.runId)\n if (existing) return Promise.resolve({ ...existing.record })\n const now = this.now()\n const record: RunRecord = {\n runId: input.runId,\n ...(input.threadId !== undefined ? { threadId: input.threadId } : {}),\n status: 'running',\n lastSeq: -1,\n createdAt: now,\n updatedAt: now,\n }\n this.runs.set(input.runId, { record, chunks: [], waiters: new Set() })\n return Promise.resolve({ ...record })\n }\n\n append(runId: string, chunk: StreamChunk): Promise<number> {\n const state = this.runs.get(runId)\n if (!state) {\n return Promise.reject(new Error(`run-log: unknown runId \"${runId}\"`))\n }\n if (isTerminalRunStatus(state.record.status)) {\n return Promise.reject(\n new Error(\n `run-log: cannot append to terminal run \"${runId}\" (status=${state.record.status})`,\n ),\n )\n }\n // Derive seq from the record's cursor (not `chunks.length`) so the gap-free\n // invariant holds the same way the durable backend computes it, even if the\n // backlog is ever trimmed/compacted.\n const seq = state.record.lastSeq + 1\n state.chunks.push(chunk)\n state.record.lastSeq = seq\n state.record.updatedAt = this.now()\n this.wake(state)\n return Promise.resolve(seq)\n }\n\n finish(\n runId: string,\n status: TerminalRunStatus,\n error?: RunError,\n ): Promise<void> {\n const state = this.runs.get(runId)\n if (!state) {\n return Promise.reject(new Error(`run-log: unknown runId \"${runId}\"`))\n }\n if (isTerminalRunStatus(state.record.status)) return Promise.resolve()\n state.record.status = status\n if (error !== undefined) state.record.error = error\n state.record.updatedAt = this.now()\n this.wake(state)\n return Promise.resolve()\n }\n\n get(runId: string): Promise<RunRecord | null> {\n const state = this.runs.get(runId)\n return Promise.resolve(state ? { ...state.record } : null)\n }\n\n async *read(\n runId: string,\n options?: RunEventLogReadOptions,\n ): AsyncIterable<RunEvent> {\n const state = this.require(runId)\n const signal = options?.signal\n let cursor = options?.fromSeq ?? -1\n while (!signal?.aborted) {\n while (cursor < state.record.lastSeq) {\n cursor += 1\n const chunk = state.chunks[cursor]\n if (chunk !== undefined) yield { seq: cursor, chunk }\n }\n if (isTerminalRunStatus(state.record.status)) return\n await this.waitForChange(state, signal)\n }\n }\n\n private waitForChange(state: RunState, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve) => {\n const wake = (): void => {\n state.waiters.delete(wake)\n if (signal) signal.removeEventListener('abort', wake)\n resolve()\n }\n state.waiters.add(wake)\n if (signal) signal.addEventListener('abort', wake, { once: true })\n })\n }\n}\n"],"names":[],"mappings":"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"}
@@ -1,53 +0,0 @@
1
- /**
2
- * Persistence seams for the sandbox layer.
3
- *
4
- * v1 ships ONLY in-memory implementations (single-process resume). These are
5
- * deliberately pluggable OPTIONAL capabilities so the future persistence
6
- * package can `provide` durable implementations (D1/Postgres/Durable Objects)
7
- * without the sandbox layer changing. Do NOT hardcode storage here.
8
- */
9
- /** One persisted sandbox instance, keyed by the compound sandbox instance key. */
10
- export interface SandboxRecord {
11
- /** Compound key (see computeSandboxKey). */
12
- key: string;
13
- /** Provider name that owns `providerSandboxId`. */
14
- provider: string;
15
- /** Provider-assigned sandbox id used to resume. */
16
- providerSandboxId: string;
17
- /** Most recent snapshot id, when the provider supports snapshots. */
18
- latestSnapshotId?: string;
19
- threadId: string;
20
- latestRunId?: string;
21
- /** Epoch ms of last write (for keepAlive / GC by the persistence layer). */
22
- updatedAt: number;
23
- }
24
- /** Maps a compound key to the provider sandbox that should be resumed. */
25
- export interface SandboxStore {
26
- get: (key: string) => Promise<SandboxRecord | null>;
27
- upsert: (record: SandboxRecord) => Promise<void>;
28
- delete: (key: string) => Promise<void>;
29
- }
30
- /**
31
- * Mutual exclusion around sandbox ensure so two concurrent runs for the same
32
- * thread don't both create a sandbox. The in-memory default is single-process;
33
- * the persistence layer provides a distributed lock (e.g. a Durable Object).
34
- */
35
- export interface LockStore {
36
- withLock: <T>(key: string, fn: () => Promise<T>) => Promise<T>;
37
- }
38
- /** In-memory {@link SandboxStore}. Resume works only within one process. */
39
- export declare class InMemorySandboxStore implements SandboxStore {
40
- private readonly map;
41
- get(key: string): Promise<SandboxRecord | null>;
42
- upsert(record: SandboxRecord): Promise<void>;
43
- delete(key: string): Promise<void>;
44
- }
45
- /**
46
- * In-memory {@link LockStore} — a per-key promise chain. Correct within a
47
- * single process; multi-instance correctness needs a distributed lock from the
48
- * persistence layer.
49
- */
50
- export declare class InMemoryLockStore implements LockStore {
51
- private readonly chains;
52
- withLock<T>(key: string, fn: () => Promise<T>): Promise<T>;
53
- }
package/dist/esm/store.js DELETED
@@ -1,34 +0,0 @@
1
- class InMemorySandboxStore {
2
- map = /* @__PURE__ */ new Map();
3
- get(key) {
4
- return Promise.resolve(this.map.get(key) ?? null);
5
- }
6
- upsert(record) {
7
- this.map.set(record.key, record);
8
- return Promise.resolve();
9
- }
10
- delete(key) {
11
- this.map.delete(key);
12
- return Promise.resolve();
13
- }
14
- }
15
- class InMemoryLockStore {
16
- chains = /* @__PURE__ */ new Map();
17
- withLock(key, fn) {
18
- const prior = this.chains.get(key) ?? Promise.resolve();
19
- const run = prior.then(fn, fn);
20
- this.chains.set(
21
- key,
22
- run.then(
23
- () => void 0,
24
- () => void 0
25
- )
26
- );
27
- return run;
28
- }
29
- }
30
- export {
31
- InMemoryLockStore,
32
- InMemorySandboxStore
33
- };
34
- //# sourceMappingURL=store.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"store.js","sources":["../../src/store.ts"],"sourcesContent":["/**\n * Persistence seams for the sandbox layer.\n *\n * v1 ships ONLY in-memory implementations (single-process resume). These are\n * deliberately pluggable OPTIONAL capabilities so the future persistence\n * package can `provide` durable implementations (D1/Postgres/Durable Objects)\n * without the sandbox layer changing. Do NOT hardcode storage here.\n */\n\n/** One persisted sandbox instance, keyed by the compound sandbox instance key. */\nexport interface SandboxRecord {\n /** Compound key (see computeSandboxKey). */\n key: string\n /** Provider name that owns `providerSandboxId`. */\n provider: string\n /** Provider-assigned sandbox id used to resume. */\n providerSandboxId: string\n /** Most recent snapshot id, when the provider supports snapshots. */\n latestSnapshotId?: string\n threadId: string\n latestRunId?: string\n /** Epoch ms of last write (for keepAlive / GC by the persistence layer). */\n updatedAt: number\n}\n\n/** Maps a compound key to the provider sandbox that should be resumed. */\nexport interface SandboxStore {\n get: (key: string) => Promise<SandboxRecord | null>\n upsert: (record: SandboxRecord) => Promise<void>\n delete: (key: string) => Promise<void>\n}\n\n/**\n * Mutual exclusion around sandbox ensure so two concurrent runs for the same\n * thread don't both create a sandbox. The in-memory default is single-process;\n * the persistence layer provides a distributed lock (e.g. a Durable Object).\n */\nexport interface LockStore {\n withLock: <T>(key: string, fn: () => Promise<T>) => Promise<T>\n}\n\n/** In-memory {@link SandboxStore}. Resume works only within one process. */\nexport class InMemorySandboxStore implements SandboxStore {\n private readonly map = new Map<string, SandboxRecord>()\n\n get(key: string): Promise<SandboxRecord | null> {\n return Promise.resolve(this.map.get(key) ?? null)\n }\n\n upsert(record: SandboxRecord): Promise<void> {\n this.map.set(record.key, record)\n return Promise.resolve()\n }\n\n delete(key: string): Promise<void> {\n this.map.delete(key)\n return Promise.resolve()\n }\n}\n\n/**\n * In-memory {@link LockStore} — a per-key promise chain. Correct within a\n * single process; multi-instance correctness needs a distributed lock from the\n * persistence layer.\n */\nexport class InMemoryLockStore implements LockStore {\n private readonly chains = new Map<string, Promise<unknown>>()\n\n withLock<T>(key: string, fn: () => Promise<T>): Promise<T> {\n const prior = this.chains.get(key) ?? Promise.resolve()\n // Chain after the prior holder regardless of how it settled.\n const run = prior.then(fn, fn)\n // Keep the chain alive but swallow rejections so one failure doesn't poison the lock.\n this.chains.set(\n key,\n run.then(\n () => undefined,\n () => undefined,\n ),\n )\n return run\n }\n}\n"],"names":[],"mappings":"AA0CO,MAAM,qBAA6C;AAAA,EACvC,0BAAU,IAAA;AAAA,EAE3B,IAAI,KAA4C;AAC9C,WAAO,QAAQ,QAAQ,KAAK,IAAI,IAAI,GAAG,KAAK,IAAI;AAAA,EAClD;AAAA,EAEA,OAAO,QAAsC;AAC3C,SAAK,IAAI,IAAI,OAAO,KAAK,MAAM;AAC/B,WAAO,QAAQ,QAAA;AAAA,EACjB;AAAA,EAEA,OAAO,KAA4B;AACjC,SAAK,IAAI,OAAO,GAAG;AACnB,WAAO,QAAQ,QAAA;AAAA,EACjB;AACF;AAOO,MAAM,kBAAuC;AAAA,EACjC,6BAAa,IAAA;AAAA,EAE9B,SAAY,KAAa,IAAkC;AACzD,UAAM,QAAQ,KAAK,OAAO,IAAI,GAAG,KAAK,QAAQ,QAAA;AAE9C,UAAM,MAAM,MAAM,KAAK,IAAI,EAAE;AAE7B,SAAK,OAAO;AAAA,MACV;AAAA,MACA,IAAI;AAAA,QACF,MAAM;AAAA,QACN,MAAM;AAAA,MAAA;AAAA,IACR;AAEF,WAAO;AAAA,EACT;AACF;"}