@tanstack/ai 0.57.0 → 0.59.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/dist/esm/activities/chat/agents/define-agent.d.ts +81 -0
- package/dist/esm/activities/chat/agents/define-agent.js +34 -0
- package/dist/esm/activities/chat/agents/define-agent.js.map +1 -0
- package/dist/esm/activities/chat/agents/route.d.ts +53 -0
- package/dist/esm/activities/chat/agents/route.js +59 -0
- package/dist/esm/activities/chat/agents/route.js.map +1 -0
- package/dist/esm/activities/chat/agents/spawn.d.ts +124 -0
- package/dist/esm/activities/chat/agents/spawn.js +490 -0
- package/dist/esm/activities/chat/agents/spawn.js.map +1 -0
- package/dist/esm/activities/chat/agents/turn.d.ts +36 -0
- package/dist/esm/activities/chat/agents/turn.js +78 -0
- package/dist/esm/activities/chat/agents/turn.js.map +1 -0
- package/dist/esm/activities/chat/index.d.ts +13 -3
- package/dist/esm/activities/chat/index.js +345 -19
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.d.ts +7 -1
- package/dist/esm/activities/chat/messages.js +94 -18
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/middleware/run-store.d.ts +43 -7
- package/dist/esm/activities/chat/middleware/run-store.js +8 -1
- package/dist/esm/activities/chat/middleware/run-store.js.map +1 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +50 -3
- package/dist/esm/activities/chat/middleware/types.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +46 -0
- package/dist/esm/activities/chat/stream/processor.js +280 -7
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +17 -3
- package/dist/esm/activities/chat/tools/tool-calls.js +56 -7
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/generateAudio/index.js +1 -1
- package/dist/esm/activities/generateImage/index.js +1 -1
- package/dist/esm/activities/generateLiveVideo/index.js +1 -1
- package/dist/esm/activities/generateSpeech/index.js +1 -1
- package/dist/esm/activities/generateTranscription/index.js +1 -1
- package/dist/esm/activities/generateVoice/index.js +1 -1
- package/dist/esm/activities/generateWorld/index.js +1 -1
- package/dist/esm/activities/index.d.ts +3 -0
- package/dist/esm/activities/index.js +5 -3
- package/dist/esm/activities/summarize/index.js +1 -1
- package/dist/esm/client.d.ts +5 -36
- package/dist/esm/client.js +4 -37
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.js +7 -4
- package/dist/esm/middlewares/content-guard.js.map +1 -1
- package/dist/esm/stream-to-response.js +12 -6
- package/dist/esm/stream-to-response.js.map +1 -1
- package/dist/esm/strip-to-spec-middleware.js +2 -1
- package/dist/esm/strip-to-spec-middleware.js.map +1 -1
- package/dist/esm/types.d.ts +128 -89
- package/dist/esm/utilities/adapter-yield-chunk.d.ts +5 -1
- package/dist/esm/utilities/ag-ui-usage.d.ts +9 -9
- package/dist/esm/utilities/ag-ui-usage.js +66 -3
- package/dist/esm/utilities/ag-ui-usage.js.map +1 -1
- package/dist/esm/utilities/ag-ui-wire.js +56 -0
- package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
- package/dist/esm/utilities/durability-batch.d.ts +8 -0
- package/dist/esm/utilities/durability-batch.js +45 -0
- package/dist/esm/utilities/durability-batch.js.map +1 -0
- package/dist/esm/utilities/normalize-stream-chunk.js +7 -2
- package/dist/esm/utilities/normalize-stream-chunk.js.map +1 -1
- package/dist/esm/utilities/spec-event-keys.js +13 -8
- package/dist/esm/utilities/spec-event-keys.js.map +1 -1
- package/dist/esm/utilities/subagent-wire.d.ts +36 -0
- package/dist/esm/utilities/subagent-wire.js +131 -0
- package/dist/esm/utilities/subagent-wire.js.map +1 -0
- package/package.json +4 -4
- package/skills/ai-core/adapter-configuration/references/grok-adapter.md +1 -1
- package/skills/ai-core/media-generation/SKILL.md +2 -2
- package/skills/ai-core/middleware/SKILL.md +7 -4
- package/src/activities/chat/agents/define-agent.ts +121 -0
- package/src/activities/chat/agents/route.ts +115 -0
- package/src/activities/chat/agents/spawn.ts +806 -0
- package/src/activities/chat/agents/turn.ts +151 -0
- package/src/activities/chat/index.ts +540 -16
- package/src/activities/chat/messages.ts +125 -15
- package/src/activities/chat/middleware/run-store.ts +56 -7
- package/src/activities/chat/middleware/types.ts +55 -2
- package/src/activities/chat/stream/processor.ts +428 -8
- package/src/activities/chat/tools/tool-calls.ts +92 -16
- package/src/activities/index.ts +15 -0
- package/src/client.ts +22 -35
- package/src/index.ts +23 -0
- package/src/middlewares/content-guard.ts +7 -5
- package/src/stream-to-response.ts +16 -6
- package/src/strip-to-spec-middleware.ts +2 -1
- package/src/types.ts +177 -95
- package/src/utilities/adapter-yield-chunk.ts +10 -2
- package/src/utilities/ag-ui-usage.test.ts +38 -0
- package/src/utilities/ag-ui-usage.ts +98 -11
- package/src/utilities/ag-ui-wire.ts +74 -0
- package/src/utilities/durability-batch.ts +48 -0
- package/src/utilities/normalize-stream-chunk.ts +10 -2
- package/src/utilities/spec-event-keys.ts +34 -7
- package/src/utilities/subagent-wire.ts +184 -0
|
@@ -38,6 +38,8 @@ import {
|
|
|
38
38
|
tanstackMetadata,
|
|
39
39
|
withTanstackMetadata,
|
|
40
40
|
} from '../../utilities/merge-metadata'
|
|
41
|
+
import { subagentHostMessageId } from '../../utilities/subagent-wire'
|
|
42
|
+
import { withDurabilityBatchHint } from '../../utilities/durability-batch'
|
|
41
43
|
import { normalizeStreamChunk } from '../../utilities/normalize-stream-chunk'
|
|
42
44
|
import { restorePublicUsage } from '../../utilities/restore-inbound-chunk'
|
|
43
45
|
import type { AdapterYieldChunk } from '../../utilities/adapter-yield-chunk'
|
|
@@ -45,6 +47,25 @@ import { normalizeToolResult } from '../../utilities/tool-result'
|
|
|
45
47
|
import { isProviderExecutedToolCall } from '../../utilities/provider-executed'
|
|
46
48
|
import { LazyToolManager } from './tools/lazy-tool-manager'
|
|
47
49
|
import { assertUniqueToolNames } from './tools/unique-tool-names'
|
|
50
|
+
import type { DefinedAgent } from './agents/define-agent'
|
|
51
|
+
import {
|
|
52
|
+
collectNamedText,
|
|
53
|
+
createSubagentSink,
|
|
54
|
+
createSyntheticSubagentTools,
|
|
55
|
+
normalizeRouterPick,
|
|
56
|
+
rebindInterrupts,
|
|
57
|
+
spawnNamedAgents,
|
|
58
|
+
subagentCallMessages,
|
|
59
|
+
withChildUsage,
|
|
60
|
+
} from './agents/spawn'
|
|
61
|
+
import type {
|
|
62
|
+
SpawnEntry,
|
|
63
|
+
SubagentSink,
|
|
64
|
+
SubagentStep,
|
|
65
|
+
SubagentStepsPlan,
|
|
66
|
+
SubagentsBag,
|
|
67
|
+
} from './agents/spawn'
|
|
68
|
+
import { SUBAGENT_PLAN_KEY, readSubagentTurn } from './agents/turn'
|
|
48
69
|
import {
|
|
49
70
|
MiddlewareAbortError,
|
|
50
71
|
ToolCallManager,
|
|
@@ -101,6 +122,7 @@ import type {
|
|
|
101
122
|
ChatStream,
|
|
102
123
|
ConstrainedModelMessage,
|
|
103
124
|
CustomEvent,
|
|
125
|
+
EmitCustomEventOptions,
|
|
104
126
|
InferSchemaType,
|
|
105
127
|
Interrupt,
|
|
106
128
|
JSONSchema,
|
|
@@ -161,6 +183,28 @@ type RuntimeToolWithApproval = AnyRuntimeTool & {
|
|
|
161
183
|
}
|
|
162
184
|
const interruptBindingMetadataKey = INTERRUPT_BINDING_METADATA_KEY
|
|
163
185
|
|
|
186
|
+
/** Resume entries a subagent tool call owns. The parent run skips them. */
|
|
187
|
+
const CHILD_RESUME_IDS = Symbol('tanstack.ai.childResumeIds')
|
|
188
|
+
|
|
189
|
+
// ponytail: no adapter maps `{ type: 'file' }` yet, so every one fails closed
|
|
190
|
+
// instead of reading the handle as a URL or base64. The Files API work
|
|
191
|
+
// replaces this with a per-adapter capability check.
|
|
192
|
+
function assertNoFileSources(
|
|
193
|
+
adapterName: string,
|
|
194
|
+
messages: ReadonlyArray<ModelMessage>,
|
|
195
|
+
): void {
|
|
196
|
+
for (const message of messages) {
|
|
197
|
+
if (!Array.isArray(message.content)) continue
|
|
198
|
+
for (const part of message.content) {
|
|
199
|
+
if ('source' in part && part.source.type === 'file') {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`${adapterName} does not support provider file-handle sources ({ type: 'file' }). Pass a data or url source.`,
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
164
208
|
interface StructuralInterruptFailure {
|
|
165
209
|
error: Error
|
|
166
210
|
errors: ReadonlyArray<InterruptSubmissionError>
|
|
@@ -393,8 +437,9 @@ type TextActivityOptionsWithContext<
|
|
|
393
437
|
[],
|
|
394
438
|
TContext = unknown,
|
|
395
439
|
TMiddleware extends Array<unknown> | undefined = undefined,
|
|
440
|
+
TAgents extends ReadonlyArray<DefinedAgent> = ReadonlyArray<DefinedAgent>,
|
|
396
441
|
> = Omit<
|
|
397
|
-
TextActivityOptions<TAdapter, TSchema, TStream, any>,
|
|
442
|
+
TextActivityOptions<TAdapter, TSchema, TStream, any, TAgents>,
|
|
398
443
|
'tools' | 'middleware' | 'context' | 'interrupts'
|
|
399
444
|
> & {
|
|
400
445
|
tools?: TTools
|
|
@@ -420,6 +465,7 @@ export interface TextActivityOptions<
|
|
|
420
465
|
TSchema extends SchemaInput | undefined,
|
|
421
466
|
TStream extends boolean,
|
|
422
467
|
TContext = unknown,
|
|
468
|
+
TAgents extends ReadonlyArray<DefinedAgent> = ReadonlyArray<DefinedAgent>,
|
|
423
469
|
> {
|
|
424
470
|
/** The text adapter to use (created by a provider function like openaiText('gpt-5.5')) */
|
|
425
471
|
adapter: TAdapter
|
|
@@ -497,6 +543,8 @@ export interface TextActivityOptions<
|
|
|
497
543
|
runId?: TextOptions['runId']
|
|
498
544
|
/** Parent run ID for AG-UI protocol nested run correlation. */
|
|
499
545
|
parentRunId?: TextOptions['parentRunId']
|
|
546
|
+
/** Subagent run id when this chat runs as a child. See `defineAgent`. */
|
|
547
|
+
subagentRunId?: TextOptions['subagentRunId']
|
|
500
548
|
/** Application state mirrored in a STATE_SNAPSHOT before an interrupt terminal. */
|
|
501
549
|
state?: TextOptions['state']
|
|
502
550
|
/**
|
|
@@ -504,6 +552,12 @@ export interface TextActivityOptions<
|
|
|
504
552
|
* before accepting new input on a thread with pending interrupts.
|
|
505
553
|
*/
|
|
506
554
|
resume?: TextOptions['resume']
|
|
555
|
+
/**
|
|
556
|
+
* Named child agents. When `router` is set, the library spawns that agent
|
|
557
|
+
* directly. When `router` is omitted, the main model gets one synthetic
|
|
558
|
+
* server tool per agent and picks the child.
|
|
559
|
+
*/
|
|
560
|
+
subagents?: SubagentsBag<TAgents>
|
|
507
561
|
/**
|
|
508
562
|
* Optional Standard Schema for structured output.
|
|
509
563
|
* When provided, the activity will:
|
|
@@ -840,6 +894,7 @@ class TextEngine<
|
|
|
840
894
|
private readonly threadId: string
|
|
841
895
|
private readonly runIdOverride?: string
|
|
842
896
|
private readonly parentRunIdOverride?: string
|
|
897
|
+
private readonly subagentRunIdOverride?: string
|
|
843
898
|
|
|
844
899
|
// Middleware support
|
|
845
900
|
private readonly middlewareRunner: MiddlewareRunner<
|
|
@@ -970,6 +1025,7 @@ class TextEngine<
|
|
|
970
1025
|
this.createId('thread')
|
|
971
1026
|
this.runIdOverride = config.params.runId
|
|
972
1027
|
this.parentRunIdOverride = config.params.parentRunId
|
|
1028
|
+
this.subagentRunIdOverride = config.params.subagentRunId
|
|
973
1029
|
|
|
974
1030
|
// Initialize middleware — devtools first. Spec stripping for the AG-UI
|
|
975
1031
|
// wire happens in toServerSentEventsStream, so in-process chat()
|
|
@@ -988,6 +1044,7 @@ class TextEngine<
|
|
|
988
1044
|
streamId: this.streamId,
|
|
989
1045
|
runId: this.runIdOverride ?? this.requestId,
|
|
990
1046
|
parentRunId: this.parentRunIdOverride,
|
|
1047
|
+
subagentRunId: this.subagentRunIdOverride,
|
|
991
1048
|
threadId: this.threadId,
|
|
992
1049
|
// Legacy alias kept on the ctx so middleware that reads
|
|
993
1050
|
// `ctx.conversationId` keeps working. Always equals `threadId`.
|
|
@@ -1000,9 +1057,9 @@ class TextEngine<
|
|
|
1000
1057
|
this.abortReason = reason
|
|
1001
1058
|
this.middlewareAbortController?.abort(reason)
|
|
1002
1059
|
},
|
|
1003
|
-
emitCustomEvent: (name, value) => {
|
|
1060
|
+
emitCustomEvent: (name, value, options) => {
|
|
1004
1061
|
this.middlewareCustomQueue.push(
|
|
1005
|
-
this.createCustomEventChunk(name, value),
|
|
1062
|
+
this.createCustomEventChunk(name, value, options),
|
|
1006
1063
|
)
|
|
1007
1064
|
const waiters = this.middlewareCustomWaiters
|
|
1008
1065
|
this.middlewareCustomWaiters = []
|
|
@@ -1514,6 +1571,8 @@ class TextEngine<
|
|
|
1514
1571
|
)
|
|
1515
1572
|
}
|
|
1516
1573
|
|
|
1574
|
+
assertNoFileSources(this.adapter.name, this.messages)
|
|
1575
|
+
|
|
1517
1576
|
for await (const raw of this.adapter.chatStream({
|
|
1518
1577
|
model: this.params.model,
|
|
1519
1578
|
messages: this.providerMessages,
|
|
@@ -2019,7 +2078,8 @@ class TextEngine<
|
|
|
2019
2078
|
this.resolveExecutableTools(executablePendingCalls),
|
|
2020
2079
|
approvals,
|
|
2021
2080
|
clientToolResults,
|
|
2022
|
-
(eventName, data) =>
|
|
2081
|
+
(eventName, data, options) =>
|
|
2082
|
+
this.createCustomEventChunk(eventName, data, options),
|
|
2023
2083
|
{
|
|
2024
2084
|
onBeforeToolCall: async (toolCall, tool, args) => {
|
|
2025
2085
|
this.logger.tools(`phase=before name=${toolCall.function.name}`, {
|
|
@@ -2080,7 +2140,8 @@ class TextEngine<
|
|
|
2080
2140
|
|
|
2081
2141
|
if (
|
|
2082
2142
|
executionResult.needsApproval.length > 0 ||
|
|
2083
|
-
executionResult.needsClientExecution.length > 0
|
|
2143
|
+
executionResult.needsClientExecution.length > 0 ||
|
|
2144
|
+
executionResult.subagentInterrupts.length > 0
|
|
2084
2145
|
) {
|
|
2085
2146
|
this.discardDeferredToolCallRunFinishedChunks()
|
|
2086
2147
|
|
|
@@ -2097,6 +2158,8 @@ class TextEngine<
|
|
|
2097
2158
|
finishEvent,
|
|
2098
2159
|
executionResult.needsApproval,
|
|
2099
2160
|
executionResult.needsClientExecution,
|
|
2161
|
+
[],
|
|
2162
|
+
executionResult.subagentInterrupts,
|
|
2100
2163
|
)
|
|
2101
2164
|
this.setToolPhase(emitted ? 'wait' : 'stop')
|
|
2102
2165
|
return emitted ? 'wait' : 'stop'
|
|
@@ -2199,7 +2262,8 @@ class TextEngine<
|
|
|
2199
2262
|
this.resolveExecutableTools(executableToolCalls),
|
|
2200
2263
|
approvals,
|
|
2201
2264
|
clientToolResults,
|
|
2202
|
-
(eventName, data) =>
|
|
2265
|
+
(eventName, data, options) =>
|
|
2266
|
+
this.createCustomEventChunk(eventName, data, options),
|
|
2203
2267
|
{
|
|
2204
2268
|
onBeforeToolCall: async (toolCall, tool, args) => {
|
|
2205
2269
|
this.logger.tools(`phase=before name=${toolCall.function.name}`, {
|
|
@@ -2288,7 +2352,8 @@ class TextEngine<
|
|
|
2288
2352
|
|
|
2289
2353
|
if (
|
|
2290
2354
|
executionResult.needsApproval.length > 0 ||
|
|
2291
|
-
executionResult.needsClientExecution.length > 0
|
|
2355
|
+
executionResult.needsClientExecution.length > 0 ||
|
|
2356
|
+
executionResult.subagentInterrupts.length > 0
|
|
2292
2357
|
) {
|
|
2293
2358
|
if (allResults.length > 0) {
|
|
2294
2359
|
for (const chunk of afterToolBoundaryChunks) {
|
|
@@ -2300,6 +2365,8 @@ class TextEngine<
|
|
|
2300
2365
|
finishEvent,
|
|
2301
2366
|
executionResult.needsApproval,
|
|
2302
2367
|
executionResult.needsClientExecution,
|
|
2368
|
+
[],
|
|
2369
|
+
executionResult.subagentInterrupts,
|
|
2303
2370
|
)
|
|
2304
2371
|
this.setToolPhase(emitted ? 'wait' : 'stop')
|
|
2305
2372
|
return
|
|
@@ -2618,6 +2685,7 @@ class TextEngine<
|
|
|
2618
2685
|
GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
|
|
2619
2686
|
> = [],
|
|
2620
2687
|
genericInterruptIds: ReadonlyArray<string> = [],
|
|
2688
|
+
childInterrupts: ReadonlyArray<Interrupt> = [],
|
|
2621
2689
|
): Array<Interrupt> {
|
|
2622
2690
|
const interrupts: Array<Interrupt> = []
|
|
2623
2691
|
|
|
@@ -2735,6 +2803,9 @@ class TextEngine<
|
|
|
2735
2803
|
})
|
|
2736
2804
|
}
|
|
2737
2805
|
|
|
2806
|
+
// A subagent tool call raised these. They keep their `subagentRunId`.
|
|
2807
|
+
interrupts.push(...childInterrupts)
|
|
2808
|
+
|
|
2738
2809
|
const ids = new Set<string>()
|
|
2739
2810
|
for (const interrupt of interrupts) {
|
|
2740
2811
|
if (ids.has(interrupt.id)) {
|
|
@@ -2756,6 +2827,7 @@ class TextEngine<
|
|
|
2756
2827
|
GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
|
|
2757
2828
|
> = [],
|
|
2758
2829
|
genericInterruptIds?: ReadonlyArray<string>,
|
|
2830
|
+
childInterrupts?: ReadonlyArray<Interrupt>,
|
|
2759
2831
|
): StreamChunk {
|
|
2760
2832
|
return {
|
|
2761
2833
|
...finishEvent,
|
|
@@ -2767,6 +2839,7 @@ class TextEngine<
|
|
|
2767
2839
|
clientRequests,
|
|
2768
2840
|
genericRequests,
|
|
2769
2841
|
genericInterruptIds,
|
|
2842
|
+
childInterrupts,
|
|
2770
2843
|
),
|
|
2771
2844
|
},
|
|
2772
2845
|
}
|
|
@@ -2916,11 +2989,13 @@ class TextEngine<
|
|
|
2916
2989
|
genericRequests: ReadonlyArray<
|
|
2917
2990
|
GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
|
|
2918
2991
|
> = [],
|
|
2992
|
+
childInterrupts: ReadonlyArray<Interrupt> = [],
|
|
2919
2993
|
): AsyncGenerator<StreamChunk, boolean, void> {
|
|
2920
2994
|
yield* this.emitSyntheticRunStarted(finishEvent)
|
|
2921
2995
|
const genericInterruptIds = genericRequests.map(() =>
|
|
2922
2996
|
this.genericInterruptId(),
|
|
2923
2997
|
)
|
|
2998
|
+
// Binding completion also binds child interrupts to this run.
|
|
2924
2999
|
const terminal = this.completeEphemeralInterruptBindings(
|
|
2925
3000
|
this.buildInterruptFinishedChunk(
|
|
2926
3001
|
finishEvent,
|
|
@@ -2928,6 +3003,7 @@ class TextEngine<
|
|
|
2928
3003
|
clientRequests,
|
|
2929
3004
|
genericRequests,
|
|
2930
3005
|
genericInterruptIds,
|
|
3006
|
+
childInterrupts,
|
|
2931
3007
|
),
|
|
2932
3008
|
)
|
|
2933
3009
|
let terminalOutputs: Array<StreamChunk>
|
|
@@ -3513,6 +3589,8 @@ class TextEngine<
|
|
|
3513
3589
|
// Apply merged config back to engine state
|
|
3514
3590
|
this.applyMiddlewareConfig(postOnConfig)
|
|
3515
3591
|
|
|
3592
|
+
assertNoFileSources(this.adapter.name, this.messages)
|
|
3593
|
+
|
|
3516
3594
|
// Build the StructuredOutputOptions the adapter expects.
|
|
3517
3595
|
// `this.adapter` is already `TAdapter extends AnyTextAdapter` per the
|
|
3518
3596
|
// class generics — no cast needed.
|
|
@@ -4009,9 +4087,25 @@ class TextEngine<
|
|
|
4009
4087
|
}
|
|
4010
4088
|
}
|
|
4011
4089
|
|
|
4090
|
+
/** Resume entries this run answers itself. A subagent tool owns the rest. */
|
|
4091
|
+
private ownResume(
|
|
4092
|
+
resume: ChatMiddlewareConfig['resume'],
|
|
4093
|
+
): ChatMiddlewareConfig['resume'] {
|
|
4094
|
+
const childIds = (
|
|
4095
|
+
this.params as { [CHILD_RESUME_IDS]?: ReadonlySet<string> }
|
|
4096
|
+
)[CHILD_RESUME_IDS]
|
|
4097
|
+
return childIds
|
|
4098
|
+
? resume?.filter((entry) => !childIds.has(entry.interruptId))
|
|
4099
|
+
: resume
|
|
4100
|
+
}
|
|
4101
|
+
|
|
4012
4102
|
private async applyEphemeralInterruptResume(
|
|
4013
|
-
|
|
4103
|
+
middlewareConfig: ChatMiddlewareConfig,
|
|
4014
4104
|
): Promise<void> {
|
|
4105
|
+
const config = {
|
|
4106
|
+
...middlewareConfig,
|
|
4107
|
+
resume: this.ownResume(middlewareConfig.resume),
|
|
4108
|
+
}
|
|
4015
4109
|
if ((config.resume?.length ?? 0) === 0) {
|
|
4016
4110
|
return
|
|
4017
4111
|
}
|
|
@@ -4244,7 +4338,7 @@ class TextEngine<
|
|
|
4244
4338
|
}> = []
|
|
4245
4339
|
const ids = new Set<string>()
|
|
4246
4340
|
const batchIndexes = new Set<number>()
|
|
4247
|
-
for (const resumeItem of this.params.resume ?? []) {
|
|
4341
|
+
for (const resumeItem of this.ownResume(this.params.resume) ?? []) {
|
|
4248
4342
|
const parsed = readGenericInterruptContinuation(resumeItem.metadata)
|
|
4249
4343
|
if (parsed.status === 'absent') continue
|
|
4250
4344
|
if (parsed.status === 'invalid') {
|
|
@@ -4554,11 +4648,12 @@ class TextEngine<
|
|
|
4554
4648
|
*/
|
|
4555
4649
|
private async *drainToolCallGenerator(
|
|
4556
4650
|
generator: AsyncGenerator<
|
|
4557
|
-
CustomEvent,
|
|
4651
|
+
CustomEvent | StreamChunk,
|
|
4558
4652
|
{
|
|
4559
4653
|
results: Array<ToolResult>
|
|
4560
4654
|
needsApproval: Array<ApprovalRequest>
|
|
4561
4655
|
needsClientExecution: Array<ClientToolRequest>
|
|
4656
|
+
subagentInterrupts: Array<Interrupt>
|
|
4562
4657
|
},
|
|
4563
4658
|
void
|
|
4564
4659
|
>,
|
|
@@ -4568,6 +4663,7 @@ class TextEngine<
|
|
|
4568
4663
|
results: Array<ToolResult>
|
|
4569
4664
|
needsApproval: Array<ApprovalRequest>
|
|
4570
4665
|
needsClientExecution: Array<ClientToolRequest>
|
|
4666
|
+
subagentInterrupts: Array<Interrupt>
|
|
4571
4667
|
},
|
|
4572
4668
|
void
|
|
4573
4669
|
> {
|
|
@@ -4583,13 +4679,15 @@ class TextEngine<
|
|
|
4583
4679
|
private createCustomEventChunk(
|
|
4584
4680
|
eventName: string,
|
|
4585
4681
|
value: Record<string, unknown>,
|
|
4682
|
+
options?: EmitCustomEventOptions,
|
|
4586
4683
|
): CustomEvent {
|
|
4587
|
-
|
|
4684
|
+
const chunk: CustomEvent = {
|
|
4588
4685
|
type: EventType.CUSTOM,
|
|
4589
4686
|
timestamp: Date.now(),
|
|
4590
4687
|
name: eventName,
|
|
4591
4688
|
value,
|
|
4592
4689
|
}
|
|
4690
|
+
return options?.batch ? withDurabilityBatchHint(chunk) : chunk
|
|
4593
4691
|
}
|
|
4594
4692
|
|
|
4595
4693
|
private createId(prefix: string): string {
|
|
@@ -4677,6 +4775,8 @@ export function chat<
|
|
|
4677
4775
|
> = [],
|
|
4678
4776
|
TContext = unknown,
|
|
4679
4777
|
const TMiddleware extends Array<unknown> | undefined = undefined,
|
|
4778
|
+
const TAgents extends ReadonlyArray<DefinedAgent> =
|
|
4779
|
+
ReadonlyArray<DefinedAgent>,
|
|
4680
4780
|
>(
|
|
4681
4781
|
options: TextActivityOptionsWithContext<
|
|
4682
4782
|
TAdapter,
|
|
@@ -4685,7 +4785,8 @@ export function chat<
|
|
|
4685
4785
|
TTools,
|
|
4686
4786
|
TInterrupts,
|
|
4687
4787
|
TContext,
|
|
4688
|
-
TMiddleware
|
|
4788
|
+
TMiddleware,
|
|
4789
|
+
TAgents
|
|
4689
4790
|
>,
|
|
4690
4791
|
): TextActivityResult<TSchema, TStream, TTools> {
|
|
4691
4792
|
validateInterruptDefinitions(options.interrupts)
|
|
@@ -4699,6 +4800,12 @@ export function chat<
|
|
|
4699
4800
|
|
|
4700
4801
|
const { outputSchema, stream } = options
|
|
4701
4802
|
|
|
4803
|
+
if (outputSchema && (options.subagents?.agents.length ?? 0) > 0) {
|
|
4804
|
+
throw new Error(
|
|
4805
|
+
'chat() does not support subagents together with outputSchema. Put outputSchema on a child chat() instead.',
|
|
4806
|
+
)
|
|
4807
|
+
}
|
|
4808
|
+
|
|
4702
4809
|
if (outputSchema && stream === true) {
|
|
4703
4810
|
return runStreamingStructuredOutput(
|
|
4704
4811
|
toRuntimeTextActivityOptions(options, {
|
|
@@ -4738,7 +4845,10 @@ type RuntimeTextActivityOptions<
|
|
|
4738
4845
|
TAdapter extends AnyTextAdapter,
|
|
4739
4846
|
TSchema extends SchemaInput | undefined,
|
|
4740
4847
|
TStream extends boolean,
|
|
4741
|
-
> = Omit<
|
|
4848
|
+
> = Omit<
|
|
4849
|
+
TextActivityOptions<TAdapter, TSchema, TStream, any, any>,
|
|
4850
|
+
'middleware'
|
|
4851
|
+
> & {
|
|
4742
4852
|
middleware?: Array<AnyChatMiddleware>
|
|
4743
4853
|
}
|
|
4744
4854
|
|
|
@@ -4767,6 +4877,7 @@ function toRuntimeTextActivityOptions<
|
|
|
4767
4877
|
TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,
|
|
4768
4878
|
TContext,
|
|
4769
4879
|
TMiddleware extends Array<unknown> | undefined,
|
|
4880
|
+
TAgents extends ReadonlyArray<DefinedAgent> = ReadonlyArray<DefinedAgent>,
|
|
4770
4881
|
>(
|
|
4771
4882
|
options: TextActivityOptionsWithContext<
|
|
4772
4883
|
TAdapter,
|
|
@@ -4775,7 +4886,8 @@ function toRuntimeTextActivityOptions<
|
|
|
4775
4886
|
TTools,
|
|
4776
4887
|
TInterrupts,
|
|
4777
4888
|
TContext,
|
|
4778
|
-
TMiddleware
|
|
4889
|
+
TMiddleware,
|
|
4890
|
+
TAgents
|
|
4779
4891
|
>,
|
|
4780
4892
|
overrides: { outputSchema: TOutputSchema; stream: TOutputStream },
|
|
4781
4893
|
): RuntimeTextActivityOptions<TAdapter, TOutputSchema, TOutputStream> {
|
|
@@ -4862,11 +4974,19 @@ function runStreamingText(
|
|
|
4862
4974
|
return stream
|
|
4863
4975
|
}
|
|
4864
4976
|
|
|
4865
|
-
async function*
|
|
4977
|
+
async function* runChatEngine(
|
|
4866
4978
|
options: RuntimeTextActivityOptions<AnyTextAdapter, undefined, boolean>,
|
|
4867
4979
|
engineRef: DeliveryEngineRef,
|
|
4868
4980
|
): AsyncIterable<StreamChunk> {
|
|
4869
|
-
const {
|
|
4981
|
+
const {
|
|
4982
|
+
adapter,
|
|
4983
|
+
middleware,
|
|
4984
|
+
context,
|
|
4985
|
+
debug,
|
|
4986
|
+
mcp,
|
|
4987
|
+
subagents: _subagents,
|
|
4988
|
+
...textOptions
|
|
4989
|
+
} = options
|
|
4870
4990
|
const model = adapter.model
|
|
4871
4991
|
const logger = resolveDebugOption(debug)
|
|
4872
4992
|
|
|
@@ -4900,6 +5020,410 @@ async function* streamTextChunks(
|
|
|
4900
5020
|
}
|
|
4901
5021
|
}
|
|
4902
5022
|
|
|
5023
|
+
async function* streamTextChunks(
|
|
5024
|
+
options: RuntimeTextActivityOptions<AnyTextAdapter, undefined, boolean>,
|
|
5025
|
+
engineRef: DeliveryEngineRef,
|
|
5026
|
+
): AsyncIterable<StreamChunk> {
|
|
5027
|
+
const bag = options.subagents
|
|
5028
|
+
const agents = bag?.agents ?? []
|
|
5029
|
+
if (bag && agents.length > 0 && bag.router) {
|
|
5030
|
+
yield* runRoutedSubagents(options, engineRef)
|
|
5031
|
+
return
|
|
5032
|
+
}
|
|
5033
|
+
if (bag && agents.length > 0) {
|
|
5034
|
+
const threadId = options.threadId ?? `thread-${Date.now()}`
|
|
5035
|
+
const runId = options.runId ?? `run-${Date.now()}`
|
|
5036
|
+
const messages = options.messages ?? []
|
|
5037
|
+
// A resume can answer a child's interrupts. Those entries go to the child
|
|
5038
|
+
// tool call. The parent run validates only its own entries.
|
|
5039
|
+
const turn = readSubagentTurn(messages, options.resume)
|
|
5040
|
+
const sink = createSubagentSink()
|
|
5041
|
+
const calls = subagentCallMessages(
|
|
5042
|
+
new Set(bag.agents.map((agent: DefinedAgent) => agent.name)),
|
|
5043
|
+
)
|
|
5044
|
+
const synthetic = createSyntheticSubagentTools(bag, {
|
|
5045
|
+
messages: turn?.before ?? messages,
|
|
5046
|
+
messagesFor: calls.messagesFor,
|
|
5047
|
+
threadId,
|
|
5048
|
+
runId,
|
|
5049
|
+
...(options.parentRunId !== undefined && {
|
|
5050
|
+
interruptedRunId: options.parentRunId,
|
|
5051
|
+
}),
|
|
5052
|
+
...(options.abortController && {
|
|
5053
|
+
abortSignal: options.abortController.signal,
|
|
5054
|
+
}),
|
|
5055
|
+
...(turn && { turn }),
|
|
5056
|
+
sink,
|
|
5057
|
+
})
|
|
5058
|
+
yield* streamWithChildUsage(
|
|
5059
|
+
runChatEngine(
|
|
5060
|
+
{
|
|
5061
|
+
...options,
|
|
5062
|
+
threadId,
|
|
5063
|
+
runId,
|
|
5064
|
+
...(turn && {
|
|
5065
|
+
[CHILD_RESUME_IDS]: new Set(
|
|
5066
|
+
turn.children.flatMap((child) =>
|
|
5067
|
+
child.resume.map((entry) => entry.interruptId),
|
|
5068
|
+
),
|
|
5069
|
+
),
|
|
5070
|
+
}),
|
|
5071
|
+
tools: [...(options.tools ?? []), ...synthetic],
|
|
5072
|
+
middleware: [...(options.middleware ?? []), calls.middleware],
|
|
5073
|
+
},
|
|
5074
|
+
engineRef,
|
|
5075
|
+
),
|
|
5076
|
+
sink,
|
|
5077
|
+
)
|
|
5078
|
+
return
|
|
5079
|
+
}
|
|
5080
|
+
yield* runChatEngine(options, engineRef)
|
|
5081
|
+
}
|
|
5082
|
+
|
|
5083
|
+
/** Add child usage to the next RUN_FINISHED of the parent. */
|
|
5084
|
+
async function* streamWithChildUsage(
|
|
5085
|
+
stream: AsyncIterable<StreamChunk>,
|
|
5086
|
+
sink: SubagentSink,
|
|
5087
|
+
): AsyncIterable<StreamChunk> {
|
|
5088
|
+
for await (const chunk of stream) {
|
|
5089
|
+
yield chunk.type === EventType.RUN_FINISHED
|
|
5090
|
+
? withChildUsage(chunk, sink)
|
|
5091
|
+
: chunk
|
|
5092
|
+
}
|
|
5093
|
+
}
|
|
5094
|
+
|
|
5095
|
+
async function* runRoutedSubagents(
|
|
5096
|
+
options: RuntimeTextActivityOptions<AnyTextAdapter, undefined, boolean>,
|
|
5097
|
+
engineRef: DeliveryEngineRef,
|
|
5098
|
+
): AsyncIterable<StreamChunk> {
|
|
5099
|
+
const bag = options.subagents
|
|
5100
|
+
if (!bag?.router) {
|
|
5101
|
+
yield* runChatEngine(options, engineRef)
|
|
5102
|
+
return
|
|
5103
|
+
}
|
|
5104
|
+
const threadId = options.threadId ?? `thread-${Date.now()}`
|
|
5105
|
+
const runId = options.runId ?? `run-${Date.now()}`
|
|
5106
|
+
const messages = options.messages ?? []
|
|
5107
|
+
const turn = readSubagentTurn(messages, options.resume)
|
|
5108
|
+
if (!turn && (options.resume?.length ?? 0) > 0) {
|
|
5109
|
+
// No child owns these answers. Main raised them, after a main pick or a
|
|
5110
|
+
// handoff. Run main alone.
|
|
5111
|
+
yield* runChatEngine(
|
|
5112
|
+
{ ...options, threadId, runId, subagents: undefined },
|
|
5113
|
+
engineRef,
|
|
5114
|
+
)
|
|
5115
|
+
return
|
|
5116
|
+
}
|
|
5117
|
+
const abortSignal = options.abortController?.signal
|
|
5118
|
+
const turnMessages = turn?.before ?? messages
|
|
5119
|
+
const subagentPersistence = readRoutedSubagentPersistence(options.middleware)
|
|
5120
|
+
await subagentPersistence?.start({
|
|
5121
|
+
threadId,
|
|
5122
|
+
runId,
|
|
5123
|
+
messages,
|
|
5124
|
+
...(options.resume && { resume: options.resume }),
|
|
5125
|
+
})
|
|
5126
|
+
try {
|
|
5127
|
+
// A resume reuses the plan that the first run put on each child's
|
|
5128
|
+
// SUBAGENT_STARTED metadata. Children that finished keep their result.
|
|
5129
|
+
// Suspended children continue.
|
|
5130
|
+
const plan =
|
|
5131
|
+
savedPlan(turn?.plan, bag.agents, {
|
|
5132
|
+
threadId,
|
|
5133
|
+
interruptedRunId: options.parentRunId ?? runId,
|
|
5134
|
+
interruptIds: options.resume?.map((entry) => entry.interruptId) ?? [],
|
|
5135
|
+
}) ??
|
|
5136
|
+
normalizeRouterPick(
|
|
5137
|
+
await bag.router({
|
|
5138
|
+
messages: turnMessages,
|
|
5139
|
+
agents: bag.agents,
|
|
5140
|
+
...(abortSignal && { abortSignal }),
|
|
5141
|
+
}),
|
|
5142
|
+
bag.agents,
|
|
5143
|
+
)
|
|
5144
|
+
// The run was stopped while the router decided. Start nothing. No
|
|
5145
|
+
// RUN_STARTED went out, so the stream ends without a terminal, but the
|
|
5146
|
+
// record start() opened still has to settle. Otherwise it stays `running`
|
|
5147
|
+
// and reconstructChat hands the client a run to tail that never emits.
|
|
5148
|
+
if (abortSignal?.aborted) {
|
|
5149
|
+
const error = new Error('Aborted')
|
|
5150
|
+
error.name = 'AbortError'
|
|
5151
|
+
await subagentPersistence?.abort({ threadId, runId, error })
|
|
5152
|
+
return
|
|
5153
|
+
}
|
|
5154
|
+
const onlyStep = plan.steps.length === 1 ? plan.steps[0] : undefined
|
|
5155
|
+
if (onlyStep?.names.length === 1 && onlyStep.names[0] === 'main') {
|
|
5156
|
+
// Earlier children own answers in this resume. Commit them first.
|
|
5157
|
+
if (turn) await subagentPersistence?.finish({ threadId, runId })
|
|
5158
|
+
yield* runChatEngine(
|
|
5159
|
+
{
|
|
5160
|
+
...options,
|
|
5161
|
+
threadId,
|
|
5162
|
+
runId,
|
|
5163
|
+
subagents: undefined,
|
|
5164
|
+
...(turn && { resume: turn.rest }),
|
|
5165
|
+
},
|
|
5166
|
+
engineRef,
|
|
5167
|
+
)
|
|
5168
|
+
return
|
|
5169
|
+
}
|
|
5170
|
+
|
|
5171
|
+
yield {
|
|
5172
|
+
type: EventType.RUN_STARTED,
|
|
5173
|
+
threadId,
|
|
5174
|
+
runId,
|
|
5175
|
+
timestamp: Date.now(),
|
|
5176
|
+
}
|
|
5177
|
+
|
|
5178
|
+
const earlier = [...(turn?.children ?? [])]
|
|
5179
|
+
const takeEarlier = (name: string) => {
|
|
5180
|
+
const index = earlier.findIndex((child) => child.name === name)
|
|
5181
|
+
return index === -1 ? undefined : earlier.splice(index, 1)[0]
|
|
5182
|
+
}
|
|
5183
|
+
const sink = createSubagentSink()
|
|
5184
|
+
const stepTexts: Array<string> = []
|
|
5185
|
+
let stepMessages = turnMessages
|
|
5186
|
+
let failure:
|
|
5187
|
+
| Extract<StreamChunk, { type: EventType.SUBAGENT_ERROR }>
|
|
5188
|
+
| undefined
|
|
5189
|
+
for (const step of plan.steps) {
|
|
5190
|
+
const entries: Array<SpawnEntry> = []
|
|
5191
|
+
const textByName = new Map<string, string>()
|
|
5192
|
+
for (const name of step.names) {
|
|
5193
|
+
const prior = takeEarlier(name)
|
|
5194
|
+
if (prior?.status === 'finished') {
|
|
5195
|
+
textByName.set(name, prior.text)
|
|
5196
|
+
continue
|
|
5197
|
+
}
|
|
5198
|
+
entries.push(
|
|
5199
|
+
prior?.status === 'suspended'
|
|
5200
|
+
? {
|
|
5201
|
+
name,
|
|
5202
|
+
resume: {
|
|
5203
|
+
subagentRunId: prior.subagentRunId,
|
|
5204
|
+
messages: prior.messages,
|
|
5205
|
+
entries: prior.resume,
|
|
5206
|
+
text: prior.text,
|
|
5207
|
+
},
|
|
5208
|
+
}
|
|
5209
|
+
: { name },
|
|
5210
|
+
)
|
|
5211
|
+
}
|
|
5212
|
+
const stepChunks: Array<StreamChunk> = []
|
|
5213
|
+
if (entries.length > 0) {
|
|
5214
|
+
for await (const chunk of spawnNamedAgents(
|
|
5215
|
+
entries,
|
|
5216
|
+
{ ...bag, order: step.order ?? bag.order },
|
|
5217
|
+
{
|
|
5218
|
+
messages: stepMessages,
|
|
5219
|
+
...(abortSignal && { abortSignal }),
|
|
5220
|
+
threadId,
|
|
5221
|
+
parentRunId: runId,
|
|
5222
|
+
...(options.parentRunId !== undefined && {
|
|
5223
|
+
interruptedRunId: options.parentRunId,
|
|
5224
|
+
}),
|
|
5225
|
+
},
|
|
5226
|
+
sink,
|
|
5227
|
+
)) {
|
|
5228
|
+
const tagged = withPlan(chunk, plan)
|
|
5229
|
+
stepChunks.push(tagged)
|
|
5230
|
+
await subagentPersistence?.chunk({ threadId, runId, chunk: tagged })
|
|
5231
|
+
yield tagged
|
|
5232
|
+
}
|
|
5233
|
+
}
|
|
5234
|
+
failure = stepChunks.find(
|
|
5235
|
+
(chunk) => chunk.type === EventType.SUBAGENT_ERROR,
|
|
5236
|
+
)
|
|
5237
|
+
if (failure) break
|
|
5238
|
+
if (sink.interrupts.length > 0) break
|
|
5239
|
+
for (const entry of entries) {
|
|
5240
|
+
const text = collectNamedText(stepChunks, [entry.name])
|
|
5241
|
+
textByName.set(
|
|
5242
|
+
entry.name,
|
|
5243
|
+
[entry.resume?.text, text].filter(Boolean).join('\n\n'),
|
|
5244
|
+
)
|
|
5245
|
+
}
|
|
5246
|
+
const text = step.names
|
|
5247
|
+
.map((name) => textByName.get(name)?.trim() ?? '')
|
|
5248
|
+
.filter((block) => block !== '')
|
|
5249
|
+
.join('\n\n')
|
|
5250
|
+
if (text) {
|
|
5251
|
+
stepTexts.push(text)
|
|
5252
|
+
stepMessages = [...stepMessages, { role: 'assistant', content: text }]
|
|
5253
|
+
}
|
|
5254
|
+
}
|
|
5255
|
+
|
|
5256
|
+
if (abortSignal?.aborted) {
|
|
5257
|
+
const error = new Error('Aborted')
|
|
5258
|
+
error.name = 'AbortError'
|
|
5259
|
+
await subagentPersistence?.abort({ threadId, runId, error })
|
|
5260
|
+
yield withChildUsage(
|
|
5261
|
+
{
|
|
5262
|
+
type: EventType.RUN_FINISHED,
|
|
5263
|
+
threadId,
|
|
5264
|
+
runId,
|
|
5265
|
+
outcome: { type: 'cancelled' },
|
|
5266
|
+
timestamp: Date.now(),
|
|
5267
|
+
},
|
|
5268
|
+
sink,
|
|
5269
|
+
)
|
|
5270
|
+
return
|
|
5271
|
+
}
|
|
5272
|
+
|
|
5273
|
+
if (failure) {
|
|
5274
|
+
// Carry the child's own cause, not a generic label, and keep the usage
|
|
5275
|
+
// the children already spent: the run failed, but the tokens were real.
|
|
5276
|
+
const message = failure.message || 'A subagent failed'
|
|
5277
|
+
await subagentPersistence?.abort({
|
|
5278
|
+
threadId,
|
|
5279
|
+
runId,
|
|
5280
|
+
error: new Error(message),
|
|
5281
|
+
})
|
|
5282
|
+
yield withChildUsage(
|
|
5283
|
+
{
|
|
5284
|
+
type: EventType.RUN_ERROR,
|
|
5285
|
+
threadId,
|
|
5286
|
+
runId,
|
|
5287
|
+
message,
|
|
5288
|
+
...(failure.code !== undefined ? { code: failure.code } : {}),
|
|
5289
|
+
timestamp: Date.now(),
|
|
5290
|
+
},
|
|
5291
|
+
sink,
|
|
5292
|
+
)
|
|
5293
|
+
return
|
|
5294
|
+
}
|
|
5295
|
+
|
|
5296
|
+
if (sink.interrupts.length > 0) {
|
|
5297
|
+
const interrupts = rebindInterrupts(sink.interrupts, runId)
|
|
5298
|
+
await subagentPersistence?.suspend?.({ threadId, runId, interrupts })
|
|
5299
|
+
yield withChildUsage(
|
|
5300
|
+
{
|
|
5301
|
+
type: EventType.RUN_FINISHED,
|
|
5302
|
+
threadId,
|
|
5303
|
+
runId,
|
|
5304
|
+
outcome: { type: 'interrupt', interrupts },
|
|
5305
|
+
timestamp: Date.now(),
|
|
5306
|
+
},
|
|
5307
|
+
sink,
|
|
5308
|
+
)
|
|
5309
|
+
return
|
|
5310
|
+
}
|
|
5311
|
+
|
|
5312
|
+
const strategy = bag.strategy ?? 'exclusive'
|
|
5313
|
+
if (strategy === 'handoff') {
|
|
5314
|
+
const childText = stepTexts.join('\n\n')
|
|
5315
|
+
// The children are done. Commit their answers and settle their records
|
|
5316
|
+
// before main runs. Main's own persistence takes over from here.
|
|
5317
|
+
await subagentPersistence?.finish({ threadId, runId })
|
|
5318
|
+
yield* streamWithChildUsage(
|
|
5319
|
+
runChatEngine(
|
|
5320
|
+
{
|
|
5321
|
+
...options,
|
|
5322
|
+
threadId,
|
|
5323
|
+
runId,
|
|
5324
|
+
subagents: undefined,
|
|
5325
|
+
...(turn && { resume: turn.rest }),
|
|
5326
|
+
messages: [
|
|
5327
|
+
...turnMessages,
|
|
5328
|
+
{
|
|
5329
|
+
// Same id as the recorder's parent row, so the stored thread
|
|
5330
|
+
// keeps one host message for the cards.
|
|
5331
|
+
id: subagentHostMessageId(runId),
|
|
5332
|
+
role: 'assistant',
|
|
5333
|
+
content: childText || 'Subagent finished.',
|
|
5334
|
+
metadata: { tanstack: { runId } },
|
|
5335
|
+
},
|
|
5336
|
+
],
|
|
5337
|
+
},
|
|
5338
|
+
engineRef,
|
|
5339
|
+
),
|
|
5340
|
+
sink,
|
|
5341
|
+
)
|
|
5342
|
+
return
|
|
5343
|
+
}
|
|
5344
|
+
|
|
5345
|
+
await subagentPersistence?.finish({ threadId, runId })
|
|
5346
|
+
yield withChildUsage(
|
|
5347
|
+
{
|
|
5348
|
+
type: EventType.RUN_FINISHED,
|
|
5349
|
+
threadId,
|
|
5350
|
+
runId,
|
|
5351
|
+
timestamp: Date.now(),
|
|
5352
|
+
},
|
|
5353
|
+
sink,
|
|
5354
|
+
)
|
|
5355
|
+
} catch (error) {
|
|
5356
|
+
await subagentPersistence?.abort({ threadId, runId, error })
|
|
5357
|
+
throw error
|
|
5358
|
+
}
|
|
5359
|
+
}
|
|
5360
|
+
|
|
5361
|
+
/**
|
|
5362
|
+
* The plan from a resumed turn, or undefined when it is absent. A plan that
|
|
5363
|
+
* names agents this chat does not have is stale: re-routing would strand the
|
|
5364
|
+
* suspended child that owns the answers, so fail the resume instead.
|
|
5365
|
+
*/
|
|
5366
|
+
function savedPlan(
|
|
5367
|
+
plan: unknown,
|
|
5368
|
+
agents: ReadonlyArray<DefinedAgent>,
|
|
5369
|
+
run: {
|
|
5370
|
+
threadId: string
|
|
5371
|
+
interruptedRunId: string
|
|
5372
|
+
interruptIds: ReadonlyArray<string>
|
|
5373
|
+
},
|
|
5374
|
+
): { steps: ReadonlyArray<SubagentStep> } | undefined {
|
|
5375
|
+
if (typeof plan !== 'object' || plan === null || !('steps' in plan)) {
|
|
5376
|
+
return undefined
|
|
5377
|
+
}
|
|
5378
|
+
try {
|
|
5379
|
+
// The plan comes back from the client. Check it like a router pick.
|
|
5380
|
+
return normalizeRouterPick(plan as SubagentStepsPlan, agents)
|
|
5381
|
+
} catch (error) {
|
|
5382
|
+
throw new InterruptResumeValidationError([
|
|
5383
|
+
{
|
|
5384
|
+
scope: 'batch',
|
|
5385
|
+
threadId: run.threadId,
|
|
5386
|
+
interruptedRunId: run.interruptedRunId,
|
|
5387
|
+
generation: 0,
|
|
5388
|
+
interruptIds: run.interruptIds,
|
|
5389
|
+
code: 'stale',
|
|
5390
|
+
message: `The saved subagent plan does not match the agents of this chat. ${
|
|
5391
|
+
error instanceof Error ? error.message : String(error)
|
|
5392
|
+
}`,
|
|
5393
|
+
source: 'server',
|
|
5394
|
+
retryable: false,
|
|
5395
|
+
},
|
|
5396
|
+
])
|
|
5397
|
+
}
|
|
5398
|
+
}
|
|
5399
|
+
|
|
5400
|
+
/** Put the router plan on a direct child's SUBAGENT_STARTED metadata. */
|
|
5401
|
+
function withPlan(
|
|
5402
|
+
chunk: StreamChunk,
|
|
5403
|
+
plan: { steps: ReadonlyArray<SubagentStep> },
|
|
5404
|
+
): StreamChunk {
|
|
5405
|
+
if (
|
|
5406
|
+
chunk.type !== EventType.SUBAGENT_STARTED ||
|
|
5407
|
+
chunk.parentSubagentRunId !== undefined
|
|
5408
|
+
) {
|
|
5409
|
+
return chunk
|
|
5410
|
+
}
|
|
5411
|
+
return withTanstackMetadata(chunk, { [SUBAGENT_PLAN_KEY]: plan })
|
|
5412
|
+
}
|
|
5413
|
+
|
|
5414
|
+
function readRoutedSubagentPersistence(
|
|
5415
|
+
middleware:
|
|
5416
|
+
| ReadonlyArray<{
|
|
5417
|
+
routedSubagentPersistence?: ChatMiddleware['routedSubagentPersistence']
|
|
5418
|
+
}>
|
|
5419
|
+
| undefined,
|
|
5420
|
+
) {
|
|
5421
|
+
for (const item of middleware ?? []) {
|
|
5422
|
+
if (item.routedSubagentPersistence) return item.routedSubagentPersistence
|
|
5423
|
+
}
|
|
5424
|
+
return undefined
|
|
5425
|
+
}
|
|
5426
|
+
|
|
4903
5427
|
/**
|
|
4904
5428
|
* Run non-streaming text - collects all content and returns as a string.
|
|
4905
5429
|
* Runs the full agentic loop (if tools are provided) but returns collected text.
|