@tanstack/ai 0.42.0 → 0.43.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.
- 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.js +598 -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 +1 -0
- 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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AnyServerTool } from '../tools/tool-definition'
|
|
2
2
|
import type { ChatMCPOptions, MCPToolSource } from './types'
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -18,7 +18,7 @@ import type { ChatMCPOptions, MCPToolSource } from './types'
|
|
|
18
18
|
* drains. If discovery results were ever cached and reused across runs, this
|
|
19
19
|
* would bind a closure over an already-closed source — bind onto a copy then.
|
|
20
20
|
*/
|
|
21
|
-
function bindReadResource(tool:
|
|
21
|
+
function bindReadResource(tool: AnyServerTool, source: MCPToolSource): void {
|
|
22
22
|
if (!source.readResource) return
|
|
23
23
|
const meta = (
|
|
24
24
|
tool.metadata as { mcp?: { uiResourceUri?: string } } | undefined
|
|
@@ -71,13 +71,13 @@ export class MCPManager {
|
|
|
71
71
|
* (no `onDiscoveryError`, or it re-threw) or a duplicate tool name; in that
|
|
72
72
|
* case it first closes any connected sources when the policy is 'close'.
|
|
73
73
|
*/
|
|
74
|
-
async discover(): Promise<Array<
|
|
74
|
+
async discover(): Promise<Array<AnyServerTool>> {
|
|
75
75
|
if (this.#sources.length === 0) return []
|
|
76
76
|
try {
|
|
77
77
|
const settled = await Promise.allSettled(
|
|
78
78
|
this.#sources.map((s) => s.tools({ lazy: this.#lazyTools })),
|
|
79
79
|
)
|
|
80
|
-
const tools: Array<
|
|
80
|
+
const tools: Array<AnyServerTool> = []
|
|
81
81
|
const zipped = this.#sources.map(
|
|
82
82
|
(source, i) => [source, settled[i]] as const,
|
|
83
83
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AnyServerTool } from '../tools/tool-definition'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The shape `readResource` resolves to — a structural subset of MCP's
|
|
@@ -26,7 +26,7 @@ export interface MCPToolSource {
|
|
|
26
26
|
// Keep the options shape in sync with ai-mcp's `ToolsOptions` — extra
|
|
27
27
|
// optional fields added there still match structurally, but chat() only
|
|
28
28
|
// forwards what is declared here.
|
|
29
|
-
tools: (options?: { lazy?: boolean }) => Promise<Array<
|
|
29
|
+
tools: (options?: { lazy?: boolean }) => Promise<Array<AnyServerTool>>
|
|
30
30
|
close: () => Promise<void>
|
|
31
31
|
/**
|
|
32
32
|
* Reads an MCP resource by URI. Used by the chat manager to eagerly fetch
|
|
@@ -211,6 +211,7 @@ function isToolCallIncluded(part: ToolCallPart): boolean {
|
|
|
211
211
|
return (
|
|
212
212
|
part.state === 'input-complete' ||
|
|
213
213
|
part.state === 'complete' ||
|
|
214
|
+
part.state === 'approval-requested' ||
|
|
214
215
|
part.state === 'approval-responded' ||
|
|
215
216
|
part.state === 'error' ||
|
|
216
217
|
part.output !== undefined
|
|
@@ -614,12 +615,13 @@ export function modelMessagesToUIMessages(
|
|
|
614
615
|
})
|
|
615
616
|
} else {
|
|
616
617
|
// No assistant message to merge into, create a standalone one
|
|
617
|
-
const toolResultUIMessage = modelMessageToUIMessage(msg)
|
|
618
|
+
const toolResultUIMessage = modelMessageToUIMessage(msg, msg.id)
|
|
618
619
|
uiMessages.push(toolResultUIMessage)
|
|
619
620
|
}
|
|
620
621
|
} else {
|
|
621
|
-
// Regular message
|
|
622
|
-
|
|
622
|
+
// Regular message. Preserve a persisted stable id so a hydrated message
|
|
623
|
+
// keeps the same identity as its live stream (enables in-place resume).
|
|
624
|
+
const uiMessage = modelMessageToUIMessage(msg, msg.id)
|
|
623
625
|
uiMessages.push(uiMessage)
|
|
624
626
|
|
|
625
627
|
// Track assistant messages for potential tool result merging
|
|
@@ -104,6 +104,6 @@ export function createChatMiddleware(): ChatMiddlewareBuilder<
|
|
|
104
104
|
// object reused across `.use()` calls, but the type accumulates `TProvided`
|
|
105
105
|
// and `TList` per call — TypeScript cannot derive that from runtime values, so
|
|
106
106
|
// a structural `as` is impossible and the double assertion is irreducible.
|
|
107
|
-
//
|
|
107
|
+
// oxlint-disable-next-line eslint-js/no-restricted-syntax -- irreducible: type-level accumulation cannot be expressed from a single runtime object
|
|
108
108
|
return builder as unknown as ChatMiddlewareBuilder<readonly [], never>
|
|
109
109
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { aiEventClient } from '@tanstack/ai-event-client'
|
|
2
|
-
import type { StreamChunk } from '../../../types'
|
|
2
|
+
import type { AgentLoopState, StreamChunk } from '../../../types'
|
|
3
3
|
import type { InternalLogger } from '../../../logger/internal-logger'
|
|
4
4
|
import type {
|
|
5
5
|
AbortInfo,
|
|
@@ -18,6 +18,12 @@ import type {
|
|
|
18
18
|
UsageInfo,
|
|
19
19
|
} from './types'
|
|
20
20
|
|
|
21
|
+
/** One middleware's terminal-hook throw, captured instead of propagated. */
|
|
22
|
+
interface HookFailure {
|
|
23
|
+
middleware: string
|
|
24
|
+
error: unknown
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
/** Check if a middleware should be skipped for instrumentation events. */
|
|
22
28
|
function shouldSkipInstrumentation(mw: ChatMiddleware<any>): boolean {
|
|
23
29
|
return mw.name === 'devtools' || mw.name === 'strip-to-spec'
|
|
@@ -474,18 +480,106 @@ export class MiddlewareRunner<TContext = unknown> {
|
|
|
474
480
|
}
|
|
475
481
|
}
|
|
476
482
|
|
|
483
|
+
/**
|
|
484
|
+
* Await ONE terminal hook and RETURN its throw instead of letting it escape
|
|
485
|
+
* the caller's loop, logging it on the `errors` channel first so the failure
|
|
486
|
+
* is never invisible. `undefined` means the hook completed.
|
|
487
|
+
*
|
|
488
|
+
* Capturing (rather than swallowing at this level) is what lets isolation and
|
|
489
|
+
* reporting coexist: every caller gives every middleware its turn, and then
|
|
490
|
+
* each decides on its own whether the collected failures are worth telling the
|
|
491
|
+
* caller about. See {@link runOnFinish} vs {@link runOnAbort} /
|
|
492
|
+
* {@link runOnError}.
|
|
493
|
+
*/
|
|
494
|
+
private async captureTerminalHook(
|
|
495
|
+
mw: ChatMiddleware<TContext>,
|
|
496
|
+
hookName: 'onFinish' | 'onAbort' | 'onError',
|
|
497
|
+
invoke: () => void | Promise<void>,
|
|
498
|
+
): Promise<HookFailure | undefined> {
|
|
499
|
+
try {
|
|
500
|
+
await invoke()
|
|
501
|
+
return undefined
|
|
502
|
+
} catch (error) {
|
|
503
|
+
this.logger.errors(`middleware ${hookName} hook failed`, {
|
|
504
|
+
middleware: mw.name ?? 'unnamed',
|
|
505
|
+
hook: hookName,
|
|
506
|
+
error,
|
|
507
|
+
})
|
|
508
|
+
return { middleware: mw.name ?? 'unnamed', error }
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
477
512
|
/**
|
|
478
513
|
* Run onFinish on all middleware in order.
|
|
514
|
+
*
|
|
515
|
+
* ISOLATED **and** REPORTED. `onFinish` is the only terminal fan-out on the
|
|
516
|
+
* SUCCESS path, and it is where `withPersistence.onFinish` writes the
|
|
517
|
+
* assistant turn through the store. So the two properties are needed together
|
|
518
|
+
* and neither may be traded for the other:
|
|
519
|
+
*
|
|
520
|
+
* - ISOLATION: every middleware's hook runs even if an earlier one threw, so a
|
|
521
|
+
* transient store error cannot skip a later middleware's own bookkeeping.
|
|
522
|
+
* Each failure is captured by {@link captureTerminalHook}, not propagated
|
|
523
|
+
* mid-loop.
|
|
524
|
+
* - REPORTING: after the loop, the failures are rethrown. `chat()`'s catch
|
|
525
|
+
* treats what we throw as a genuine error (it is not a
|
|
526
|
+
* `MiddlewareAbortError`, and `structuralInterruptFailure` does not match
|
|
527
|
+
* it) and rethrows it out of the generator.
|
|
528
|
+
*
|
|
529
|
+
* What that rethrow can and cannot achieve depends on the transport, because
|
|
530
|
+
* this fan-out is awaited AFTER the adapter's `RUN_FINISHED` has already been
|
|
531
|
+
* yielded (`chat()` yields terminal chunks while streaming, then awaits this
|
|
532
|
+
* hook on its way out). The success terminal is therefore already gone; the
|
|
533
|
+
* rethrow can only append to what the consumer saw, never retract it:
|
|
534
|
+
*
|
|
535
|
+
* - NON-DURABLE transport: the throw escapes the generator mid-response, and
|
|
536
|
+
* the SSE / HTTP-stream encoder turns it into a TRAILING `RUN_ERROR` on the
|
|
537
|
+
* wire carrying the store's own message and `code`. `ai-client` surfaces
|
|
538
|
+
* that as an error status, so the user is not told the turn was saved when
|
|
539
|
+
* it was not.
|
|
540
|
+
* - DURABLE transport: the throw reaches the durability sink instead. The
|
|
541
|
+
* terminal was already persisted AND forwarded, so the sink deliberately
|
|
542
|
+
* does NOT append a second, contradictory terminal, and `terminalForwarded`
|
|
543
|
+
* (see `stream-to-response.ts`) suppresses the rethrow to the live consumer.
|
|
544
|
+
* The `RUN_FINISHED` stands and the failure is RECORDED SERVER-SIDE on the
|
|
545
|
+
* sink's `errors` channel. That is the intended outcome, not a gap: the save
|
|
546
|
+
* failed, not the run — the consumer did receive the complete stream, so
|
|
547
|
+
* telling it the run errored would be the lie. What the rethrow buys here is
|
|
548
|
+
* that the sink sees the failure at all; while this loop swallowed, the only
|
|
549
|
+
* trace anywhere was {@link captureTerminalHook}'s log line.
|
|
550
|
+
*
|
|
551
|
+
* Either way, swallowing is the one option ruled out: a failed
|
|
552
|
+
* `messages.append` would otherwise leave a `completed` run record with the
|
|
553
|
+
* assistant turn missing from storage and nothing beyond a middleware log
|
|
554
|
+
* line, and the client would go on to send a history the server has no record
|
|
555
|
+
* of.
|
|
556
|
+
*
|
|
557
|
+
* A single failure is rethrown AS-IS so the store's own error — its message,
|
|
558
|
+
* `cause`, `code` and `instanceof` identity — is what reaches the caller and
|
|
559
|
+
* the wire; wrapping the common case would bury it. Two or more become an
|
|
560
|
+
* `AggregateError` (never a `MiddlewareAbortError`, so it cannot be mistaken
|
|
561
|
+
* for an abort) rather than picking a winner and dropping the rest.
|
|
479
562
|
*/
|
|
480
563
|
async runOnFinish(
|
|
481
564
|
ctx: ChatMiddlewareContext<TContext>,
|
|
482
565
|
info: FinishInfo,
|
|
483
566
|
): Promise<void> {
|
|
567
|
+
const failures: Array<HookFailure> = []
|
|
568
|
+
let firstFailure: HookFailure | undefined
|
|
569
|
+
|
|
484
570
|
for (const mw of this.middlewares) {
|
|
485
|
-
|
|
571
|
+
const hook = mw.onFinish
|
|
572
|
+
if (hook) {
|
|
486
573
|
const skip = shouldSkipInstrumentation(mw)
|
|
487
574
|
const start = Date.now()
|
|
488
|
-
await
|
|
575
|
+
const failure = await this.captureTerminalHook(mw, 'onFinish', () =>
|
|
576
|
+
hook.call(mw, ctx, info),
|
|
577
|
+
)
|
|
578
|
+
if (failure !== undefined) {
|
|
579
|
+
firstFailure ??= failure
|
|
580
|
+
failures.push(failure)
|
|
581
|
+
continue
|
|
582
|
+
}
|
|
489
583
|
if (!skip) {
|
|
490
584
|
this.logger.middleware(
|
|
491
585
|
`hook=onFinish middleware=${mw.name ?? 'unnamed'}`,
|
|
@@ -502,21 +596,49 @@ export class MiddlewareRunner<TContext = unknown> {
|
|
|
502
596
|
}
|
|
503
597
|
}
|
|
504
598
|
}
|
|
599
|
+
|
|
600
|
+
if (firstFailure !== undefined) {
|
|
601
|
+
throw failures.length === 1
|
|
602
|
+
? firstFailure.error
|
|
603
|
+
: new AggregateError(
|
|
604
|
+
failures.map((f) => f.error),
|
|
605
|
+
`${failures.length} middleware onFinish hooks failed: ` +
|
|
606
|
+
failures.map((f) => f.middleware).join(', '),
|
|
607
|
+
)
|
|
608
|
+
}
|
|
505
609
|
}
|
|
506
610
|
|
|
507
611
|
/**
|
|
508
612
|
* Run onAbort on all middleware in order.
|
|
613
|
+
*
|
|
614
|
+
* ISOLATED and DELIBERATELY SWALLOWED. `onAbort` is a pure teardown fan-out
|
|
615
|
+
* released from `chat()`'s `finally`, on a path where the outcome is already
|
|
616
|
+
* decided: the run stopped, and the caller is being told why. A throw here has
|
|
617
|
+
* nothing better to report than the abort reason it would DISPLACE — the
|
|
618
|
+
* `finally` would surface a flaky store's error in place of "client
|
|
619
|
+
* disconnected" — so failures are logged on the `errors` channel and go no
|
|
620
|
+
* further. That is not a silent failure; it is refusing to let teardown
|
|
621
|
+
* rewrite an outcome it did not produce.
|
|
622
|
+
*
|
|
623
|
+
* Isolation matters independently: these hooks release PER-MIDDLEWARE
|
|
624
|
+
* resources (`withSandbox.onAbort` detaches or destroys the sandbox and stamps
|
|
625
|
+
* `detachedSince`; `withPersistence.onAbort` records the run status through the
|
|
626
|
+
* store), so an unguarded loop turns one transient store error into a
|
|
627
|
+
* permanently leaked sandbox for every middleware ordered after it.
|
|
509
628
|
*/
|
|
510
629
|
async runOnAbort(
|
|
511
630
|
ctx: ChatMiddlewareContext<TContext>,
|
|
512
631
|
info: AbortInfo,
|
|
513
632
|
): Promise<void> {
|
|
514
633
|
for (const mw of this.middlewares) {
|
|
515
|
-
|
|
634
|
+
const hook = mw.onAbort
|
|
635
|
+
if (hook) {
|
|
516
636
|
const skip = shouldSkipInstrumentation(mw)
|
|
517
637
|
const start = Date.now()
|
|
518
|
-
await
|
|
519
|
-
|
|
638
|
+
const failure = await this.captureTerminalHook(mw, 'onAbort', () =>
|
|
639
|
+
hook.call(mw, ctx, info),
|
|
640
|
+
)
|
|
641
|
+
if (failure === undefined && !skip) {
|
|
520
642
|
this.logger.middleware(
|
|
521
643
|
`hook=onAbort middleware=${mw.name ?? 'unnamed'}`,
|
|
522
644
|
{ middleware: mw.name ?? 'unnamed', hook: 'onAbort' },
|
|
@@ -536,17 +658,32 @@ export class MiddlewareRunner<TContext = unknown> {
|
|
|
536
658
|
|
|
537
659
|
/**
|
|
538
660
|
* Run onError on all middleware in order.
|
|
661
|
+
*
|
|
662
|
+
* ISOLATED and DELIBERATELY SWALLOWED, for the same reason as
|
|
663
|
+
* {@link runOnAbort} and NOT merely because it is teardown: the run has
|
|
664
|
+
* already failed, `info.error` IS that failure, and `chat()` rethrows it to the
|
|
665
|
+
* caller the moment this fan-out returns. A propagated hook throw could only
|
|
666
|
+
* REPLACE the run's real error with a teardown artifact — strictly less
|
|
667
|
+
* information for the caller, who is already learning the run failed. Reporting
|
|
668
|
+
* would buy nothing and cost the diagnosis, so failures are logged on the
|
|
669
|
+
* `errors` channel and stop there.
|
|
670
|
+
*
|
|
671
|
+
* Contrast {@link runOnFinish}, where nothing else is telling the caller
|
|
672
|
+
* anything is wrong — which is why that one reports.
|
|
539
673
|
*/
|
|
540
674
|
async runOnError(
|
|
541
675
|
ctx: ChatMiddlewareContext<TContext>,
|
|
542
676
|
info: ErrorInfo,
|
|
543
677
|
): Promise<void> {
|
|
544
678
|
for (const mw of this.middlewares) {
|
|
545
|
-
|
|
679
|
+
const hook = mw.onError
|
|
680
|
+
if (hook) {
|
|
546
681
|
const skip = shouldSkipInstrumentation(mw)
|
|
547
682
|
const start = Date.now()
|
|
548
|
-
await
|
|
549
|
-
|
|
683
|
+
const failure = await this.captureTerminalHook(mw, 'onError', () =>
|
|
684
|
+
hook.call(mw, ctx, info),
|
|
685
|
+
)
|
|
686
|
+
if (failure === undefined && !skip) {
|
|
550
687
|
this.logger.middleware(
|
|
551
688
|
`hook=onError middleware=${mw.name ?? 'unnamed'}`,
|
|
552
689
|
{ middleware: mw.name ?? 'unnamed', hook: 'onError' },
|
|
@@ -595,6 +732,46 @@ export class MiddlewareRunner<TContext = unknown> {
|
|
|
595
732
|
}
|
|
596
733
|
}
|
|
597
734
|
|
|
735
|
+
/**
|
|
736
|
+
* Run onShouldContinue through middleware in order (AND semantics).
|
|
737
|
+
* Any explicit `false` stops further iterations; `true` / void / undefined pass.
|
|
738
|
+
* Called after `agentLoopStrategy` has already approved continuation.
|
|
739
|
+
*/
|
|
740
|
+
async runOnShouldContinue(
|
|
741
|
+
ctx: ChatMiddlewareContext<TContext>,
|
|
742
|
+
state: AgentLoopState,
|
|
743
|
+
): Promise<boolean> {
|
|
744
|
+
for (const mw of this.middlewares) {
|
|
745
|
+
if (mw.onShouldContinue) {
|
|
746
|
+
const skip = shouldSkipInstrumentation(mw)
|
|
747
|
+
const start = Date.now()
|
|
748
|
+
const result = await mw.onShouldContinue(ctx, state)
|
|
749
|
+
if (!skip) {
|
|
750
|
+
this.logger.middleware(
|
|
751
|
+
`hook=onShouldContinue middleware=${mw.name ?? 'unnamed'}`,
|
|
752
|
+
{
|
|
753
|
+
middleware: mw.name ?? 'unnamed',
|
|
754
|
+
hook: 'onShouldContinue',
|
|
755
|
+
result,
|
|
756
|
+
},
|
|
757
|
+
)
|
|
758
|
+
aiEventClient.emit('middleware:hook:executed', {
|
|
759
|
+
...instrumentCtx(ctx),
|
|
760
|
+
middlewareName: mw.name || 'unnamed',
|
|
761
|
+
hookName: 'onShouldContinue',
|
|
762
|
+
iteration: ctx.iteration,
|
|
763
|
+
duration: Date.now() - start,
|
|
764
|
+
hasTransform: result === false,
|
|
765
|
+
})
|
|
766
|
+
}
|
|
767
|
+
if (result === false) {
|
|
768
|
+
return false
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return true
|
|
773
|
+
}
|
|
774
|
+
|
|
598
775
|
/**
|
|
599
776
|
* Run onToolPhaseComplete on all middleware in order.
|
|
600
777
|
* Called after all tool calls in an iteration have been processed.
|
|
@@ -3,6 +3,8 @@ export type {
|
|
|
3
3
|
ChatMiddlewareContext,
|
|
4
4
|
ChatMiddlewarePhase,
|
|
5
5
|
ChatMiddlewareConfig,
|
|
6
|
+
ChatResumeToolState,
|
|
7
|
+
ChatResumeGenericResolution,
|
|
6
8
|
StructuredOutputMiddlewareConfig,
|
|
7
9
|
ToolCallHookContext,
|
|
8
10
|
BeforeToolCallDecision,
|
|
@@ -39,3 +41,27 @@ export type {
|
|
|
39
41
|
} from './builder'
|
|
40
42
|
export { validateCapabilities } from './validate'
|
|
41
43
|
export type { AnyChatMiddleware } from './types'
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
LocksCapability,
|
|
47
|
+
getLocks,
|
|
48
|
+
provideLocks,
|
|
49
|
+
InMemoryLockStore,
|
|
50
|
+
withLocks,
|
|
51
|
+
defineLock,
|
|
52
|
+
} from './locks'
|
|
53
|
+
export type { LockStore } from './locks'
|
|
54
|
+
|
|
55
|
+
export {
|
|
56
|
+
isRunStatus,
|
|
57
|
+
isTerminalRunStatus,
|
|
58
|
+
defineRunStore,
|
|
59
|
+
InMemoryRunStore,
|
|
60
|
+
} from './run-store'
|
|
61
|
+
export type {
|
|
62
|
+
RunStatus,
|
|
63
|
+
TerminalRunStatus,
|
|
64
|
+
RunRecord,
|
|
65
|
+
RunError,
|
|
66
|
+
RunStore,
|
|
67
|
+
} from './run-store'
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Distributed-mutex primitive — the neutral home for the `'locks'` capability.
|
|
3
|
+
*
|
|
4
|
+
* Capability identity is by object reference (see `createCapability`). Any
|
|
5
|
+
* middleware may PROVIDE a {@link LockStore} via {@link withLocks} /
|
|
6
|
+
* {@link provideLocks}; consumers (notably `@tanstack/ai-sandbox` `ensure`)
|
|
7
|
+
* read it with {@link getLocks}. Coordination, not state persistence.
|
|
8
|
+
*/
|
|
9
|
+
import { createCapability } from './capabilities'
|
|
10
|
+
import { defineChatMiddleware } from './define'
|
|
11
|
+
import type { ChatMiddleware, ChatMiddlewareContext } from './types'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Mutual exclusion around a critical section keyed by `key`. A distributed
|
|
15
|
+
* backend (e.g. a Cloudflare Durable Object) is the only kind safe across
|
|
16
|
+
* instances; the in-memory default is correct within a single process only.
|
|
17
|
+
* Lease-backed implementations abort `signal` as soon as ownership can no longer
|
|
18
|
+
* be guaranteed; the callback must stop externally visible mutations when it
|
|
19
|
+
* aborts. Callbacks that ignore `signal` (e.g. the sandbox `ensure` critical
|
|
20
|
+
* section) remain valid — a `() => Promise<T>` is assignable to the
|
|
21
|
+
* signal-taking parameter.
|
|
22
|
+
*/
|
|
23
|
+
export interface LockStore {
|
|
24
|
+
withLock: <T>(
|
|
25
|
+
key: string,
|
|
26
|
+
fn: (signal: AbortSignal) => Promise<T>,
|
|
27
|
+
) => Promise<T>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Type a {@link LockStore} implementation inline: pass the object and get
|
|
32
|
+
* autocomplete + contract checking, with no separate `: LockStore` annotation.
|
|
33
|
+
* Hand the result to {@link withLocks}.
|
|
34
|
+
*/
|
|
35
|
+
export function defineLock(lock: LockStore): LockStore {
|
|
36
|
+
return lock
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The lock capability. Provided by {@link withLocks} or any middleware that
|
|
41
|
+
* calls {@link provideLocks}.
|
|
42
|
+
*/
|
|
43
|
+
export const LocksCapability = createCapability<LockStore>()('locks')
|
|
44
|
+
|
|
45
|
+
/** Destructured accessors: `getLocks(ctx)` / `provideLocks(ctx, store)`. */
|
|
46
|
+
export const [getLocks, provideLocks] = LocksCapability
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* In-memory {@link LockStore} — a per-key promise chain. Correct within a single
|
|
50
|
+
* process; multi-instance correctness needs a distributed lock backend.
|
|
51
|
+
*/
|
|
52
|
+
export class InMemoryLockStore implements LockStore {
|
|
53
|
+
private readonly chains = new Map<string, Promise<unknown>>()
|
|
54
|
+
|
|
55
|
+
withLock<T>(
|
|
56
|
+
key: string,
|
|
57
|
+
fn: (signal: AbortSignal) => Promise<T>,
|
|
58
|
+
): Promise<T> {
|
|
59
|
+
const prior = this.chains.get(key) ?? Promise.resolve()
|
|
60
|
+
const runCriticalSection = () => fn(new AbortController().signal)
|
|
61
|
+
// Chain after the prior holder regardless of how it settled.
|
|
62
|
+
const run = prior.then(runCriticalSection, runCriticalSection)
|
|
63
|
+
// Swallow rejections so one failure doesn't poison the lock, then drop the
|
|
64
|
+
// chain entry once this tail is still the latest — otherwise long-lived
|
|
65
|
+
// processes accumulate settled promises for every distinct key forever.
|
|
66
|
+
const settled = run.then(
|
|
67
|
+
() => undefined,
|
|
68
|
+
() => undefined,
|
|
69
|
+
)
|
|
70
|
+
this.chains.set(key, settled)
|
|
71
|
+
void settled.then(() => {
|
|
72
|
+
if (this.chains.get(key) === settled) {
|
|
73
|
+
this.chains.delete(key)
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
return run
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Provide a {@link LockStore} on the chat middleware capability bus.
|
|
82
|
+
*
|
|
83
|
+
* Coordination only — independent of chat state persistence. A lock provided
|
|
84
|
+
* here reaches any later middleware that reads {@link LocksCapability}
|
|
85
|
+
* (including `withSandbox`).
|
|
86
|
+
*
|
|
87
|
+
* ```ts
|
|
88
|
+
* middleware: [
|
|
89
|
+
* withLocks(distributedLocks),
|
|
90
|
+
* withSandbox(sandbox),
|
|
91
|
+
* ]
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export function withLocks(locks: LockStore): ChatMiddleware {
|
|
95
|
+
return defineChatMiddleware({
|
|
96
|
+
name: 'locks',
|
|
97
|
+
provides: [LocksCapability],
|
|
98
|
+
setup(ctx: ChatMiddlewareContext) {
|
|
99
|
+
provideLocks(ctx, locks)
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal seam that lets a SLOW middleware ask the persistence layer to store
|
|
3
|
+
* the user's pending turn NOW, before that middleware starts its slow work.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. Chat persistence stores the pending turn from `onStart`, which
|
|
6
|
+
* is the earliest hook that holds the merged message list. `onStart` runs after
|
|
7
|
+
* EVERY middleware `setup`, and that is normally a few milliseconds. `withSandbox`
|
|
8
|
+
* breaks the assumption: its `setup` creates a sandbox and clones a repository,
|
|
9
|
+
* which takes minutes. For that whole window the thread holds nothing, so a reload
|
|
10
|
+
* — or a second device — asks the server for the conversation and is told it is
|
|
11
|
+
* empty. The user sees no sign of the message they just sent.
|
|
12
|
+
*
|
|
13
|
+
* The provider owns the rule for WHAT to store. A caller must not rebuild that
|
|
14
|
+
* rule: `saveThread` replaces the whole thread, so a caller that stored only the
|
|
15
|
+
* newly-sent message would delete the history. Asking the owner to store the turn
|
|
16
|
+
* keeps the merge in one place.
|
|
17
|
+
*
|
|
18
|
+
* OPT-IN, so nothing changes for a fast run. Persistence offers this seam on every
|
|
19
|
+
* durable run; only a middleware that is about to be slow calls it. A run with no
|
|
20
|
+
* such middleware never calls it, and the `onStart` store stays the only one.
|
|
21
|
+
*
|
|
22
|
+
* A SEAM RATHER THAN A NEW MIDDLEWARE HOOK, deliberately. A lifecycle hook that
|
|
23
|
+
* runs before `setup` would be public API, and it would have to explain itself to
|
|
24
|
+
* every middleware author. This concern has one provider (`withPersistence`) and
|
|
25
|
+
* one caller (`withSandbox`), and it reaches across packages through the same
|
|
26
|
+
* internal channel the sandbox layer already uses for its runtime.
|
|
27
|
+
*/
|
|
28
|
+
import { createCapability } from './capabilities'
|
|
29
|
+
|
|
30
|
+
export interface PendingTurnSnapshot {
|
|
31
|
+
/**
|
|
32
|
+
* Store the user's pending turn now.
|
|
33
|
+
*
|
|
34
|
+
* Idempotent: the later `onStart` store replaces the thread with the same or a
|
|
35
|
+
* more complete list, so calling this changes what is visible EARLIER without
|
|
36
|
+
* changing what is visible at the end.
|
|
37
|
+
*
|
|
38
|
+
* Rejects only if the store itself fails. Callers treat that as non-fatal — a
|
|
39
|
+
* run that cannot pre-store its turn is still a run worth doing.
|
|
40
|
+
*/
|
|
41
|
+
snapshot: () => Promise<void>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const PendingTurnCapability =
|
|
45
|
+
createCapability<PendingTurnSnapshot>()('pending-turn')
|
|
46
|
+
|
|
47
|
+
export const [getPendingTurn, providePendingTurn] = PendingTurnCapability
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal seam the chat engine PROVIDES so middleware can learn that the
|
|
3
|
+
* DELIVERY socket closed while the run is still going — and, crucially, learn it
|
|
4
|
+
* WITHOUT the run being cancelled.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS EXISTS. Before it, the only way a disconnect reached middleware was for
|
|
7
|
+
* the application to mirror `request.signal` into `chat()`'s `abortController`,
|
|
8
|
+
* which ABORTS THE RUN. For a durable run that is precisely wrong, and wrong in
|
|
9
|
+
* the most expensive direction: `chat()` returns at its `isCancelled()` check
|
|
10
|
+
* immediately after middleware `setup`, so the harness adapter's `chatStream` is
|
|
11
|
+
* never called and the agent in the sandbox that `setup` just spent minutes
|
|
12
|
+
* creating is NEVER LAUNCHED. The user switched away during "starting the
|
|
13
|
+
* sandbox", came back, and found an empty log belonging to a run that had done
|
|
14
|
+
* nothing — with no takeover able to recover it, because an agent that never
|
|
15
|
+
* started wrote no journal to replay. Applications were forced to choose between
|
|
16
|
+
* "the middleware learns about the disconnect" and "the run survives it".
|
|
17
|
+
*
|
|
18
|
+
* So a disconnect is delivered as a NOTIFICATION. `withSandbox` uses it to stamp
|
|
19
|
+
* `detachedSince`/`sandboxKey` and publish the detach verdict while the run keeps
|
|
20
|
+
* producing into its still-open durable log — which is exactly what a re-attaching
|
|
21
|
+
* client tails to catch up.
|
|
22
|
+
*
|
|
23
|
+
* A SUBSCRIPTION RATHER THAN A MIDDLEWARE HOOK, deliberately. `ChatMiddleware` is
|
|
24
|
+
* public API and a new lifecycle hook there is a permanent commitment — including
|
|
25
|
+
* the obligation to explain that it is the one hook that is NOT terminal. This
|
|
26
|
+
* concern has exactly one consumer in the tree (`withSandbox`) and it reaches it
|
|
27
|
+
* through the same internal channel the sandbox layer already uses for its runtime
|
|
28
|
+
* (`SandboxRuntimeCapability`), so it ships with no public surface at all.
|
|
29
|
+
*
|
|
30
|
+
* DISPATCHED FROM THE TRANSPORT, NOT FROM THE RUN'S UNWINDING. That is what makes
|
|
31
|
+
* it prompt. A run suspended inside a minutes-wide `setup` cannot dispatch anything
|
|
32
|
+
* from its own `finally`, because the `finally` is reached only once the generator
|
|
33
|
+
* unwinds — which is what made `detachedSince` land three minutes late.
|
|
34
|
+
*/
|
|
35
|
+
import { createCapability } from './capabilities'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Registry of disconnect listeners for one run.
|
|
39
|
+
*
|
|
40
|
+
* Listeners must do BOOKKEEPING ONLY. The run is still executing, so releasing
|
|
41
|
+
* anything it depends on — stopping a file watcher, destroying a sandbox — breaks
|
|
42
|
+
* a healthy run. Teardown belongs in the terminal hooks, which still run exactly
|
|
43
|
+
* once afterwards.
|
|
44
|
+
*
|
|
45
|
+
* A listener may return a promise; the engine awaits all of them before the run
|
|
46
|
+
* finishes, so bookkeeping cannot be lost to a race with the run's own completion.
|
|
47
|
+
*/
|
|
48
|
+
export interface RunDisconnect {
|
|
49
|
+
/**
|
|
50
|
+
* Register `listener`, called at most once per run when the delivery socket
|
|
51
|
+
* closes. Registering after the socket has ALREADY closed calls `listener`
|
|
52
|
+
* immediately — otherwise a middleware whose `setup` was still running during
|
|
53
|
+
* the disconnect would silently never hear about it, which is the exact window
|
|
54
|
+
* the common disconnect lands in.
|
|
55
|
+
*/
|
|
56
|
+
subscribe: (listener: () => void | Promise<void>) => void
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const RunDisconnectCapability =
|
|
60
|
+
createCapability<RunDisconnect>()('run-disconnect')
|
|
61
|
+
|
|
62
|
+
export const [getRunDisconnect, provideRunDisconnect] = RunDisconnectCapability
|