@tanstack/ai 0.42.0 → 0.43.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.
- package/README.md +15 -1
- package/dist/esm/activities/chat/adapter.js +23 -16
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/agent-loop-strategies.d.ts +5 -36
- package/dist/esm/activities/chat/agent-loop-strategies.js +75 -21
- package/dist/esm/activities/chat/agent-loop-strategies.js.map +1 -1
- package/dist/esm/activities/chat/cancel.d.ts +40 -0
- package/dist/esm/activities/chat/cancel.js +54 -0
- package/dist/esm/activities/chat/cancel.js.map +1 -0
- package/dist/esm/activities/chat/index.d.ts +28 -21
- package/dist/esm/activities/chat/index.js +2100 -1813
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/mcp/manager.d.ts +2 -2
- package/dist/esm/activities/chat/mcp/manager.js +90 -77
- package/dist/esm/activities/chat/mcp/manager.js.map +1 -1
- package/dist/esm/activities/chat/mcp/types.d.ts +2 -2
- package/dist/esm/activities/chat/messages.js +397 -346
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/middleware/builder.js +17 -15
- package/dist/esm/activities/chat/middleware/builder.js.map +1 -1
- package/dist/esm/activities/chat/middleware/capabilities.js +78 -43
- package/dist/esm/activities/chat/middleware/capabilities.js.map +1 -1
- package/dist/esm/activities/chat/middleware/compose.d.ts +94 -1
- package/dist/esm/activities/chat/middleware/compose.js +623 -531
- package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
- package/dist/esm/activities/chat/middleware/define.js +12 -5
- package/dist/esm/activities/chat/middleware/define.js.map +1 -1
- package/dist/esm/activities/chat/middleware/index.d.ts +5 -1
- package/dist/esm/activities/chat/middleware/locks.d.ts +50 -0
- package/dist/esm/activities/chat/middleware/locks.js +71 -0
- package/dist/esm/activities/chat/middleware/locks.js.map +1 -0
- package/dist/esm/activities/chat/middleware/pending-turn.d.ts +15 -0
- package/dist/esm/activities/chat/middleware/pending-turn.js +35 -0
- package/dist/esm/activities/chat/middleware/pending-turn.js.map +1 -0
- package/dist/esm/activities/chat/middleware/run-disconnect.d.ts +23 -0
- package/dist/esm/activities/chat/middleware/run-disconnect.js +42 -0
- package/dist/esm/activities/chat/middleware/run-disconnect.js.map +1 -0
- package/dist/esm/activities/chat/middleware/run-store.d.ts +283 -0
- package/dist/esm/activities/chat/middleware/run-store.js +176 -0
- package/dist/esm/activities/chat/middleware/run-store.js.map +1 -0
- package/dist/esm/activities/chat/middleware/sandbox-runtime.js +14 -8
- package/dist/esm/activities/chat/middleware/sandbox-runtime.js.map +1 -1
- package/dist/esm/activities/chat/middleware/tool-cache-middleware.js +79 -70
- package/dist/esm/activities/chat/middleware/tool-cache-middleware.js.map +1 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +59 -2
- package/dist/esm/activities/chat/middleware/validate.js +23 -28
- package/dist/esm/activities/chat/middleware/validate.js.map +1 -1
- package/dist/esm/activities/chat/stream/json-parser.js +39 -25
- package/dist/esm/activities/chat/stream/json-parser.js.map +1 -1
- package/dist/esm/activities/chat/stream/message-updaters.js +275 -234
- package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +24 -4
- package/dist/esm/activities/chat/stream/processor.js +1341 -1542
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/stream/strategies.js +69 -53
- package/dist/esm/activities/chat/stream/strategies.js.map +1 -1
- package/dist/esm/activities/chat/tools/approval-schema.d.ts +19 -0
- package/dist/esm/activities/chat/tools/approval-schema.js +117 -0
- package/dist/esm/activities/chat/tools/approval-schema.js.map +1 -0
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js +164 -191
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
- package/dist/esm/activities/chat/tools/lazy-tools.js +24 -12
- package/dist/esm/activities/chat/tools/lazy-tools.js.map +1 -1
- package/dist/esm/activities/chat/tools/schema-converter.js +293 -146
- package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +18 -2
- package/dist/esm/activities/chat/tools/tool-calls.js +522 -531
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-definition.d.ts +75 -16
- package/dist/esm/activities/chat/tools/tool-definition.js +95 -23
- package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
- package/dist/esm/activities/error-payload.js +85 -47
- package/dist/esm/activities/error-payload.js.map +1 -1
- package/dist/esm/activities/generateAudio/adapter.js +22 -15
- package/dist/esm/activities/generateAudio/adapter.js.map +1 -1
- package/dist/esm/activities/generateAudio/index.d.ts +4 -0
- package/dist/esm/activities/generateAudio/index.js +141 -105
- package/dist/esm/activities/generateAudio/index.js.map +1 -1
- package/dist/esm/activities/generateImage/adapter.js +22 -15
- package/dist/esm/activities/generateImage/adapter.js.map +1 -1
- package/dist/esm/activities/generateImage/index.d.ts +4 -0
- package/dist/esm/activities/generateImage/index.js +155 -111
- package/dist/esm/activities/generateImage/index.js.map +1 -1
- package/dist/esm/activities/generateSpeech/adapter.js +22 -15
- package/dist/esm/activities/generateSpeech/adapter.js.map +1 -1
- package/dist/esm/activities/generateSpeech/index.d.ts +4 -0
- package/dist/esm/activities/generateSpeech/index.js +159 -110
- package/dist/esm/activities/generateSpeech/index.js.map +1 -1
- package/dist/esm/activities/generateTranscription/adapter.js +22 -15
- package/dist/esm/activities/generateTranscription/adapter.js.map +1 -1
- package/dist/esm/activities/generateTranscription/index.d.ts +4 -0
- package/dist/esm/activities/generateTranscription/index.js +159 -100
- package/dist/esm/activities/generateTranscription/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/adapter.js +36 -29
- package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.d.ts +143 -19
- package/dist/esm/activities/generateVideo/index.js +456 -279
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/snap.js +60 -48
- package/dist/esm/activities/generateVideo/snap.js.map +1 -1
- package/dist/esm/activities/index.js +8 -34
- package/dist/esm/activities/middleware/index.d.ts +1 -1
- package/dist/esm/activities/middleware/run.d.ts +10 -0
- package/dist/esm/activities/middleware/run.js +53 -29
- package/dist/esm/activities/middleware/run.js.map +1 -1
- package/dist/esm/activities/middleware/types.d.ts +44 -6
- package/dist/esm/activities/stream-generation-result.d.ts +4 -1
- package/dist/esm/activities/stream-generation-result.js +79 -44
- package/dist/esm/activities/stream-generation-result.js.map +1 -1
- package/dist/esm/activities/summarize/adapter.js +22 -15
- package/dist/esm/activities/summarize/adapter.js.map +1 -1
- package/dist/esm/activities/summarize/chat-stream-summarize.js +252 -202
- package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
- package/dist/esm/activities/summarize/index.d.ts +27 -0
- package/dist/esm/activities/summarize/index.js +268 -102
- package/dist/esm/activities/summarize/index.js.map +1 -1
- package/dist/esm/adapter-internals.d.ts +2 -1
- package/dist/esm/adapter-internals.js +4 -11
- package/dist/esm/client.d.ts +25 -3
- package/dist/esm/client.js +131 -64
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/custom-events.d.ts +76 -0
- package/dist/esm/custom-events.js +37 -0
- package/dist/esm/custom-events.js.map +1 -0
- package/dist/esm/delivery-detach.d.ts +50 -0
- package/dist/esm/delivery-detach.js +71 -0
- package/dist/esm/delivery-detach.js.map +1 -0
- package/dist/esm/delivery-disconnect.d.ts +62 -0
- package/dist/esm/delivery-disconnect.js +81 -0
- package/dist/esm/delivery-disconnect.js.map +1 -0
- package/dist/esm/extend-adapter.js +19 -17
- package/dist/esm/extend-adapter.js.map +1 -1
- package/dist/esm/index.d.ts +24 -6
- package/dist/esm/index.js +30 -98
- package/dist/esm/interrupt-resume.d.ts +71 -0
- package/dist/esm/interrupt-resume.js +438 -0
- package/dist/esm/interrupt-resume.js.map +1 -0
- package/dist/esm/interrupt-serialization.d.ts +12 -0
- package/dist/esm/interrupt-serialization.js +178 -0
- package/dist/esm/interrupt-serialization.js.map +1 -0
- package/dist/esm/interrupts.d.ts +84 -0
- package/dist/esm/interrupts.js +31 -0
- package/dist/esm/interrupts.js.map +1 -0
- package/dist/esm/locks.d.ts +10 -0
- package/dist/esm/locks.js +2 -0
- package/dist/esm/logger/console-logger.js +101 -78
- package/dist/esm/logger/console-logger.js.map +1 -1
- package/dist/esm/logger/internal-logger.js +104 -89
- package/dist/esm/logger/internal-logger.js.map +1 -1
- package/dist/esm/logger/resolve.js +54 -49
- package/dist/esm/logger/resolve.js.map +1 -1
- package/dist/esm/logger/types.d.ts +1 -1
- package/dist/esm/middlewares/content-guard.js +142 -148
- package/dist/esm/middlewares/content-guard.js.map +1 -1
- package/dist/esm/middlewares/index.js +2 -6
- package/dist/esm/middlewares/otel.d.ts +3 -1
- package/dist/esm/middlewares/otel.js +599 -732
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/middlewares/usage-attributes.js +47 -40
- package/dist/esm/middlewares/usage-attributes.js.map +1 -1
- package/dist/esm/realtime/event-emitter.js +24 -25
- package/dist/esm/realtime/event-emitter.js.map +1 -1
- package/dist/esm/realtime/index.d.ts +5 -9
- package/dist/esm/realtime/index.js +29 -6
- package/dist/esm/realtime/index.js.map +1 -1
- package/dist/esm/scope.d.ts +47 -0
- package/dist/esm/stream-durability.d.ts +171 -0
- package/dist/esm/stream-durability.js +295 -0
- package/dist/esm/stream-durability.js.map +1 -0
- package/dist/esm/stream-to-response.d.ts +178 -13
- package/dist/esm/stream-to-response.js +663 -115
- package/dist/esm/stream-to-response.js.map +1 -1
- package/dist/esm/strip-to-spec-middleware.js +30 -16
- package/dist/esm/strip-to-spec-middleware.js.map +1 -1
- package/dist/esm/system-prompts.js +27 -21
- package/dist/esm/system-prompts.js.map +1 -1
- package/dist/esm/tool-registry.js +72 -45
- package/dist/esm/tool-registry.js.map +1 -1
- package/dist/esm/tools/provider-tool.js +14 -5
- package/dist/esm/tools/provider-tool.js.map +1 -1
- package/dist/esm/types.d.ts +321 -42
- package/dist/esm/types.js +2 -0
- package/dist/esm/utilities/ag-ui-wire.js +79 -93
- package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
- package/dist/esm/utilities/chat-params.d.ts +26 -4
- package/dist/esm/utilities/chat-params.js +218 -92
- package/dist/esm/utilities/chat-params.js.map +1 -1
- package/dist/esm/utilities/errors.js +28 -18
- package/dist/esm/utilities/errors.js.map +1 -1
- package/dist/esm/utilities/media-prompt.js +46 -41
- package/dist/esm/utilities/media-prompt.js.map +1 -1
- package/dist/esm/utilities/numbers.js +13 -10
- package/dist/esm/utilities/numbers.js.map +1 -1
- package/dist/esm/utilities/provider-executed.js +20 -11
- package/dist/esm/utilities/provider-executed.js.map +1 -1
- package/dist/esm/utilities/sampling-keys.js +31 -19
- package/dist/esm/utilities/sampling-keys.js.map +1 -1
- package/dist/esm/utilities/tool-result.js +42 -30
- package/dist/esm/utilities/tool-result.js.map +1 -1
- package/dist/esm/utilities/usage.js +27 -9
- package/dist/esm/utilities/usage.js.map +1 -1
- package/dist/esm/utils.js +26 -18
- package/dist/esm/utils.js.map +1 -1
- package/package.json +10 -6
- package/skills/ai-core/SKILL.md +69 -18
- package/skills/ai-core/adapter-configuration/SKILL.md +44 -21
- package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +1 -3
- package/skills/ai-core/adapter-configuration/references/byteplus-adapter.md +148 -0
- package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +2 -6
- package/skills/ai-core/adapter-configuration/references/groq-adapter.md +2 -6
- package/skills/ai-core/adapter-configuration/references/openai-adapter.md +1 -3
- package/skills/ai-core/ag-ui-protocol/SKILL.md +1 -1
- package/skills/ai-core/chat-experience/SKILL.md +98 -11
- package/skills/ai-core/client-persistence/SKILL.md +277 -0
- package/skills/ai-core/custom-backend-integration/SKILL.md +1 -1
- package/skills/ai-core/debug-logging/SKILL.md +1 -1
- package/skills/ai-core/locks/SKILL.md +143 -0
- package/skills/ai-core/media-generation/SKILL.md +144 -12
- package/skills/ai-core/middleware/SKILL.md +258 -33
- package/skills/ai-core/structured-outputs/SKILL.md +1 -1
- package/skills/ai-core/tool-calling/SKILL.md +54 -61
- package/src/activities/chat/agent-loop-strategies.ts +5 -39
- package/src/activities/chat/cancel.ts +81 -0
- package/src/activities/chat/index.ts +1091 -200
- package/src/activities/chat/mcp/manager.ts +4 -4
- package/src/activities/chat/mcp/types.ts +2 -2
- package/src/activities/chat/messages.ts +5 -3
- package/src/activities/chat/middleware/builder.ts +1 -1
- package/src/activities/chat/middleware/compose.ts +186 -9
- package/src/activities/chat/middleware/index.ts +26 -0
- package/src/activities/chat/middleware/locks.ts +102 -0
- package/src/activities/chat/middleware/pending-turn.ts +47 -0
- package/src/activities/chat/middleware/run-disconnect.ts +62 -0
- package/src/activities/chat/middleware/run-store.ts +412 -0
- package/src/activities/chat/middleware/types.ts +62 -1
- package/src/activities/chat/stream/processor.ts +189 -5
- package/src/activities/chat/tools/approval-schema.ts +205 -0
- package/src/activities/chat/tools/tool-calls.ts +106 -13
- package/src/activities/chat/tools/tool-definition.ts +210 -39
- package/src/activities/generateAudio/index.ts +20 -3
- package/src/activities/generateImage/index.ts +20 -3
- package/src/activities/generateSpeech/index.ts +25 -3
- package/src/activities/generateTranscription/index.ts +26 -3
- package/src/activities/generateVideo/index.ts +345 -82
- package/src/activities/middleware/index.ts +2 -0
- package/src/activities/middleware/run.ts +31 -0
- package/src/activities/middleware/types.ts +49 -5
- package/src/activities/stream-generation-result.ts +30 -2
- package/src/activities/summarize/chat-stream-summarize.ts +5 -0
- package/src/activities/summarize/index.ts +200 -10
- package/src/adapter-internals.ts +10 -1
- package/src/client.ts +244 -0
- package/src/custom-events.ts +107 -0
- package/src/delivery-detach.ts +72 -0
- package/src/delivery-disconnect.ts +84 -0
- package/src/index.ts +138 -1
- package/src/interrupt-resume.ts +824 -0
- package/src/interrupt-serialization.ts +183 -0
- package/src/interrupts.ts +146 -0
- package/src/locks.ts +17 -0
- package/src/logger/types.ts +1 -1
- package/src/middlewares/otel.ts +23 -5
- package/src/realtime/index.ts +5 -9
- package/src/scope.ts +47 -0
- package/src/stream-durability.ts +598 -0
- package/src/stream-to-response.ts +1051 -95
- package/src/strip-to-spec-middleware.ts +3 -3
- package/src/types.ts +405 -45
- package/src/utilities/chat-params.ts +245 -55
- package/dist/esm/activities/index.js.map +0 -1
- package/dist/esm/adapter-internals.js.map +0 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/middlewares/index.js.map +0 -1
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { TokenUsage } from '../../../types.js';
|
|
2
|
+
/** A terminal run status: no further events will be appended. */
|
|
3
|
+
export type TerminalRunStatus = 'completed' | 'failed' | 'aborted';
|
|
4
|
+
/**
|
|
5
|
+
* Lifecycle status of one run (one agent turn within a conversation).
|
|
6
|
+
*
|
|
7
|
+
* `interrupted` is a human-in-the-loop PAUSE that interrupt-resume continues
|
|
8
|
+
* from — it is deliberately NOT terminal, and must never be conflated with
|
|
9
|
+
* `aborted` (an explicit cancellation).
|
|
10
|
+
*
|
|
11
|
+
* The two are now written by different hooks and cannot be confused:
|
|
12
|
+
*
|
|
13
|
+
* - `'interrupted'` is written ONLY by `withPersistence`'s `onInterrupt`, and
|
|
14
|
+
* carries NO `finishedAt` (a non-terminal status has not finished).
|
|
15
|
+
* - `'aborted'` is written by `withPersistence`'s `onAbort`, and only for an
|
|
16
|
+
* abort that is an explicit cancel or that is ending the run for good.
|
|
17
|
+
* - A mere client disconnect on a run with durable storage wired writes
|
|
18
|
+
* NEITHER: the record stays `'running'` and gains `detachedSince`, because the
|
|
19
|
+
* agent is still running and a later attach can take it over.
|
|
20
|
+
*
|
|
21
|
+
* Intent is never inferred from the abort itself — see `RUN_CANCEL_REASON` and
|
|
22
|
+
* `requestRunCancel` in `../cancel`.
|
|
23
|
+
*/
|
|
24
|
+
export type RunStatus = 'running' | 'interrupted' | TerminalRunStatus;
|
|
25
|
+
/**
|
|
26
|
+
* Whether `value` is a {@link RunStatus} — the guard a backend validates a row
|
|
27
|
+
* with at DESERIALIZATION.
|
|
28
|
+
*
|
|
29
|
+
* `RunStatus` is a compile-time claim about a storage column. A row arrives as
|
|
30
|
+
* JSON out of D1, a Durable Object, or Postgres, and nothing in the type system
|
|
31
|
+
* checked what that column actually held, so a `RunStore` implementation should
|
|
32
|
+
* run its row's `status` through this before handing the record on. The readers
|
|
33
|
+
* downstream act DESTRUCTIVELY on the answer — `@tanstack/ai-sandbox`'s journal
|
|
34
|
+
* sweep DELETES the journal of a run it believes terminal — so a row that lies
|
|
35
|
+
* about its status is not a display bug.
|
|
36
|
+
*/
|
|
37
|
+
export declare function isRunStatus(value: unknown): value is RunStatus;
|
|
38
|
+
/**
|
|
39
|
+
* Whether `status` means no further events will be appended. Narrows, so a
|
|
40
|
+
* caller inside the guard can pass `status` where a {@link TerminalRunStatus}
|
|
41
|
+
* is required without a cast.
|
|
42
|
+
*
|
|
43
|
+
* `Object.hasOwn`, never `in`: `in` walks the prototype chain, so a row whose
|
|
44
|
+
* `status` column held `'toString'` or `'constructor'` would be reported
|
|
45
|
+
* terminal. `status` is TYPED `RunStatus`, but every value reaching here comes
|
|
46
|
+
* off a user-implemented {@link RunStore} and the type is only a claim (see
|
|
47
|
+
* {@link isRunStatus}). A false `true` deletes a live run's journal
|
|
48
|
+
* (`@tanstack/ai-sandbox`'s journal sweep), fails its attach as `'terminal-run'`
|
|
49
|
+
* (`attach-preflight`), and refuses to drive it (`stream-to-response.ts`).
|
|
50
|
+
*/
|
|
51
|
+
export declare function isTerminalRunStatus(status: RunStatus): status is TerminalRunStatus;
|
|
52
|
+
/**
|
|
53
|
+
* Why a run failed.
|
|
54
|
+
*
|
|
55
|
+
* A bare message is an LLM provider's prose: it changes between model
|
|
56
|
+
* versions and cannot be branched on. `code` is what a consumer switches over
|
|
57
|
+
* to decide whether to retry, escalate, or surface a specific UI.
|
|
58
|
+
*/
|
|
59
|
+
export interface RunError {
|
|
60
|
+
message: string;
|
|
61
|
+
/** Stable, machine-branchable classification, when the provider supplies one. */
|
|
62
|
+
code?: string;
|
|
63
|
+
}
|
|
64
|
+
/** Durable bookkeeping for a single run. */
|
|
65
|
+
export interface RunRecord {
|
|
66
|
+
runId: string;
|
|
67
|
+
/**
|
|
68
|
+
* Conversation this run belongs to — the `Scope.threadId`.
|
|
69
|
+
*
|
|
70
|
+
* Generation jobs (a one-shot `generate()` with no conversation) must not
|
|
71
|
+
* reuse this record by faking `threadId = requestId`; they need a separate
|
|
72
|
+
* job store. `withGenerationPersistence` currently does exactly that and
|
|
73
|
+
* labels itself a stopgap — do not copy it.
|
|
74
|
+
*/
|
|
75
|
+
threadId: string;
|
|
76
|
+
status: RunStatus;
|
|
77
|
+
startedAt: number;
|
|
78
|
+
finishedAt?: number;
|
|
79
|
+
error?: RunError;
|
|
80
|
+
usage?: TokenUsage;
|
|
81
|
+
/**
|
|
82
|
+
* Compound sandbox key this run was bound to, when it ran in a sandbox.
|
|
83
|
+
* Recorded so a future reclaimer can identify the sandbox to tear down
|
|
84
|
+
* without re-deriving the key. Written by `withSandbox`'s detach path
|
|
85
|
+
* (`onAbort` in `@tanstack/ai-sandbox`'s `middleware.ts`) at the same time as
|
|
86
|
+
* `detachedSince`, when a disconnect leaves the run detached rather than
|
|
87
|
+
* destroying the sandbox. A backend must round-trip this field — see
|
|
88
|
+
* `listReclaimable` below for who eventually reads it.
|
|
89
|
+
*/
|
|
90
|
+
sandboxKey?: string;
|
|
91
|
+
/**
|
|
92
|
+
* Epoch ms when the last viewer detached; absent while someone is attached.
|
|
93
|
+
* Written by `withSandbox`'s detach path (`onAbort` in `@tanstack/ai-sandbox`'s
|
|
94
|
+
* `middleware.ts`) alongside `sandboxKey`, when a disconnect leaves the
|
|
95
|
+
* agent running rather than tearing the sandbox down. A backend must
|
|
96
|
+
* round-trip this field: `listReclaimable` depends on it, and
|
|
97
|
+
* `@tanstack/ai-sandbox`'s `reapDetachedRuns` sweeps the candidates it
|
|
98
|
+
* surfaces (see that method's doc comment).
|
|
99
|
+
*/
|
|
100
|
+
detachedSince?: number;
|
|
101
|
+
/**
|
|
102
|
+
* Set by an explicit out-of-band cancel, to be distinguished from a mere
|
|
103
|
+
* client disconnect (the two produce an identical TCP close, so intent is not
|
|
104
|
+
* inferable from the disconnect).
|
|
105
|
+
*
|
|
106
|
+
* Written by `requestRunCancel` and read by `wasCancelRequested` (both in
|
|
107
|
+
* `../cancel`). Deliberately NOT a status: recording intent is not the same as
|
|
108
|
+
* the run having stopped, and only the driver knows when it has.
|
|
109
|
+
*/
|
|
110
|
+
cancelRequested?: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Monotonic fencing token for the run's driver. Bumped by each host that
|
|
113
|
+
* successfully claims the run (see `withRunClaim` in `@tanstack/ai-sandbox`),
|
|
114
|
+
* so a superseded host can discover it lost by comparing the stored value
|
|
115
|
+
* against the one it holds.
|
|
116
|
+
*
|
|
117
|
+
* A lock alone cannot provide this: it tells the winner it won, but gives a
|
|
118
|
+
* loser nothing to read. Absent on a run that was never claimed.
|
|
119
|
+
*/
|
|
120
|
+
driverEpoch?: number;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Durable store for run lifecycle records.
|
|
124
|
+
*
|
|
125
|
+
* REQUIRED: `createOrResume`, `update`, `get`, `findActiveRun`. Every backend
|
|
126
|
+
* must implement all four — they are what the persistence middleware calls
|
|
127
|
+
* unconditionally. `findActiveRun` is required rather than feature-detected
|
|
128
|
+
* because a backend that has not implemented it is indistinguishable from one
|
|
129
|
+
* whose answer is legitimately `null`, so reconnect would silently do nothing
|
|
130
|
+
* instead of failing at build time. It was optional for exactly one release
|
|
131
|
+
* cycle and cost precisely that.
|
|
132
|
+
*
|
|
133
|
+
* OPTIONAL: `listByThread`, `listReclaimable`. Each serves one higher-level
|
|
134
|
+
* feature (thread history, reclaim reaping) and callers feature-detect them,
|
|
135
|
+
* degrading gracefully when a backend omits them.
|
|
136
|
+
*/
|
|
137
|
+
export interface RunStore {
|
|
138
|
+
/**
|
|
139
|
+
* Create a run record, or return the existing one unchanged if `runId` is
|
|
140
|
+
* already present.
|
|
141
|
+
*
|
|
142
|
+
* INVARIANT (idempotency): an existing record is returned **unchanged** and
|
|
143
|
+
* the passed `threadId`/`startedAt`/`status` are ignored. This is what makes
|
|
144
|
+
* resuming a run safe. `status` defaults to `'running'` on first creation.
|
|
145
|
+
*/
|
|
146
|
+
createOrResume: (input: Pick<RunRecord, 'runId' | 'threadId' | 'startedAt'> & {
|
|
147
|
+
status?: RunStatus;
|
|
148
|
+
}) => Promise<RunRecord>;
|
|
149
|
+
/**
|
|
150
|
+
* Patch a record's mutable fields.
|
|
151
|
+
*
|
|
152
|
+
* INVARIANT: updating an unknown `runId` is a **no-op** — it must not throw
|
|
153
|
+
* and must not create a record.
|
|
154
|
+
*/
|
|
155
|
+
update: (runId: string, patch: Partial<Pick<RunRecord, 'status' | 'finishedAt' | 'error' | 'usage' | 'sandboxKey' | 'detachedSince' | 'cancelRequested' | 'driverEpoch'>>) => Promise<void>;
|
|
156
|
+
/** Current record, or null when unknown. */
|
|
157
|
+
get: (runId: string) => Promise<RunRecord | null>;
|
|
158
|
+
/**
|
|
159
|
+
* Every run in a conversation, ascending by `startedAt`. OPTIONAL: only
|
|
160
|
+
* needed to render a thread's past agent activity. Consumers feature-detect.
|
|
161
|
+
*/
|
|
162
|
+
listByThread?: (threadId: string) => Promise<Array<RunRecord>>;
|
|
163
|
+
/**
|
|
164
|
+
* Runs that may be reclaimed: ALL THREE of `status === 'running'`,
|
|
165
|
+
* `detachedSince` is set, and `detachedSince <= now - ttlMs`. The cutoff is
|
|
166
|
+
* **inclusive** — a run detached at exactly `now - ttlMs` IS reclaimable.
|
|
167
|
+
*
|
|
168
|
+
* OPTIONAL: only needed by a reaper. Consumers feature-detect.
|
|
169
|
+
*
|
|
170
|
+
* `detachedSince` is populated by `withSandbox`'s detach path (see
|
|
171
|
+
* {@link RunRecord.detachedSince}). The sweep over the candidates this
|
|
172
|
+
* surfaces is `@tanstack/ai-sandbox`'s `reapDetachedRuns`: it finalizes a run
|
|
173
|
+
* whose agent already finished, expires one past its TTL, and reclaims the
|
|
174
|
+
* sandbox. That is a function, not a scheduler — the application invokes it
|
|
175
|
+
* (cron, queue, `alarm()`, `waitUntil`) — and a backend that omits this
|
|
176
|
+
* method cannot be reaped at all.
|
|
177
|
+
*/
|
|
178
|
+
listReclaimable?: (opts: {
|
|
179
|
+
now: number;
|
|
180
|
+
ttlMs: number;
|
|
181
|
+
}) => Promise<Array<RunRecord>>;
|
|
182
|
+
/**
|
|
183
|
+
* The most recent `'running'` run for `threadId`, or `null` if none is active.
|
|
184
|
+
*
|
|
185
|
+
* REQUIRED. This resolves "does this thread have a live run to attach to?"
|
|
186
|
+
* from the STABLE thread id, which is the durable basis for reconnecting a
|
|
187
|
+
* client (a reload, or the same thread opened on another device) — independent
|
|
188
|
+
* of the ephemeral run id, which a single turn may mint several of. When more
|
|
189
|
+
* than one run is `'running'`, the one with the greatest `startedAt` wins.
|
|
190
|
+
*
|
|
191
|
+
* A backend that stubs this to `null` turns reconnect off silently, because
|
|
192
|
+
* `null` is also the correct answer for an idle thread. A backend with no run
|
|
193
|
+
* lifecycle at all should omit the whole `runs` store instead — capability
|
|
194
|
+
* tiers belong at the store level, not the method level.
|
|
195
|
+
*/
|
|
196
|
+
findActiveRun: (threadId: string) => Promise<RunRecord | null>;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Type a {@link RunStore} implementation inline: pass the object and get
|
|
200
|
+
* autocomplete plus contract checking with no separate annotation. Mirrors
|
|
201
|
+
* `defineLock` / `defineSandboxInstanceStore`.
|
|
202
|
+
*
|
|
203
|
+
* The generic return preserves the argument's own type, so an optional method
|
|
204
|
+
* the implementation actually provides stays known-present on the result
|
|
205
|
+
* instead of collapsing back to `| undefined` on the interface.
|
|
206
|
+
*/
|
|
207
|
+
export declare function defineRunStore<const T extends RunStore>(store: T): T;
|
|
208
|
+
/**
|
|
209
|
+
* Whether the current run can be DETACHED rather than destroyed when its client
|
|
210
|
+
* disconnects — `true` only when some middleware has both a {@link RunStore} and
|
|
211
|
+
* a durable event log wired (`withSandbox`'s `runs` + `durability.adapter`).
|
|
212
|
+
*
|
|
213
|
+
* Lives in core for the same reason `LockStore` does: it is a coordination fact
|
|
214
|
+
* that two consumer packages must agree on, and neither may depend on the other.
|
|
215
|
+
* `@tanstack/ai-sandbox` provides it; `@tanstack/ai-persistence` reads it to
|
|
216
|
+
* decide whether an abort is terminal (`'aborted'`) or a detach (write nothing).
|
|
217
|
+
* A persistence → sandbox import would be a layering inversion.
|
|
218
|
+
*
|
|
219
|
+
* Consumers read it with `{ optional: true }`: absent means "not detachable",
|
|
220
|
+
* which is every app that has not wired durability.
|
|
221
|
+
*
|
|
222
|
+
* Typed `true`, not `boolean`: ABSENCE is the negative, so a published `false`
|
|
223
|
+
* has no meaning — and a consumer that tests PRESENCE rather than the value
|
|
224
|
+
* would read one as "detachable". Narrowing the payload makes that
|
|
225
|
+
* unrepresentable instead of merely undocumented.
|
|
226
|
+
*/
|
|
227
|
+
export declare const DetachableRunCapability: import('./capabilities.js').Capability<true, "detachable-run">;
|
|
228
|
+
/**
|
|
229
|
+
* Destructured accessors: `getDetachableRun(ctx, { optional: true })` /
|
|
230
|
+
* `provideDetachableRun(ctx, true)`.
|
|
231
|
+
*/
|
|
232
|
+
export declare const getDetachableRun: import('./capabilities.js').CapabilityGetter<true>, provideDetachableRun: import('./capabilities.js').CapabilityProvider<true>;
|
|
233
|
+
/**
|
|
234
|
+
* Whether this run's teardown DID detach — the disconnect was survived, the
|
|
235
|
+
* agent is still working, and a later attach can take the run over.
|
|
236
|
+
*
|
|
237
|
+
* The past-tense counterpart of {@link DetachableRunCapability}, and the two must
|
|
238
|
+
* not be confused:
|
|
239
|
+
*
|
|
240
|
+
* - **detachABLE** is published at `setup`, and only says a disconnect *may* be
|
|
241
|
+
* survived (a `RunStore` and a durable log are wired).
|
|
242
|
+
* - **detachED** is published on the ABORT path, by the middleware that actually
|
|
243
|
+
* makes the call — `withSandbox`'s `onAbort`, which is the only actor that has
|
|
244
|
+
* resolved BOTH out-of-band cancel bands (`AbortInfo.cancelRequested` and
|
|
245
|
+
* `wasCancelRequested` on the record) and `detachOnDisconnect`. An explicit
|
|
246
|
+
* cancel, a non-detachable disconnect, an error, and a normal finish all leave
|
|
247
|
+
* it unpublished.
|
|
248
|
+
*
|
|
249
|
+
* Its consumer is the durable DELIVERY sink in `stream-to-response.ts`: a
|
|
250
|
+
* detached run's log must stay OPEN and un-terminalized so the takeover can
|
|
251
|
+
* continue it (see `wasRunDetached` in `../../../delivery-detach`). Reading it
|
|
252
|
+
* is safe and race-free only because a `for await` over the chat stream awaits
|
|
253
|
+
* the generator's `return()` — and therefore the whole `onAbort` chain — before
|
|
254
|
+
* the sink's own `finally` runs.
|
|
255
|
+
*
|
|
256
|
+
* Read with `{ optional: true }`: absent means "not detached", which is every
|
|
257
|
+
* other exit path and every app that has not wired durability.
|
|
258
|
+
*
|
|
259
|
+
* Typed `true`, not `boolean`, for the same reason as
|
|
260
|
+
* {@link DetachableRunCapability}: absence is the only negative, so publishing
|
|
261
|
+
* `false` must not be representable.
|
|
262
|
+
*/
|
|
263
|
+
export declare const RunDetachedCapability: import('./capabilities.js').Capability<true, "run-detached">;
|
|
264
|
+
/**
|
|
265
|
+
* Destructured accessors: `getRunDetached(ctx, { optional: true })` /
|
|
266
|
+
* `provideRunDetached(ctx, true)`.
|
|
267
|
+
*/
|
|
268
|
+
export declare const getRunDetached: import('./capabilities.js').CapabilityGetter<true>, provideRunDetached: import('./capabilities.js').CapabilityProvider<true>;
|
|
269
|
+
/** In-memory {@link RunStore}. Single process only. */
|
|
270
|
+
export declare class InMemoryRunStore implements RunStore {
|
|
271
|
+
private readonly runs;
|
|
272
|
+
createOrResume(input: Pick<RunRecord, 'runId' | 'threadId' | 'startedAt'> & {
|
|
273
|
+
status?: RunStatus;
|
|
274
|
+
}): Promise<RunRecord>;
|
|
275
|
+
update(runId: string, patch: Partial<Pick<RunRecord, 'status' | 'finishedAt' | 'error' | 'usage' | 'sandboxKey' | 'detachedSince' | 'cancelRequested' | 'driverEpoch'>>): Promise<void>;
|
|
276
|
+
get(runId: string): Promise<RunRecord | null>;
|
|
277
|
+
listByThread(threadId: string): Promise<Array<RunRecord>>;
|
|
278
|
+
listReclaimable(opts: {
|
|
279
|
+
now: number;
|
|
280
|
+
ttlMs: number;
|
|
281
|
+
}): Promise<Array<RunRecord>>;
|
|
282
|
+
findActiveRun(threadId: string): Promise<RunRecord | null>;
|
|
283
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { createCapability } from "./capabilities.js";
|
|
2
|
+
//#region src/activities/chat/middleware/run-store.ts
|
|
3
|
+
/**
|
|
4
|
+
* Run lifecycle types — the neutral home for what a "run" is.
|
|
5
|
+
*
|
|
6
|
+
* Shared by `@tanstack/ai-persistence` (which exposes a `runs` store through
|
|
7
|
+
* `withPersistence`) and `@tanstack/ai-sandbox` (whose run driver records run
|
|
8
|
+
* status). Living in core is what lets one `RunRecord` per run be shared by
|
|
9
|
+
* both, instead of each package keeping its own and disagreeing. Same rationale
|
|
10
|
+
* as `LockStore` (`packages/ai/src/locks.ts`), which is likewise a
|
|
11
|
+
* coordination primitive that core owns so that no consumer package has to.
|
|
12
|
+
*/
|
|
13
|
+
var TERMINAL = {
|
|
14
|
+
completed: true,
|
|
15
|
+
failed: true,
|
|
16
|
+
aborted: true
|
|
17
|
+
};
|
|
18
|
+
var ALL_STATUSES = {
|
|
19
|
+
running: true,
|
|
20
|
+
interrupted: true,
|
|
21
|
+
completed: true,
|
|
22
|
+
failed: true,
|
|
23
|
+
aborted: true
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Whether `value` is a {@link RunStatus} — the guard a backend validates a row
|
|
27
|
+
* with at DESERIALIZATION.
|
|
28
|
+
*
|
|
29
|
+
* `RunStatus` is a compile-time claim about a storage column. A row arrives as
|
|
30
|
+
* JSON out of D1, a Durable Object, or Postgres, and nothing in the type system
|
|
31
|
+
* checked what that column actually held, so a `RunStore` implementation should
|
|
32
|
+
* run its row's `status` through this before handing the record on. The readers
|
|
33
|
+
* downstream act DESTRUCTIVELY on the answer — `@tanstack/ai-sandbox`'s journal
|
|
34
|
+
* sweep DELETES the journal of a run it believes terminal — so a row that lies
|
|
35
|
+
* about its status is not a display bug.
|
|
36
|
+
*/
|
|
37
|
+
function isRunStatus(value) {
|
|
38
|
+
return typeof value === "string" && Object.hasOwn(ALL_STATUSES, value);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Whether `status` means no further events will be appended. Narrows, so a
|
|
42
|
+
* caller inside the guard can pass `status` where a {@link TerminalRunStatus}
|
|
43
|
+
* is required without a cast.
|
|
44
|
+
*
|
|
45
|
+
* `Object.hasOwn`, never `in`: `in` walks the prototype chain, so a row whose
|
|
46
|
+
* `status` column held `'toString'` or `'constructor'` would be reported
|
|
47
|
+
* terminal. `status` is TYPED `RunStatus`, but every value reaching here comes
|
|
48
|
+
* off a user-implemented {@link RunStore} and the type is only a claim (see
|
|
49
|
+
* {@link isRunStatus}). A false `true` deletes a live run's journal
|
|
50
|
+
* (`@tanstack/ai-sandbox`'s journal sweep), fails its attach as `'terminal-run'`
|
|
51
|
+
* (`attach-preflight`), and refuses to drive it (`stream-to-response.ts`).
|
|
52
|
+
*/
|
|
53
|
+
function isTerminalRunStatus(status) {
|
|
54
|
+
return Object.hasOwn(TERMINAL, status);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Type a {@link RunStore} implementation inline: pass the object and get
|
|
58
|
+
* autocomplete plus contract checking with no separate annotation. Mirrors
|
|
59
|
+
* `defineLock` / `defineSandboxInstanceStore`.
|
|
60
|
+
*
|
|
61
|
+
* The generic return preserves the argument's own type, so an optional method
|
|
62
|
+
* the implementation actually provides stays known-present on the result
|
|
63
|
+
* instead of collapsing back to `| undefined` on the interface.
|
|
64
|
+
*/
|
|
65
|
+
function defineRunStore(store) {
|
|
66
|
+
return store;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Whether the current run can be DETACHED rather than destroyed when its client
|
|
70
|
+
* disconnects — `true` only when some middleware has both a {@link RunStore} and
|
|
71
|
+
* a durable event log wired (`withSandbox`'s `runs` + `durability.adapter`).
|
|
72
|
+
*
|
|
73
|
+
* Lives in core for the same reason `LockStore` does: it is a coordination fact
|
|
74
|
+
* that two consumer packages must agree on, and neither may depend on the other.
|
|
75
|
+
* `@tanstack/ai-sandbox` provides it; `@tanstack/ai-persistence` reads it to
|
|
76
|
+
* decide whether an abort is terminal (`'aborted'`) or a detach (write nothing).
|
|
77
|
+
* A persistence → sandbox import would be a layering inversion.
|
|
78
|
+
*
|
|
79
|
+
* Consumers read it with `{ optional: true }`: absent means "not detachable",
|
|
80
|
+
* which is every app that has not wired durability.
|
|
81
|
+
*
|
|
82
|
+
* Typed `true`, not `boolean`: ABSENCE is the negative, so a published `false`
|
|
83
|
+
* has no meaning — and a consumer that tests PRESENCE rather than the value
|
|
84
|
+
* would read one as "detachable". Narrowing the payload makes that
|
|
85
|
+
* unrepresentable instead of merely undocumented.
|
|
86
|
+
*/
|
|
87
|
+
var DetachableRunCapability = createCapability()("detachable-run");
|
|
88
|
+
/**
|
|
89
|
+
* Destructured accessors: `getDetachableRun(ctx, { optional: true })` /
|
|
90
|
+
* `provideDetachableRun(ctx, true)`.
|
|
91
|
+
*/
|
|
92
|
+
var [getDetachableRun, provideDetachableRun] = DetachableRunCapability;
|
|
93
|
+
/**
|
|
94
|
+
* Whether this run's teardown DID detach — the disconnect was survived, the
|
|
95
|
+
* agent is still working, and a later attach can take the run over.
|
|
96
|
+
*
|
|
97
|
+
* The past-tense counterpart of {@link DetachableRunCapability}, and the two must
|
|
98
|
+
* not be confused:
|
|
99
|
+
*
|
|
100
|
+
* - **detachABLE** is published at `setup`, and only says a disconnect *may* be
|
|
101
|
+
* survived (a `RunStore` and a durable log are wired).
|
|
102
|
+
* - **detachED** is published on the ABORT path, by the middleware that actually
|
|
103
|
+
* makes the call — `withSandbox`'s `onAbort`, which is the only actor that has
|
|
104
|
+
* resolved BOTH out-of-band cancel bands (`AbortInfo.cancelRequested` and
|
|
105
|
+
* `wasCancelRequested` on the record) and `detachOnDisconnect`. An explicit
|
|
106
|
+
* cancel, a non-detachable disconnect, an error, and a normal finish all leave
|
|
107
|
+
* it unpublished.
|
|
108
|
+
*
|
|
109
|
+
* Its consumer is the durable DELIVERY sink in `stream-to-response.ts`: a
|
|
110
|
+
* detached run's log must stay OPEN and un-terminalized so the takeover can
|
|
111
|
+
* continue it (see `wasRunDetached` in `../../../delivery-detach`). Reading it
|
|
112
|
+
* is safe and race-free only because a `for await` over the chat stream awaits
|
|
113
|
+
* the generator's `return()` — and therefore the whole `onAbort` chain — before
|
|
114
|
+
* the sink's own `finally` runs.
|
|
115
|
+
*
|
|
116
|
+
* Read with `{ optional: true }`: absent means "not detached", which is every
|
|
117
|
+
* other exit path and every app that has not wired durability.
|
|
118
|
+
*
|
|
119
|
+
* Typed `true`, not `boolean`, for the same reason as
|
|
120
|
+
* {@link DetachableRunCapability}: absence is the only negative, so publishing
|
|
121
|
+
* `false` must not be representable.
|
|
122
|
+
*/
|
|
123
|
+
var RunDetachedCapability = createCapability()("run-detached");
|
|
124
|
+
/**
|
|
125
|
+
* Destructured accessors: `getRunDetached(ctx, { optional: true })` /
|
|
126
|
+
* `provideRunDetached(ctx, true)`.
|
|
127
|
+
*/
|
|
128
|
+
var [getRunDetached, provideRunDetached] = RunDetachedCapability;
|
|
129
|
+
/** In-memory {@link RunStore}. Single process only. */
|
|
130
|
+
var InMemoryRunStore = class {
|
|
131
|
+
runs = /* @__PURE__ */ new Map();
|
|
132
|
+
createOrResume(input) {
|
|
133
|
+
const existing = this.runs.get(input.runId);
|
|
134
|
+
if (existing) return Promise.resolve(existing);
|
|
135
|
+
const record = {
|
|
136
|
+
runId: input.runId,
|
|
137
|
+
threadId: input.threadId,
|
|
138
|
+
status: input.status ?? "running",
|
|
139
|
+
startedAt: input.startedAt
|
|
140
|
+
};
|
|
141
|
+
this.runs.set(record.runId, record);
|
|
142
|
+
return Promise.resolve(record);
|
|
143
|
+
}
|
|
144
|
+
update(runId, patch) {
|
|
145
|
+
const existing = this.runs.get(runId);
|
|
146
|
+
if (existing) this.runs.set(runId, {
|
|
147
|
+
...existing,
|
|
148
|
+
...patch
|
|
149
|
+
});
|
|
150
|
+
return Promise.resolve();
|
|
151
|
+
}
|
|
152
|
+
get(runId) {
|
|
153
|
+
return Promise.resolve(this.runs.get(runId) ?? null);
|
|
154
|
+
}
|
|
155
|
+
listByThread(threadId) {
|
|
156
|
+
const matching = [...this.runs.values()].filter((run) => run.threadId === threadId).sort((a, b) => a.startedAt - b.startedAt);
|
|
157
|
+
return Promise.resolve(matching);
|
|
158
|
+
}
|
|
159
|
+
listReclaimable(opts) {
|
|
160
|
+
const cutoff = opts.now - opts.ttlMs;
|
|
161
|
+
const matching = [...this.runs.values()].filter((run) => run.status === "running" && run.detachedSince !== void 0 && run.detachedSince <= cutoff);
|
|
162
|
+
return Promise.resolve(matching);
|
|
163
|
+
}
|
|
164
|
+
findActiveRun(threadId) {
|
|
165
|
+
let active = null;
|
|
166
|
+
for (const run of this.runs.values()) {
|
|
167
|
+
if (run.threadId !== threadId || run.status !== "running") continue;
|
|
168
|
+
if (active === null || run.startedAt > active.startedAt) active = run;
|
|
169
|
+
}
|
|
170
|
+
return Promise.resolve(active);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
//#endregion
|
|
174
|
+
export { DetachableRunCapability, InMemoryRunStore, RunDetachedCapability, defineRunStore, getDetachableRun, getRunDetached, isRunStatus, isTerminalRunStatus, provideDetachableRun, provideRunDetached };
|
|
175
|
+
|
|
176
|
+
//# sourceMappingURL=run-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-store.js","names":[],"sources":["../../../../../src/activities/chat/middleware/run-store.ts"],"sourcesContent":["/**\n * Run lifecycle types — the neutral home for what a \"run\" is.\n *\n * Shared by `@tanstack/ai-persistence` (which exposes a `runs` store through\n * `withPersistence`) and `@tanstack/ai-sandbox` (whose run driver records run\n * status). Living in core is what lets one `RunRecord` per run be shared by\n * both, instead of each package keeping its own and disagreeing. Same rationale\n * as `LockStore` (`packages/ai/src/locks.ts`), which is likewise a\n * coordination primitive that core owns so that no consumer package has to.\n */\nimport { createCapability } from './capabilities'\nimport type { TokenUsage } from '../../../types'\n\n/** A terminal run status: no further events will be appended. */\nexport type TerminalRunStatus = 'completed' | 'failed' | 'aborted'\n\n/**\n * Lifecycle status of one run (one agent turn within a conversation).\n *\n * `interrupted` is a human-in-the-loop PAUSE that interrupt-resume continues\n * from — it is deliberately NOT terminal, and must never be conflated with\n * `aborted` (an explicit cancellation).\n *\n * The two are now written by different hooks and cannot be confused:\n *\n * - `'interrupted'` is written ONLY by `withPersistence`'s `onInterrupt`, and\n * carries NO `finishedAt` (a non-terminal status has not finished).\n * - `'aborted'` is written by `withPersistence`'s `onAbort`, and only for an\n * abort that is an explicit cancel or that is ending the run for good.\n * - A mere client disconnect on a run with durable storage wired writes\n * NEITHER: the record stays `'running'` and gains `detachedSince`, because the\n * agent is still running and a later attach can take it over.\n *\n * Intent is never inferred from the abort itself — see `RUN_CANCEL_REASON` and\n * `requestRunCancel` in `../cancel`.\n */\nexport type RunStatus = 'running' | 'interrupted' | TerminalRunStatus\n\n// A Record keyed by the union is exhaustiveness-checked: adding a member to\n// TerminalRunStatus is a compile error here until this map is updated. A\n// `Set<RunStatus>` would silently answer `false` for the new member instead.\nconst TERMINAL: Record<TerminalRunStatus, true> = {\n completed: true,\n failed: true,\n aborted: true,\n}\n\n// Same exhaustiveness trick over the FULL union, for {@link isRunStatus}.\nconst ALL_STATUSES: Record<RunStatus, true> = {\n running: true,\n interrupted: true,\n completed: true,\n failed: true,\n aborted: true,\n}\n\n/**\n * Whether `value` is a {@link RunStatus} — the guard a backend validates a row\n * with at DESERIALIZATION.\n *\n * `RunStatus` is a compile-time claim about a storage column. A row arrives as\n * JSON out of D1, a Durable Object, or Postgres, and nothing in the type system\n * checked what that column actually held, so a `RunStore` implementation should\n * run its row's `status` through this before handing the record on. The readers\n * downstream act DESTRUCTIVELY on the answer — `@tanstack/ai-sandbox`'s journal\n * sweep DELETES the journal of a run it believes terminal — so a row that lies\n * about its status is not a display bug.\n */\nexport function isRunStatus(value: unknown): value is RunStatus {\n return typeof value === 'string' && Object.hasOwn(ALL_STATUSES, value)\n}\n\n/**\n * Whether `status` means no further events will be appended. Narrows, so a\n * caller inside the guard can pass `status` where a {@link TerminalRunStatus}\n * is required without a cast.\n *\n * `Object.hasOwn`, never `in`: `in` walks the prototype chain, so a row whose\n * `status` column held `'toString'` or `'constructor'` would be reported\n * terminal. `status` is TYPED `RunStatus`, but every value reaching here comes\n * off a user-implemented {@link RunStore} and the type is only a claim (see\n * {@link isRunStatus}). A false `true` deletes a live run's journal\n * (`@tanstack/ai-sandbox`'s journal sweep), fails its attach as `'terminal-run'`\n * (`attach-preflight`), and refuses to drive it (`stream-to-response.ts`).\n */\nexport function isTerminalRunStatus(\n status: RunStatus,\n): status is TerminalRunStatus {\n return Object.hasOwn(TERMINAL, status)\n}\n\n/**\n * Why a run failed.\n *\n * A bare message is an LLM provider's prose: it changes between model\n * versions and cannot be branched on. `code` is what a consumer switches over\n * to decide whether to retry, escalate, or surface a specific UI.\n */\nexport interface RunError {\n message: string\n /** Stable, machine-branchable classification, when the provider supplies one. */\n code?: string\n}\n\n/** Durable bookkeeping for a single run. */\nexport interface RunRecord {\n runId: string\n /**\n * Conversation this run belongs to — the `Scope.threadId`.\n *\n * Generation jobs (a one-shot `generate()` with no conversation) must not\n * reuse this record by faking `threadId = requestId`; they need a separate\n * job store. `withGenerationPersistence` currently does exactly that and\n * labels itself a stopgap — do not copy it.\n */\n threadId: string\n status: RunStatus\n startedAt: number\n finishedAt?: number\n error?: RunError\n usage?: TokenUsage\n /**\n * Compound sandbox key this run was bound to, when it ran in a sandbox.\n * Recorded so a future reclaimer can identify the sandbox to tear down\n * without re-deriving the key. Written by `withSandbox`'s detach path\n * (`onAbort` in `@tanstack/ai-sandbox`'s `middleware.ts`) at the same time as\n * `detachedSince`, when a disconnect leaves the run detached rather than\n * destroying the sandbox. A backend must round-trip this field — see\n * `listReclaimable` below for who eventually reads it.\n */\n sandboxKey?: string\n /**\n * Epoch ms when the last viewer detached; absent while someone is attached.\n * Written by `withSandbox`'s detach path (`onAbort` in `@tanstack/ai-sandbox`'s\n * `middleware.ts`) alongside `sandboxKey`, when a disconnect leaves the\n * agent running rather than tearing the sandbox down. A backend must\n * round-trip this field: `listReclaimable` depends on it, and\n * `@tanstack/ai-sandbox`'s `reapDetachedRuns` sweeps the candidates it\n * surfaces (see that method's doc comment).\n */\n detachedSince?: number\n /**\n * Set by an explicit out-of-band cancel, to be distinguished from a mere\n * client disconnect (the two produce an identical TCP close, so intent is not\n * inferable from the disconnect).\n *\n * Written by `requestRunCancel` and read by `wasCancelRequested` (both in\n * `../cancel`). Deliberately NOT a status: recording intent is not the same as\n * the run having stopped, and only the driver knows when it has.\n */\n cancelRequested?: boolean\n /**\n * Monotonic fencing token for the run's driver. Bumped by each host that\n * successfully claims the run (see `withRunClaim` in `@tanstack/ai-sandbox`),\n * so a superseded host can discover it lost by comparing the stored value\n * against the one it holds.\n *\n * A lock alone cannot provide this: it tells the winner it won, but gives a\n * loser nothing to read. Absent on a run that was never claimed.\n */\n driverEpoch?: number\n}\n\n/**\n * Durable store for run lifecycle records.\n *\n * REQUIRED: `createOrResume`, `update`, `get`, `findActiveRun`. Every backend\n * must implement all four — they are what the persistence middleware calls\n * unconditionally. `findActiveRun` is required rather than feature-detected\n * because a backend that has not implemented it is indistinguishable from one\n * whose answer is legitimately `null`, so reconnect would silently do nothing\n * instead of failing at build time. It was optional for exactly one release\n * cycle and cost precisely that.\n *\n * OPTIONAL: `listByThread`, `listReclaimable`. Each serves one higher-level\n * feature (thread history, reclaim reaping) and callers feature-detect them,\n * degrading gracefully when a backend omits them.\n */\nexport interface RunStore {\n /**\n * Create a run record, or return the existing one unchanged if `runId` is\n * already present.\n *\n * INVARIANT (idempotency): an existing record is returned **unchanged** and\n * the passed `threadId`/`startedAt`/`status` are ignored. This is what makes\n * resuming a run safe. `status` defaults to `'running'` on first creation.\n */\n createOrResume: (\n input: Pick<RunRecord, 'runId' | 'threadId' | 'startedAt'> & {\n status?: RunStatus\n },\n ) => Promise<RunRecord>\n /**\n * Patch a record's mutable fields.\n *\n * INVARIANT: updating an unknown `runId` is a **no-op** — it must not throw\n * and must not create a record.\n */\n update: (\n runId: string,\n patch: Partial<\n Pick<\n RunRecord,\n | 'status'\n | 'finishedAt'\n | 'error'\n | 'usage'\n | 'sandboxKey'\n | 'detachedSince'\n | 'cancelRequested'\n | 'driverEpoch'\n >\n >,\n ) => Promise<void>\n /** Current record, or null when unknown. */\n get: (runId: string) => Promise<RunRecord | null>\n /**\n * Every run in a conversation, ascending by `startedAt`. OPTIONAL: only\n * needed to render a thread's past agent activity. Consumers feature-detect.\n */\n listByThread?: (threadId: string) => Promise<Array<RunRecord>>\n /**\n * Runs that may be reclaimed: ALL THREE of `status === 'running'`,\n * `detachedSince` is set, and `detachedSince <= now - ttlMs`. The cutoff is\n * **inclusive** — a run detached at exactly `now - ttlMs` IS reclaimable.\n *\n * OPTIONAL: only needed by a reaper. Consumers feature-detect.\n *\n * `detachedSince` is populated by `withSandbox`'s detach path (see\n * {@link RunRecord.detachedSince}). The sweep over the candidates this\n * surfaces is `@tanstack/ai-sandbox`'s `reapDetachedRuns`: it finalizes a run\n * whose agent already finished, expires one past its TTL, and reclaims the\n * sandbox. That is a function, not a scheduler — the application invokes it\n * (cron, queue, `alarm()`, `waitUntil`) — and a backend that omits this\n * method cannot be reaped at all.\n */\n listReclaimable?: (opts: {\n now: number\n ttlMs: number\n }) => Promise<Array<RunRecord>>\n /**\n * The most recent `'running'` run for `threadId`, or `null` if none is active.\n *\n * REQUIRED. This resolves \"does this thread have a live run to attach to?\"\n * from the STABLE thread id, which is the durable basis for reconnecting a\n * client (a reload, or the same thread opened on another device) — independent\n * of the ephemeral run id, which a single turn may mint several of. When more\n * than one run is `'running'`, the one with the greatest `startedAt` wins.\n *\n * A backend that stubs this to `null` turns reconnect off silently, because\n * `null` is also the correct answer for an idle thread. A backend with no run\n * lifecycle at all should omit the whole `runs` store instead — capability\n * tiers belong at the store level, not the method level.\n */\n findActiveRun: (threadId: string) => Promise<RunRecord | null>\n}\n\n/**\n * Type a {@link RunStore} implementation inline: pass the object and get\n * autocomplete plus contract checking with no separate annotation. Mirrors\n * `defineLock` / `defineSandboxInstanceStore`.\n *\n * The generic return preserves the argument's own type, so an optional method\n * the implementation actually provides stays known-present on the result\n * instead of collapsing back to `| undefined` on the interface.\n */\nexport function defineRunStore<const T extends RunStore>(store: T): T {\n return store\n}\n\n/**\n * Whether the current run can be DETACHED rather than destroyed when its client\n * disconnects — `true` only when some middleware has both a {@link RunStore} and\n * a durable event log wired (`withSandbox`'s `runs` + `durability.adapter`).\n *\n * Lives in core for the same reason `LockStore` does: it is a coordination fact\n * that two consumer packages must agree on, and neither may depend on the other.\n * `@tanstack/ai-sandbox` provides it; `@tanstack/ai-persistence` reads it to\n * decide whether an abort is terminal (`'aborted'`) or a detach (write nothing).\n * A persistence → sandbox import would be a layering inversion.\n *\n * Consumers read it with `{ optional: true }`: absent means \"not detachable\",\n * which is every app that has not wired durability.\n *\n * Typed `true`, not `boolean`: ABSENCE is the negative, so a published `false`\n * has no meaning — and a consumer that tests PRESENCE rather than the value\n * would read one as \"detachable\". Narrowing the payload makes that\n * unrepresentable instead of merely undocumented.\n */\nexport const DetachableRunCapability =\n createCapability<true>()('detachable-run')\n\n/**\n * Destructured accessors: `getDetachableRun(ctx, { optional: true })` /\n * `provideDetachableRun(ctx, true)`.\n */\nexport const [getDetachableRun, provideDetachableRun] = DetachableRunCapability\n\n/**\n * Whether this run's teardown DID detach — the disconnect was survived, the\n * agent is still working, and a later attach can take the run over.\n *\n * The past-tense counterpart of {@link DetachableRunCapability}, and the two must\n * not be confused:\n *\n * - **detachABLE** is published at `setup`, and only says a disconnect *may* be\n * survived (a `RunStore` and a durable log are wired).\n * - **detachED** is published on the ABORT path, by the middleware that actually\n * makes the call — `withSandbox`'s `onAbort`, which is the only actor that has\n * resolved BOTH out-of-band cancel bands (`AbortInfo.cancelRequested` and\n * `wasCancelRequested` on the record) and `detachOnDisconnect`. An explicit\n * cancel, a non-detachable disconnect, an error, and a normal finish all leave\n * it unpublished.\n *\n * Its consumer is the durable DELIVERY sink in `stream-to-response.ts`: a\n * detached run's log must stay OPEN and un-terminalized so the takeover can\n * continue it (see `wasRunDetached` in `../../../delivery-detach`). Reading it\n * is safe and race-free only because a `for await` over the chat stream awaits\n * the generator's `return()` — and therefore the whole `onAbort` chain — before\n * the sink's own `finally` runs.\n *\n * Read with `{ optional: true }`: absent means \"not detached\", which is every\n * other exit path and every app that has not wired durability.\n *\n * Typed `true`, not `boolean`, for the same reason as\n * {@link DetachableRunCapability}: absence is the only negative, so publishing\n * `false` must not be representable.\n */\nexport const RunDetachedCapability = createCapability<true>()('run-detached')\n\n/**\n * Destructured accessors: `getRunDetached(ctx, { optional: true })` /\n * `provideRunDetached(ctx, true)`.\n */\nexport const [getRunDetached, provideRunDetached] = RunDetachedCapability\n\n/** In-memory {@link RunStore}. Single process only. */\nexport class InMemoryRunStore implements RunStore {\n private readonly runs = new Map<string, RunRecord>()\n\n createOrResume(\n input: Pick<RunRecord, 'runId' | 'threadId' | 'startedAt'> & {\n status?: RunStatus\n },\n ): Promise<RunRecord> {\n const existing = this.runs.get(input.runId)\n if (existing) return Promise.resolve(existing)\n const record: RunRecord = {\n runId: input.runId,\n threadId: input.threadId,\n status: input.status ?? 'running',\n startedAt: input.startedAt,\n }\n this.runs.set(record.runId, record)\n return Promise.resolve(record)\n }\n\n update(\n runId: string,\n patch: Partial<\n Pick<\n RunRecord,\n | 'status'\n | 'finishedAt'\n | 'error'\n | 'usage'\n | 'sandboxKey'\n | 'detachedSince'\n | 'cancelRequested'\n | 'driverEpoch'\n >\n >,\n ): Promise<void> {\n const existing = this.runs.get(runId)\n if (existing) this.runs.set(runId, { ...existing, ...patch })\n return Promise.resolve()\n }\n\n get(runId: string): Promise<RunRecord | null> {\n return Promise.resolve(this.runs.get(runId) ?? null)\n }\n\n listByThread(threadId: string): Promise<Array<RunRecord>> {\n const matching = [...this.runs.values()]\n .filter((run) => run.threadId === threadId)\n .sort((a, b) => a.startedAt - b.startedAt)\n return Promise.resolve(matching)\n }\n\n listReclaimable(opts: {\n now: number\n ttlMs: number\n }): Promise<Array<RunRecord>> {\n const cutoff = opts.now - opts.ttlMs\n const matching = [...this.runs.values()].filter(\n (run) =>\n run.status === 'running' &&\n run.detachedSince !== undefined &&\n run.detachedSince <= cutoff,\n )\n return Promise.resolve(matching)\n }\n\n findActiveRun(threadId: string): Promise<RunRecord | null> {\n let active: RunRecord | null = null\n for (const run of this.runs.values()) {\n if (run.threadId !== threadId || run.status !== 'running') continue\n if (active === null || run.startedAt > active.startedAt) active = run\n }\n return Promise.resolve(active)\n }\n}\n"],"mappings":";;;;;;;;;;;;AAyCA,IAAM,WAA4C;CAChD,WAAW;CACX,QAAQ;CACR,SAAS;AACX;AAGA,IAAM,eAAwC;CAC5C,SAAS;CACT,aAAa;CACb,WAAW;CACX,QAAQ;CACR,SAAS;AACX;;;;;;;;;;;;;AAcA,SAAgB,YAAY,OAAoC;CAC9D,OAAO,OAAO,UAAU,YAAY,OAAO,OAAO,cAAc,KAAK;AACvE;;;;;;;;;;;;;;AAeA,SAAgB,oBACd,QAC6B;CAC7B,OAAO,OAAO,OAAO,UAAU,MAAM;AACvC;;;;;;;;;;AAiLA,SAAgB,eAAyC,OAAa;CACpE,OAAO;AACT;;;;;;;;;;;;;;;;;;;;AAqBA,IAAa,0BACX,iBAAuB,CAAC,CAAC,gBAAgB;;;;;AAM3C,IAAa,CAAC,kBAAkB,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCxD,IAAa,wBAAwB,iBAAuB,CAAC,CAAC,cAAc;;;;;AAM5E,IAAa,CAAC,gBAAgB,sBAAsB;;AAGpD,IAAa,mBAAb,MAAkD;CAChD,uBAAwB,IAAI,IAAuB;CAEnD,eACE,OAGoB;EACpB,MAAM,WAAW,KAAK,KAAK,IAAI,MAAM,KAAK;EAC1C,IAAI,UAAU,OAAO,QAAQ,QAAQ,QAAQ;EAC7C,MAAM,SAAoB;GACxB,OAAO,MAAM;GACb,UAAU,MAAM;GAChB,QAAQ,MAAM,UAAU;GACxB,WAAW,MAAM;EACnB;EACA,KAAK,KAAK,IAAI,OAAO,OAAO,MAAM;EAClC,OAAO,QAAQ,QAAQ,MAAM;CAC/B;CAEA,OACE,OACA,OAae;EACf,MAAM,WAAW,KAAK,KAAK,IAAI,KAAK;EACpC,IAAI,UAAU,KAAK,KAAK,IAAI,OAAO;GAAE,GAAG;GAAU,GAAG;EAAM,CAAC;EAC5D,OAAO,QAAQ,QAAQ;CACzB;CAEA,IAAI,OAA0C;EAC5C,OAAO,QAAQ,QAAQ,KAAK,KAAK,IAAI,KAAK,KAAK,IAAI;CACrD;CAEA,aAAa,UAA6C;EACxD,MAAM,WAAW,CAAC,GAAG,KAAK,KAAK,OAAO,CAAC,CAAC,CACrC,QAAQ,QAAQ,IAAI,aAAa,QAAQ,CAAC,CAC1C,MAAM,GAAG,MAAM,EAAE,YAAY,EAAE,SAAS;EAC3C,OAAO,QAAQ,QAAQ,QAAQ;CACjC;CAEA,gBAAgB,MAGc;EAC5B,MAAM,SAAS,KAAK,MAAM,KAAK;EAC/B,MAAM,WAAW,CAAC,GAAG,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,QACtC,QACC,IAAI,WAAW,aACf,IAAI,kBAAkB,KAAA,KACtB,IAAI,iBAAiB,MACzB;EACA,OAAO,QAAQ,QAAQ,QAAQ;CACjC;CAEA,cAAc,UAA6C;EACzD,IAAI,SAA2B;EAC/B,KAAK,MAAM,OAAO,KAAK,KAAK,OAAO,GAAG;GACpC,IAAI,IAAI,aAAa,YAAY,IAAI,WAAW,WAAW;GAC3D,IAAI,WAAW,QAAQ,IAAI,YAAY,OAAO,WAAW,SAAS;EACpE;EACA,OAAO,QAAQ,QAAQ,MAAM;CAC/B;AACF"}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { createCapability } from "./capabilities.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
//#region src/activities/chat/middleware/sandbox-runtime.ts
|
|
3
|
+
/**
|
|
4
|
+
* Internal runtime seam the chat engine PROVIDES so the sandbox middleware can
|
|
5
|
+
* surface file events without a public ctx method. `emit` runs every
|
|
6
|
+
* middleware's `sandbox` hooks AND emits a CUSTOM `sandbox.file` chunk into the
|
|
7
|
+
* stream; `logger` lets the sandbox layer log under the `sandbox` debug
|
|
8
|
+
* category. Consumed (optionally) by `withSandbox` in `@tanstack/ai-sandbox`.
|
|
9
|
+
*/
|
|
10
|
+
var SandboxRuntimeCapability = createCapability()("sandbox-runtime");
|
|
11
|
+
var [getSandboxRuntime, provideSandboxRuntime] = SandboxRuntimeCapability;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { SandboxRuntimeCapability, getSandboxRuntime, provideSandboxRuntime };
|
|
14
|
+
|
|
15
|
+
//# sourceMappingURL=sandbox-runtime.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox-runtime.js","sources":["../../../../../src/activities/chat/middleware/sandbox-runtime.ts"],"sourcesContent":["/**\n * Internal runtime seam the chat engine PROVIDES so the sandbox middleware can\n * surface file events without a public ctx method. `emit` runs every\n * middleware's `sandbox` hooks AND emits a CUSTOM `sandbox.file` chunk into the\n * stream; `logger` lets the sandbox layer log under the `sandbox` debug\n * category. Consumed (optionally) by `withSandbox` in `@tanstack/ai-sandbox`.\n */\nimport { createCapability } from './capabilities'\nimport type { InternalLogger } from '../../../logger/internal-logger'\nimport type { SandboxFileHookEvent } from './types'\n\nexport interface SandboxRuntime {\n emit: (event: SandboxFileHookEvent) => void\n /** Emit an opt-in per-file `sandbox.file.diff` CUSTOM chunk. */\n emitFileDiff: (value: { path: string; diff: string }) => void\n logger: InternalLogger\n}\n\nexport const SandboxRuntimeCapability =\n createCapability<SandboxRuntime>()('sandbox-runtime')\n\nexport const [getSandboxRuntime, provideSandboxRuntime] =\n SandboxRuntimeCapability\n"],"
|
|
1
|
+
{"version":3,"file":"sandbox-runtime.js","names":[],"sources":["../../../../../src/activities/chat/middleware/sandbox-runtime.ts"],"sourcesContent":["/**\n * Internal runtime seam the chat engine PROVIDES so the sandbox middleware can\n * surface file events without a public ctx method. `emit` runs every\n * middleware's `sandbox` hooks AND emits a CUSTOM `sandbox.file` chunk into the\n * stream; `logger` lets the sandbox layer log under the `sandbox` debug\n * category. Consumed (optionally) by `withSandbox` in `@tanstack/ai-sandbox`.\n */\nimport { createCapability } from './capabilities'\nimport type { InternalLogger } from '../../../logger/internal-logger'\nimport type { SandboxFileHookEvent } from './types'\n\nexport interface SandboxRuntime {\n emit: (event: SandboxFileHookEvent) => void\n /** Emit an opt-in per-file `sandbox.file.diff` CUSTOM chunk. */\n emitFileDiff: (value: { path: string; diff: string }) => void\n logger: InternalLogger\n}\n\nexport const SandboxRuntimeCapability =\n createCapability<SandboxRuntime>()('sandbox-runtime')\n\nexport const [getSandboxRuntime, provideSandboxRuntime] =\n SandboxRuntimeCapability\n"],"mappings":";;;;;;;;;AAkBA,IAAa,2BACX,iBAAiC,CAAC,CAAC,iBAAiB;AAEtD,IAAa,CAAC,mBAAmB,yBAC/B"}
|