@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
|
@@ -11,6 +11,17 @@ import { stripToSpecMiddleware } from '../../strip-to-spec-middleware'
|
|
|
11
11
|
import { streamToText } from '../../stream-to-response.js'
|
|
12
12
|
import { resolveDebugOption } from '../../logger/resolve'
|
|
13
13
|
import { EventType } from '../../types'
|
|
14
|
+
import {
|
|
15
|
+
INTERRUPT_BINDING_METADATA_KEY,
|
|
16
|
+
InterruptResumeValidationError,
|
|
17
|
+
readUnopenedInterruptBinding,
|
|
18
|
+
validateInterruptResumeBatch,
|
|
19
|
+
} from '../../interrupt-resume'
|
|
20
|
+
import { INTERRUPT_BINDING_VERSION } from '../../interrupts'
|
|
21
|
+
import {
|
|
22
|
+
canonicalInterruptJson,
|
|
23
|
+
digestInterruptJson,
|
|
24
|
+
} from '../../interrupt-serialization'
|
|
14
25
|
import { normalizeToolResult } from '../../utilities/tool-result'
|
|
15
26
|
import { isProviderExecutedToolCall } from '../../utilities/provider-executed'
|
|
16
27
|
import { LazyToolManager } from './tools/lazy-tool-manager'
|
|
@@ -25,18 +36,33 @@ import {
|
|
|
25
36
|
isStandardSchema,
|
|
26
37
|
parseWithStandardSchema,
|
|
27
38
|
} from './tools/schema-converter'
|
|
39
|
+
import {
|
|
40
|
+
hashSchemaInput,
|
|
41
|
+
normalizeApprovalSchema,
|
|
42
|
+
} from './tools/approval-schema'
|
|
28
43
|
import { maxIterations as maxIterationsStrategy } from './agent-loop-strategies'
|
|
44
|
+
import { isCancelRequestedReason } from './cancel'
|
|
29
45
|
import { convertMessagesToModelMessages, generateMessageId } from './messages'
|
|
30
46
|
import { MiddlewareRunner } from './middleware/compose'
|
|
47
|
+
import { getRunDetached } from './middleware/run-store'
|
|
48
|
+
import { publishRunDetachedSignal } from '../../delivery-detach'
|
|
49
|
+
import { publishRunDisconnectHandler } from '../../delivery-disconnect'
|
|
31
50
|
import { provideSandboxRuntime } from './middleware/sandbox-runtime'
|
|
51
|
+
import { provideRunDisconnect } from './middleware/run-disconnect'
|
|
32
52
|
import { CapabilityRegistry } from './middleware/capabilities'
|
|
33
53
|
import { validateCapabilities } from './middleware/validate'
|
|
34
54
|
import { MCPManager } from './mcp/manager'
|
|
55
|
+
import type {
|
|
56
|
+
InterruptBinding,
|
|
57
|
+
InterruptSubmissionError,
|
|
58
|
+
ToolApprovalResolution,
|
|
59
|
+
} from '../../interrupts'
|
|
35
60
|
import type {
|
|
36
61
|
ApprovalRequest,
|
|
37
62
|
ClientToolRequest,
|
|
38
63
|
ToolResult,
|
|
39
64
|
} from './tools/tool-calls'
|
|
65
|
+
import type { ApprovalSchemaConfig } from './tools/tool-definition'
|
|
40
66
|
import type {
|
|
41
67
|
AnyTextAdapter,
|
|
42
68
|
StructuredOutputOptions,
|
|
@@ -45,13 +71,15 @@ import type {
|
|
|
45
71
|
import type {
|
|
46
72
|
AgentLoopStrategy,
|
|
47
73
|
AnyTool,
|
|
48
|
-
ChatStream,
|
|
49
74
|
ConstrainedModelMessage,
|
|
50
75
|
CustomEvent,
|
|
51
76
|
InferSchemaType,
|
|
77
|
+
Interrupt,
|
|
52
78
|
JSONSchema,
|
|
53
79
|
LazyToolsConfig,
|
|
80
|
+
MessagesSnapshotEvent,
|
|
54
81
|
ModelMessage,
|
|
82
|
+
ProviderTool,
|
|
55
83
|
RunFinishedEvent,
|
|
56
84
|
SchemaInput,
|
|
57
85
|
StreamChunk,
|
|
@@ -63,6 +91,7 @@ import type {
|
|
|
63
91
|
ToolCallArgsEvent,
|
|
64
92
|
ToolCallEndEvent,
|
|
65
93
|
ToolCallStartEvent,
|
|
94
|
+
TypedStreamChunk,
|
|
66
95
|
UIMessage,
|
|
67
96
|
} from '../../types'
|
|
68
97
|
import type {
|
|
@@ -70,6 +99,7 @@ import type {
|
|
|
70
99
|
ChatMiddleware,
|
|
71
100
|
ChatMiddlewareConfig,
|
|
72
101
|
ChatMiddlewareContext,
|
|
102
|
+
ChatResumeToolState,
|
|
73
103
|
SandboxFileHookEvent,
|
|
74
104
|
StructuredOutputMiddlewareConfig,
|
|
75
105
|
} from './middleware/types'
|
|
@@ -77,7 +107,6 @@ import type { CheckCoverage } from './middleware/builder'
|
|
|
77
107
|
import type { SystemPrompt } from '../../system-prompts'
|
|
78
108
|
import type { InternalLogger } from '../../logger/internal-logger'
|
|
79
109
|
import type { DebugOption } from '../../logger/types'
|
|
80
|
-
import type { ProviderTool } from '../../tools/provider-tool'
|
|
81
110
|
import type {
|
|
82
111
|
ContextFromMiddleware,
|
|
83
112
|
ContextFromTool,
|
|
@@ -95,6 +124,150 @@ import type { ChatMCPOptions } from './mcp/types'
|
|
|
95
124
|
export const kind = 'text' as const
|
|
96
125
|
|
|
97
126
|
type AnyRuntimeTool = AnyTool
|
|
127
|
+
type RuntimeToolWithApproval = AnyRuntimeTool & {
|
|
128
|
+
approvalSchema?: ApprovalSchemaConfig
|
|
129
|
+
}
|
|
130
|
+
const interruptBindingMetadataKey = INTERRUPT_BINDING_METADATA_KEY
|
|
131
|
+
|
|
132
|
+
interface StructuralInterruptFailure {
|
|
133
|
+
error: Error
|
|
134
|
+
errors: ReadonlyArray<InterruptSubmissionError>
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isInterruptSubmissionError(
|
|
138
|
+
value: unknown,
|
|
139
|
+
): value is InterruptSubmissionError {
|
|
140
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
143
|
+
if (
|
|
144
|
+
!('scope' in value) ||
|
|
145
|
+
!('code' in value) ||
|
|
146
|
+
!('message' in value) ||
|
|
147
|
+
!('source' in value) ||
|
|
148
|
+
!('retryable' in value) ||
|
|
149
|
+
!('threadId' in value) ||
|
|
150
|
+
!('interruptedRunId' in value) ||
|
|
151
|
+
!('generation' in value) ||
|
|
152
|
+
typeof value.code !== 'string' ||
|
|
153
|
+
typeof value.message !== 'string' ||
|
|
154
|
+
typeof value.retryable !== 'boolean' ||
|
|
155
|
+
typeof value.threadId !== 'string' ||
|
|
156
|
+
typeof value.interruptedRunId !== 'string' ||
|
|
157
|
+
typeof value.generation !== 'number'
|
|
158
|
+
) {
|
|
159
|
+
return false
|
|
160
|
+
}
|
|
161
|
+
if (value.scope === 'item') {
|
|
162
|
+
return (
|
|
163
|
+
'interruptId' in value &&
|
|
164
|
+
typeof value.interruptId === 'string' &&
|
|
165
|
+
(value.source === 'client' || value.source === 'server')
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
return (
|
|
169
|
+
value.scope === 'batch' &&
|
|
170
|
+
'interruptIds' in value &&
|
|
171
|
+
Array.isArray(value.interruptIds) &&
|
|
172
|
+
value.interruptIds.every((id) => typeof id === 'string') &&
|
|
173
|
+
(value.source === 'client' ||
|
|
174
|
+
value.source === 'server' ||
|
|
175
|
+
value.source === 'transport')
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function structuralInterruptFailure(
|
|
180
|
+
error: unknown,
|
|
181
|
+
): StructuralInterruptFailure | undefined {
|
|
182
|
+
if (
|
|
183
|
+
!(error instanceof Error) ||
|
|
184
|
+
error.name !== 'InterruptResumeValidationError' ||
|
|
185
|
+
!('errors' in error) ||
|
|
186
|
+
!Array.isArray(error.errors) ||
|
|
187
|
+
error.errors.length === 0 ||
|
|
188
|
+
!error.errors.every(isInterruptSubmissionError)
|
|
189
|
+
) {
|
|
190
|
+
return undefined
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
error,
|
|
194
|
+
errors: error.errors,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function normalizePublicInterruptBinding(
|
|
199
|
+
value: unknown,
|
|
200
|
+
expectedInterruptId: string,
|
|
201
|
+
): InterruptBinding | undefined {
|
|
202
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
203
|
+
return undefined
|
|
204
|
+
}
|
|
205
|
+
const binding: Record<string, unknown> = Object.fromEntries(
|
|
206
|
+
Object.entries(value),
|
|
207
|
+
)
|
|
208
|
+
if (
|
|
209
|
+
binding.interruptId !== expectedInterruptId ||
|
|
210
|
+
// A binding version we don't recognise belongs to another producer. Drop
|
|
211
|
+
// it rather than reading our fields out of it.
|
|
212
|
+
(binding.v !== undefined && binding.v !== INTERRUPT_BINDING_VERSION) ||
|
|
213
|
+
typeof binding.interruptedRunId !== 'string' ||
|
|
214
|
+
typeof binding.generation !== 'number' ||
|
|
215
|
+
!Number.isInteger(binding.generation) ||
|
|
216
|
+
binding.generation < 0 ||
|
|
217
|
+
typeof binding.responseSchemaHash !== 'string' ||
|
|
218
|
+
(binding.expiresAt !== undefined && typeof binding.expiresAt !== 'string')
|
|
219
|
+
) {
|
|
220
|
+
return undefined
|
|
221
|
+
}
|
|
222
|
+
const base = {
|
|
223
|
+
v: INTERRUPT_BINDING_VERSION,
|
|
224
|
+
interruptId: binding.interruptId,
|
|
225
|
+
interruptedRunId: binding.interruptedRunId,
|
|
226
|
+
generation: binding.generation,
|
|
227
|
+
responseSchemaHash: binding.responseSchemaHash,
|
|
228
|
+
...(typeof binding.expiresAt === 'string'
|
|
229
|
+
? { expiresAt: binding.expiresAt }
|
|
230
|
+
: {}),
|
|
231
|
+
}
|
|
232
|
+
if (binding.kind === 'generic') {
|
|
233
|
+
return { kind: binding.kind, ...base }
|
|
234
|
+
}
|
|
235
|
+
if (
|
|
236
|
+
typeof binding.toolName !== 'string' ||
|
|
237
|
+
typeof binding.toolCallId !== 'string'
|
|
238
|
+
) {
|
|
239
|
+
return undefined
|
|
240
|
+
}
|
|
241
|
+
if (
|
|
242
|
+
binding.kind === 'client-tool-execution' &&
|
|
243
|
+
typeof binding.outputSchemaHash === 'string'
|
|
244
|
+
) {
|
|
245
|
+
return {
|
|
246
|
+
kind: binding.kind,
|
|
247
|
+
...base,
|
|
248
|
+
toolName: binding.toolName,
|
|
249
|
+
toolCallId: binding.toolCallId,
|
|
250
|
+
outputSchemaHash: binding.outputSchemaHash,
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (
|
|
254
|
+
binding.kind === 'tool-approval' &&
|
|
255
|
+
Object.prototype.hasOwnProperty.call(binding, 'originalArgs') &&
|
|
256
|
+
typeof binding.inputSchemaHash === 'string' &&
|
|
257
|
+
typeof binding.approvalSchemaHash === 'string'
|
|
258
|
+
) {
|
|
259
|
+
return {
|
|
260
|
+
kind: binding.kind,
|
|
261
|
+
...base,
|
|
262
|
+
toolName: binding.toolName,
|
|
263
|
+
toolCallId: binding.toolCallId,
|
|
264
|
+
originalArgs: binding.originalArgs,
|
|
265
|
+
inputSchemaHash: binding.inputSchemaHash,
|
|
266
|
+
approvalSchemaHash: binding.approvalSchemaHash,
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return undefined
|
|
270
|
+
}
|
|
98
271
|
|
|
99
272
|
// The leaf context-inference primitives (KnownContext, MergeContext,
|
|
100
273
|
// UnionToIntersection, DefinedContext, ContextFromTool, ContextFromMiddleware)
|
|
@@ -171,6 +344,7 @@ type TextActivityOptionsWithContext<
|
|
|
171
344
|
* @template TAdapter - The text adapter type (created by a provider function)
|
|
172
345
|
* @template TSchema - Optional Standard Schema for structured output
|
|
173
346
|
* @template TStream - Whether to stream the output (default: true)
|
|
347
|
+
* @template TContext - Runtime context value threaded to middleware hooks and server tools
|
|
174
348
|
*/
|
|
175
349
|
export interface TextActivityOptions<
|
|
176
350
|
TAdapter extends AnyTextAdapter,
|
|
@@ -178,7 +352,7 @@ export interface TextActivityOptions<
|
|
|
178
352
|
TStream extends boolean,
|
|
179
353
|
TContext = unknown,
|
|
180
354
|
> {
|
|
181
|
-
/** The text adapter to use (created by a provider function like openaiText('gpt-
|
|
355
|
+
/** The text adapter to use (created by a provider function like openaiText('gpt-5.5')) */
|
|
182
356
|
adapter: TAdapter
|
|
183
357
|
/**
|
|
184
358
|
* Conversation messages. Accepts:
|
|
@@ -221,7 +395,7 @@ export interface TextActivityOptions<
|
|
|
221
395
|
* compile-time error on the array element.
|
|
222
396
|
*/
|
|
223
397
|
tools?:
|
|
224
|
-
|
|
|
398
|
+
| ReadonlyArray<
|
|
225
399
|
| (AnyRuntimeTool & { readonly '~toolKind'?: never })
|
|
226
400
|
| ProviderTool<string, TAdapter['~types']['toolCapabilities'][number]>
|
|
227
401
|
>
|
|
@@ -240,11 +414,6 @@ export interface TextActivityOptions<
|
|
|
240
414
|
abortController?: TextOptions['abortController']
|
|
241
415
|
/** Strategy for controlling the agent loop */
|
|
242
416
|
agentLoopStrategy?: TextOptions['agentLoopStrategy']
|
|
243
|
-
/**
|
|
244
|
-
* Cap how many tool calls from a single model turn are executed.
|
|
245
|
-
* Excess calls receive error results. See {@link TextOptions.maxToolCallsPerTurn}.
|
|
246
|
-
*/
|
|
247
|
-
maxToolCallsPerTurn?: TextOptions['maxToolCallsPerTurn']
|
|
248
417
|
/**
|
|
249
418
|
* Optional configuration for lazy-tool discovery (tools marked `lazy: true`).
|
|
250
419
|
* Tunes how much of each lazy tool's description appears in the discovery
|
|
@@ -259,6 +428,13 @@ export interface TextActivityOptions<
|
|
|
259
428
|
runId?: TextOptions['runId']
|
|
260
429
|
/** Parent run ID for AG-UI protocol nested run correlation. */
|
|
261
430
|
parentRunId?: TextOptions['parentRunId']
|
|
431
|
+
/** Application state mirrored in a STATE_SNAPSHOT before an interrupt terminal. */
|
|
432
|
+
state?: TextOptions['state']
|
|
433
|
+
/**
|
|
434
|
+
* AG-UI interrupt resume responses. Persistence middleware validates these
|
|
435
|
+
* before accepting new input on a thread with pending interrupts.
|
|
436
|
+
*/
|
|
437
|
+
resume?: TextOptions['resume']
|
|
262
438
|
/**
|
|
263
439
|
* Optional Standard Schema for structured output.
|
|
264
440
|
* When provided, the activity will:
|
|
@@ -270,7 +446,7 @@ export interface TextActivityOptions<
|
|
|
270
446
|
* @example
|
|
271
447
|
* ```ts
|
|
272
448
|
* const result = await chat({
|
|
273
|
-
* adapter: openaiText('gpt-
|
|
449
|
+
* adapter: openaiText('gpt-5.5'),
|
|
274
450
|
* messages: [{ role: 'user', content: 'Generate a person' }],
|
|
275
451
|
* outputSchema: z.object({ name: z.string(), age: z.number() })
|
|
276
452
|
* })
|
|
@@ -280,7 +456,7 @@ export interface TextActivityOptions<
|
|
|
280
456
|
outputSchema?: TSchema
|
|
281
457
|
/**
|
|
282
458
|
* Whether to stream the text result.
|
|
283
|
-
* When true (default), returns an AsyncIterable<
|
|
459
|
+
* When true (default), returns an AsyncIterable<TypedStreamChunk<TTools>> for streaming output.
|
|
284
460
|
* When false, returns a Promise<string> with the collected text content.
|
|
285
461
|
*
|
|
286
462
|
* Note: If outputSchema is provided, this option is ignored and the result
|
|
@@ -291,7 +467,7 @@ export interface TextActivityOptions<
|
|
|
291
467
|
* @example Non-streaming text
|
|
292
468
|
* ```ts
|
|
293
469
|
* const text = await chat({
|
|
294
|
-
* adapter: openaiText('gpt-
|
|
470
|
+
* adapter: openaiText('gpt-5.5'),
|
|
295
471
|
* messages: [{ role: 'user', content: 'Hello!' }],
|
|
296
472
|
* stream: false
|
|
297
473
|
* })
|
|
@@ -306,7 +482,7 @@ export interface TextActivityOptions<
|
|
|
306
482
|
* @example
|
|
307
483
|
* ```ts
|
|
308
484
|
* const stream = chat({
|
|
309
|
-
* adapter: openaiText('gpt-
|
|
485
|
+
* adapter: openaiText('gpt-5.5'),
|
|
310
486
|
* messages: [...],
|
|
311
487
|
* middleware: [loggingMiddleware, redactionMiddleware],
|
|
312
488
|
* })
|
|
@@ -372,12 +548,18 @@ export function createChatOptions<
|
|
|
372
548
|
TTools,
|
|
373
549
|
TMiddleware
|
|
374
550
|
>,
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
551
|
+
// Preserve the concrete `tools` tuple on the returned options (so a later
|
|
552
|
+
// `chat({ ...opts })` still narrows tool-call events to the tool names)
|
|
553
|
+
// while threading the inferred runtime context like the bare options type.
|
|
554
|
+
): Omit<
|
|
555
|
+
TextActivityOptions<
|
|
556
|
+
TAdapter,
|
|
557
|
+
TSchema,
|
|
558
|
+
TStream,
|
|
559
|
+
InferredContext<TTools, TMiddleware>
|
|
560
|
+
>,
|
|
561
|
+
'tools'
|
|
562
|
+
> & { tools?: TTools } {
|
|
381
563
|
return options
|
|
382
564
|
}
|
|
383
565
|
|
|
@@ -394,7 +576,10 @@ export function createChatOptions<
|
|
|
394
576
|
* - If outputSchema is provided without explicit stream:true:
|
|
395
577
|
* Promise<InferSchemaType<TSchema>>.
|
|
396
578
|
* - If stream is explicitly false (no schema): Promise<string>.
|
|
397
|
-
* - Otherwise (default): AsyncIterable<
|
|
579
|
+
* - Otherwise (default): AsyncIterable<TypedStreamChunk<TTools>>.
|
|
580
|
+
*
|
|
581
|
+
* When tools with typed schemas are provided, the stream chunks include
|
|
582
|
+
* type-safe `toolName` and `input` fields on tool call events.
|
|
398
583
|
*
|
|
399
584
|
* `[TStream] extends [true]` is used (not `TStream extends true`) so that the
|
|
400
585
|
* default `boolean` value of `TStream` does *not* match the streaming branch.
|
|
@@ -404,13 +589,23 @@ export function createChatOptions<
|
|
|
404
589
|
export type TextActivityResult<
|
|
405
590
|
TSchema extends SchemaInput | undefined,
|
|
406
591
|
TStream extends boolean = boolean,
|
|
592
|
+
// Unconstrained so `chat()` can forward its inferred `options['tools']` type
|
|
593
|
+
// (which may be `undefined` or the broad `AnyRuntimeTool | ProviderTool`
|
|
594
|
+
// array) directly; non-tool-array inputs normalize to the default below.
|
|
595
|
+
TTools = ReadonlyArray<AnyTool>,
|
|
407
596
|
> = TSchema extends SchemaInput
|
|
408
597
|
? [TStream] extends [true]
|
|
409
598
|
? StructuredOutputStream<InferSchemaType<TSchema>>
|
|
410
599
|
: Promise<InferSchemaType<TSchema>>
|
|
411
600
|
: [TStream] extends [false]
|
|
412
601
|
? Promise<string>
|
|
413
|
-
:
|
|
602
|
+
: AsyncIterable<
|
|
603
|
+
TypedStreamChunk<
|
|
604
|
+
TTools extends ReadonlyArray<AnyTool>
|
|
605
|
+
? TTools
|
|
606
|
+
: ReadonlyArray<AnyTool>
|
|
607
|
+
>
|
|
608
|
+
>
|
|
414
609
|
|
|
415
610
|
// ===========================
|
|
416
611
|
// ChatEngine Implementation
|
|
@@ -478,23 +673,6 @@ interface TextEngineConfig<
|
|
|
478
673
|
type ToolPhaseResult = 'continue' | 'stop' | 'wait'
|
|
479
674
|
type CyclePhase = 'processText' | 'executeToolCalls'
|
|
480
675
|
|
|
481
|
-
/**
|
|
482
|
-
* Validate and normalize `maxToolCallsPerTurn`.
|
|
483
|
-
* Unset → unlimited. `0` → execute none. Negatives / non-finite → throw
|
|
484
|
-
* (Array#slice treats negatives as "from end", which is not a useful cap).
|
|
485
|
-
*/
|
|
486
|
-
function resolveMaxToolCallsPerTurn(
|
|
487
|
-
cap: number | undefined,
|
|
488
|
-
): number | undefined {
|
|
489
|
-
if (cap == null) return undefined
|
|
490
|
-
if (!Number.isFinite(cap) || cap < 0) {
|
|
491
|
-
throw new Error(
|
|
492
|
-
`maxToolCallsPerTurn must be a non-negative finite number, got ${cap}`,
|
|
493
|
-
)
|
|
494
|
-
}
|
|
495
|
-
return Math.floor(cap)
|
|
496
|
-
}
|
|
497
|
-
|
|
498
676
|
/**
|
|
499
677
|
* Combine two optional AbortSignals into one that aborts when either does.
|
|
500
678
|
* Returns the other signal directly when one is absent or already aborted.
|
|
@@ -559,13 +737,17 @@ class TextEngine<
|
|
|
559
737
|
private eventOptions?: Record<string, unknown> | undefined
|
|
560
738
|
private eventToolNames?: Array<string>
|
|
561
739
|
private finishedEvent: RunFinishedEvent | null = null
|
|
740
|
+
private deferredToolCallRunFinishedChunks: Array<StreamChunk> = []
|
|
562
741
|
private earlyTermination = false
|
|
563
742
|
private toolPhase: ToolPhaseResult = 'continue'
|
|
564
743
|
private cyclePhase: CyclePhase = 'processText'
|
|
565
|
-
private readonly maxToolCallsPerTurn: number | undefined
|
|
566
744
|
// Client state extracted from initial messages (before conversion to ModelMessage)
|
|
567
|
-
private readonly initialApprovals: Map<string,
|
|
745
|
+
private readonly initialApprovals: Map<string, ToolApprovalResolution>
|
|
568
746
|
private readonly initialClientToolResults: Map<string, any>
|
|
747
|
+
private readonly resumeApprovals = new Map<string, ToolApprovalResolution>()
|
|
748
|
+
private readonly resumeClientToolResults = new Map<string, any>()
|
|
749
|
+
private readonly resumeDeniedToolResults = new Map<string, unknown>()
|
|
750
|
+
private readonly resumeCancelledToolCallIds = new Set<string>()
|
|
569
751
|
|
|
570
752
|
// AG-UI protocol IDs
|
|
571
753
|
private readonly threadId: string
|
|
@@ -583,6 +765,14 @@ class TextEngine<
|
|
|
583
765
|
// observe both cancellation sources via ctx.abortSignal.
|
|
584
766
|
private readonly toolAbortSignal?: AbortSignal
|
|
585
767
|
private terminalHookCalled = false
|
|
768
|
+
/**
|
|
769
|
+
* Latched the first time the delivery socket closes; see `notifyDisconnected`.
|
|
770
|
+
* Also read by `subscribe` so a listener registered AFTER the disconnect (a
|
|
771
|
+
* middleware whose `setup` was still running at the time — the common case) is
|
|
772
|
+
* called immediately rather than never.
|
|
773
|
+
*/
|
|
774
|
+
private disconnected = false
|
|
775
|
+
private readonly disconnectListeners: Array<() => void | Promise<void>> = []
|
|
586
776
|
|
|
587
777
|
private readonly logger: InternalLogger
|
|
588
778
|
|
|
@@ -628,9 +818,6 @@ class TextEngine<
|
|
|
628
818
|
this.systemPrompts = config.params.systemPrompts || []
|
|
629
819
|
this.loopStrategy =
|
|
630
820
|
config.params.agentLoopStrategy || maxIterationsStrategy(5)
|
|
631
|
-
this.maxToolCallsPerTurn = resolveMaxToolCallsPerTurn(
|
|
632
|
-
config.params.maxToolCallsPerTurn,
|
|
633
|
-
)
|
|
634
821
|
this.initialMessageCount = config.params.messages.length
|
|
635
822
|
|
|
636
823
|
// Extract client state (approvals, client tool results) from original messages BEFORE conversion
|
|
@@ -692,6 +879,7 @@ class TextEngine<
|
|
|
692
879
|
requestId: this.requestId,
|
|
693
880
|
streamId: this.streamId,
|
|
694
881
|
runId: this.runIdOverride ?? this.requestId,
|
|
882
|
+
parentRunId: this.parentRunIdOverride,
|
|
695
883
|
threadId: this.threadId,
|
|
696
884
|
// Legacy alias kept on the ctx so middleware that reads
|
|
697
885
|
// `ctx.conversationId` keeps working. Always equals `threadId`.
|
|
@@ -739,6 +927,22 @@ class TextEngine<
|
|
|
739
927
|
provide: (capability, value) => capability[1](this.middlewareCtx, value),
|
|
740
928
|
}
|
|
741
929
|
|
|
930
|
+
// Provide the internal RunDisconnect capability BEFORE `setup` runs, so a
|
|
931
|
+
// middleware can subscribe from inside its own `setup` — which is where the
|
|
932
|
+
// subscription has to happen, because `setup` is the long await the common
|
|
933
|
+
// disconnect lands in.
|
|
934
|
+
//
|
|
935
|
+
// `subscribe` calls back IMMEDIATELY when the socket has already closed. That
|
|
936
|
+
// ordering is load-bearing rather than defensive: a middleware whose `setup`
|
|
937
|
+
// was still running during the disconnect would otherwise register a listener
|
|
938
|
+
// for an event that has already been and gone, and silently never detach.
|
|
939
|
+
provideRunDisconnect(this.middlewareCtx, {
|
|
940
|
+
subscribe: (listener) => {
|
|
941
|
+
this.disconnectListeners.push(listener)
|
|
942
|
+
if (this.disconnected) this.runDisconnectListener(listener)
|
|
943
|
+
},
|
|
944
|
+
})
|
|
945
|
+
|
|
742
946
|
// Provide the internal SandboxRuntime capability so harness adapters and
|
|
743
947
|
// sandbox middleware can emit file events. The sink logs, fans the event
|
|
744
948
|
// out through the middleware `onFile*` hooks (fire-and-forget), and queues
|
|
@@ -829,6 +1033,7 @@ class TextEngine<
|
|
|
829
1033
|
initialConfig,
|
|
830
1034
|
)
|
|
831
1035
|
this.applyMiddlewareConfig(transformedConfig)
|
|
1036
|
+
await this.applyEphemeralInterruptResume(transformedConfig)
|
|
832
1037
|
|
|
833
1038
|
// Run onStart (devtools middleware emits text:request:started and initial messages here)
|
|
834
1039
|
await this.middlewareRunner.runOnStart(this.middlewareCtx)
|
|
@@ -882,7 +1087,7 @@ class TextEngine<
|
|
|
882
1087
|
}
|
|
883
1088
|
|
|
884
1089
|
this.endCycle()
|
|
885
|
-
} while (this.shouldContinue())
|
|
1090
|
+
} while (await this.shouldContinue())
|
|
886
1091
|
}
|
|
887
1092
|
|
|
888
1093
|
this.logger.agentLoop('run finished', {
|
|
@@ -893,12 +1098,15 @@ class TextEngine<
|
|
|
893
1098
|
// requested AND the run hasn't already errored/aborted, run it through
|
|
894
1099
|
// the middleware pipeline. The terminal hook fires once at the very
|
|
895
1100
|
// end (after finalization), not after the agent loop.
|
|
1101
|
+
// Actionable waits already emitted a RUN_FINISHED interrupt terminal, so
|
|
1102
|
+
// do not run finalization after `processToolCalls()` pauses the stream.
|
|
896
1103
|
//
|
|
897
1104
|
// Native combined mode takes a different path: the agent loop's final-
|
|
898
1105
|
// turn text IS the schema-constrained JSON, so we harvest it from
|
|
899
1106
|
// `accumulatedContent` instead of issuing a second provider call.
|
|
900
1107
|
if (
|
|
901
1108
|
this.finalStructuredOutput &&
|
|
1109
|
+
this.toolPhase !== 'wait' &&
|
|
902
1110
|
!this.isCancelled() &&
|
|
903
1111
|
!this.finalizationError
|
|
904
1112
|
) {
|
|
@@ -946,6 +1154,41 @@ class TextEngine<
|
|
|
946
1154
|
}
|
|
947
1155
|
}
|
|
948
1156
|
} catch (error: unknown) {
|
|
1157
|
+
if (
|
|
1158
|
+
error instanceof Error &&
|
|
1159
|
+
error.name === 'InterruptReplaySignal' &&
|
|
1160
|
+
'continuationRunId' in error &&
|
|
1161
|
+
typeof error.continuationRunId === 'string'
|
|
1162
|
+
) {
|
|
1163
|
+
this.terminalHookCalled = true
|
|
1164
|
+
yield {
|
|
1165
|
+
type: EventType.RUN_FINISHED,
|
|
1166
|
+
timestamp: Date.now(),
|
|
1167
|
+
threadId: this.threadId,
|
|
1168
|
+
runId: this.runIdOverride ?? this.requestId,
|
|
1169
|
+
finishReason: 'stop',
|
|
1170
|
+
outcome: { type: 'success' },
|
|
1171
|
+
result: {
|
|
1172
|
+
replayed: true,
|
|
1173
|
+
continuationRunId: error.continuationRunId,
|
|
1174
|
+
},
|
|
1175
|
+
}
|
|
1176
|
+
return
|
|
1177
|
+
}
|
|
1178
|
+
const interruptFailure = structuralInterruptFailure(error)
|
|
1179
|
+
if (interruptFailure) {
|
|
1180
|
+
this.terminalHookCalled = true
|
|
1181
|
+
this.logger.errors('chat interrupt resume failed', {
|
|
1182
|
+
error,
|
|
1183
|
+
threadId: this.middlewareCtx.threadId,
|
|
1184
|
+
})
|
|
1185
|
+
await this.middlewareRunner.runOnError(this.middlewareCtx, {
|
|
1186
|
+
error: interruptFailure.error,
|
|
1187
|
+
duration: Date.now() - this.streamStartTime,
|
|
1188
|
+
})
|
|
1189
|
+
yield this.buildInterruptRunErrorChunk(error)
|
|
1190
|
+
return
|
|
1191
|
+
}
|
|
949
1192
|
if (!this.terminalHookCalled) {
|
|
950
1193
|
this.terminalHookCalled = true
|
|
951
1194
|
if (error instanceof MiddlewareAbortError) {
|
|
@@ -954,6 +1197,7 @@ class TextEngine<
|
|
|
954
1197
|
await this.middlewareRunner.runOnAbort(this.middlewareCtx, {
|
|
955
1198
|
reason: error.message,
|
|
956
1199
|
duration: Date.now() - this.streamStartTime,
|
|
1200
|
+
cancelRequested: isCancelRequestedReason(error.message),
|
|
957
1201
|
})
|
|
958
1202
|
} else {
|
|
959
1203
|
// Genuine error — call onError
|
|
@@ -975,9 +1219,11 @@ class TextEngine<
|
|
|
975
1219
|
// Check for abort terminal hook
|
|
976
1220
|
if (!this.terminalHookCalled && this.isCancelled()) {
|
|
977
1221
|
this.terminalHookCalled = true
|
|
1222
|
+
const reason = this.resolveAbortReason()
|
|
978
1223
|
await this.middlewareRunner.runOnAbort(this.middlewareCtx, {
|
|
979
|
-
reason
|
|
1224
|
+
reason,
|
|
980
1225
|
duration: Date.now() - this.streamStartTime,
|
|
1226
|
+
cancelRequested: isCancelRequestedReason(reason),
|
|
981
1227
|
})
|
|
982
1228
|
}
|
|
983
1229
|
|
|
@@ -1080,6 +1326,15 @@ class TextEngine<
|
|
|
1080
1326
|
? this.finalStructuredOutput.jsonSchema
|
|
1081
1327
|
: undefined
|
|
1082
1328
|
|
|
1329
|
+
const { approvals } = this.collectClientState()
|
|
1330
|
+
const adapterApprovals = new Map<string, boolean>()
|
|
1331
|
+
for (const [approvalId, resolution] of approvals) {
|
|
1332
|
+
adapterApprovals.set(
|
|
1333
|
+
approvalId,
|
|
1334
|
+
typeof resolution === 'boolean' ? resolution : resolution.approved,
|
|
1335
|
+
)
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1083
1338
|
for await (const chunk of this.adapter.chatStream({
|
|
1084
1339
|
model: this.params.model,
|
|
1085
1340
|
messages: this.messages,
|
|
@@ -1095,7 +1350,7 @@ class TextEngine<
|
|
|
1095
1350
|
// Expose provided capabilities (e.g. sandbox) to harness adapters.
|
|
1096
1351
|
capabilities: this.middlewareCtx,
|
|
1097
1352
|
// Client approval decisions, for harness interactive-approval resolution.
|
|
1098
|
-
approvals:
|
|
1353
|
+
approvals: adapterApprovals,
|
|
1099
1354
|
...(combinedSchema ? { outputSchema: combinedSchema } : {}),
|
|
1100
1355
|
})) {
|
|
1101
1356
|
if (this.isCancelled()) {
|
|
@@ -1170,6 +1425,10 @@ class TextEngine<
|
|
|
1170
1425
|
) {
|
|
1171
1426
|
continue
|
|
1172
1427
|
}
|
|
1428
|
+
if (this.shouldDeferToolCallRunFinished(outputChunk)) {
|
|
1429
|
+
this.deferredToolCallRunFinishedChunks.push(outputChunk)
|
|
1430
|
+
continue
|
|
1431
|
+
}
|
|
1173
1432
|
this.logger.output(`type=${outputChunk.type}`, { chunk: outputChunk })
|
|
1174
1433
|
yield outputChunk
|
|
1175
1434
|
this.middlewareCtx.chunkIndex++
|
|
@@ -1334,14 +1593,13 @@ class TextEngine<
|
|
|
1334
1593
|
|
|
1335
1594
|
const finishEvent = this.createSyntheticFinishedEvent()
|
|
1336
1595
|
|
|
1337
|
-
// Same fan-out budget as live model turns (seeded history / resume).
|
|
1338
1596
|
// Count is deduped so wait→resume after a live turn does not double-count.
|
|
1339
|
-
|
|
1340
|
-
|
|
1597
|
+
// Per-turn execution caps are app middleware via onBeforeToolCall skip.
|
|
1598
|
+
this.recordToolCalls(pendingToolCalls)
|
|
1341
1599
|
|
|
1342
1600
|
// Handle undiscovered lazy tool calls with self-correcting error messages
|
|
1343
1601
|
const undiscoveredLazyResults: Array<ToolResult> = []
|
|
1344
|
-
const executablePendingCalls =
|
|
1602
|
+
const executablePendingCalls = pendingToolCalls.filter((tc) => {
|
|
1345
1603
|
if (this.lazyToolManager.isUndiscoveredLazyTool(tc.function.name)) {
|
|
1346
1604
|
undiscoveredLazyResults.push({
|
|
1347
1605
|
toolCallId: tc.id,
|
|
@@ -1358,9 +1616,10 @@ class TextEngine<
|
|
|
1358
1616
|
return true
|
|
1359
1617
|
})
|
|
1360
1618
|
|
|
1361
|
-
// Non-executed outcomes (undiscovered lazy
|
|
1362
|
-
//
|
|
1363
|
-
|
|
1619
|
+
// Non-executed outcomes (undiscovered lazy). Emitted after executed
|
|
1620
|
+
// results so the stream prefers real results first. Per-turn skips are
|
|
1621
|
+
// produced by middleware via onBeforeToolCall and appear in execution results.
|
|
1622
|
+
const deferredErrorResults = [...undiscoveredLazyResults]
|
|
1364
1623
|
|
|
1365
1624
|
// Build args lookup so buildToolResultChunks can emit TOOL_CALL_START +
|
|
1366
1625
|
// TOOL_CALL_ARGS before TOOL_CALL_END during continuation re-executions.
|
|
@@ -1421,6 +1680,10 @@ class TextEngine<
|
|
|
1421
1680
|
},
|
|
1422
1681
|
this.middlewareCtx.context,
|
|
1423
1682
|
this.toolAbortSignal,
|
|
1683
|
+
{
|
|
1684
|
+
deniedToolResults: this.resumeDeniedToolResults,
|
|
1685
|
+
cancelledToolCallIds: this.resumeCancelledToolCallIds,
|
|
1686
|
+
},
|
|
1424
1687
|
)
|
|
1425
1688
|
|
|
1426
1689
|
// Consume the async generator, yielding custom events and collecting the return value
|
|
@@ -1446,39 +1709,27 @@ class TextEngine<
|
|
|
1446
1709
|
executionResult.needsApproval.length > 0 ||
|
|
1447
1710
|
executionResult.needsClientExecution.length > 0
|
|
1448
1711
|
) {
|
|
1712
|
+
this.discardDeferredToolCallRunFinishedChunks()
|
|
1713
|
+
|
|
1449
1714
|
if (allResults.length > 0) {
|
|
1450
1715
|
for (const chunk of this.buildToolResultChunks(
|
|
1451
1716
|
allResults,
|
|
1452
1717
|
finishEvent,
|
|
1453
|
-
argsMap,
|
|
1454
1718
|
)) {
|
|
1455
1719
|
yield* this.pipeThroughMiddleware(chunk)
|
|
1456
1720
|
}
|
|
1457
1721
|
}
|
|
1458
1722
|
|
|
1459
|
-
|
|
1460
|
-
executionResult.needsApproval,
|
|
1723
|
+
const emitted = yield* this.emitActionableInterruptBoundary(
|
|
1461
1724
|
finishEvent,
|
|
1462
|
-
|
|
1463
|
-
yield* this.pipeThroughMiddleware(chunk)
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
for (const chunk of this.buildClientToolChunks(
|
|
1725
|
+
executionResult.needsApproval,
|
|
1467
1726
|
executionResult.needsClientExecution,
|
|
1468
|
-
|
|
1469
|
-
)
|
|
1470
|
-
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
this.setToolPhase('wait')
|
|
1474
|
-
return 'wait'
|
|
1727
|
+
)
|
|
1728
|
+
this.setToolPhase(emitted ? 'wait' : 'stop')
|
|
1729
|
+
return emitted ? 'wait' : 'stop'
|
|
1475
1730
|
}
|
|
1476
1731
|
|
|
1477
|
-
const toolResultChunks = this.buildToolResultChunks(
|
|
1478
|
-
allResults,
|
|
1479
|
-
finishEvent,
|
|
1480
|
-
argsMap,
|
|
1481
|
-
)
|
|
1732
|
+
const toolResultChunks = this.buildToolResultChunks(allResults, finishEvent)
|
|
1482
1733
|
|
|
1483
1734
|
for (const chunk of toolResultChunks) {
|
|
1484
1735
|
yield* this.pipeThroughMiddleware(chunk)
|
|
@@ -1504,15 +1755,15 @@ class TextEngine<
|
|
|
1504
1755
|
return
|
|
1505
1756
|
}
|
|
1506
1757
|
|
|
1507
|
-
// Count every model-emitted tool call
|
|
1508
|
-
|
|
1509
|
-
|
|
1758
|
+
// Count every model-emitted tool call. Per-turn execution caps are app
|
|
1759
|
+
// middleware via onBeforeToolCall skip.
|
|
1760
|
+
this.recordToolCalls(toolCalls)
|
|
1510
1761
|
|
|
1511
1762
|
this.addAssistantToolCallMessage(toolCalls)
|
|
1512
1763
|
|
|
1513
1764
|
// Handle undiscovered lazy tool calls with self-correcting error messages
|
|
1514
1765
|
const undiscoveredLazyResults: Array<ToolResult> = []
|
|
1515
|
-
const executableToolCalls =
|
|
1766
|
+
const executableToolCalls = toolCalls.filter((tc) => {
|
|
1516
1767
|
if (this.lazyToolManager.isUndiscoveredLazyTool(tc.function.name)) {
|
|
1517
1768
|
undiscoveredLazyResults.push({
|
|
1518
1769
|
toolCallId: tc.id,
|
|
@@ -1529,13 +1780,14 @@ class TextEngine<
|
|
|
1529
1780
|
return true
|
|
1530
1781
|
})
|
|
1531
1782
|
|
|
1532
|
-
// Non-executed outcomes (undiscovered lazy
|
|
1533
|
-
//
|
|
1534
|
-
const deferredErrorResults = [...undiscoveredLazyResults
|
|
1783
|
+
// Non-executed outcomes (undiscovered lazy). Per-turn skips come from
|
|
1784
|
+
// middleware and appear in execution results.
|
|
1785
|
+
const deferredErrorResults = [...undiscoveredLazyResults]
|
|
1535
1786
|
|
|
1536
1787
|
if (executableToolCalls.length === 0) {
|
|
1537
|
-
|
|
1538
|
-
//
|
|
1788
|
+
yield* this.flushDeferredToolCallRunFinishedChunks()
|
|
1789
|
+
// All tool calls were undiscovered lazy tools — errors emitted, continue
|
|
1790
|
+
// loop (strategy / onShouldContinue may stop).
|
|
1539
1791
|
if (deferredErrorResults.length > 0) {
|
|
1540
1792
|
for (const chunk of this.buildToolResultChunks(
|
|
1541
1793
|
deferredErrorResults,
|
|
@@ -1589,6 +1841,10 @@ class TextEngine<
|
|
|
1589
1841
|
},
|
|
1590
1842
|
this.middlewareCtx.context,
|
|
1591
1843
|
this.toolAbortSignal,
|
|
1844
|
+
{
|
|
1845
|
+
deniedToolResults: this.resumeDeniedToolResults,
|
|
1846
|
+
cancelledToolCallIds: this.resumeCancelledToolCallIds,
|
|
1847
|
+
},
|
|
1592
1848
|
)
|
|
1593
1849
|
|
|
1594
1850
|
// Consume the async generator, yielding custom events and collecting the return value
|
|
@@ -1626,24 +1882,17 @@ class TextEngine<
|
|
|
1626
1882
|
}
|
|
1627
1883
|
}
|
|
1628
1884
|
|
|
1629
|
-
|
|
1630
|
-
executionResult.needsApproval,
|
|
1885
|
+
const emitted = yield* this.emitActionableInterruptBoundary(
|
|
1631
1886
|
finishEvent,
|
|
1632
|
-
|
|
1633
|
-
yield* this.pipeThroughMiddleware(chunk)
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
for (const chunk of this.buildClientToolChunks(
|
|
1887
|
+
executionResult.needsApproval,
|
|
1637
1888
|
executionResult.needsClientExecution,
|
|
1638
|
-
|
|
1639
|
-
)
|
|
1640
|
-
yield* this.pipeThroughMiddleware(chunk)
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
this.setToolPhase('wait')
|
|
1889
|
+
)
|
|
1890
|
+
this.setToolPhase(emitted ? 'wait' : 'stop')
|
|
1644
1891
|
return
|
|
1645
1892
|
}
|
|
1646
1893
|
|
|
1894
|
+
yield* this.flushDeferredToolCallRunFinishedChunks()
|
|
1895
|
+
|
|
1647
1896
|
const toolResultChunks = this.buildToolResultChunks(allResults, finishEvent)
|
|
1648
1897
|
|
|
1649
1898
|
for (const chunk of toolResultChunks) {
|
|
@@ -1666,6 +1915,28 @@ class TextEngine<
|
|
|
1666
1915
|
this.setToolPhase('continue')
|
|
1667
1916
|
}
|
|
1668
1917
|
|
|
1918
|
+
private shouldDeferToolCallRunFinished(chunk: StreamChunk): boolean {
|
|
1919
|
+
return (
|
|
1920
|
+
chunk.type === EventType.RUN_FINISHED &&
|
|
1921
|
+
this.finishedEvent?.finishReason === 'tool_calls' &&
|
|
1922
|
+
this.tools.length > 0 &&
|
|
1923
|
+
this.toolCallManager.hasToolCalls()
|
|
1924
|
+
)
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
private *flushDeferredToolCallRunFinishedChunks(): Generator<StreamChunk> {
|
|
1928
|
+
for (const chunk of this.deferredToolCallRunFinishedChunks) {
|
|
1929
|
+
this.logger.output(`type=${chunk.type}`, { chunk })
|
|
1930
|
+
yield chunk
|
|
1931
|
+
this.middlewareCtx.chunkIndex++
|
|
1932
|
+
}
|
|
1933
|
+
this.deferredToolCallRunFinishedChunks = []
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
private discardDeferredToolCallRunFinishedChunks(): void {
|
|
1937
|
+
this.deferredToolCallRunFinishedChunks = []
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1669
1940
|
private shouldExecuteToolPhase(): boolean {
|
|
1670
1941
|
return (
|
|
1671
1942
|
this.finishedEvent?.finishReason === 'tool_calls' &&
|
|
@@ -1688,6 +1959,7 @@ class TextEngine<
|
|
|
1688
1959
|
}),
|
|
1689
1960
|
},
|
|
1690
1961
|
]
|
|
1962
|
+
this.middlewareCtx.messages = this.messages
|
|
1691
1963
|
}
|
|
1692
1964
|
|
|
1693
1965
|
/**
|
|
@@ -1698,10 +1970,10 @@ class TextEngine<
|
|
|
1698
1970
|
private extractClientStateFromOriginalMessages(
|
|
1699
1971
|
originalMessages: Array<any>,
|
|
1700
1972
|
): {
|
|
1701
|
-
approvals: Map<string,
|
|
1973
|
+
approvals: Map<string, ToolApprovalResolution>
|
|
1702
1974
|
clientToolResults: Map<string, any>
|
|
1703
1975
|
} {
|
|
1704
|
-
const approvals = new Map<string,
|
|
1976
|
+
const approvals = new Map<string, ToolApprovalResolution>()
|
|
1705
1977
|
const clientToolResults = new Map<string, any>()
|
|
1706
1978
|
|
|
1707
1979
|
for (const message of originalMessages) {
|
|
@@ -1730,12 +2002,18 @@ class TextEngine<
|
|
|
1730
2002
|
}
|
|
1731
2003
|
|
|
1732
2004
|
private collectClientState(): {
|
|
1733
|
-
approvals: Map<string,
|
|
2005
|
+
approvals: Map<string, ToolApprovalResolution>
|
|
1734
2006
|
clientToolResults: Map<string, any>
|
|
1735
2007
|
} {
|
|
1736
2008
|
// Start with the initial client state extracted from original messages
|
|
1737
2009
|
const approvals = new Map(this.initialApprovals)
|
|
1738
2010
|
const clientToolResults = new Map(this.initialClientToolResults)
|
|
2011
|
+
for (const [approvalId, approved] of this.resumeApprovals) {
|
|
2012
|
+
approvals.set(approvalId, approved)
|
|
2013
|
+
}
|
|
2014
|
+
for (const [toolCallId, result] of this.resumeClientToolResults) {
|
|
2015
|
+
clientToolResults.set(toolCallId, result)
|
|
2016
|
+
}
|
|
1739
2017
|
|
|
1740
2018
|
// Also check current messages for any additional tool results (from server tools)
|
|
1741
2019
|
for (const message of this.messages) {
|
|
@@ -1774,54 +2052,301 @@ class TextEngine<
|
|
|
1774
2052
|
return { approvals, clientToolResults }
|
|
1775
2053
|
}
|
|
1776
2054
|
|
|
1777
|
-
private
|
|
2055
|
+
private buildActionableInterrupts(
|
|
1778
2056
|
approvals: Array<ApprovalRequest>,
|
|
1779
|
-
|
|
1780
|
-
): Array<
|
|
1781
|
-
const
|
|
2057
|
+
clientRequests: Array<ClientToolRequest>,
|
|
2058
|
+
): Array<Interrupt> {
|
|
2059
|
+
const interrupts: Array<Interrupt> = []
|
|
1782
2060
|
|
|
1783
2061
|
for (const approval of approvals) {
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
2062
|
+
const tool = this.tools.find(
|
|
2063
|
+
(candidate) => candidate.name === approval.toolName,
|
|
2064
|
+
) as RuntimeToolWithApproval | undefined
|
|
2065
|
+
const normalized = normalizeApprovalSchema(
|
|
2066
|
+
tool?.approvalSchema,
|
|
2067
|
+
tool?.inputSchema,
|
|
2068
|
+
)
|
|
2069
|
+
interrupts.push({
|
|
2070
|
+
id: approval.approvalId,
|
|
2071
|
+
// Display hint only. `reason` is free-form AG-UI text that another
|
|
2072
|
+
// producer can also spell `tool_call`, so it never decides ownership —
|
|
2073
|
+
// the binding in `metadata` does.
|
|
2074
|
+
reason: 'tool_call',
|
|
2075
|
+
message: `Approval required to run ${approval.toolName}`,
|
|
2076
|
+
toolCallId: approval.toolCallId,
|
|
2077
|
+
responseSchema: normalized.responseSchema,
|
|
2078
|
+
metadata: {
|
|
2079
|
+
kind: 'approval',
|
|
1791
2080
|
toolName: approval.toolName,
|
|
1792
2081
|
input: approval.input,
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
2082
|
+
[interruptBindingMetadataKey]: {
|
|
2083
|
+
v: INTERRUPT_BINDING_VERSION,
|
|
2084
|
+
kind: 'tool-approval',
|
|
2085
|
+
interruptId: approval.approvalId,
|
|
2086
|
+
toolName: approval.toolName,
|
|
2087
|
+
toolCallId: approval.toolCallId,
|
|
2088
|
+
originalArgs: approval.input,
|
|
2089
|
+
inputSchemaHash: hashSchemaInput(tool?.inputSchema),
|
|
2090
|
+
approvalSchemaHash: normalized.approvalSchemaHash,
|
|
2091
|
+
responseSchemaHash: normalized.responseSchemaHash,
|
|
1796
2092
|
},
|
|
1797
2093
|
},
|
|
1798
|
-
}
|
|
2094
|
+
})
|
|
1799
2095
|
}
|
|
1800
2096
|
|
|
1801
|
-
|
|
2097
|
+
for (const clientTool of clientRequests) {
|
|
2098
|
+
const tool = this.tools.find(
|
|
2099
|
+
(candidate) => candidate.name === clientTool.toolName,
|
|
2100
|
+
)
|
|
2101
|
+
const responseSchema = convertSchemaToJsonSchema(tool?.outputSchema) ?? {}
|
|
2102
|
+
interrupts.push({
|
|
2103
|
+
id: `client_tool_${clientTool.toolCallId}`,
|
|
2104
|
+
reason: 'tanstack:client_tool_execution',
|
|
2105
|
+
message: `Client tool ${clientTool.toolName} is ready to run`,
|
|
2106
|
+
toolCallId: clientTool.toolCallId,
|
|
2107
|
+
responseSchema,
|
|
2108
|
+
metadata: {
|
|
2109
|
+
kind: 'client_tool',
|
|
2110
|
+
toolName: clientTool.toolName,
|
|
2111
|
+
input: clientTool.input,
|
|
2112
|
+
[interruptBindingMetadataKey]: {
|
|
2113
|
+
v: INTERRUPT_BINDING_VERSION,
|
|
2114
|
+
kind: 'client-tool-execution',
|
|
2115
|
+
interruptId: `client_tool_${clientTool.toolCallId}`,
|
|
2116
|
+
toolName: clientTool.toolName,
|
|
2117
|
+
toolCallId: clientTool.toolCallId,
|
|
2118
|
+
outputSchemaHash: hashSchemaInput(tool?.outputSchema),
|
|
2119
|
+
responseSchemaHash: digestInterruptJson(
|
|
2120
|
+
canonicalInterruptJson(responseSchema),
|
|
2121
|
+
),
|
|
2122
|
+
},
|
|
2123
|
+
},
|
|
2124
|
+
})
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
return interrupts
|
|
1802
2128
|
}
|
|
1803
2129
|
|
|
1804
|
-
private
|
|
2130
|
+
private buildInterruptFinishedChunk(
|
|
2131
|
+
finishEvent: RunFinishedEvent,
|
|
2132
|
+
approvals: Array<ApprovalRequest>,
|
|
1805
2133
|
clientRequests: Array<ClientToolRequest>,
|
|
2134
|
+
): StreamChunk {
|
|
2135
|
+
return {
|
|
2136
|
+
...finishEvent,
|
|
2137
|
+
timestamp: Date.now(),
|
|
2138
|
+
outcome: {
|
|
2139
|
+
type: 'interrupt',
|
|
2140
|
+
interrupts: this.buildActionableInterrupts(approvals, clientRequests),
|
|
2141
|
+
},
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
private buildMessagesSnapshotChunk(): StreamChunk {
|
|
2146
|
+
const messages: MessagesSnapshotEvent['messages'] = this.messages.map(
|
|
2147
|
+
(message, index) => {
|
|
2148
|
+
const content =
|
|
2149
|
+
typeof message.content === 'string'
|
|
2150
|
+
? message.content
|
|
2151
|
+
: message.content === null
|
|
2152
|
+
? undefined
|
|
2153
|
+
: JSON.stringify(message.content)
|
|
2154
|
+
return {
|
|
2155
|
+
id: `snapshot_${this.runIdOverride ?? this.requestId}_${index}`,
|
|
2156
|
+
role: message.role,
|
|
2157
|
+
...(content !== undefined ? { content } : {}),
|
|
2158
|
+
...('toolCalls' in message && message.toolCalls
|
|
2159
|
+
? { toolCalls: message.toolCalls }
|
|
2160
|
+
: {}),
|
|
2161
|
+
...('toolCallId' in message && message.toolCallId
|
|
2162
|
+
? { toolCallId: message.toolCallId }
|
|
2163
|
+
: {}),
|
|
2164
|
+
} as MessagesSnapshotEvent['messages'][number]
|
|
2165
|
+
},
|
|
2166
|
+
)
|
|
2167
|
+
return {
|
|
2168
|
+
type: EventType.MESSAGES_SNAPSHOT,
|
|
2169
|
+
timestamp: Date.now(),
|
|
2170
|
+
model: this.params.model,
|
|
2171
|
+
messages,
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
private publicInterruptTerminal(chunk: StreamChunk): StreamChunk {
|
|
2176
|
+
if (
|
|
2177
|
+
chunk.type !== EventType.RUN_FINISHED ||
|
|
2178
|
+
chunk.outcome?.type !== 'interrupt'
|
|
2179
|
+
) {
|
|
2180
|
+
return chunk
|
|
2181
|
+
}
|
|
2182
|
+
return {
|
|
2183
|
+
...chunk,
|
|
2184
|
+
outcome: {
|
|
2185
|
+
...chunk.outcome,
|
|
2186
|
+
interrupts: chunk.outcome.interrupts.map((interrupt) => {
|
|
2187
|
+
if (
|
|
2188
|
+
!interrupt.metadata ||
|
|
2189
|
+
typeof interrupt.metadata !== 'object' ||
|
|
2190
|
+
Array.isArray(interrupt.metadata)
|
|
2191
|
+
) {
|
|
2192
|
+
return interrupt
|
|
2193
|
+
}
|
|
2194
|
+
const metadata = { ...interrupt.metadata }
|
|
2195
|
+
const binding = normalizePublicInterruptBinding(
|
|
2196
|
+
metadata[interruptBindingMetadataKey],
|
|
2197
|
+
interrupt.id,
|
|
2198
|
+
)
|
|
2199
|
+
if (binding) {
|
|
2200
|
+
metadata[interruptBindingMetadataKey] = binding
|
|
2201
|
+
} else {
|
|
2202
|
+
delete metadata[interruptBindingMetadataKey]
|
|
2203
|
+
}
|
|
2204
|
+
return { ...interrupt, metadata }
|
|
2205
|
+
}),
|
|
2206
|
+
},
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
private interruptFailure(error: unknown): {
|
|
2211
|
+
message: string
|
|
2212
|
+
code: string
|
|
2213
|
+
errors?: ReadonlyArray<InterruptSubmissionError>
|
|
2214
|
+
} {
|
|
2215
|
+
const structured = structuralInterruptFailure(error)
|
|
2216
|
+
if (structured) {
|
|
2217
|
+
return {
|
|
2218
|
+
message: structured.error.message,
|
|
2219
|
+
code: structured.errors[0]?.code ?? 'server',
|
|
2220
|
+
errors: structured.errors,
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
if (error && typeof error === 'object' && 'errors' in error) {
|
|
2224
|
+
const errors = error.errors
|
|
2225
|
+
if (Array.isArray(errors)) {
|
|
2226
|
+
const first = errors[0]
|
|
2227
|
+
if (first && typeof first === 'object') {
|
|
2228
|
+
const message =
|
|
2229
|
+
'message' in first && typeof first.message === 'string'
|
|
2230
|
+
? first.message
|
|
2231
|
+
: 'Interrupt persistence failed.'
|
|
2232
|
+
const code =
|
|
2233
|
+
'code' in first && typeof first.code === 'string'
|
|
2234
|
+
? first.code
|
|
2235
|
+
: 'server'
|
|
2236
|
+
return { message, code }
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
return {
|
|
2241
|
+
message:
|
|
2242
|
+
error instanceof Error
|
|
2243
|
+
? error.message
|
|
2244
|
+
: 'Interrupt persistence failed.',
|
|
2245
|
+
code: 'server',
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
private buildInterruptRunErrorChunk(error: unknown): StreamChunk {
|
|
2250
|
+
const failure = this.interruptFailure(error)
|
|
2251
|
+
return {
|
|
2252
|
+
type: EventType.RUN_ERROR,
|
|
2253
|
+
timestamp: Date.now(),
|
|
2254
|
+
runId: this.runIdOverride ?? this.requestId,
|
|
2255
|
+
threadId: this.threadId,
|
|
2256
|
+
message: failure.message,
|
|
2257
|
+
code: failure.code,
|
|
2258
|
+
error: { message: failure.message, code: failure.code },
|
|
2259
|
+
...(failure.errors !== undefined
|
|
2260
|
+
? { 'tanstack:interruptErrors': failure.errors }
|
|
2261
|
+
: {}),
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
private async *emitInterruptRunError(
|
|
2266
|
+
error: unknown,
|
|
2267
|
+
): AsyncGenerator<StreamChunk, void, void> {
|
|
2268
|
+
const failure = this.interruptFailure(error)
|
|
2269
|
+
this.finalizationError = {
|
|
2270
|
+
message: failure.message,
|
|
2271
|
+
code: failure.code,
|
|
2272
|
+
cause: error,
|
|
2273
|
+
}
|
|
2274
|
+
yield* this.pipeThroughMiddleware(this.buildInterruptRunErrorChunk(error))
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
private async *emitActionableInterruptBoundary(
|
|
1806
2278
|
finishEvent: RunFinishedEvent,
|
|
1807
|
-
|
|
1808
|
-
|
|
2279
|
+
approvals: Array<ApprovalRequest>,
|
|
2280
|
+
clientRequests: Array<ClientToolRequest>,
|
|
2281
|
+
): AsyncGenerator<StreamChunk, boolean, void> {
|
|
2282
|
+
const terminal = this.completeEphemeralInterruptBindings(
|
|
2283
|
+
this.buildInterruptFinishedChunk(finishEvent, approvals, clientRequests),
|
|
2284
|
+
)
|
|
2285
|
+
let terminalOutputs: Array<StreamChunk>
|
|
2286
|
+
try {
|
|
2287
|
+
terminalOutputs = await this.middlewareRunner.runOnChunk(
|
|
2288
|
+
this.middlewareCtx,
|
|
2289
|
+
terminal,
|
|
2290
|
+
)
|
|
2291
|
+
} catch (error) {
|
|
2292
|
+
yield* this.emitInterruptRunError(error)
|
|
2293
|
+
return false
|
|
2294
|
+
}
|
|
1809
2295
|
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
2296
|
+
yield* this.pipeThroughMiddleware(this.buildMessagesSnapshotChunk())
|
|
2297
|
+
if (this.params.state !== undefined) {
|
|
2298
|
+
yield* this.pipeThroughMiddleware({
|
|
2299
|
+
type: EventType.STATE_SNAPSHOT,
|
|
1813
2300
|
timestamp: Date.now(),
|
|
1814
|
-
model:
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
toolCallId: clientTool.toolCallId,
|
|
1818
|
-
toolName: clientTool.toolName,
|
|
1819
|
-
input: clientTool.input,
|
|
1820
|
-
},
|
|
1821
|
-
} as StreamChunk)
|
|
2301
|
+
model: this.params.model,
|
|
2302
|
+
snapshot: this.params.state,
|
|
2303
|
+
})
|
|
1822
2304
|
}
|
|
2305
|
+
for (const output of terminalOutputs) {
|
|
2306
|
+
yield this.publicInterruptTerminal(output)
|
|
2307
|
+
this.middlewareCtx.chunkIndex++
|
|
2308
|
+
}
|
|
2309
|
+
return true
|
|
2310
|
+
}
|
|
1823
2311
|
|
|
1824
|
-
|
|
2312
|
+
private completeEphemeralInterruptBindings(chunk: StreamChunk): StreamChunk {
|
|
2313
|
+
if (
|
|
2314
|
+
chunk.type !== EventType.RUN_FINISHED ||
|
|
2315
|
+
chunk.outcome?.type !== 'interrupt'
|
|
2316
|
+
) {
|
|
2317
|
+
return chunk
|
|
2318
|
+
}
|
|
2319
|
+
const interruptedRunId = this.runIdOverride ?? this.requestId
|
|
2320
|
+
return {
|
|
2321
|
+
...chunk,
|
|
2322
|
+
outcome: {
|
|
2323
|
+
...chunk.outcome,
|
|
2324
|
+
interrupts: chunk.outcome.interrupts.map((interrupt) => {
|
|
2325
|
+
if (
|
|
2326
|
+
!interrupt.metadata ||
|
|
2327
|
+
typeof interrupt.metadata !== 'object' ||
|
|
2328
|
+
Array.isArray(interrupt.metadata)
|
|
2329
|
+
) {
|
|
2330
|
+
return interrupt
|
|
2331
|
+
}
|
|
2332
|
+
const metadata = { ...interrupt.metadata }
|
|
2333
|
+
const unopened = metadata[interruptBindingMetadataKey]
|
|
2334
|
+
if (
|
|
2335
|
+
unopened === null ||
|
|
2336
|
+
typeof unopened !== 'object' ||
|
|
2337
|
+
Array.isArray(unopened)
|
|
2338
|
+
) {
|
|
2339
|
+
return interrupt
|
|
2340
|
+
}
|
|
2341
|
+
metadata[interruptBindingMetadataKey] = {
|
|
2342
|
+
...unopened,
|
|
2343
|
+
interruptedRunId,
|
|
2344
|
+
generation: 0,
|
|
2345
|
+
}
|
|
2346
|
+
return { ...interrupt, metadata }
|
|
2347
|
+
}),
|
|
2348
|
+
},
|
|
2349
|
+
}
|
|
1825
2350
|
}
|
|
1826
2351
|
|
|
1827
2352
|
private buildToolResultChunks(
|
|
@@ -1845,6 +2370,7 @@ class TextEngine<
|
|
|
1845
2370
|
// argsMap is set only on continuation re-executions, where the adapter
|
|
1846
2371
|
// never streamed these calls. Otherwise it already emitted END, so a
|
|
1847
2372
|
// second one here would be an orphan that fails verifyEvents (#519).
|
|
2373
|
+
// When we do emit END, attach parsed `input`/`output` for TypedStreamChunk.
|
|
1848
2374
|
if (argsMap) {
|
|
1849
2375
|
chunks.push({
|
|
1850
2376
|
type: 'TOOL_CALL_START',
|
|
@@ -1853,7 +2379,7 @@ class TextEngine<
|
|
|
1853
2379
|
toolCallId: result.toolCallId,
|
|
1854
2380
|
toolCallName: result.toolName,
|
|
1855
2381
|
toolName: result.toolName,
|
|
1856
|
-
}
|
|
2382
|
+
})
|
|
1857
2383
|
|
|
1858
2384
|
const args = argsMap.get(result.toolCallId) ?? '{}'
|
|
1859
2385
|
chunks.push({
|
|
@@ -1873,8 +2399,10 @@ class TextEngine<
|
|
|
1873
2399
|
toolCallName: result.toolName,
|
|
1874
2400
|
toolName: result.toolName,
|
|
1875
2401
|
result: wireContent,
|
|
2402
|
+
...(result.input !== undefined && { input: result.input }),
|
|
2403
|
+
...(result.output !== undefined && { output: result.output }),
|
|
1876
2404
|
...(result.state !== undefined && { state: result.state }),
|
|
1877
|
-
}
|
|
2405
|
+
})
|
|
1878
2406
|
}
|
|
1879
2407
|
|
|
1880
2408
|
// AG-UI spec TOOL_CALL_RESULT event (content is string-only per spec)
|
|
@@ -1922,6 +2450,7 @@ class TextEngine<
|
|
|
1922
2450
|
} else {
|
|
1923
2451
|
this.messages = [...this.messages, newToolMessage]
|
|
1924
2452
|
}
|
|
2453
|
+
this.middlewareCtx.messages = this.messages
|
|
1925
2454
|
}
|
|
1926
2455
|
|
|
1927
2456
|
return chunks
|
|
@@ -1976,6 +2505,54 @@ class TextEngine<
|
|
|
1976
2505
|
return pending
|
|
1977
2506
|
}
|
|
1978
2507
|
|
|
2508
|
+
/**
|
|
2509
|
+
* Find a tool call by id in message history (including already-completed ones).
|
|
2510
|
+
* Used when the client has already attached a tool result for UI before resume.
|
|
2511
|
+
*/
|
|
2512
|
+
private findToolCallInMessages(toolCallId: string): ToolCall | undefined {
|
|
2513
|
+
for (const message of this.messages) {
|
|
2514
|
+
if (message.role !== 'assistant' || !message.toolCalls) continue
|
|
2515
|
+
for (const toolCall of message.toolCalls) {
|
|
2516
|
+
if (toolCall.id === toolCallId) return toolCall
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
return undefined
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
/**
|
|
2523
|
+
* Tool calls that must be reconstructed as interrupt pending for ephemeral
|
|
2524
|
+
* resume. Includes outstanding tools plus client tools that already have
|
|
2525
|
+
* results in history when the resume batch still carries `client_tool_*`
|
|
2526
|
+
* entries (the client writes local tool results before submitting resume).
|
|
2527
|
+
*/
|
|
2528
|
+
private getToolCallsForEphemeralResume(
|
|
2529
|
+
resume: ReadonlyArray<{ interruptId: string }> | undefined,
|
|
2530
|
+
): Array<ToolCall> {
|
|
2531
|
+
const pending = this.getPendingToolCallsFromMessages()
|
|
2532
|
+
const byId = new Map(pending.map((toolCall) => [toolCall.id, toolCall]))
|
|
2533
|
+
for (const entry of resume ?? []) {
|
|
2534
|
+
// Recover tool calls the client already finalized in history for two
|
|
2535
|
+
// resume-batch cases that no longer look "pending":
|
|
2536
|
+
// - `client_tool_*`: a client tool wrote its output before resuming.
|
|
2537
|
+
// - `approval_*`: a DENIED approval wrote its denial result, so the
|
|
2538
|
+
// call reads as completed. Without this it drops out of the
|
|
2539
|
+
// reconstructed batch and the resume entry fails as unknown-interrupt.
|
|
2540
|
+
let toolCallId: string | undefined
|
|
2541
|
+
if (entry.interruptId.startsWith('client_tool_')) {
|
|
2542
|
+
toolCallId = entry.interruptId.slice('client_tool_'.length)
|
|
2543
|
+
} else if (entry.interruptId.startsWith('approval_')) {
|
|
2544
|
+
toolCallId = entry.interruptId.slice('approval_'.length)
|
|
2545
|
+
}
|
|
2546
|
+
if (toolCallId === undefined || byId.has(toolCallId)) continue
|
|
2547
|
+
const toolCall = this.findToolCallInMessages(toolCallId)
|
|
2548
|
+
if (toolCall && !isProviderExecutedToolCall(toolCall)) {
|
|
2549
|
+
pending.push(toolCall)
|
|
2550
|
+
byId.set(toolCallId, toolCall)
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
return pending
|
|
2554
|
+
}
|
|
2555
|
+
|
|
1979
2556
|
private createSyntheticFinishedEvent(): RunFinishedEvent {
|
|
1980
2557
|
return {
|
|
1981
2558
|
type: 'RUN_FINISHED',
|
|
@@ -1987,35 +2564,44 @@ class TextEngine<
|
|
|
1987
2564
|
} as RunFinishedEvent
|
|
1988
2565
|
}
|
|
1989
2566
|
|
|
1990
|
-
private shouldContinue(): boolean {
|
|
2567
|
+
private async shouldContinue(): Promise<boolean> {
|
|
2568
|
+
// Always enter the tool-execution half-cycle after a model turn.
|
|
1991
2569
|
if (this.cyclePhase === 'executeToolCalls') {
|
|
1992
2570
|
return true
|
|
1993
2571
|
}
|
|
1994
2572
|
|
|
2573
|
+
const state = {
|
|
2574
|
+
iterationCount: this.iterationCount,
|
|
2575
|
+
messages: this.messages,
|
|
2576
|
+
finishReason: this.lastFinishReason,
|
|
2577
|
+
toolCallCount: this.toolCallCount,
|
|
2578
|
+
lastTurnToolCallCount: this.lastTurnToolCallCount,
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
// Evaluate strategy and middleware unconditionally (even when the
|
|
2582
|
+
// strategy already says stop) so every onShouldContinue observer still
|
|
2583
|
+
// sees the final counters; AND all three at the end.
|
|
2584
|
+
const strategyContinues = this.loopStrategy(state)
|
|
2585
|
+
const middlewareContinues = await this.middlewareRunner.runOnShouldContinue(
|
|
2586
|
+
this.middlewareCtx,
|
|
2587
|
+
state,
|
|
2588
|
+
)
|
|
2589
|
+
|
|
1995
2590
|
return (
|
|
1996
|
-
this.
|
|
1997
|
-
iterationCount: this.iterationCount,
|
|
1998
|
-
messages: this.messages,
|
|
1999
|
-
finishReason: this.lastFinishReason,
|
|
2000
|
-
toolCallCount: this.toolCallCount,
|
|
2001
|
-
lastTurnToolCallCount: this.lastTurnToolCallCount,
|
|
2002
|
-
}) && this.toolPhase === 'continue'
|
|
2591
|
+
strategyContinues && middlewareContinues && this.toolPhase === 'continue'
|
|
2003
2592
|
)
|
|
2004
2593
|
}
|
|
2005
2594
|
|
|
2006
2595
|
/**
|
|
2007
|
-
* Record tool calls (deduped by id)
|
|
2008
|
-
*
|
|
2009
|
-
* error results so every tool_call still has a matching result.
|
|
2596
|
+
* Record tool calls (deduped by id) toward `toolCallCount` /
|
|
2597
|
+
* `lastTurnToolCallCount` for strategies and middleware `onShouldContinue`.
|
|
2010
2598
|
*
|
|
2011
2599
|
* Used for both live model turns and pending/resume batches. IDs already
|
|
2012
2600
|
* counted in this run (e.g. wait→resume after a live turn) are not
|
|
2013
|
-
* re-added to `toolCallCount`.
|
|
2601
|
+
* re-added to `toolCallCount`. Per-turn execution caps are app middleware
|
|
2602
|
+
* (`onBeforeToolCall` skip), not engine policy.
|
|
2014
2603
|
*/
|
|
2015
|
-
private
|
|
2016
|
-
toExecute: Array<ToolCall>
|
|
2017
|
-
skippedResults: Array<ToolResult>
|
|
2018
|
-
} {
|
|
2604
|
+
private recordToolCalls(toolCalls: Array<ToolCall>): void {
|
|
2019
2605
|
this.lastTurnToolCallCount = toolCalls.length
|
|
2020
2606
|
let newlyCounted = 0
|
|
2021
2607
|
for (const tc of toolCalls) {
|
|
@@ -2025,34 +2611,6 @@ class TextEngine<
|
|
|
2025
2611
|
}
|
|
2026
2612
|
}
|
|
2027
2613
|
this.toolCallCount += newlyCounted
|
|
2028
|
-
|
|
2029
|
-
const cap = this.maxToolCallsPerTurn
|
|
2030
|
-
if (cap == null || toolCalls.length <= cap) {
|
|
2031
|
-
return { toExecute: toolCalls, skippedResults: [] }
|
|
2032
|
-
}
|
|
2033
|
-
|
|
2034
|
-
this.logger.agentLoop(
|
|
2035
|
-
`maxToolCallsPerTurn=${cap} skipped=${toolCalls.length - cap}`,
|
|
2036
|
-
{
|
|
2037
|
-
maxToolCallsPerTurn: cap,
|
|
2038
|
-
emitted: toolCalls.length,
|
|
2039
|
-
skipped: toolCalls.length - cap,
|
|
2040
|
-
},
|
|
2041
|
-
)
|
|
2042
|
-
|
|
2043
|
-
const toExecute = toolCalls.slice(0, cap)
|
|
2044
|
-
const skippedResults: Array<ToolResult> = toolCalls
|
|
2045
|
-
.slice(cap)
|
|
2046
|
-
.map((tc) => ({
|
|
2047
|
-
toolCallId: tc.id,
|
|
2048
|
-
toolName: tc.function.name,
|
|
2049
|
-
result: {
|
|
2050
|
-
error: `Skipped: exceeded maxToolCallsPerTurn (${cap})`,
|
|
2051
|
-
},
|
|
2052
|
-
state: 'output-error' as const,
|
|
2053
|
-
}))
|
|
2054
|
-
|
|
2055
|
-
return { toExecute, skippedResults }
|
|
2056
2614
|
}
|
|
2057
2615
|
|
|
2058
2616
|
private isAborted(): boolean {
|
|
@@ -2067,6 +2625,100 @@ class TextEngine<
|
|
|
2067
2625
|
return this.isAborted() || this.isMiddlewareAborted()
|
|
2068
2626
|
}
|
|
2069
2627
|
|
|
2628
|
+
/**
|
|
2629
|
+
* The reason to report on `AbortInfo` for a cancelled run.
|
|
2630
|
+
*
|
|
2631
|
+
* `this.abortReason` only ever holds a *middleware*-initiated reason
|
|
2632
|
+
* (`ctx.abort(reason)` / `MiddlewareAbortError`). A caller that aborts its own
|
|
2633
|
+
* controller — `abortController.abort(RUN_CANCEL_REASON)`, the in-process
|
|
2634
|
+
* cancel channel — never touches that field, so the reason has to be read back
|
|
2635
|
+
* off the caller's signal, which is the signal `isCancelled()` consults via
|
|
2636
|
+
* `isAborted()`. A signal aborted with no reason carries a DOMException rather
|
|
2637
|
+
* than a string, so non-string reasons are reported as absent.
|
|
2638
|
+
*/
|
|
2639
|
+
private resolveAbortReason(): string | undefined {
|
|
2640
|
+
if (this.abortReason !== undefined) return this.abortReason
|
|
2641
|
+
const signalReason: unknown = this.effectiveSignal?.reason
|
|
2642
|
+
return typeof signalReason === 'string' ? signalReason : undefined
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
/**
|
|
2646
|
+
* Whether this run's teardown declared its abort a DETACH — see
|
|
2647
|
+
* {@link RunDetachedCapability}. Only `withSandbox`'s `onAbort` publishes it,
|
|
2648
|
+
* and only for a plain, intentless disconnect of a detachable run, so every
|
|
2649
|
+
* other exit path answers `false`.
|
|
2650
|
+
*
|
|
2651
|
+
* Surfaced on the engine (rather than the ctx being handed out) so the
|
|
2652
|
+
* capability read stays inside core, and so the delivery sink learns the
|
|
2653
|
+
* verdict through {@link publishRunDetachedSignal} instead of reaching into a
|
|
2654
|
+
* middleware context it has no business holding.
|
|
2655
|
+
*
|
|
2656
|
+
* @internal
|
|
2657
|
+
*/
|
|
2658
|
+
wasDetached(): boolean {
|
|
2659
|
+
return getRunDetached(this.middlewareCtx, { optional: true }) === true
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
/**
|
|
2663
|
+
* The delivery socket closed while this run was still going.
|
|
2664
|
+
*
|
|
2665
|
+
* Notifies every subscriber (see {@link RunDisconnectCapability}) and RETURNS
|
|
2666
|
+
* IMMEDIATELY. Synchronous on purpose: it is called from
|
|
2667
|
+
* `ReadableStream.cancel()`, which must not be made to wait on a run-store
|
|
2668
|
+
* write, and the caller ({@link notifyRunDisconnected}) has no consumer left to
|
|
2669
|
+
* report to anyway.
|
|
2670
|
+
*
|
|
2671
|
+
* Subscribers therefore run CONCURRENTLY with the still-executing run — which is
|
|
2672
|
+
* the entire point. The run is typically suspended inside a slow middleware
|
|
2673
|
+
* `setup` at this moment, so anything dispatched from the run's own unwinding
|
|
2674
|
+
* would be minutes late. Nothing on this path aborts the run: a durable run
|
|
2675
|
+
* outlives its viewer.
|
|
2676
|
+
*
|
|
2677
|
+
* Each subscriber's promise is parked on `deferredPromises`, which the run awaits
|
|
2678
|
+
* in its `finally`, so bookkeeping cannot be lost to a race with the run's own
|
|
2679
|
+
* completion even though nothing awaits it here.
|
|
2680
|
+
*
|
|
2681
|
+
* IDEMPOTENT. A second cancel, or one arriving after a terminal hook already ran,
|
|
2682
|
+
* is ignored: the terminal hooks own the run's outcome, and re-stamping
|
|
2683
|
+
* `detachedSince` on a run that has already finished would hand a completed run
|
|
2684
|
+
* to the reaper as reclaimable work.
|
|
2685
|
+
*
|
|
2686
|
+
* @internal
|
|
2687
|
+
*/
|
|
2688
|
+
notifyDisconnected(): void {
|
|
2689
|
+
if (this.disconnected || this.terminalHookCalled) return
|
|
2690
|
+
this.disconnected = true
|
|
2691
|
+
for (const listener of this.disconnectListeners) {
|
|
2692
|
+
this.runDisconnectListener(listener)
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
/**
|
|
2697
|
+
* Invoke one disconnect listener, isolated and with its failure SWALLOWED after
|
|
2698
|
+
* logging.
|
|
2699
|
+
*
|
|
2700
|
+
* There is no caller left to report to — the socket this would report on is the
|
|
2701
|
+
* one that just closed — and a rejection parked on `deferredPromises` would
|
|
2702
|
+
* surface as the run's failure, replacing a healthy outcome with a bookkeeping
|
|
2703
|
+
* error. Isolation matters for the usual reason too: one subscriber's failing
|
|
2704
|
+
* write must not skip the next one's.
|
|
2705
|
+
*/
|
|
2706
|
+
private runDisconnectListener(listener: () => void | Promise<void>): void {
|
|
2707
|
+
let result: void | Promise<void>
|
|
2708
|
+
try {
|
|
2709
|
+
result = listener()
|
|
2710
|
+
} catch (error) {
|
|
2711
|
+
this.logger.errors('run disconnect listener failed', { error })
|
|
2712
|
+
return
|
|
2713
|
+
}
|
|
2714
|
+
if (result === undefined) return
|
|
2715
|
+
this.deferredPromises.push(
|
|
2716
|
+
result.catch((error: unknown) => {
|
|
2717
|
+
this.logger.errors('run disconnect listener failed', { error })
|
|
2718
|
+
}),
|
|
2719
|
+
)
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2070
2722
|
/**
|
|
2071
2723
|
* Run the final structured-output adapter call through the middleware
|
|
2072
2724
|
* pipeline. Yields chunks to the caller only when
|
|
@@ -2622,12 +3274,181 @@ class TextEngine<
|
|
|
2622
3274
|
messages: this.messages,
|
|
2623
3275
|
systemPrompts: [...this.systemPrompts],
|
|
2624
3276
|
tools: [...this.tools],
|
|
3277
|
+
resume: this.params.resume,
|
|
3278
|
+
resumeToolState: {
|
|
3279
|
+
approvals: this.resumeApprovals,
|
|
3280
|
+
clientToolResults: this.resumeClientToolResults,
|
|
3281
|
+
deniedToolResults: this.resumeDeniedToolResults,
|
|
3282
|
+
cancelledToolCallIds: this.resumeCancelledToolCallIds,
|
|
3283
|
+
},
|
|
2625
3284
|
metadata: this.params.metadata,
|
|
2626
3285
|
modelOptions: this.params.modelOptions,
|
|
2627
3286
|
}
|
|
2628
3287
|
}
|
|
2629
3288
|
|
|
3289
|
+
private async applyEphemeralInterruptResume(
|
|
3290
|
+
config: ChatMiddlewareConfig,
|
|
3291
|
+
): Promise<void> {
|
|
3292
|
+
if ((config.resume?.length ?? 0) === 0) {
|
|
3293
|
+
return
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
const interruptedRunId = this.parentRunIdOverride
|
|
3297
|
+
if (!interruptedRunId) {
|
|
3298
|
+
throw new InterruptResumeValidationError([
|
|
3299
|
+
{
|
|
3300
|
+
scope: 'batch',
|
|
3301
|
+
threadId: this.threadId,
|
|
3302
|
+
interruptedRunId: this.runIdOverride ?? this.requestId,
|
|
3303
|
+
generation: 0,
|
|
3304
|
+
interruptIds: config.resume?.map((entry) => entry.interruptId) ?? [],
|
|
3305
|
+
code: 'stale',
|
|
3306
|
+
message:
|
|
3307
|
+
'Interrupt continuation requires parentRunId to identify the interrupted run.',
|
|
3308
|
+
source: 'server',
|
|
3309
|
+
retryable: false,
|
|
3310
|
+
},
|
|
3311
|
+
])
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
const approvalRequests: Array<ApprovalRequest> = []
|
|
3315
|
+
const clientRequests: Array<ClientToolRequest> = []
|
|
3316
|
+
// Prefer resume-aware reconstruction so client-tool outputs already written
|
|
3317
|
+
// into history for UI still validate against the resume batch.
|
|
3318
|
+
const pendingToolCalls = this.getToolCallsForEphemeralResume(config.resume)
|
|
3319
|
+
const resumeInterruptIds = new Set(
|
|
3320
|
+
config.resume?.map((entry) => entry.interruptId),
|
|
3321
|
+
)
|
|
3322
|
+
const toolInputs = new Map<string, unknown>()
|
|
3323
|
+
const toolsByCallId = new Map<string, AnyRuntimeTool>()
|
|
3324
|
+
const clientExecutionCallIds = new Set<string>()
|
|
3325
|
+
|
|
3326
|
+
for (const toolCall of pendingToolCalls) {
|
|
3327
|
+
const tool = this.tools.find(
|
|
3328
|
+
(candidate) => candidate.name === toolCall.function.name,
|
|
3329
|
+
)
|
|
3330
|
+
if (!tool) continue
|
|
3331
|
+
toolsByCallId.set(toolCall.id, tool)
|
|
3332
|
+
let input: unknown = {}
|
|
3333
|
+
try {
|
|
3334
|
+
const parsed = JSON.parse(toolCall.function.arguments.trim() || '{}')
|
|
3335
|
+
input = parsed && typeof parsed === 'object' ? parsed : {}
|
|
3336
|
+
} catch {
|
|
3337
|
+
input = {}
|
|
3338
|
+
}
|
|
3339
|
+
toolInputs.set(toolCall.id, input)
|
|
3340
|
+
if (
|
|
3341
|
+
!tool.execute &&
|
|
3342
|
+
resumeInterruptIds.has(`client_tool_${toolCall.id}`)
|
|
3343
|
+
) {
|
|
3344
|
+
clientExecutionCallIds.add(toolCall.id)
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
// Mirror executeToolCalls' scheduling boundary. Server execution remains
|
|
3349
|
+
// gated while any approval is outstanding, but plain client tools are
|
|
3350
|
+
// represented in the same interrupt batch because requesting their output
|
|
3351
|
+
// does not execute a server-side effect.
|
|
3352
|
+
for (const toolCall of pendingToolCalls) {
|
|
3353
|
+
const tool = toolsByCallId.get(toolCall.id)
|
|
3354
|
+
if (tool?.needsApproval && !clientExecutionCallIds.has(toolCall.id)) {
|
|
3355
|
+
approvalRequests.push({
|
|
3356
|
+
toolCallId: toolCall.id,
|
|
3357
|
+
toolName: toolCall.function.name,
|
|
3358
|
+
input: toolInputs.get(toolCall.id) ?? {},
|
|
3359
|
+
approvalId: `approval_${toolCall.id}`,
|
|
3360
|
+
})
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
for (const toolCall of pendingToolCalls) {
|
|
3365
|
+
const tool = toolsByCallId.get(toolCall.id)
|
|
3366
|
+
if (
|
|
3367
|
+
tool !== undefined &&
|
|
3368
|
+
!tool.execute &&
|
|
3369
|
+
(!tool.needsApproval || clientExecutionCallIds.has(toolCall.id))
|
|
3370
|
+
) {
|
|
3371
|
+
clientRequests.push({
|
|
3372
|
+
toolCallId: toolCall.id,
|
|
3373
|
+
toolName: toolCall.function.name,
|
|
3374
|
+
input: toolInputs.get(toolCall.id) ?? {},
|
|
3375
|
+
})
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
const pending = this.buildActionableInterrupts(
|
|
3380
|
+
approvalRequests,
|
|
3381
|
+
clientRequests,
|
|
3382
|
+
).flatMap((descriptor) => {
|
|
3383
|
+
const unopened = readUnopenedInterruptBinding(descriptor)
|
|
3384
|
+
return unopened
|
|
3385
|
+
? [
|
|
3386
|
+
{
|
|
3387
|
+
interruptId: descriptor.id,
|
|
3388
|
+
payload: descriptor,
|
|
3389
|
+
binding: {
|
|
3390
|
+
...unopened,
|
|
3391
|
+
interruptedRunId,
|
|
3392
|
+
generation: 0,
|
|
3393
|
+
} satisfies InterruptBinding,
|
|
3394
|
+
},
|
|
3395
|
+
]
|
|
3396
|
+
: []
|
|
3397
|
+
})
|
|
3398
|
+
const validated = await validateInterruptResumeBatch({
|
|
3399
|
+
threadId: this.threadId,
|
|
3400
|
+
interruptedRunId,
|
|
3401
|
+
generation: 0,
|
|
3402
|
+
pending,
|
|
3403
|
+
resume: config.resume,
|
|
3404
|
+
tools: this.tools,
|
|
3405
|
+
})
|
|
3406
|
+
if (validated.errors.length > 0 || !validated.resumeToolState) {
|
|
3407
|
+
throw new InterruptResumeValidationError(validated.errors)
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
// A client-tool execution interrupt can only be emitted after an
|
|
3411
|
+
// approval-required client tool was approved in the preceding ephemeral
|
|
3412
|
+
// run. Reconstruct that phase marker from the trusted `client_tool_*`
|
|
3413
|
+
// continuation so executeToolCalls consumes the validated client output
|
|
3414
|
+
// instead of asking for approval again.
|
|
3415
|
+
const approvals = new Map(validated.resumeToolState.approvals)
|
|
3416
|
+
for (const request of clientRequests) {
|
|
3417
|
+
if (toolsByCallId.get(request.toolCallId)?.needsApproval) {
|
|
3418
|
+
approvals.set(request.toolCallId, true)
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
this.applyResumeToolState({
|
|
3422
|
+
...validated.resumeToolState,
|
|
3423
|
+
approvals,
|
|
3424
|
+
})
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3427
|
+
private applyResumeToolState(state: ChatResumeToolState | undefined): void {
|
|
3428
|
+
if (state?.approvals) {
|
|
3429
|
+
for (const [approvalId, resolution] of state.approvals) {
|
|
3430
|
+
this.resumeApprovals.set(approvalId, resolution)
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
if (state?.clientToolResults) {
|
|
3434
|
+
for (const [toolCallId, result] of state.clientToolResults) {
|
|
3435
|
+
this.resumeClientToolResults.set(toolCallId, result)
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
if (state?.deniedToolResults) {
|
|
3439
|
+
for (const [toolCallId, result] of state.deniedToolResults) {
|
|
3440
|
+
this.resumeDeniedToolResults.set(toolCallId, result)
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
if (state?.cancelledToolCallIds) {
|
|
3444
|
+
for (const toolCallId of state.cancelledToolCallIds) {
|
|
3445
|
+
this.resumeCancelledToolCallIds.add(toolCallId)
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
|
|
2630
3450
|
private applyMiddlewareConfig(config: ChatMiddlewareConfig): void {
|
|
3451
|
+
this.applyResumeToolState(config.resumeToolState)
|
|
2631
3452
|
this.messages = config.messages
|
|
2632
3453
|
this.systemPrompts = config.systemPrompts
|
|
2633
3454
|
this.tools = config.tools
|
|
@@ -2718,7 +3539,7 @@ class TextEngine<
|
|
|
2718
3539
|
model: this.params.model,
|
|
2719
3540
|
name: eventName,
|
|
2720
3541
|
value,
|
|
2721
|
-
}
|
|
3542
|
+
}
|
|
2722
3543
|
}
|
|
2723
3544
|
|
|
2724
3545
|
private createId(prefix: string): string {
|
|
@@ -2745,7 +3566,7 @@ class TextEngine<
|
|
|
2745
3566
|
* import { openaiText } from '@tanstack/ai-openai'
|
|
2746
3567
|
*
|
|
2747
3568
|
* for await (const chunk of chat({
|
|
2748
|
-
* adapter: openaiText('gpt-
|
|
3569
|
+
* adapter: openaiText('gpt-5.5'),
|
|
2749
3570
|
* messages: [{ role: 'user', content: 'What is the weather?' }],
|
|
2750
3571
|
* tools: [weatherTool]
|
|
2751
3572
|
* })) {
|
|
@@ -2758,7 +3579,7 @@ class TextEngine<
|
|
|
2758
3579
|
* @example One-shot text (streaming without tools)
|
|
2759
3580
|
* ```ts
|
|
2760
3581
|
* for await (const chunk of chat({
|
|
2761
|
-
* adapter: openaiText('gpt-
|
|
3582
|
+
* adapter: openaiText('gpt-5.5'),
|
|
2762
3583
|
* messages: [{ role: 'user', content: 'Hello!' }]
|
|
2763
3584
|
* })) {
|
|
2764
3585
|
* console.log(chunk)
|
|
@@ -2768,7 +3589,7 @@ class TextEngine<
|
|
|
2768
3589
|
* @example Non-streaming text (stream: false)
|
|
2769
3590
|
* ```ts
|
|
2770
3591
|
* const text = await chat({
|
|
2771
|
-
* adapter: openaiText('gpt-
|
|
3592
|
+
* adapter: openaiText('gpt-5.5'),
|
|
2772
3593
|
* messages: [{ role: 'user', content: 'Hello!' }],
|
|
2773
3594
|
* stream: false
|
|
2774
3595
|
* })
|
|
@@ -2780,7 +3601,7 @@ class TextEngine<
|
|
|
2780
3601
|
* import { z } from 'zod'
|
|
2781
3602
|
*
|
|
2782
3603
|
* const result = await chat({
|
|
2783
|
-
* adapter: openaiText('gpt-
|
|
3604
|
+
* adapter: openaiText('gpt-5.5'),
|
|
2784
3605
|
* messages: [{ role: 'user', content: 'Research and summarize the topic' }],
|
|
2785
3606
|
* tools: [researchTool, analyzeTool],
|
|
2786
3607
|
* outputSchema: z.object({
|
|
@@ -2820,7 +3641,7 @@ export function chat<
|
|
|
2820
3641
|
TTools,
|
|
2821
3642
|
TMiddleware
|
|
2822
3643
|
>,
|
|
2823
|
-
): TextActivityResult<TSchema, TStream> {
|
|
3644
|
+
): TextActivityResult<TSchema, TStream, TTools> {
|
|
2824
3645
|
validateCapabilities(options.middleware ?? [], options.adapter)
|
|
2825
3646
|
|
|
2826
3647
|
const { outputSchema, stream } = options
|
|
@@ -2833,7 +3654,7 @@ export function chat<
|
|
|
2833
3654
|
...options,
|
|
2834
3655
|
outputSchema,
|
|
2835
3656
|
stream,
|
|
2836
|
-
}) as TextActivityResult<TSchema, TStream>
|
|
3657
|
+
}) as TextActivityResult<TSchema, TStream, TTools>
|
|
2837
3658
|
}
|
|
2838
3659
|
|
|
2839
3660
|
// If outputSchema is provided, run agentic structured output (Promise<T>)
|
|
@@ -2841,7 +3662,7 @@ export function chat<
|
|
|
2841
3662
|
return runAgenticStructuredOutput({
|
|
2842
3663
|
...options,
|
|
2843
3664
|
outputSchema,
|
|
2844
|
-
}) as TextActivityResult<TSchema, TStream>
|
|
3665
|
+
}) as TextActivityResult<TSchema, TStream, TTools>
|
|
2845
3666
|
}
|
|
2846
3667
|
|
|
2847
3668
|
// If stream is explicitly false, run non-streaming text
|
|
@@ -2850,7 +3671,7 @@ export function chat<
|
|
|
2850
3671
|
...options,
|
|
2851
3672
|
outputSchema: undefined,
|
|
2852
3673
|
stream,
|
|
2853
|
-
}) as TextActivityResult<TSchema, TStream>
|
|
3674
|
+
}) as TextActivityResult<TSchema, TStream, TTools>
|
|
2854
3675
|
}
|
|
2855
3676
|
|
|
2856
3677
|
// Otherwise, run streaming text (default)
|
|
@@ -2858,14 +3679,70 @@ export function chat<
|
|
|
2858
3679
|
...options,
|
|
2859
3680
|
outputSchema: undefined,
|
|
2860
3681
|
stream,
|
|
2861
|
-
}) as TextActivityResult<TSchema, TStream>
|
|
3682
|
+
}) as TextActivityResult<TSchema, TStream, TTools>
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
/**
|
|
3686
|
+
* The slice of the engine that the durable delivery sink reaches back into, in
|
|
3687
|
+
* BOTH directions: it reads the detach verdict (`wasDetached`) and pushes the
|
|
3688
|
+
* socket-closed fact in (`notifyDisconnected`). Filled by the generator body as
|
|
3689
|
+
* soon as its engine exists.
|
|
3690
|
+
*/
|
|
3691
|
+
interface DeliveryEngineRef {
|
|
3692
|
+
current?: {
|
|
3693
|
+
wasDetached: () => boolean
|
|
3694
|
+
notifyDisconnected: () => void
|
|
3695
|
+
}
|
|
2862
3696
|
}
|
|
2863
3697
|
|
|
2864
3698
|
/**
|
|
2865
|
-
*
|
|
3699
|
+
* Publish both delivery-side seams for `stream`.
|
|
3700
|
+
*
|
|
3701
|
+
* Shared by the two streaming paths so they cannot drift apart — the
|
|
3702
|
+
* structured-output path having been wired for one seam and not the other is
|
|
3703
|
+
* exactly the bug `publishRunDetachedSignal` picked up last time (a durable
|
|
3704
|
+
* `chat({ outputSchema, stream: true })` could never detach).
|
|
2866
3705
|
*/
|
|
2867
|
-
|
|
3706
|
+
function publishDeliverySeams(
|
|
3707
|
+
stream: object,
|
|
3708
|
+
engineRef: DeliveryEngineRef,
|
|
3709
|
+
): void {
|
|
3710
|
+
// A thunk, evaluated on the sink's teardown path: the engine does not exist
|
|
3711
|
+
// yet, and the verdict it will report is only written during `onAbort`.
|
|
3712
|
+
publishRunDetachedSignal(
|
|
3713
|
+
stream,
|
|
3714
|
+
() => engineRef.current?.wasDetached() === true,
|
|
3715
|
+
)
|
|
3716
|
+
// The inbound direction. Dropped if the socket closes before the body has run
|
|
3717
|
+
// far enough to have an engine, which is correct: there is no run state to
|
|
3718
|
+
// record yet, and `setup` has not begun, so nothing is leaked by not knowing.
|
|
3719
|
+
publishRunDisconnectHandler(stream, () => {
|
|
3720
|
+
engineRef.current?.notifyDisconnected()
|
|
3721
|
+
})
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
/**
|
|
3725
|
+
* Run streaming text (agentic or one-shot depending on tools).
|
|
3726
|
+
*
|
|
3727
|
+
* A thin, NON-generator wrapper, because the stream object is also the key the
|
|
3728
|
+
* durable delivery sink looks the run's detach verdict up under (see
|
|
3729
|
+
* `../../delivery-detach`) and delivers its disconnect notification through (see
|
|
3730
|
+
* `../../delivery-disconnect`). A generator function cannot reach the generator it
|
|
3731
|
+
* returns, so the identity has to be minted out here and the engine reached back
|
|
3732
|
+
* through `engineRef`, which the body fills as soon as its engine exists.
|
|
3733
|
+
*/
|
|
3734
|
+
function runStreamingText<TContext = unknown>(
|
|
2868
3735
|
options: TextActivityOptions<AnyTextAdapter, undefined, true, TContext>,
|
|
3736
|
+
): AsyncIterable<StreamChunk> {
|
|
3737
|
+
const engineRef: DeliveryEngineRef = {}
|
|
3738
|
+
const stream = streamTextChunks(options, engineRef)
|
|
3739
|
+
publishDeliverySeams(stream, engineRef)
|
|
3740
|
+
return stream
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
async function* streamTextChunks<TContext = unknown>(
|
|
3744
|
+
options: TextActivityOptions<AnyTextAdapter, undefined, true, TContext>,
|
|
3745
|
+
engineRef: DeliveryEngineRef,
|
|
2869
3746
|
): AsyncIterable<StreamChunk> {
|
|
2870
3747
|
const { adapter, middleware, context, debug, mcp, ...textOptions } = options
|
|
2871
3748
|
const model = adapter.model
|
|
@@ -2890,6 +3767,7 @@ async function* runStreamingText<TContext = unknown>(
|
|
|
2890
3767
|
},
|
|
2891
3768
|
logger,
|
|
2892
3769
|
)
|
|
3770
|
+
engineRef.current = engine
|
|
2893
3771
|
|
|
2894
3772
|
try {
|
|
2895
3773
|
for await (const chunk of engine.run()) {
|
|
@@ -2909,7 +3787,7 @@ function runNonStreamingText<TContext = unknown>(
|
|
|
2909
3787
|
): Promise<string> {
|
|
2910
3788
|
// Run the streaming text and collect all text using streamToText.
|
|
2911
3789
|
const stream = runStreamingText(
|
|
2912
|
-
//
|
|
3790
|
+
// oxlint-disable-next-line eslint-js/no-restricted-syntax -- generic-stream remap: caller is non-streaming (false), but runStreamingText is invoked internally to collect text; concrete `false`→`true` literals don't structurally overlap.
|
|
2913
3791
|
options as unknown as TextActivityOptions<
|
|
2914
3792
|
AnyTextAdapter,
|
|
2915
3793
|
undefined,
|
|
@@ -3233,25 +4111,36 @@ function runStreamingStructuredOutput<
|
|
|
3233
4111
|
undoNullWidening(data, nullWideningMap)
|
|
3234
4112
|
|
|
3235
4113
|
// The implementation generator yields the broader internal type
|
|
3236
|
-
// (`StreamChunk | StructuredOutputCompleteEvent<T>`) so
|
|
3237
|
-
// CustomEvents can flow through
|
|
3238
|
-
//
|
|
3239
|
-
//
|
|
3240
|
-
//
|
|
3241
|
-
|
|
4114
|
+
// (`StreamChunk | StructuredOutputCompleteEvent<T>`) so middleware and
|
|
4115
|
+
// tool-emitted CustomEvents can flow through. Core approval/client-tool
|
|
4116
|
+
// waits are represented by RUN_FINISHED interrupt outcomes, not by direct
|
|
4117
|
+
// CUSTOM wait events.
|
|
4118
|
+
// The contained cast keeps the public stream type focused on
|
|
4119
|
+
// structured-output completion.
|
|
4120
|
+
//
|
|
4121
|
+
// Same seam as `runStreamingText`: this wrapper is NOT a generator, so the
|
|
4122
|
+
// stream identity can be minted here and the engine reached back through
|
|
4123
|
+
// `engineRef` once the impl body has its engine. Without this a durable
|
|
4124
|
+
// structured-output stream could never detach — the sink would find no verdict
|
|
4125
|
+
// and terminalize a healthy detached run's log — nor survive a disconnect.
|
|
4126
|
+
const engineRef: DeliveryEngineRef = {}
|
|
4127
|
+
const stream = runStreamingStructuredOutputImpl(
|
|
3242
4128
|
options,
|
|
3243
4129
|
jsonSchema,
|
|
3244
4130
|
normalize,
|
|
3245
|
-
|
|
4131
|
+
engineRef,
|
|
4132
|
+
)
|
|
4133
|
+
publishDeliverySeams(stream, engineRef)
|
|
4134
|
+
return stream as StructuredOutputStream<InferSchemaType<TSchema>>
|
|
3246
4135
|
}
|
|
3247
4136
|
|
|
3248
4137
|
/**
|
|
3249
4138
|
* Internal generator return type — broader than the public
|
|
3250
|
-
* `StructuredOutputStream<T>`. The
|
|
3251
|
-
*
|
|
3252
|
-
*
|
|
3253
|
-
*
|
|
3254
|
-
* `
|
|
4139
|
+
* `StructuredOutputStream<T>`. The structured-output completion event remains
|
|
4140
|
+
* the pinned public CUSTOM event for this stream; approval and client-tool
|
|
4141
|
+
* waits now surface as RUN_FINISHED interrupt outcomes. At runtime, tools can
|
|
4142
|
+
* still emit arbitrary user-defined `CustomEvent`s through the
|
|
4143
|
+
* `emitCustomEvent` context API; those flow
|
|
3255
4144
|
* through this generator with `name: string` and are widened out at the
|
|
3256
4145
|
* public boundary because keeping them would collapse the typed narrow back
|
|
3257
4146
|
* to `any`. The cast inside `runStreamingStructuredOutput` is where that
|
|
@@ -3268,6 +4157,7 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
3268
4157
|
options: TextActivityOptions<AnyTextAdapter, TSchema, true, TContext>,
|
|
3269
4158
|
jsonSchema: NonNullable<ReturnType<typeof convertSchemaToJsonSchema>>,
|
|
3270
4159
|
normalize: (data: unknown) => unknown,
|
|
4160
|
+
engineRef: DeliveryEngineRef,
|
|
3271
4161
|
): StructuredOutputStreamInternal<InferSchemaType<TSchema>> {
|
|
3272
4162
|
const {
|
|
3273
4163
|
adapter,
|
|
@@ -3318,6 +4208,7 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
3318
4208
|
},
|
|
3319
4209
|
logger,
|
|
3320
4210
|
)
|
|
4211
|
+
engineRef.current = engine
|
|
3321
4212
|
|
|
3322
4213
|
try {
|
|
3323
4214
|
for await (const chunk of engine.run()) {
|