@tanstack/ai 0.41.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/dist/esm/activities/chat/adapter.js +23 -16
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/agent-loop-strategies.d.ts +10 -4
- package/dist/esm/activities/chat/agent-loop-strategies.js +75 -17
- 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 -16
- package/dist/esm/activities/chat/index.js +2100 -1744
- 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 +23 -5
- package/dist/esm/index.js +30 -97
- package/dist/esm/interrupt-resume.d.ts +71 -0
- package/dist/esm/interrupt-resume.js +438 -0
- package/dist/esm/interrupt-resume.js.map +1 -0
- package/dist/esm/interrupt-serialization.d.ts +12 -0
- package/dist/esm/interrupt-serialization.js +178 -0
- package/dist/esm/interrupt-serialization.js.map +1 -0
- package/dist/esm/interrupts.d.ts +84 -0
- package/dist/esm/interrupts.js +31 -0
- package/dist/esm/interrupts.js.map +1 -0
- package/dist/esm/locks.d.ts +10 -0
- package/dist/esm/locks.js +2 -0
- package/dist/esm/logger/console-logger.js +101 -78
- package/dist/esm/logger/console-logger.js.map +1 -1
- package/dist/esm/logger/internal-logger.js +104 -89
- package/dist/esm/logger/internal-logger.js.map +1 -1
- package/dist/esm/logger/resolve.js +54 -49
- package/dist/esm/logger/resolve.js.map +1 -1
- package/dist/esm/logger/types.d.ts +1 -1
- package/dist/esm/middlewares/content-guard.js +142 -148
- package/dist/esm/middlewares/content-guard.js.map +1 -1
- package/dist/esm/middlewares/index.js +2 -6
- package/dist/esm/middlewares/otel.js +598 -732
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/middlewares/usage-attributes.js +47 -40
- package/dist/esm/middlewares/usage-attributes.js.map +1 -1
- package/dist/esm/realtime/event-emitter.js +24 -25
- package/dist/esm/realtime/event-emitter.js.map +1 -1
- package/dist/esm/realtime/index.d.ts +5 -9
- package/dist/esm/realtime/index.js +29 -6
- package/dist/esm/realtime/index.js.map +1 -1
- package/dist/esm/scope.d.ts +47 -0
- package/dist/esm/stream-durability.d.ts +171 -0
- package/dist/esm/stream-durability.js +295 -0
- package/dist/esm/stream-durability.js.map +1 -0
- package/dist/esm/stream-to-response.d.ts +178 -13
- package/dist/esm/stream-to-response.js +663 -115
- package/dist/esm/stream-to-response.js.map +1 -1
- package/dist/esm/strip-to-spec-middleware.js +30 -16
- package/dist/esm/strip-to-spec-middleware.js.map +1 -1
- package/dist/esm/system-prompts.js +27 -21
- package/dist/esm/system-prompts.js.map +1 -1
- package/dist/esm/tool-registry.js +72 -45
- package/dist/esm/tool-registry.js.map +1 -1
- package/dist/esm/tools/provider-tool.js +14 -5
- package/dist/esm/tools/provider-tool.js.map +1 -1
- package/dist/esm/types.d.ts +332 -21
- 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 +156 -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 -59
- package/src/activities/chat/agent-loop-strategies.ts +10 -4
- package/src/activities/chat/cancel.ts +81 -0
- package/src/activities/chat/index.ts +1152 -153
- 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 -0
- package/src/interrupt-resume.ts +824 -0
- package/src/interrupt-serialization.ts +183 -0
- package/src/interrupts.ts +146 -0
- package/src/locks.ts +17 -0
- package/src/logger/types.ts +1 -1
- package/src/middlewares/otel.ts +1 -0
- package/src/realtime/index.ts +5 -9
- package/src/scope.ts +47 -0
- package/src/stream-durability.ts +598 -0
- package/src/stream-to-response.ts +1051 -95
- package/src/strip-to-spec-middleware.ts +3 -3
- package/src/types.ts +416 -24
- 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
|
>
|
|
@@ -254,6 +428,13 @@ export interface TextActivityOptions<
|
|
|
254
428
|
runId?: TextOptions['runId']
|
|
255
429
|
/** Parent run ID for AG-UI protocol nested run correlation. */
|
|
256
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']
|
|
257
438
|
/**
|
|
258
439
|
* Optional Standard Schema for structured output.
|
|
259
440
|
* When provided, the activity will:
|
|
@@ -265,7 +446,7 @@ export interface TextActivityOptions<
|
|
|
265
446
|
* @example
|
|
266
447
|
* ```ts
|
|
267
448
|
* const result = await chat({
|
|
268
|
-
* adapter: openaiText('gpt-
|
|
449
|
+
* adapter: openaiText('gpt-5.5'),
|
|
269
450
|
* messages: [{ role: 'user', content: 'Generate a person' }],
|
|
270
451
|
* outputSchema: z.object({ name: z.string(), age: z.number() })
|
|
271
452
|
* })
|
|
@@ -275,7 +456,7 @@ export interface TextActivityOptions<
|
|
|
275
456
|
outputSchema?: TSchema
|
|
276
457
|
/**
|
|
277
458
|
* Whether to stream the text result.
|
|
278
|
-
* When true (default), returns an AsyncIterable<
|
|
459
|
+
* When true (default), returns an AsyncIterable<TypedStreamChunk<TTools>> for streaming output.
|
|
279
460
|
* When false, returns a Promise<string> with the collected text content.
|
|
280
461
|
*
|
|
281
462
|
* Note: If outputSchema is provided, this option is ignored and the result
|
|
@@ -286,7 +467,7 @@ export interface TextActivityOptions<
|
|
|
286
467
|
* @example Non-streaming text
|
|
287
468
|
* ```ts
|
|
288
469
|
* const text = await chat({
|
|
289
|
-
* adapter: openaiText('gpt-
|
|
470
|
+
* adapter: openaiText('gpt-5.5'),
|
|
290
471
|
* messages: [{ role: 'user', content: 'Hello!' }],
|
|
291
472
|
* stream: false
|
|
292
473
|
* })
|
|
@@ -301,7 +482,7 @@ export interface TextActivityOptions<
|
|
|
301
482
|
* @example
|
|
302
483
|
* ```ts
|
|
303
484
|
* const stream = chat({
|
|
304
|
-
* adapter: openaiText('gpt-
|
|
485
|
+
* adapter: openaiText('gpt-5.5'),
|
|
305
486
|
* messages: [...],
|
|
306
487
|
* middleware: [loggingMiddleware, redactionMiddleware],
|
|
307
488
|
* })
|
|
@@ -367,12 +548,18 @@ export function createChatOptions<
|
|
|
367
548
|
TTools,
|
|
368
549
|
TMiddleware
|
|
369
550
|
>,
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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 } {
|
|
376
563
|
return options
|
|
377
564
|
}
|
|
378
565
|
|
|
@@ -389,7 +576,10 @@ export function createChatOptions<
|
|
|
389
576
|
* - If outputSchema is provided without explicit stream:true:
|
|
390
577
|
* Promise<InferSchemaType<TSchema>>.
|
|
391
578
|
* - If stream is explicitly false (no schema): Promise<string>.
|
|
392
|
-
* - 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.
|
|
393
583
|
*
|
|
394
584
|
* `[TStream] extends [true]` is used (not `TStream extends true`) so that the
|
|
395
585
|
* default `boolean` value of `TStream` does *not* match the streaming branch.
|
|
@@ -399,13 +589,23 @@ export function createChatOptions<
|
|
|
399
589
|
export type TextActivityResult<
|
|
400
590
|
TSchema extends SchemaInput | undefined,
|
|
401
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>,
|
|
402
596
|
> = TSchema extends SchemaInput
|
|
403
597
|
? [TStream] extends [true]
|
|
404
598
|
? StructuredOutputStream<InferSchemaType<TSchema>>
|
|
405
599
|
: Promise<InferSchemaType<TSchema>>
|
|
406
600
|
: [TStream] extends [false]
|
|
407
601
|
? Promise<string>
|
|
408
|
-
:
|
|
602
|
+
: AsyncIterable<
|
|
603
|
+
TypedStreamChunk<
|
|
604
|
+
TTools extends ReadonlyArray<AnyTool>
|
|
605
|
+
? TTools
|
|
606
|
+
: ReadonlyArray<AnyTool>
|
|
607
|
+
>
|
|
608
|
+
>
|
|
409
609
|
|
|
410
610
|
// ===========================
|
|
411
611
|
// ChatEngine Implementation
|
|
@@ -519,6 +719,12 @@ class TextEngine<
|
|
|
519
719
|
|
|
520
720
|
private messages: Array<ModelMessage>
|
|
521
721
|
private iterationCount = 0
|
|
722
|
+
/** Cumulative tool calls counted in this run (emitted + pending resume). */
|
|
723
|
+
private toolCallCount = 0
|
|
724
|
+
/** Tool calls in the most recent budgeted batch (0 when none). */
|
|
725
|
+
private lastTurnToolCallCount = 0
|
|
726
|
+
/** Tool call IDs already counted toward `toolCallCount` (avoids double-count on resume). */
|
|
727
|
+
private readonly countedToolCallIds = new Set<string>()
|
|
522
728
|
private lastFinishReason: string | null = null
|
|
523
729
|
private streamStartTime = 0
|
|
524
730
|
private totalChunkCount = 0
|
|
@@ -531,12 +737,17 @@ class TextEngine<
|
|
|
531
737
|
private eventOptions?: Record<string, unknown> | undefined
|
|
532
738
|
private eventToolNames?: Array<string>
|
|
533
739
|
private finishedEvent: RunFinishedEvent | null = null
|
|
740
|
+
private deferredToolCallRunFinishedChunks: Array<StreamChunk> = []
|
|
534
741
|
private earlyTermination = false
|
|
535
742
|
private toolPhase: ToolPhaseResult = 'continue'
|
|
536
743
|
private cyclePhase: CyclePhase = 'processText'
|
|
537
744
|
// Client state extracted from initial messages (before conversion to ModelMessage)
|
|
538
|
-
private readonly initialApprovals: Map<string,
|
|
745
|
+
private readonly initialApprovals: Map<string, ToolApprovalResolution>
|
|
539
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>()
|
|
540
751
|
|
|
541
752
|
// AG-UI protocol IDs
|
|
542
753
|
private readonly threadId: string
|
|
@@ -554,6 +765,14 @@ class TextEngine<
|
|
|
554
765
|
// observe both cancellation sources via ctx.abortSignal.
|
|
555
766
|
private readonly toolAbortSignal?: AbortSignal
|
|
556
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>> = []
|
|
557
776
|
|
|
558
777
|
private readonly logger: InternalLogger
|
|
559
778
|
|
|
@@ -660,6 +879,7 @@ class TextEngine<
|
|
|
660
879
|
requestId: this.requestId,
|
|
661
880
|
streamId: this.streamId,
|
|
662
881
|
runId: this.runIdOverride ?? this.requestId,
|
|
882
|
+
parentRunId: this.parentRunIdOverride,
|
|
663
883
|
threadId: this.threadId,
|
|
664
884
|
// Legacy alias kept on the ctx so middleware that reads
|
|
665
885
|
// `ctx.conversationId` keeps working. Always equals `threadId`.
|
|
@@ -707,6 +927,22 @@ class TextEngine<
|
|
|
707
927
|
provide: (capability, value) => capability[1](this.middlewareCtx, value),
|
|
708
928
|
}
|
|
709
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
|
+
|
|
710
946
|
// Provide the internal SandboxRuntime capability so harness adapters and
|
|
711
947
|
// sandbox middleware can emit file events. The sink logs, fans the event
|
|
712
948
|
// out through the middleware `onFile*` hooks (fire-and-forget), and queues
|
|
@@ -797,6 +1033,7 @@ class TextEngine<
|
|
|
797
1033
|
initialConfig,
|
|
798
1034
|
)
|
|
799
1035
|
this.applyMiddlewareConfig(transformedConfig)
|
|
1036
|
+
await this.applyEphemeralInterruptResume(transformedConfig)
|
|
800
1037
|
|
|
801
1038
|
// Run onStart (devtools middleware emits text:request:started and initial messages here)
|
|
802
1039
|
await this.middlewareRunner.runOnStart(this.middlewareCtx)
|
|
@@ -850,7 +1087,7 @@ class TextEngine<
|
|
|
850
1087
|
}
|
|
851
1088
|
|
|
852
1089
|
this.endCycle()
|
|
853
|
-
} while (this.shouldContinue())
|
|
1090
|
+
} while (await this.shouldContinue())
|
|
854
1091
|
}
|
|
855
1092
|
|
|
856
1093
|
this.logger.agentLoop('run finished', {
|
|
@@ -861,12 +1098,15 @@ class TextEngine<
|
|
|
861
1098
|
// requested AND the run hasn't already errored/aborted, run it through
|
|
862
1099
|
// the middleware pipeline. The terminal hook fires once at the very
|
|
863
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.
|
|
864
1103
|
//
|
|
865
1104
|
// Native combined mode takes a different path: the agent loop's final-
|
|
866
1105
|
// turn text IS the schema-constrained JSON, so we harvest it from
|
|
867
1106
|
// `accumulatedContent` instead of issuing a second provider call.
|
|
868
1107
|
if (
|
|
869
1108
|
this.finalStructuredOutput &&
|
|
1109
|
+
this.toolPhase !== 'wait' &&
|
|
870
1110
|
!this.isCancelled() &&
|
|
871
1111
|
!this.finalizationError
|
|
872
1112
|
) {
|
|
@@ -914,6 +1154,41 @@ class TextEngine<
|
|
|
914
1154
|
}
|
|
915
1155
|
}
|
|
916
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
|
+
}
|
|
917
1192
|
if (!this.terminalHookCalled) {
|
|
918
1193
|
this.terminalHookCalled = true
|
|
919
1194
|
if (error instanceof MiddlewareAbortError) {
|
|
@@ -922,6 +1197,7 @@ class TextEngine<
|
|
|
922
1197
|
await this.middlewareRunner.runOnAbort(this.middlewareCtx, {
|
|
923
1198
|
reason: error.message,
|
|
924
1199
|
duration: Date.now() - this.streamStartTime,
|
|
1200
|
+
cancelRequested: isCancelRequestedReason(error.message),
|
|
925
1201
|
})
|
|
926
1202
|
} else {
|
|
927
1203
|
// Genuine error — call onError
|
|
@@ -943,9 +1219,11 @@ class TextEngine<
|
|
|
943
1219
|
// Check for abort terminal hook
|
|
944
1220
|
if (!this.terminalHookCalled && this.isCancelled()) {
|
|
945
1221
|
this.terminalHookCalled = true
|
|
1222
|
+
const reason = this.resolveAbortReason()
|
|
946
1223
|
await this.middlewareRunner.runOnAbort(this.middlewareCtx, {
|
|
947
|
-
reason
|
|
1224
|
+
reason,
|
|
948
1225
|
duration: Date.now() - this.streamStartTime,
|
|
1226
|
+
cancelRequested: isCancelRequestedReason(reason),
|
|
949
1227
|
})
|
|
950
1228
|
}
|
|
951
1229
|
|
|
@@ -1048,6 +1326,15 @@ class TextEngine<
|
|
|
1048
1326
|
? this.finalStructuredOutput.jsonSchema
|
|
1049
1327
|
: undefined
|
|
1050
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
|
+
|
|
1051
1338
|
for await (const chunk of this.adapter.chatStream({
|
|
1052
1339
|
model: this.params.model,
|
|
1053
1340
|
messages: this.messages,
|
|
@@ -1063,7 +1350,7 @@ class TextEngine<
|
|
|
1063
1350
|
// Expose provided capabilities (e.g. sandbox) to harness adapters.
|
|
1064
1351
|
capabilities: this.middlewareCtx,
|
|
1065
1352
|
// Client approval decisions, for harness interactive-approval resolution.
|
|
1066
|
-
approvals:
|
|
1353
|
+
approvals: adapterApprovals,
|
|
1067
1354
|
...(combinedSchema ? { outputSchema: combinedSchema } : {}),
|
|
1068
1355
|
})) {
|
|
1069
1356
|
if (this.isCancelled()) {
|
|
@@ -1138,6 +1425,10 @@ class TextEngine<
|
|
|
1138
1425
|
) {
|
|
1139
1426
|
continue
|
|
1140
1427
|
}
|
|
1428
|
+
if (this.shouldDeferToolCallRunFinished(outputChunk)) {
|
|
1429
|
+
this.deferredToolCallRunFinishedChunks.push(outputChunk)
|
|
1430
|
+
continue
|
|
1431
|
+
}
|
|
1141
1432
|
this.logger.output(`type=${outputChunk.type}`, { chunk: outputChunk })
|
|
1142
1433
|
yield outputChunk
|
|
1143
1434
|
this.middlewareCtx.chunkIndex++
|
|
@@ -1302,6 +1593,10 @@ class TextEngine<
|
|
|
1302
1593
|
|
|
1303
1594
|
const finishEvent = this.createSyntheticFinishedEvent()
|
|
1304
1595
|
|
|
1596
|
+
// Count is deduped so wait→resume after a live turn does not double-count.
|
|
1597
|
+
// Per-turn execution caps are app middleware via onBeforeToolCall skip.
|
|
1598
|
+
this.recordToolCalls(pendingToolCalls)
|
|
1599
|
+
|
|
1305
1600
|
// Handle undiscovered lazy tool calls with self-correcting error messages
|
|
1306
1601
|
const undiscoveredLazyResults: Array<ToolResult> = []
|
|
1307
1602
|
const executablePendingCalls = pendingToolCalls.filter((tc) => {
|
|
@@ -1321,16 +1616,28 @@ class TextEngine<
|
|
|
1321
1616
|
return true
|
|
1322
1617
|
})
|
|
1323
1618
|
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
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]
|
|
1623
|
+
|
|
1624
|
+
// Build args lookup so buildToolResultChunks can emit TOOL_CALL_START +
|
|
1625
|
+
// TOOL_CALL_ARGS before TOOL_CALL_END during continuation re-executions.
|
|
1626
|
+
const argsMap = new Map<string, string>()
|
|
1627
|
+
for (const tc of pendingToolCalls) {
|
|
1628
|
+
argsMap.set(tc.id, tc.function.arguments)
|
|
1331
1629
|
}
|
|
1332
1630
|
|
|
1333
1631
|
if (executablePendingCalls.length === 0) {
|
|
1632
|
+
if (deferredErrorResults.length > 0) {
|
|
1633
|
+
for (const chunk of this.buildToolResultChunks(
|
|
1634
|
+
deferredErrorResults,
|
|
1635
|
+
finishEvent,
|
|
1636
|
+
argsMap,
|
|
1637
|
+
)) {
|
|
1638
|
+
yield* this.pipeThroughMiddleware(chunk)
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1334
1641
|
return 'continue'
|
|
1335
1642
|
}
|
|
1336
1643
|
|
|
@@ -1373,6 +1680,10 @@ class TextEngine<
|
|
|
1373
1680
|
},
|
|
1374
1681
|
this.middlewareCtx.context,
|
|
1375
1682
|
this.toolAbortSignal,
|
|
1683
|
+
{
|
|
1684
|
+
deniedToolResults: this.resumeDeniedToolResults,
|
|
1685
|
+
cancelledToolCallIds: this.resumeCancelledToolCallIds,
|
|
1686
|
+
},
|
|
1376
1687
|
)
|
|
1377
1688
|
|
|
1378
1689
|
// Consume the async generator, yielding custom events and collecting the return value
|
|
@@ -1384,58 +1695,41 @@ class TextEngine<
|
|
|
1384
1695
|
return 'stop'
|
|
1385
1696
|
}
|
|
1386
1697
|
|
|
1698
|
+
const allResults = [...executionResult.results, ...deferredErrorResults]
|
|
1699
|
+
|
|
1387
1700
|
// Notify middleware of tool phase completion (devtools emits aggregate events here)
|
|
1388
1701
|
await this.middlewareRunner.runOnToolPhaseComplete(this.middlewareCtx, {
|
|
1389
1702
|
toolCalls: pendingToolCalls,
|
|
1390
|
-
results:
|
|
1703
|
+
results: allResults,
|
|
1391
1704
|
needsApproval: executionResult.needsApproval,
|
|
1392
1705
|
needsClientExecution: executionResult.needsClientExecution,
|
|
1393
1706
|
})
|
|
1394
1707
|
|
|
1395
|
-
// Build args lookup so buildToolResultChunks can emit TOOL_CALL_START +
|
|
1396
|
-
// TOOL_CALL_ARGS before TOOL_CALL_END during continuation re-executions.
|
|
1397
|
-
const argsMap = new Map<string, string>()
|
|
1398
|
-
for (const tc of pendingToolCalls) {
|
|
1399
|
-
argsMap.set(tc.id, tc.function.arguments)
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
1708
|
if (
|
|
1403
1709
|
executionResult.needsApproval.length > 0 ||
|
|
1404
1710
|
executionResult.needsClientExecution.length > 0
|
|
1405
1711
|
) {
|
|
1406
|
-
|
|
1712
|
+
this.discardDeferredToolCallRunFinishedChunks()
|
|
1713
|
+
|
|
1714
|
+
if (allResults.length > 0) {
|
|
1407
1715
|
for (const chunk of this.buildToolResultChunks(
|
|
1408
|
-
|
|
1716
|
+
allResults,
|
|
1409
1717
|
finishEvent,
|
|
1410
|
-
argsMap,
|
|
1411
1718
|
)) {
|
|
1412
1719
|
yield* this.pipeThroughMiddleware(chunk)
|
|
1413
1720
|
}
|
|
1414
1721
|
}
|
|
1415
1722
|
|
|
1416
|
-
|
|
1417
|
-
executionResult.needsApproval,
|
|
1723
|
+
const emitted = yield* this.emitActionableInterruptBoundary(
|
|
1418
1724
|
finishEvent,
|
|
1419
|
-
|
|
1420
|
-
yield* this.pipeThroughMiddleware(chunk)
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
for (const chunk of this.buildClientToolChunks(
|
|
1725
|
+
executionResult.needsApproval,
|
|
1424
1726
|
executionResult.needsClientExecution,
|
|
1425
|
-
|
|
1426
|
-
)
|
|
1427
|
-
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
this.setToolPhase('wait')
|
|
1431
|
-
return 'wait'
|
|
1727
|
+
)
|
|
1728
|
+
this.setToolPhase(emitted ? 'wait' : 'stop')
|
|
1729
|
+
return emitted ? 'wait' : 'stop'
|
|
1432
1730
|
}
|
|
1433
1731
|
|
|
1434
|
-
const toolResultChunks = this.buildToolResultChunks(
|
|
1435
|
-
executionResult.results,
|
|
1436
|
-
finishEvent,
|
|
1437
|
-
argsMap,
|
|
1438
|
-
)
|
|
1732
|
+
const toolResultChunks = this.buildToolResultChunks(allResults, finishEvent)
|
|
1439
1733
|
|
|
1440
1734
|
for (const chunk of toolResultChunks) {
|
|
1441
1735
|
yield* this.pipeThroughMiddleware(chunk)
|
|
@@ -1446,6 +1740,8 @@ class TextEngine<
|
|
|
1446
1740
|
|
|
1447
1741
|
private async *processToolCalls(): AsyncGenerator<StreamChunk, void, void> {
|
|
1448
1742
|
if (!this.shouldExecuteToolPhase()) {
|
|
1743
|
+
// Text-only turn — clear per-turn count so strategies see 0 tools.
|
|
1744
|
+
this.lastTurnToolCallCount = 0
|
|
1449
1745
|
this.setToolPhase('stop')
|
|
1450
1746
|
return
|
|
1451
1747
|
}
|
|
@@ -1454,10 +1750,15 @@ class TextEngine<
|
|
|
1454
1750
|
const finishEvent = this.finishedEvent
|
|
1455
1751
|
|
|
1456
1752
|
if (!finishEvent || toolCalls.length === 0) {
|
|
1753
|
+
this.lastTurnToolCallCount = 0
|
|
1457
1754
|
this.setToolPhase('stop')
|
|
1458
1755
|
return
|
|
1459
1756
|
}
|
|
1460
1757
|
|
|
1758
|
+
// Count every model-emitted tool call. Per-turn execution caps are app
|
|
1759
|
+
// middleware via onBeforeToolCall skip.
|
|
1760
|
+
this.recordToolCalls(toolCalls)
|
|
1761
|
+
|
|
1461
1762
|
this.addAssistantToolCallMessage(toolCalls)
|
|
1462
1763
|
|
|
1463
1764
|
// Handle undiscovered lazy tool calls with self-correcting error messages
|
|
@@ -1479,17 +1780,22 @@ class TextEngine<
|
|
|
1479
1780
|
return true
|
|
1480
1781
|
})
|
|
1481
1782
|
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
this.finishedEvent,
|
|
1486
|
-
)) {
|
|
1487
|
-
yield* this.pipeThroughMiddleware(chunk)
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1783
|
+
// Non-executed outcomes (undiscovered lazy). Per-turn skips come from
|
|
1784
|
+
// middleware and appear in execution results.
|
|
1785
|
+
const deferredErrorResults = [...undiscoveredLazyResults]
|
|
1490
1786
|
|
|
1491
1787
|
if (executableToolCalls.length === 0) {
|
|
1492
|
-
|
|
1788
|
+
yield* this.flushDeferredToolCallRunFinishedChunks()
|
|
1789
|
+
// All tool calls were undiscovered lazy tools — errors emitted, continue
|
|
1790
|
+
// loop (strategy / onShouldContinue may stop).
|
|
1791
|
+
if (deferredErrorResults.length > 0) {
|
|
1792
|
+
for (const chunk of this.buildToolResultChunks(
|
|
1793
|
+
deferredErrorResults,
|
|
1794
|
+
finishEvent,
|
|
1795
|
+
)) {
|
|
1796
|
+
yield* this.pipeThroughMiddleware(chunk)
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1493
1799
|
this.toolCallManager.clear()
|
|
1494
1800
|
this.setToolPhase('continue')
|
|
1495
1801
|
return
|
|
@@ -1535,6 +1841,10 @@ class TextEngine<
|
|
|
1535
1841
|
},
|
|
1536
1842
|
this.middlewareCtx.context,
|
|
1537
1843
|
this.toolAbortSignal,
|
|
1844
|
+
{
|
|
1845
|
+
deniedToolResults: this.resumeDeniedToolResults,
|
|
1846
|
+
cancelledToolCallIds: this.resumeCancelledToolCallIds,
|
|
1847
|
+
},
|
|
1538
1848
|
)
|
|
1539
1849
|
|
|
1540
1850
|
// Consume the async generator, yielding custom events and collecting the return value
|
|
@@ -1548,10 +1858,13 @@ class TextEngine<
|
|
|
1548
1858
|
return
|
|
1549
1859
|
}
|
|
1550
1860
|
|
|
1861
|
+
// Executed results first, then deferred errors (fan-out skips / undiscovered)
|
|
1862
|
+
const allResults = [...executionResult.results, ...deferredErrorResults]
|
|
1863
|
+
|
|
1551
1864
|
// Notify middleware of tool phase completion (devtools emits aggregate events here)
|
|
1552
1865
|
await this.middlewareRunner.runOnToolPhaseComplete(this.middlewareCtx, {
|
|
1553
1866
|
toolCalls,
|
|
1554
|
-
results:
|
|
1867
|
+
results: allResults,
|
|
1555
1868
|
needsApproval: executionResult.needsApproval,
|
|
1556
1869
|
needsClientExecution: executionResult.needsClientExecution,
|
|
1557
1870
|
})
|
|
@@ -1560,37 +1873,27 @@ class TextEngine<
|
|
|
1560
1873
|
executionResult.needsApproval.length > 0 ||
|
|
1561
1874
|
executionResult.needsClientExecution.length > 0
|
|
1562
1875
|
) {
|
|
1563
|
-
if (
|
|
1876
|
+
if (allResults.length > 0) {
|
|
1564
1877
|
for (const chunk of this.buildToolResultChunks(
|
|
1565
|
-
|
|
1878
|
+
allResults,
|
|
1566
1879
|
finishEvent,
|
|
1567
1880
|
)) {
|
|
1568
1881
|
yield* this.pipeThroughMiddleware(chunk)
|
|
1569
1882
|
}
|
|
1570
1883
|
}
|
|
1571
1884
|
|
|
1572
|
-
|
|
1573
|
-
executionResult.needsApproval,
|
|
1885
|
+
const emitted = yield* this.emitActionableInterruptBoundary(
|
|
1574
1886
|
finishEvent,
|
|
1575
|
-
|
|
1576
|
-
yield* this.pipeThroughMiddleware(chunk)
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
for (const chunk of this.buildClientToolChunks(
|
|
1887
|
+
executionResult.needsApproval,
|
|
1580
1888
|
executionResult.needsClientExecution,
|
|
1581
|
-
|
|
1582
|
-
)
|
|
1583
|
-
yield* this.pipeThroughMiddleware(chunk)
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
this.setToolPhase('wait')
|
|
1889
|
+
)
|
|
1890
|
+
this.setToolPhase(emitted ? 'wait' : 'stop')
|
|
1587
1891
|
return
|
|
1588
1892
|
}
|
|
1589
1893
|
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
)
|
|
1894
|
+
yield* this.flushDeferredToolCallRunFinishedChunks()
|
|
1895
|
+
|
|
1896
|
+
const toolResultChunks = this.buildToolResultChunks(allResults, finishEvent)
|
|
1594
1897
|
|
|
1595
1898
|
for (const chunk of toolResultChunks) {
|
|
1596
1899
|
yield* this.pipeThroughMiddleware(chunk)
|
|
@@ -1612,6 +1915,28 @@ class TextEngine<
|
|
|
1612
1915
|
this.setToolPhase('continue')
|
|
1613
1916
|
}
|
|
1614
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
|
+
|
|
1615
1940
|
private shouldExecuteToolPhase(): boolean {
|
|
1616
1941
|
return (
|
|
1617
1942
|
this.finishedEvent?.finishReason === 'tool_calls' &&
|
|
@@ -1634,6 +1959,7 @@ class TextEngine<
|
|
|
1634
1959
|
}),
|
|
1635
1960
|
},
|
|
1636
1961
|
]
|
|
1962
|
+
this.middlewareCtx.messages = this.messages
|
|
1637
1963
|
}
|
|
1638
1964
|
|
|
1639
1965
|
/**
|
|
@@ -1644,10 +1970,10 @@ class TextEngine<
|
|
|
1644
1970
|
private extractClientStateFromOriginalMessages(
|
|
1645
1971
|
originalMessages: Array<any>,
|
|
1646
1972
|
): {
|
|
1647
|
-
approvals: Map<string,
|
|
1973
|
+
approvals: Map<string, ToolApprovalResolution>
|
|
1648
1974
|
clientToolResults: Map<string, any>
|
|
1649
1975
|
} {
|
|
1650
|
-
const approvals = new Map<string,
|
|
1976
|
+
const approvals = new Map<string, ToolApprovalResolution>()
|
|
1651
1977
|
const clientToolResults = new Map<string, any>()
|
|
1652
1978
|
|
|
1653
1979
|
for (const message of originalMessages) {
|
|
@@ -1676,12 +2002,18 @@ class TextEngine<
|
|
|
1676
2002
|
}
|
|
1677
2003
|
|
|
1678
2004
|
private collectClientState(): {
|
|
1679
|
-
approvals: Map<string,
|
|
2005
|
+
approvals: Map<string, ToolApprovalResolution>
|
|
1680
2006
|
clientToolResults: Map<string, any>
|
|
1681
2007
|
} {
|
|
1682
2008
|
// Start with the initial client state extracted from original messages
|
|
1683
2009
|
const approvals = new Map(this.initialApprovals)
|
|
1684
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
|
+
}
|
|
1685
2017
|
|
|
1686
2018
|
// Also check current messages for any additional tool results (from server tools)
|
|
1687
2019
|
for (const message of this.messages) {
|
|
@@ -1720,54 +2052,301 @@ class TextEngine<
|
|
|
1720
2052
|
return { approvals, clientToolResults }
|
|
1721
2053
|
}
|
|
1722
2054
|
|
|
1723
|
-
private
|
|
2055
|
+
private buildActionableInterrupts(
|
|
1724
2056
|
approvals: Array<ApprovalRequest>,
|
|
1725
|
-
|
|
1726
|
-
): Array<
|
|
1727
|
-
const
|
|
2057
|
+
clientRequests: Array<ClientToolRequest>,
|
|
2058
|
+
): Array<Interrupt> {
|
|
2059
|
+
const interrupts: Array<Interrupt> = []
|
|
1728
2060
|
|
|
1729
2061
|
for (const approval of approvals) {
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
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',
|
|
1737
2080
|
toolName: approval.toolName,
|
|
1738
2081
|
input: approval.input,
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
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,
|
|
1742
2092
|
},
|
|
1743
2093
|
},
|
|
1744
|
-
}
|
|
2094
|
+
})
|
|
1745
2095
|
}
|
|
1746
2096
|
|
|
1747
|
-
|
|
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
|
|
1748
2128
|
}
|
|
1749
2129
|
|
|
1750
|
-
private
|
|
2130
|
+
private buildInterruptFinishedChunk(
|
|
2131
|
+
finishEvent: RunFinishedEvent,
|
|
2132
|
+
approvals: Array<ApprovalRequest>,
|
|
1751
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(
|
|
1752
2278
|
finishEvent: RunFinishedEvent,
|
|
1753
|
-
|
|
1754
|
-
|
|
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
|
+
}
|
|
1755
2295
|
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
2296
|
+
yield* this.pipeThroughMiddleware(this.buildMessagesSnapshotChunk())
|
|
2297
|
+
if (this.params.state !== undefined) {
|
|
2298
|
+
yield* this.pipeThroughMiddleware({
|
|
2299
|
+
type: EventType.STATE_SNAPSHOT,
|
|
1759
2300
|
timestamp: Date.now(),
|
|
1760
|
-
model:
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
toolCallId: clientTool.toolCallId,
|
|
1764
|
-
toolName: clientTool.toolName,
|
|
1765
|
-
input: clientTool.input,
|
|
1766
|
-
},
|
|
1767
|
-
} as StreamChunk)
|
|
2301
|
+
model: this.params.model,
|
|
2302
|
+
snapshot: this.params.state,
|
|
2303
|
+
})
|
|
1768
2304
|
}
|
|
2305
|
+
for (const output of terminalOutputs) {
|
|
2306
|
+
yield this.publicInterruptTerminal(output)
|
|
2307
|
+
this.middlewareCtx.chunkIndex++
|
|
2308
|
+
}
|
|
2309
|
+
return true
|
|
2310
|
+
}
|
|
1769
2311
|
|
|
1770
|
-
|
|
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
|
+
}
|
|
1771
2350
|
}
|
|
1772
2351
|
|
|
1773
2352
|
private buildToolResultChunks(
|
|
@@ -1791,6 +2370,7 @@ class TextEngine<
|
|
|
1791
2370
|
// argsMap is set only on continuation re-executions, where the adapter
|
|
1792
2371
|
// never streamed these calls. Otherwise it already emitted END, so a
|
|
1793
2372
|
// second one here would be an orphan that fails verifyEvents (#519).
|
|
2373
|
+
// When we do emit END, attach parsed `input`/`output` for TypedStreamChunk.
|
|
1794
2374
|
if (argsMap) {
|
|
1795
2375
|
chunks.push({
|
|
1796
2376
|
type: 'TOOL_CALL_START',
|
|
@@ -1799,7 +2379,7 @@ class TextEngine<
|
|
|
1799
2379
|
toolCallId: result.toolCallId,
|
|
1800
2380
|
toolCallName: result.toolName,
|
|
1801
2381
|
toolName: result.toolName,
|
|
1802
|
-
}
|
|
2382
|
+
})
|
|
1803
2383
|
|
|
1804
2384
|
const args = argsMap.get(result.toolCallId) ?? '{}'
|
|
1805
2385
|
chunks.push({
|
|
@@ -1819,8 +2399,10 @@ class TextEngine<
|
|
|
1819
2399
|
toolCallName: result.toolName,
|
|
1820
2400
|
toolName: result.toolName,
|
|
1821
2401
|
result: wireContent,
|
|
2402
|
+
...(result.input !== undefined && { input: result.input }),
|
|
2403
|
+
...(result.output !== undefined && { output: result.output }),
|
|
1822
2404
|
...(result.state !== undefined && { state: result.state }),
|
|
1823
|
-
}
|
|
2405
|
+
})
|
|
1824
2406
|
}
|
|
1825
2407
|
|
|
1826
2408
|
// AG-UI spec TOOL_CALL_RESULT event (content is string-only per spec)
|
|
@@ -1868,6 +2450,7 @@ class TextEngine<
|
|
|
1868
2450
|
} else {
|
|
1869
2451
|
this.messages = [...this.messages, newToolMessage]
|
|
1870
2452
|
}
|
|
2453
|
+
this.middlewareCtx.messages = this.messages
|
|
1871
2454
|
}
|
|
1872
2455
|
|
|
1873
2456
|
return chunks
|
|
@@ -1922,6 +2505,54 @@ class TextEngine<
|
|
|
1922
2505
|
return pending
|
|
1923
2506
|
}
|
|
1924
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
|
+
|
|
1925
2556
|
private createSyntheticFinishedEvent(): RunFinishedEvent {
|
|
1926
2557
|
return {
|
|
1927
2558
|
type: 'RUN_FINISHED',
|
|
@@ -1933,20 +2564,55 @@ class TextEngine<
|
|
|
1933
2564
|
} as RunFinishedEvent
|
|
1934
2565
|
}
|
|
1935
2566
|
|
|
1936
|
-
private shouldContinue(): boolean {
|
|
2567
|
+
private async shouldContinue(): Promise<boolean> {
|
|
2568
|
+
// Always enter the tool-execution half-cycle after a model turn.
|
|
1937
2569
|
if (this.cyclePhase === 'executeToolCalls') {
|
|
1938
2570
|
return true
|
|
1939
2571
|
}
|
|
1940
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
|
+
|
|
1941
2590
|
return (
|
|
1942
|
-
this.
|
|
1943
|
-
iterationCount: this.iterationCount,
|
|
1944
|
-
messages: this.messages,
|
|
1945
|
-
finishReason: this.lastFinishReason,
|
|
1946
|
-
}) && this.toolPhase === 'continue'
|
|
2591
|
+
strategyContinues && middlewareContinues && this.toolPhase === 'continue'
|
|
1947
2592
|
)
|
|
1948
2593
|
}
|
|
1949
2594
|
|
|
2595
|
+
/**
|
|
2596
|
+
* Record tool calls (deduped by id) toward `toolCallCount` /
|
|
2597
|
+
* `lastTurnToolCallCount` for strategies and middleware `onShouldContinue`.
|
|
2598
|
+
*
|
|
2599
|
+
* Used for both live model turns and pending/resume batches. IDs already
|
|
2600
|
+
* counted in this run (e.g. wait→resume after a live turn) are not
|
|
2601
|
+
* re-added to `toolCallCount`. Per-turn execution caps are app middleware
|
|
2602
|
+
* (`onBeforeToolCall` skip), not engine policy.
|
|
2603
|
+
*/
|
|
2604
|
+
private recordToolCalls(toolCalls: Array<ToolCall>): void {
|
|
2605
|
+
this.lastTurnToolCallCount = toolCalls.length
|
|
2606
|
+
let newlyCounted = 0
|
|
2607
|
+
for (const tc of toolCalls) {
|
|
2608
|
+
if (!this.countedToolCallIds.has(tc.id)) {
|
|
2609
|
+
this.countedToolCallIds.add(tc.id)
|
|
2610
|
+
newlyCounted++
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
this.toolCallCount += newlyCounted
|
|
2614
|
+
}
|
|
2615
|
+
|
|
1950
2616
|
private isAborted(): boolean {
|
|
1951
2617
|
return !!this.effectiveSignal?.aborted
|
|
1952
2618
|
}
|
|
@@ -1959,6 +2625,100 @@ class TextEngine<
|
|
|
1959
2625
|
return this.isAborted() || this.isMiddlewareAborted()
|
|
1960
2626
|
}
|
|
1961
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
|
+
|
|
1962
2722
|
/**
|
|
1963
2723
|
* Run the final structured-output adapter call through the middleware
|
|
1964
2724
|
* pipeline. Yields chunks to the caller only when
|
|
@@ -2514,12 +3274,181 @@ class TextEngine<
|
|
|
2514
3274
|
messages: this.messages,
|
|
2515
3275
|
systemPrompts: [...this.systemPrompts],
|
|
2516
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
|
+
},
|
|
2517
3284
|
metadata: this.params.metadata,
|
|
2518
3285
|
modelOptions: this.params.modelOptions,
|
|
2519
3286
|
}
|
|
2520
3287
|
}
|
|
2521
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
|
+
|
|
2522
3450
|
private applyMiddlewareConfig(config: ChatMiddlewareConfig): void {
|
|
3451
|
+
this.applyResumeToolState(config.resumeToolState)
|
|
2523
3452
|
this.messages = config.messages
|
|
2524
3453
|
this.systemPrompts = config.systemPrompts
|
|
2525
3454
|
this.tools = config.tools
|
|
@@ -2610,7 +3539,7 @@ class TextEngine<
|
|
|
2610
3539
|
model: this.params.model,
|
|
2611
3540
|
name: eventName,
|
|
2612
3541
|
value,
|
|
2613
|
-
}
|
|
3542
|
+
}
|
|
2614
3543
|
}
|
|
2615
3544
|
|
|
2616
3545
|
private createId(prefix: string): string {
|
|
@@ -2637,7 +3566,7 @@ class TextEngine<
|
|
|
2637
3566
|
* import { openaiText } from '@tanstack/ai-openai'
|
|
2638
3567
|
*
|
|
2639
3568
|
* for await (const chunk of chat({
|
|
2640
|
-
* adapter: openaiText('gpt-
|
|
3569
|
+
* adapter: openaiText('gpt-5.5'),
|
|
2641
3570
|
* messages: [{ role: 'user', content: 'What is the weather?' }],
|
|
2642
3571
|
* tools: [weatherTool]
|
|
2643
3572
|
* })) {
|
|
@@ -2650,7 +3579,7 @@ class TextEngine<
|
|
|
2650
3579
|
* @example One-shot text (streaming without tools)
|
|
2651
3580
|
* ```ts
|
|
2652
3581
|
* for await (const chunk of chat({
|
|
2653
|
-
* adapter: openaiText('gpt-
|
|
3582
|
+
* adapter: openaiText('gpt-5.5'),
|
|
2654
3583
|
* messages: [{ role: 'user', content: 'Hello!' }]
|
|
2655
3584
|
* })) {
|
|
2656
3585
|
* console.log(chunk)
|
|
@@ -2660,7 +3589,7 @@ class TextEngine<
|
|
|
2660
3589
|
* @example Non-streaming text (stream: false)
|
|
2661
3590
|
* ```ts
|
|
2662
3591
|
* const text = await chat({
|
|
2663
|
-
* adapter: openaiText('gpt-
|
|
3592
|
+
* adapter: openaiText('gpt-5.5'),
|
|
2664
3593
|
* messages: [{ role: 'user', content: 'Hello!' }],
|
|
2665
3594
|
* stream: false
|
|
2666
3595
|
* })
|
|
@@ -2672,7 +3601,7 @@ class TextEngine<
|
|
|
2672
3601
|
* import { z } from 'zod'
|
|
2673
3602
|
*
|
|
2674
3603
|
* const result = await chat({
|
|
2675
|
-
* adapter: openaiText('gpt-
|
|
3604
|
+
* adapter: openaiText('gpt-5.5'),
|
|
2676
3605
|
* messages: [{ role: 'user', content: 'Research and summarize the topic' }],
|
|
2677
3606
|
* tools: [researchTool, analyzeTool],
|
|
2678
3607
|
* outputSchema: z.object({
|
|
@@ -2712,7 +3641,7 @@ export function chat<
|
|
|
2712
3641
|
TTools,
|
|
2713
3642
|
TMiddleware
|
|
2714
3643
|
>,
|
|
2715
|
-
): TextActivityResult<TSchema, TStream> {
|
|
3644
|
+
): TextActivityResult<TSchema, TStream, TTools> {
|
|
2716
3645
|
validateCapabilities(options.middleware ?? [], options.adapter)
|
|
2717
3646
|
|
|
2718
3647
|
const { outputSchema, stream } = options
|
|
@@ -2725,7 +3654,7 @@ export function chat<
|
|
|
2725
3654
|
...options,
|
|
2726
3655
|
outputSchema,
|
|
2727
3656
|
stream,
|
|
2728
|
-
}) as TextActivityResult<TSchema, TStream>
|
|
3657
|
+
}) as TextActivityResult<TSchema, TStream, TTools>
|
|
2729
3658
|
}
|
|
2730
3659
|
|
|
2731
3660
|
// If outputSchema is provided, run agentic structured output (Promise<T>)
|
|
@@ -2733,7 +3662,7 @@ export function chat<
|
|
|
2733
3662
|
return runAgenticStructuredOutput({
|
|
2734
3663
|
...options,
|
|
2735
3664
|
outputSchema,
|
|
2736
|
-
}) as TextActivityResult<TSchema, TStream>
|
|
3665
|
+
}) as TextActivityResult<TSchema, TStream, TTools>
|
|
2737
3666
|
}
|
|
2738
3667
|
|
|
2739
3668
|
// If stream is explicitly false, run non-streaming text
|
|
@@ -2742,7 +3671,7 @@ export function chat<
|
|
|
2742
3671
|
...options,
|
|
2743
3672
|
outputSchema: undefined,
|
|
2744
3673
|
stream,
|
|
2745
|
-
}) as TextActivityResult<TSchema, TStream>
|
|
3674
|
+
}) as TextActivityResult<TSchema, TStream, TTools>
|
|
2746
3675
|
}
|
|
2747
3676
|
|
|
2748
3677
|
// Otherwise, run streaming text (default)
|
|
@@ -2750,14 +3679,70 @@ export function chat<
|
|
|
2750
3679
|
...options,
|
|
2751
3680
|
outputSchema: undefined,
|
|
2752
3681
|
stream,
|
|
2753
|
-
}) 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
|
+
}
|
|
3696
|
+
}
|
|
3697
|
+
|
|
3698
|
+
/**
|
|
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).
|
|
3705
|
+
*/
|
|
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
|
+
})
|
|
2754
3722
|
}
|
|
2755
3723
|
|
|
2756
3724
|
/**
|
|
2757
|
-
* Run streaming text (agentic or one-shot depending on tools)
|
|
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.
|
|
2758
3733
|
*/
|
|
2759
|
-
|
|
3734
|
+
function runStreamingText<TContext = unknown>(
|
|
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>(
|
|
2760
3744
|
options: TextActivityOptions<AnyTextAdapter, undefined, true, TContext>,
|
|
3745
|
+
engineRef: DeliveryEngineRef,
|
|
2761
3746
|
): AsyncIterable<StreamChunk> {
|
|
2762
3747
|
const { adapter, middleware, context, debug, mcp, ...textOptions } = options
|
|
2763
3748
|
const model = adapter.model
|
|
@@ -2782,6 +3767,7 @@ async function* runStreamingText<TContext = unknown>(
|
|
|
2782
3767
|
},
|
|
2783
3768
|
logger,
|
|
2784
3769
|
)
|
|
3770
|
+
engineRef.current = engine
|
|
2785
3771
|
|
|
2786
3772
|
try {
|
|
2787
3773
|
for await (const chunk of engine.run()) {
|
|
@@ -2801,7 +3787,7 @@ function runNonStreamingText<TContext = unknown>(
|
|
|
2801
3787
|
): Promise<string> {
|
|
2802
3788
|
// Run the streaming text and collect all text using streamToText.
|
|
2803
3789
|
const stream = runStreamingText(
|
|
2804
|
-
//
|
|
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.
|
|
2805
3791
|
options as unknown as TextActivityOptions<
|
|
2806
3792
|
AnyTextAdapter,
|
|
2807
3793
|
undefined,
|
|
@@ -3125,25 +4111,36 @@ function runStreamingStructuredOutput<
|
|
|
3125
4111
|
undoNullWidening(data, nullWideningMap)
|
|
3126
4112
|
|
|
3127
4113
|
// The implementation generator yields the broader internal type
|
|
3128
|
-
// (`StreamChunk | StructuredOutputCompleteEvent<T>`) so
|
|
3129
|
-
// CustomEvents can flow through
|
|
3130
|
-
//
|
|
3131
|
-
//
|
|
3132
|
-
//
|
|
3133
|
-
|
|
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(
|
|
3134
4128
|
options,
|
|
3135
4129
|
jsonSchema,
|
|
3136
4130
|
normalize,
|
|
3137
|
-
|
|
4131
|
+
engineRef,
|
|
4132
|
+
)
|
|
4133
|
+
publishDeliverySeams(stream, engineRef)
|
|
4134
|
+
return stream as StructuredOutputStream<InferSchemaType<TSchema>>
|
|
3138
4135
|
}
|
|
3139
4136
|
|
|
3140
4137
|
/**
|
|
3141
4138
|
* Internal generator return type — broader than the public
|
|
3142
|
-
* `StructuredOutputStream<T>`. The
|
|
3143
|
-
*
|
|
3144
|
-
*
|
|
3145
|
-
*
|
|
3146
|
-
* `
|
|
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
|
|
3147
4144
|
* through this generator with `name: string` and are widened out at the
|
|
3148
4145
|
* public boundary because keeping them would collapse the typed narrow back
|
|
3149
4146
|
* to `any`. The cast inside `runStreamingStructuredOutput` is where that
|
|
@@ -3160,6 +4157,7 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
3160
4157
|
options: TextActivityOptions<AnyTextAdapter, TSchema, true, TContext>,
|
|
3161
4158
|
jsonSchema: NonNullable<ReturnType<typeof convertSchemaToJsonSchema>>,
|
|
3162
4159
|
normalize: (data: unknown) => unknown,
|
|
4160
|
+
engineRef: DeliveryEngineRef,
|
|
3163
4161
|
): StructuredOutputStreamInternal<InferSchemaType<TSchema>> {
|
|
3164
4162
|
const {
|
|
3165
4163
|
adapter,
|
|
@@ -3210,6 +4208,7 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
3210
4208
|
},
|
|
3211
4209
|
logger,
|
|
3212
4210
|
)
|
|
4211
|
+
engineRef.current = engine
|
|
3213
4212
|
|
|
3214
4213
|
try {
|
|
3215
4214
|
for await (const chunk of engine.run()) {
|