@tanstack/ai 0.58.0 → 0.61.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/adapter.d.ts +9 -0
- package/dist/esm/activities/chat/adapter.js +1 -0
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- 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 +462 -26
- 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 +99 -19
- 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 +47 -1
- package/dist/esm/activities/chat/middleware/types.js.map +1 -1
- package/dist/esm/activities/chat/stream/message-updaters.js +9 -2
- package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +46 -1
- package/dist/esm/activities/chat/stream/processor.js +294 -18
- 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 -5
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/embed/adapter.d.ts +7 -0
- package/dist/esm/activities/embed/adapter.js +1 -0
- package/dist/esm/activities/embed/adapter.js.map +1 -1
- package/dist/esm/activities/embed/index.js +2 -0
- package/dist/esm/activities/embed/index.js.map +1 -1
- package/dist/esm/activities/files/adapter.d.ts +97 -0
- package/dist/esm/activities/files/adapter.js +45 -0
- package/dist/esm/activities/files/adapter.js.map +1 -0
- package/dist/esm/activities/files/index.d.ts +66 -0
- package/dist/esm/activities/files/index.js +78 -0
- package/dist/esm/activities/files/index.js.map +1 -0
- package/dist/esm/activities/generateAudio/index.js +1 -1
- package/dist/esm/activities/generateImage/adapter.d.ts +8 -0
- package/dist/esm/activities/generateImage/adapter.js +1 -0
- package/dist/esm/activities/generateImage/adapter.js.map +1 -1
- package/dist/esm/activities/generateImage/index.js +3 -1
- package/dist/esm/activities/generateImage/index.js.map +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/generateVideo/adapter.d.ts +8 -0
- package/dist/esm/activities/generateVideo/adapter.js +1 -0
- package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.js +3 -0
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/generateVoice/index.js +1 -1
- package/dist/esm/activities/generateWorld/adapter.d.ts +4 -2
- package/dist/esm/activities/generateWorld/adapter.js.map +1 -1
- package/dist/esm/activities/generateWorld/index.d.ts +4 -3
- package/dist/esm/activities/generateWorld/index.js +6 -5
- package/dist/esm/activities/generateWorld/index.js.map +1 -1
- package/dist/esm/activities/index.d.ts +9 -3
- package/dist/esm/activities/index.js +17 -13
- package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +2 -0
- package/dist/esm/activities/summarize/chat-stream-summarize.js +8 -8
- package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
- package/dist/esm/activities/summarize/index.js +1 -1
- package/dist/esm/client.d.ts +7 -36
- package/dist/esm/client.js +5 -37
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +8 -2
- package/dist/esm/index.js +9 -4
- package/dist/esm/middlewares/content-guard.js.map +1 -1
- package/dist/esm/types.d.ts +179 -98
- 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 +90 -13
- package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
- package/dist/esm/utilities/content-source.d.ts +60 -0
- package/dist/esm/utilities/content-source.js +85 -0
- package/dist/esm/utilities/content-source.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/provider-executed.d.ts +7 -0
- package/dist/esm/utilities/provider-executed.js +10 -1
- package/dist/esm/utilities/provider-executed.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/dist/esm/utilities/tool-result.d.ts +12 -2
- package/dist/esm/utilities/tool-result.js +23 -3
- package/dist/esm/utilities/tool-result.js.map +1 -1
- package/package.json +3 -3
- package/skills/ai-core/adapter-configuration/SKILL.md +62 -0
- package/skills/ai-core/adapter-configuration/references/grok-adapter.md +1 -1
- package/skills/ai-core/chat-experience/SKILL.md +14 -0
- package/skills/ai-core/media-generation/SKILL.md +10 -2
- package/skills/ai-core/middleware/SKILL.md +7 -4
- package/src/activities/chat/adapter.ts +10 -0
- 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 +734 -30
- package/src/activities/chat/messages.ts +137 -16
- package/src/activities/chat/middleware/run-store.ts +56 -7
- package/src/activities/chat/middleware/types.ts +47 -0
- package/src/activities/chat/stream/message-updaters.ts +24 -2
- package/src/activities/chat/stream/processor.ts +452 -30
- package/src/activities/chat/tools/tool-calls.ts +83 -11
- package/src/activities/embed/adapter.ts +7 -0
- package/src/activities/embed/index.ts +5 -0
- package/src/activities/files/adapter.ts +120 -0
- package/src/activities/files/index.ts +113 -0
- package/src/activities/generateImage/adapter.ts +8 -0
- package/src/activities/generateImage/index.ts +4 -0
- package/src/activities/generateVideo/adapter.ts +8 -0
- package/src/activities/generateVideo/index.ts +7 -0
- package/src/activities/generateWorld/adapter.ts +4 -2
- package/src/activities/generateWorld/index.ts +7 -6
- package/src/activities/index.ts +40 -1
- package/src/activities/summarize/chat-stream-summarize.ts +22 -12
- package/src/client.ts +29 -35
- package/src/index.ts +39 -0
- package/src/middlewares/content-guard.ts +7 -5
- package/src/types.ts +226 -103
- 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 +134 -16
- package/src/utilities/content-source.ts +138 -0
- package/src/utilities/normalize-stream-chunk.ts +10 -2
- package/src/utilities/provider-executed.ts +13 -0
- package/src/utilities/spec-event-keys.ts +34 -7
- package/src/utilities/subagent-wire.ts +184 -0
- package/src/utilities/tool-result.ts +38 -2
|
@@ -38,14 +38,39 @@ import {
|
|
|
38
38
|
tanstackMetadata,
|
|
39
39
|
withTanstackMetadata,
|
|
40
40
|
} from '../../utilities/merge-metadata'
|
|
41
|
+
import { subagentHostMessageId } from '../../utilities/subagent-wire'
|
|
41
42
|
import { withDurabilityBatchHint } from '../../utilities/durability-batch'
|
|
42
43
|
import { normalizeStreamChunk } from '../../utilities/normalize-stream-chunk'
|
|
43
44
|
import { restorePublicUsage } from '../../utilities/restore-inbound-chunk'
|
|
44
45
|
import type { AdapterYieldChunk } from '../../utilities/adapter-yield-chunk'
|
|
45
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
normalizeToolResult,
|
|
48
|
+
parseToolOutput,
|
|
49
|
+
toolResultErrorText,
|
|
50
|
+
} from '../../utilities/tool-result'
|
|
46
51
|
import { isProviderExecutedToolCall } from '../../utilities/provider-executed'
|
|
52
|
+
import { assertMessagesFileSourceSupport } from '../../utilities/content-source'
|
|
47
53
|
import { LazyToolManager } from './tools/lazy-tool-manager'
|
|
48
54
|
import { assertUniqueToolNames } from './tools/unique-tool-names'
|
|
55
|
+
import type { DefinedAgent } from './agents/define-agent'
|
|
56
|
+
import {
|
|
57
|
+
collectNamedText,
|
|
58
|
+
createSubagentSink,
|
|
59
|
+
createSyntheticSubagentTools,
|
|
60
|
+
normalizeRouterPick,
|
|
61
|
+
rebindInterrupts,
|
|
62
|
+
spawnNamedAgents,
|
|
63
|
+
subagentCallMessages,
|
|
64
|
+
withChildUsage,
|
|
65
|
+
} from './agents/spawn'
|
|
66
|
+
import type {
|
|
67
|
+
SpawnEntry,
|
|
68
|
+
SubagentSink,
|
|
69
|
+
SubagentStep,
|
|
70
|
+
SubagentStepsPlan,
|
|
71
|
+
SubagentsBag,
|
|
72
|
+
} from './agents/spawn'
|
|
73
|
+
import { SUBAGENT_PLAN_KEY, readSubagentTurn } from './agents/turn'
|
|
49
74
|
import {
|
|
50
75
|
MiddlewareAbortError,
|
|
51
76
|
ToolCallManager,
|
|
@@ -150,6 +175,12 @@ import type {
|
|
|
150
175
|
} from './runtime-context-types'
|
|
151
176
|
import type { ChatMCPOptions } from './mcp/types'
|
|
152
177
|
|
|
178
|
+
/** One entry of the per-iteration arrival order (see `turnParts`). */
|
|
179
|
+
type TurnPart =
|
|
180
|
+
| { type: 'thinking'; index: number }
|
|
181
|
+
| { type: 'text'; content: string }
|
|
182
|
+
| { type: 'call'; id: string; providerExecuted: boolean }
|
|
183
|
+
|
|
153
184
|
// ===========================
|
|
154
185
|
// Activity Kind
|
|
155
186
|
// ===========================
|
|
@@ -163,6 +194,9 @@ type RuntimeToolWithApproval = AnyRuntimeTool & {
|
|
|
163
194
|
}
|
|
164
195
|
const interruptBindingMetadataKey = INTERRUPT_BINDING_METADATA_KEY
|
|
165
196
|
|
|
197
|
+
/** Resume entries a subagent tool call owns. The parent run skips them. */
|
|
198
|
+
const CHILD_RESUME_IDS = Symbol('tanstack.ai.childResumeIds')
|
|
199
|
+
|
|
166
200
|
interface StructuralInterruptFailure {
|
|
167
201
|
error: Error
|
|
168
202
|
errors: ReadonlyArray<InterruptSubmissionError>
|
|
@@ -395,8 +429,9 @@ type TextActivityOptionsWithContext<
|
|
|
395
429
|
[],
|
|
396
430
|
TContext = unknown,
|
|
397
431
|
TMiddleware extends Array<unknown> | undefined = undefined,
|
|
432
|
+
TAgents extends ReadonlyArray<DefinedAgent> = ReadonlyArray<DefinedAgent>,
|
|
398
433
|
> = Omit<
|
|
399
|
-
TextActivityOptions<TAdapter, TSchema, TStream, any>,
|
|
434
|
+
TextActivityOptions<TAdapter, TSchema, TStream, any, TAgents>,
|
|
400
435
|
'tools' | 'middleware' | 'context' | 'interrupts'
|
|
401
436
|
> & {
|
|
402
437
|
tools?: TTools
|
|
@@ -422,6 +457,7 @@ export interface TextActivityOptions<
|
|
|
422
457
|
TSchema extends SchemaInput | undefined,
|
|
423
458
|
TStream extends boolean,
|
|
424
459
|
TContext = unknown,
|
|
460
|
+
TAgents extends ReadonlyArray<DefinedAgent> = ReadonlyArray<DefinedAgent>,
|
|
425
461
|
> {
|
|
426
462
|
/** The text adapter to use (created by a provider function like openaiText('gpt-5.5')) */
|
|
427
463
|
adapter: TAdapter
|
|
@@ -499,6 +535,8 @@ export interface TextActivityOptions<
|
|
|
499
535
|
runId?: TextOptions['runId']
|
|
500
536
|
/** Parent run ID for AG-UI protocol nested run correlation. */
|
|
501
537
|
parentRunId?: TextOptions['parentRunId']
|
|
538
|
+
/** Subagent run id when this chat runs as a child. See `defineAgent`. */
|
|
539
|
+
subagentRunId?: TextOptions['subagentRunId']
|
|
502
540
|
/** Application state mirrored in a STATE_SNAPSHOT before an interrupt terminal. */
|
|
503
541
|
state?: TextOptions['state']
|
|
504
542
|
/**
|
|
@@ -506,6 +544,12 @@ export interface TextActivityOptions<
|
|
|
506
544
|
* before accepting new input on a thread with pending interrupts.
|
|
507
545
|
*/
|
|
508
546
|
resume?: TextOptions['resume']
|
|
547
|
+
/**
|
|
548
|
+
* Named child agents. When `router` is set, the library spawns that agent
|
|
549
|
+
* directly. When `router` is omitted, the main model gets one synthetic
|
|
550
|
+
* server tool per agent and picks the child.
|
|
551
|
+
*/
|
|
552
|
+
subagents?: SubagentsBag<TAgents>
|
|
509
553
|
/**
|
|
510
554
|
* Optional Standard Schema for structured output.
|
|
511
555
|
* When provided, the activity will:
|
|
@@ -809,6 +853,15 @@ class TextEngine<
|
|
|
809
853
|
private accumulatedContent = ''
|
|
810
854
|
private accumulatedThinking: Array<{ content: string; signature?: string }> =
|
|
811
855
|
[]
|
|
856
|
+
/**
|
|
857
|
+
* Arrival order of this iteration's thinking steps, text and tool calls.
|
|
858
|
+
* A ModelMessage keeps `thinking` apart from `content`/`toolCalls`, so a
|
|
859
|
+
* provider turn that thinks between provider-executed tools would otherwise
|
|
860
|
+
* be recorded as "all thinking, then text, then tools" and the provider
|
|
861
|
+
* rejects the replay (signed thinking must keep its position). `null` once
|
|
862
|
+
* the order could no longer be tracked (callers fall back to one message).
|
|
863
|
+
*/
|
|
864
|
+
private turnParts: Array<TurnPart> | null = []
|
|
812
865
|
private currentThinkingContent = ''
|
|
813
866
|
private currentThinkingSignature = ''
|
|
814
867
|
private eventOptions?: Record<string, unknown> | undefined
|
|
@@ -842,6 +895,7 @@ class TextEngine<
|
|
|
842
895
|
private readonly threadId: string
|
|
843
896
|
private readonly runIdOverride?: string
|
|
844
897
|
private readonly parentRunIdOverride?: string
|
|
898
|
+
private readonly subagentRunIdOverride?: string
|
|
845
899
|
|
|
846
900
|
// Middleware support
|
|
847
901
|
private readonly middlewareRunner: MiddlewareRunner<
|
|
@@ -972,6 +1026,7 @@ class TextEngine<
|
|
|
972
1026
|
this.createId('thread')
|
|
973
1027
|
this.runIdOverride = config.params.runId
|
|
974
1028
|
this.parentRunIdOverride = config.params.parentRunId
|
|
1029
|
+
this.subagentRunIdOverride = config.params.subagentRunId
|
|
975
1030
|
|
|
976
1031
|
// Initialize middleware — devtools first. Spec stripping for the AG-UI
|
|
977
1032
|
// wire happens in toServerSentEventsStream, so in-process chat()
|
|
@@ -990,6 +1045,7 @@ class TextEngine<
|
|
|
990
1045
|
streamId: this.streamId,
|
|
991
1046
|
runId: this.runIdOverride ?? this.requestId,
|
|
992
1047
|
parentRunId: this.parentRunIdOverride,
|
|
1048
|
+
subagentRunId: this.subagentRunIdOverride,
|
|
993
1049
|
threadId: this.threadId,
|
|
994
1050
|
// Legacy alias kept on the ctx so middleware that reads
|
|
995
1051
|
// `ctx.conversationId` keeps working. Always equals `threadId`.
|
|
@@ -1449,6 +1505,7 @@ class TextEngine<
|
|
|
1449
1505
|
this.streamIdentityCaptured = false
|
|
1450
1506
|
this.accumulatedContent = ''
|
|
1451
1507
|
this.accumulatedThinking = []
|
|
1508
|
+
this.turnParts = []
|
|
1452
1509
|
this.currentThinkingContent = ''
|
|
1453
1510
|
this.currentThinkingSignature = ''
|
|
1454
1511
|
|
|
@@ -1516,6 +1573,13 @@ class TextEngine<
|
|
|
1516
1573
|
)
|
|
1517
1574
|
}
|
|
1518
1575
|
|
|
1576
|
+
// Fail closed on `{ type: 'file' }` sources for adapters that haven't
|
|
1577
|
+
// declared support — an adapter written before the file arm existed would
|
|
1578
|
+
// otherwise fall through to its URL/data branch and silently mis-map the
|
|
1579
|
+
// reference. Checked per model call so tool results added mid-loop are
|
|
1580
|
+
// covered too.
|
|
1581
|
+
assertMessagesFileSourceSupport(this.adapter, this.messages)
|
|
1582
|
+
|
|
1519
1583
|
for await (const raw of this.adapter.chatStream({
|
|
1520
1584
|
model: this.params.model,
|
|
1521
1585
|
messages: this.providerMessages,
|
|
@@ -1748,12 +1812,27 @@ class TextEngine<
|
|
|
1748
1812
|
// Adapters still emit leftover cumulative `content` on RAW yields.
|
|
1749
1813
|
// Alignment suppresses already-delivered deltas, so this snapshot is
|
|
1750
1814
|
// what a takeover saves as the full assistant text.
|
|
1815
|
+
const before = this.accumulatedContent
|
|
1751
1816
|
if (typeof extra.content === 'string' && extra.content !== '') {
|
|
1752
1817
|
this.accumulatedContent = extra.content
|
|
1753
1818
|
} else {
|
|
1754
1819
|
this.accumulatedContent += chunk.delta
|
|
1755
1820
|
}
|
|
1756
1821
|
this.middlewareCtx.accumulatedContent = this.accumulatedContent
|
|
1822
|
+
if (!this.turnParts) return
|
|
1823
|
+
if (!this.accumulatedContent.startsWith(before)) {
|
|
1824
|
+
// A cumulative `content` snapshot rewrote earlier text; order unknown.
|
|
1825
|
+
this.turnParts = null
|
|
1826
|
+
return
|
|
1827
|
+
}
|
|
1828
|
+
const delta = this.accumulatedContent.slice(before.length)
|
|
1829
|
+
if (delta === '') return
|
|
1830
|
+
const last = this.turnParts[this.turnParts.length - 1]
|
|
1831
|
+
if (last && last.type === 'text') {
|
|
1832
|
+
last.content += delta
|
|
1833
|
+
} else {
|
|
1834
|
+
this.turnParts.push({ type: 'text', content: delta })
|
|
1835
|
+
}
|
|
1757
1836
|
}
|
|
1758
1837
|
|
|
1759
1838
|
private captureStreamMessageIdentity(messageId: string): void {
|
|
@@ -1778,6 +1857,20 @@ class TextEngine<
|
|
|
1778
1857
|
this.captureStreamMessageIdentity(chunk.parentMessageId)
|
|
1779
1858
|
}
|
|
1780
1859
|
this.toolCallManager.addToolCallStartEvent(chunk)
|
|
1860
|
+
if (
|
|
1861
|
+
this.turnParts &&
|
|
1862
|
+
!this.turnParts.some(
|
|
1863
|
+
(part) => part.type === 'call' && part.id === chunk.toolCallId,
|
|
1864
|
+
)
|
|
1865
|
+
) {
|
|
1866
|
+
this.turnParts.push({
|
|
1867
|
+
type: 'call',
|
|
1868
|
+
id: chunk.toolCallId,
|
|
1869
|
+
providerExecuted: isProviderExecutedToolCall({
|
|
1870
|
+
metadata: chunk.metadata,
|
|
1871
|
+
}),
|
|
1872
|
+
})
|
|
1873
|
+
}
|
|
1781
1874
|
const metadata = chunk.metadata
|
|
1782
1875
|
const thoughtSignature =
|
|
1783
1876
|
metadata != null &&
|
|
@@ -1884,17 +1977,48 @@ class TextEngine<
|
|
|
1884
1977
|
signature: this.currentThinkingSignature,
|
|
1885
1978
|
}),
|
|
1886
1979
|
})
|
|
1980
|
+
if (this.turnParts) {
|
|
1981
|
+
const placeholder = [...this.turnParts]
|
|
1982
|
+
.reverse()
|
|
1983
|
+
.find(
|
|
1984
|
+
(part): part is Extract<TurnPart, { type: 'thinking' }> =>
|
|
1985
|
+
part.type === 'thinking' && part.index === -1,
|
|
1986
|
+
)
|
|
1987
|
+
const index = this.accumulatedThinking.length - 1
|
|
1988
|
+
if (placeholder) {
|
|
1989
|
+
placeholder.index = index
|
|
1990
|
+
} else {
|
|
1991
|
+
this.turnParts.push({ type: 'thinking', index })
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1887
1994
|
this.currentThinkingContent = ''
|
|
1888
1995
|
this.currentThinkingSignature = ''
|
|
1889
1996
|
}
|
|
1890
1997
|
}
|
|
1891
1998
|
|
|
1999
|
+
/**
|
|
2000
|
+
* Record where the current thinking step sits among this turn's parts. A
|
|
2001
|
+
* step is finalized only when the next step starts (or the turn ends), by
|
|
2002
|
+
* which time later tool calls have already arrived, so the position has to
|
|
2003
|
+
* be noted when the step's first content or signature shows up.
|
|
2004
|
+
*/
|
|
2005
|
+
private noteThinkingStepPosition(): void {
|
|
2006
|
+
if (
|
|
2007
|
+
this.turnParts &&
|
|
2008
|
+
this.currentThinkingContent === '' &&
|
|
2009
|
+
this.currentThinkingSignature === ''
|
|
2010
|
+
) {
|
|
2011
|
+
this.turnParts.push({ type: 'thinking', index: -1 })
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
|
|
1892
2015
|
private handleStepStartedEvent(): void {
|
|
1893
2016
|
this.finalizeCurrentThinkingStep()
|
|
1894
2017
|
}
|
|
1895
2018
|
|
|
1896
2019
|
private handleStepFinishedEvent(chunk: AdapterYieldChunk): void {
|
|
1897
2020
|
if (typeof chunk.signature === 'string' && chunk.signature !== '') {
|
|
2021
|
+
this.noteThinkingStepPosition()
|
|
1898
2022
|
this.currentThinkingSignature = chunk.signature
|
|
1899
2023
|
}
|
|
1900
2024
|
}
|
|
@@ -1902,6 +2026,7 @@ class TextEngine<
|
|
|
1902
2026
|
private handleReasoningMessageContentEvent(
|
|
1903
2027
|
chunk: Extract<StreamChunk, { type: 'REASONING_MESSAGE_CONTENT' }>,
|
|
1904
2028
|
): void {
|
|
2029
|
+
this.noteThinkingStepPosition()
|
|
1905
2030
|
this.currentThinkingContent += chunk.delta
|
|
1906
2031
|
}
|
|
1907
2032
|
|
|
@@ -1922,6 +2047,7 @@ class TextEngine<
|
|
|
1922
2047
|
}
|
|
1923
2048
|
return
|
|
1924
2049
|
}
|
|
2050
|
+
this.noteThinkingStepPosition()
|
|
1925
2051
|
this.currentThinkingSignature = chunk.encryptedValue
|
|
1926
2052
|
}
|
|
1927
2053
|
|
|
@@ -2083,7 +2209,8 @@ class TextEngine<
|
|
|
2083
2209
|
|
|
2084
2210
|
if (
|
|
2085
2211
|
executionResult.needsApproval.length > 0 ||
|
|
2086
|
-
executionResult.needsClientExecution.length > 0
|
|
2212
|
+
executionResult.needsClientExecution.length > 0 ||
|
|
2213
|
+
executionResult.subagentInterrupts.length > 0
|
|
2087
2214
|
) {
|
|
2088
2215
|
this.discardDeferredToolCallRunFinishedChunks()
|
|
2089
2216
|
|
|
@@ -2100,6 +2227,8 @@ class TextEngine<
|
|
|
2100
2227
|
finishEvent,
|
|
2101
2228
|
executionResult.needsApproval,
|
|
2102
2229
|
executionResult.needsClientExecution,
|
|
2230
|
+
[],
|
|
2231
|
+
executionResult.subagentInterrupts,
|
|
2103
2232
|
)
|
|
2104
2233
|
this.setToolPhase(emitted ? 'wait' : 'stop')
|
|
2105
2234
|
return emitted ? 'wait' : 'stop'
|
|
@@ -2292,7 +2421,8 @@ class TextEngine<
|
|
|
2292
2421
|
|
|
2293
2422
|
if (
|
|
2294
2423
|
executionResult.needsApproval.length > 0 ||
|
|
2295
|
-
executionResult.needsClientExecution.length > 0
|
|
2424
|
+
executionResult.needsClientExecution.length > 0 ||
|
|
2425
|
+
executionResult.subagentInterrupts.length > 0
|
|
2296
2426
|
) {
|
|
2297
2427
|
if (allResults.length > 0) {
|
|
2298
2428
|
for (const chunk of afterToolBoundaryChunks) {
|
|
@@ -2304,6 +2434,8 @@ class TextEngine<
|
|
|
2304
2434
|
finishEvent,
|
|
2305
2435
|
executionResult.needsApproval,
|
|
2306
2436
|
executionResult.needsClientExecution,
|
|
2437
|
+
[],
|
|
2438
|
+
executionResult.subagentInterrupts,
|
|
2307
2439
|
)
|
|
2308
2440
|
this.setToolPhase(emitted ? 'wait' : 'stop')
|
|
2309
2441
|
return
|
|
@@ -2401,21 +2533,110 @@ class TextEngine<
|
|
|
2401
2533
|
)
|
|
2402
2534
|
}
|
|
2403
2535
|
|
|
2536
|
+
/**
|
|
2537
|
+
* Split this iteration into assistant ModelMessages that keep the provider's
|
|
2538
|
+
* block order: a new segment starts at every thinking step that follows a
|
|
2539
|
+
* provider-executed tool call (the rule buildAssistantMessages applies to
|
|
2540
|
+
* UIMessages). Segments after the first get `${id}-segment-${n}` ids.
|
|
2541
|
+
* Returns null when no split is needed or the order could not be tracked,
|
|
2542
|
+
* so callers fall back to the single-message shape.
|
|
2543
|
+
*/
|
|
2544
|
+
private buildOrderedAssistantSegments(
|
|
2545
|
+
toolCalls: ReadonlyArray<ToolCall>,
|
|
2546
|
+
id: string | undefined,
|
|
2547
|
+
createdAt: Date | undefined,
|
|
2548
|
+
): Array<ModelMessage> | null {
|
|
2549
|
+
const parts = this.turnParts
|
|
2550
|
+
if (!parts) return null
|
|
2551
|
+
const providerCallIds = new Set(
|
|
2552
|
+
parts.flatMap((part) =>
|
|
2553
|
+
part.type === 'call' && part.providerExecuted ? [part.id] : [],
|
|
2554
|
+
),
|
|
2555
|
+
)
|
|
2556
|
+
type Segment = {
|
|
2557
|
+
thinking: Array<{ content: string; signature?: string }>
|
|
2558
|
+
text: string
|
|
2559
|
+
callIds: Array<string>
|
|
2560
|
+
}
|
|
2561
|
+
let current: Segment = { thinking: [], text: '', callIds: [] }
|
|
2562
|
+
const segments: Array<Segment> = [current]
|
|
2563
|
+
let split = false
|
|
2564
|
+
for (const part of parts) {
|
|
2565
|
+
if (part.type === 'thinking') {
|
|
2566
|
+
const thinking = this.accumulatedThinking[part.index]
|
|
2567
|
+
if (!thinking) return null
|
|
2568
|
+
if (current.callIds.some((callId) => providerCallIds.has(callId))) {
|
|
2569
|
+
current = { thinking: [thinking], text: '', callIds: [] }
|
|
2570
|
+
segments.push(current)
|
|
2571
|
+
split = true
|
|
2572
|
+
} else {
|
|
2573
|
+
current.thinking.push(thinking)
|
|
2574
|
+
}
|
|
2575
|
+
} else if (part.type === 'text') {
|
|
2576
|
+
current.text += part.content
|
|
2577
|
+
} else {
|
|
2578
|
+
current.callIds.push(part.id)
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
if (!split) return null
|
|
2582
|
+
if (
|
|
2583
|
+
segments.map((segment) => segment.text).join('') !==
|
|
2584
|
+
this.accumulatedContent
|
|
2585
|
+
) {
|
|
2586
|
+
return null
|
|
2587
|
+
}
|
|
2588
|
+
if (
|
|
2589
|
+
segments.reduce((n, segment) => n + segment.thinking.length, 0) !==
|
|
2590
|
+
this.accumulatedThinking.length
|
|
2591
|
+
) {
|
|
2592
|
+
return null
|
|
2593
|
+
}
|
|
2594
|
+
const placed = new Set(segments.flatMap((segment) => segment.callIds))
|
|
2595
|
+
for (const toolCall of toolCalls) {
|
|
2596
|
+
if (!placed.has(toolCall.id)) current.callIds.push(toolCall.id)
|
|
2597
|
+
}
|
|
2598
|
+
return segments.map((segment, index) => {
|
|
2599
|
+
const segmentCalls = toolCalls.filter((toolCall) =>
|
|
2600
|
+
segment.callIds.includes(toolCall.id),
|
|
2601
|
+
)
|
|
2602
|
+
return {
|
|
2603
|
+
role: 'assistant',
|
|
2604
|
+
content: segment.text || null,
|
|
2605
|
+
...(segmentCalls.length > 0 && { toolCalls: segmentCalls }),
|
|
2606
|
+
id:
|
|
2607
|
+
id === undefined
|
|
2608
|
+
? undefined
|
|
2609
|
+
: index === 0
|
|
2610
|
+
? id
|
|
2611
|
+
: `${id}-segment-${index}`,
|
|
2612
|
+
createdAt,
|
|
2613
|
+
...(segment.thinking.length > 0 && { thinking: segment.thinking }),
|
|
2614
|
+
}
|
|
2615
|
+
})
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2404
2618
|
private addAssistantToolCallMessage(toolCalls: Array<ToolCall>): void {
|
|
2405
2619
|
this.finalizeCurrentThinkingStep()
|
|
2406
2620
|
|
|
2621
|
+
const segments = this.buildOrderedAssistantSegments(
|
|
2622
|
+
toolCalls,
|
|
2623
|
+
this.currentMessageId ?? undefined,
|
|
2624
|
+
this.currentMessageCreatedAt ?? undefined,
|
|
2625
|
+
)
|
|
2407
2626
|
this.messages = [
|
|
2408
2627
|
...this.messages,
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2628
|
+
...(segments ?? [
|
|
2629
|
+
{
|
|
2630
|
+
role: 'assistant' as const,
|
|
2631
|
+
content: this.accumulatedContent || null,
|
|
2632
|
+
toolCalls,
|
|
2633
|
+
id: this.currentMessageId ?? undefined,
|
|
2634
|
+
createdAt: this.currentMessageCreatedAt ?? undefined,
|
|
2635
|
+
...(this.accumulatedThinking.length > 0 && {
|
|
2636
|
+
thinking: this.accumulatedThinking,
|
|
2637
|
+
}),
|
|
2638
|
+
},
|
|
2639
|
+
]),
|
|
2419
2640
|
]
|
|
2420
2641
|
this.middlewareCtx.messages = this.messages
|
|
2421
2642
|
}
|
|
@@ -2494,13 +2715,23 @@ class TextEngine<
|
|
|
2494
2715
|
!currentTurnAlreadyRecorded &&
|
|
2495
2716
|
(this.accumulatedContent !== '' || thinking)
|
|
2496
2717
|
) {
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2718
|
+
const id = this.currentMessageId ?? this.createId('msg')
|
|
2719
|
+
const createdAt = this.currentMessageCreatedAt ?? new Date()
|
|
2720
|
+
messages.push(
|
|
2721
|
+
...(this.buildOrderedAssistantSegments(
|
|
2722
|
+
this.toolCallManager.getToolCalls(),
|
|
2723
|
+
id,
|
|
2724
|
+
createdAt,
|
|
2725
|
+
) ?? [
|
|
2726
|
+
{
|
|
2727
|
+
role: 'assistant',
|
|
2728
|
+
content: this.accumulatedContent || null,
|
|
2729
|
+
id,
|
|
2730
|
+
createdAt,
|
|
2731
|
+
...(thinking ? { thinking } : {}),
|
|
2732
|
+
},
|
|
2733
|
+
]),
|
|
2734
|
+
)
|
|
2504
2735
|
}
|
|
2505
2736
|
if (structuredOutput) {
|
|
2506
2737
|
messages.push({
|
|
@@ -2622,6 +2853,7 @@ class TextEngine<
|
|
|
2622
2853
|
GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
|
|
2623
2854
|
> = [],
|
|
2624
2855
|
genericInterruptIds: ReadonlyArray<string> = [],
|
|
2856
|
+
childInterrupts: ReadonlyArray<Interrupt> = [],
|
|
2625
2857
|
): Array<Interrupt> {
|
|
2626
2858
|
const interrupts: Array<Interrupt> = []
|
|
2627
2859
|
|
|
@@ -2739,6 +2971,9 @@ class TextEngine<
|
|
|
2739
2971
|
})
|
|
2740
2972
|
}
|
|
2741
2973
|
|
|
2974
|
+
// A subagent tool call raised these. They keep their `subagentRunId`.
|
|
2975
|
+
interrupts.push(...childInterrupts)
|
|
2976
|
+
|
|
2742
2977
|
const ids = new Set<string>()
|
|
2743
2978
|
for (const interrupt of interrupts) {
|
|
2744
2979
|
if (ids.has(interrupt.id)) {
|
|
@@ -2760,6 +2995,7 @@ class TextEngine<
|
|
|
2760
2995
|
GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
|
|
2761
2996
|
> = [],
|
|
2762
2997
|
genericInterruptIds?: ReadonlyArray<string>,
|
|
2998
|
+
childInterrupts?: ReadonlyArray<Interrupt>,
|
|
2763
2999
|
): StreamChunk {
|
|
2764
3000
|
return {
|
|
2765
3001
|
...finishEvent,
|
|
@@ -2771,6 +3007,7 @@ class TextEngine<
|
|
|
2771
3007
|
clientRequests,
|
|
2772
3008
|
genericRequests,
|
|
2773
3009
|
genericInterruptIds,
|
|
3010
|
+
childInterrupts,
|
|
2774
3011
|
),
|
|
2775
3012
|
},
|
|
2776
3013
|
}
|
|
@@ -2920,11 +3157,13 @@ class TextEngine<
|
|
|
2920
3157
|
genericRequests: ReadonlyArray<
|
|
2921
3158
|
GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
|
|
2922
3159
|
> = [],
|
|
3160
|
+
childInterrupts: ReadonlyArray<Interrupt> = [],
|
|
2923
3161
|
): AsyncGenerator<StreamChunk, boolean, void> {
|
|
2924
3162
|
yield* this.emitSyntheticRunStarted(finishEvent)
|
|
2925
3163
|
const genericInterruptIds = genericRequests.map(() =>
|
|
2926
3164
|
this.genericInterruptId(),
|
|
2927
3165
|
)
|
|
3166
|
+
// Binding completion also binds child interrupts to this run.
|
|
2928
3167
|
const terminal = this.completeEphemeralInterruptBindings(
|
|
2929
3168
|
this.buildInterruptFinishedChunk(
|
|
2930
3169
|
finishEvent,
|
|
@@ -2932,6 +3171,7 @@ class TextEngine<
|
|
|
2932
3171
|
clientRequests,
|
|
2933
3172
|
genericRequests,
|
|
2934
3173
|
genericInterruptIds,
|
|
3174
|
+
childInterrupts,
|
|
2935
3175
|
),
|
|
2936
3176
|
)
|
|
2937
3177
|
let terminalOutputs: Array<StreamChunk>
|
|
@@ -3025,6 +3265,8 @@ class TextEngine<
|
|
|
3025
3265
|
const approvalRequests: Array<ApprovalRequest> = []
|
|
3026
3266
|
const clientRequests: Array<ClientToolRequest> = []
|
|
3027
3267
|
for (const toolCall of toolCalls) {
|
|
3268
|
+
// Provider-executed calls are complete; never surface them as client work.
|
|
3269
|
+
if (isProviderExecutedToolCall(toolCall)) continue
|
|
3028
3270
|
const tool = this.resolveExecutableTools([toolCall]).find(
|
|
3029
3271
|
(candidate) => candidate.name === toolCall.function.name,
|
|
3030
3272
|
) as RuntimeToolWithApproval | undefined
|
|
@@ -3182,6 +3424,9 @@ class TextEngine<
|
|
|
3182
3424
|
role: 'tool',
|
|
3183
3425
|
content,
|
|
3184
3426
|
toolCallId: result.toolCallId,
|
|
3427
|
+
...(result.state === 'output-error' && {
|
|
3428
|
+
error: toolResultErrorText(parseToolOutput(wireContent)),
|
|
3429
|
+
}),
|
|
3185
3430
|
}
|
|
3186
3431
|
|
|
3187
3432
|
if (placeholderIdx >= 0) {
|
|
@@ -3517,6 +3762,12 @@ class TextEngine<
|
|
|
3517
3762
|
// Apply merged config back to engine state
|
|
3518
3763
|
this.applyMiddlewareConfig(postOnConfig)
|
|
3519
3764
|
|
|
3765
|
+
// Schema-only structured output with no tools skips the agent loop, so
|
|
3766
|
+
// `streamModelResponse` never runs this check. Middleware can also
|
|
3767
|
+
// replace `this.messages` above. Fail closed here before the
|
|
3768
|
+
// structured-output adapter call.
|
|
3769
|
+
assertMessagesFileSourceSupport(this.adapter, this.messages)
|
|
3770
|
+
|
|
3520
3771
|
// Build the StructuredOutputOptions the adapter expects.
|
|
3521
3772
|
// `this.adapter` is already `TAdapter extends AnyTextAdapter` per the
|
|
3522
3773
|
// class generics — no cast needed.
|
|
@@ -3858,7 +4109,16 @@ class TextEngine<
|
|
|
3858
4109
|
const yieldChunks = this.finalStructuredOutput.yieldChunks
|
|
3859
4110
|
const source = this.finalStructuredOutput.source ?? 'text'
|
|
3860
4111
|
|
|
3861
|
-
|
|
4112
|
+
// A final turn cut off at the output cap holds truncated JSON, or none
|
|
4113
|
+
// for a reasoning model that spent the budget. Report the token limit
|
|
4114
|
+
// instead of a parse or missing-result error (#1426).
|
|
4115
|
+
if (this.lastFinishReason === 'length') {
|
|
4116
|
+
this.finalizationError = {
|
|
4117
|
+
message:
|
|
4118
|
+
'The response was cut off because the maximum token limit was reached (finish_reason=length); raise the output token limit.',
|
|
4119
|
+
code: 'max_tokens',
|
|
4120
|
+
}
|
|
4121
|
+
} else if (source === 'event') {
|
|
3862
4122
|
if (!this.structuredOutputResult) {
|
|
3863
4123
|
this.finalizationError = {
|
|
3864
4124
|
message: 'missing structured result',
|
|
@@ -4013,9 +4273,25 @@ class TextEngine<
|
|
|
4013
4273
|
}
|
|
4014
4274
|
}
|
|
4015
4275
|
|
|
4276
|
+
/** Resume entries this run answers itself. A subagent tool owns the rest. */
|
|
4277
|
+
private ownResume(
|
|
4278
|
+
resume: ChatMiddlewareConfig['resume'],
|
|
4279
|
+
): ChatMiddlewareConfig['resume'] {
|
|
4280
|
+
const childIds = (
|
|
4281
|
+
this.params as { [CHILD_RESUME_IDS]?: ReadonlySet<string> }
|
|
4282
|
+
)[CHILD_RESUME_IDS]
|
|
4283
|
+
return childIds
|
|
4284
|
+
? resume?.filter((entry) => !childIds.has(entry.interruptId))
|
|
4285
|
+
: resume
|
|
4286
|
+
}
|
|
4287
|
+
|
|
4016
4288
|
private async applyEphemeralInterruptResume(
|
|
4017
|
-
|
|
4289
|
+
middlewareConfig: ChatMiddlewareConfig,
|
|
4018
4290
|
): Promise<void> {
|
|
4291
|
+
const config = {
|
|
4292
|
+
...middlewareConfig,
|
|
4293
|
+
resume: this.ownResume(middlewareConfig.resume),
|
|
4294
|
+
}
|
|
4019
4295
|
if ((config.resume?.length ?? 0) === 0) {
|
|
4020
4296
|
return
|
|
4021
4297
|
}
|
|
@@ -4248,7 +4524,7 @@ class TextEngine<
|
|
|
4248
4524
|
}> = []
|
|
4249
4525
|
const ids = new Set<string>()
|
|
4250
4526
|
const batchIndexes = new Set<number>()
|
|
4251
|
-
for (const resumeItem of this.params.resume ?? []) {
|
|
4527
|
+
for (const resumeItem of this.ownResume(this.params.resume) ?? []) {
|
|
4252
4528
|
const parsed = readGenericInterruptContinuation(resumeItem.metadata)
|
|
4253
4529
|
if (parsed.status === 'absent') continue
|
|
4254
4530
|
if (parsed.status === 'invalid') {
|
|
@@ -4558,11 +4834,12 @@ class TextEngine<
|
|
|
4558
4834
|
*/
|
|
4559
4835
|
private async *drainToolCallGenerator(
|
|
4560
4836
|
generator: AsyncGenerator<
|
|
4561
|
-
CustomEvent,
|
|
4837
|
+
CustomEvent | StreamChunk,
|
|
4562
4838
|
{
|
|
4563
4839
|
results: Array<ToolResult>
|
|
4564
4840
|
needsApproval: Array<ApprovalRequest>
|
|
4565
4841
|
needsClientExecution: Array<ClientToolRequest>
|
|
4842
|
+
subagentInterrupts: Array<Interrupt>
|
|
4566
4843
|
},
|
|
4567
4844
|
void
|
|
4568
4845
|
>,
|
|
@@ -4572,6 +4849,7 @@ class TextEngine<
|
|
|
4572
4849
|
results: Array<ToolResult>
|
|
4573
4850
|
needsApproval: Array<ApprovalRequest>
|
|
4574
4851
|
needsClientExecution: Array<ClientToolRequest>
|
|
4852
|
+
subagentInterrupts: Array<Interrupt>
|
|
4575
4853
|
},
|
|
4576
4854
|
void
|
|
4577
4855
|
> {
|
|
@@ -4683,6 +4961,8 @@ export function chat<
|
|
|
4683
4961
|
> = [],
|
|
4684
4962
|
TContext = unknown,
|
|
4685
4963
|
const TMiddleware extends Array<unknown> | undefined = undefined,
|
|
4964
|
+
const TAgents extends ReadonlyArray<DefinedAgent> =
|
|
4965
|
+
ReadonlyArray<DefinedAgent>,
|
|
4686
4966
|
>(
|
|
4687
4967
|
options: TextActivityOptionsWithContext<
|
|
4688
4968
|
TAdapter,
|
|
@@ -4691,7 +4971,8 @@ export function chat<
|
|
|
4691
4971
|
TTools,
|
|
4692
4972
|
TInterrupts,
|
|
4693
4973
|
TContext,
|
|
4694
|
-
TMiddleware
|
|
4974
|
+
TMiddleware,
|
|
4975
|
+
TAgents
|
|
4695
4976
|
>,
|
|
4696
4977
|
): TextActivityResult<TSchema, TStream, TTools> {
|
|
4697
4978
|
validateInterruptDefinitions(options.interrupts)
|
|
@@ -4705,6 +4986,12 @@ export function chat<
|
|
|
4705
4986
|
|
|
4706
4987
|
const { outputSchema, stream } = options
|
|
4707
4988
|
|
|
4989
|
+
if (outputSchema && (options.subagents?.agents.length ?? 0) > 0) {
|
|
4990
|
+
throw new Error(
|
|
4991
|
+
'chat() does not support subagents together with outputSchema. Put outputSchema on a child chat() instead.',
|
|
4992
|
+
)
|
|
4993
|
+
}
|
|
4994
|
+
|
|
4708
4995
|
if (outputSchema && stream === true) {
|
|
4709
4996
|
return runStreamingStructuredOutput(
|
|
4710
4997
|
toRuntimeTextActivityOptions(options, {
|
|
@@ -4744,7 +5031,10 @@ type RuntimeTextActivityOptions<
|
|
|
4744
5031
|
TAdapter extends AnyTextAdapter,
|
|
4745
5032
|
TSchema extends SchemaInput | undefined,
|
|
4746
5033
|
TStream extends boolean,
|
|
4747
|
-
> = Omit<
|
|
5034
|
+
> = Omit<
|
|
5035
|
+
TextActivityOptions<TAdapter, TSchema, TStream, any, any>,
|
|
5036
|
+
'middleware'
|
|
5037
|
+
> & {
|
|
4748
5038
|
middleware?: Array<AnyChatMiddleware>
|
|
4749
5039
|
}
|
|
4750
5040
|
|
|
@@ -4773,6 +5063,7 @@ function toRuntimeTextActivityOptions<
|
|
|
4773
5063
|
TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,
|
|
4774
5064
|
TContext,
|
|
4775
5065
|
TMiddleware extends Array<unknown> | undefined,
|
|
5066
|
+
TAgents extends ReadonlyArray<DefinedAgent> = ReadonlyArray<DefinedAgent>,
|
|
4776
5067
|
>(
|
|
4777
5068
|
options: TextActivityOptionsWithContext<
|
|
4778
5069
|
TAdapter,
|
|
@@ -4781,7 +5072,8 @@ function toRuntimeTextActivityOptions<
|
|
|
4781
5072
|
TTools,
|
|
4782
5073
|
TInterrupts,
|
|
4783
5074
|
TContext,
|
|
4784
|
-
TMiddleware
|
|
5075
|
+
TMiddleware,
|
|
5076
|
+
TAgents
|
|
4785
5077
|
>,
|
|
4786
5078
|
overrides: { outputSchema: TOutputSchema; stream: TOutputStream },
|
|
4787
5079
|
): RuntimeTextActivityOptions<TAdapter, TOutputSchema, TOutputStream> {
|
|
@@ -4868,11 +5160,19 @@ function runStreamingText(
|
|
|
4868
5160
|
return stream
|
|
4869
5161
|
}
|
|
4870
5162
|
|
|
4871
|
-
async function*
|
|
5163
|
+
async function* runChatEngine(
|
|
4872
5164
|
options: RuntimeTextActivityOptions<AnyTextAdapter, undefined, boolean>,
|
|
4873
5165
|
engineRef: DeliveryEngineRef,
|
|
4874
5166
|
): AsyncIterable<StreamChunk> {
|
|
4875
|
-
const {
|
|
5167
|
+
const {
|
|
5168
|
+
adapter,
|
|
5169
|
+
middleware,
|
|
5170
|
+
context,
|
|
5171
|
+
debug,
|
|
5172
|
+
mcp,
|
|
5173
|
+
subagents: _subagents,
|
|
5174
|
+
...textOptions
|
|
5175
|
+
} = options
|
|
4876
5176
|
const model = adapter.model
|
|
4877
5177
|
const logger = resolveDebugOption(debug)
|
|
4878
5178
|
|
|
@@ -4906,6 +5206,410 @@ async function* streamTextChunks(
|
|
|
4906
5206
|
}
|
|
4907
5207
|
}
|
|
4908
5208
|
|
|
5209
|
+
async function* streamTextChunks(
|
|
5210
|
+
options: RuntimeTextActivityOptions<AnyTextAdapter, undefined, boolean>,
|
|
5211
|
+
engineRef: DeliveryEngineRef,
|
|
5212
|
+
): AsyncIterable<StreamChunk> {
|
|
5213
|
+
const bag = options.subagents
|
|
5214
|
+
const agents = bag?.agents ?? []
|
|
5215
|
+
if (bag && agents.length > 0 && bag.router) {
|
|
5216
|
+
yield* runRoutedSubagents(options, engineRef)
|
|
5217
|
+
return
|
|
5218
|
+
}
|
|
5219
|
+
if (bag && agents.length > 0) {
|
|
5220
|
+
const threadId = options.threadId ?? `thread-${Date.now()}`
|
|
5221
|
+
const runId = options.runId ?? `run-${Date.now()}`
|
|
5222
|
+
const messages = options.messages ?? []
|
|
5223
|
+
// A resume can answer a child's interrupts. Those entries go to the child
|
|
5224
|
+
// tool call. The parent run validates only its own entries.
|
|
5225
|
+
const turn = readSubagentTurn(messages, options.resume)
|
|
5226
|
+
const sink = createSubagentSink()
|
|
5227
|
+
const calls = subagentCallMessages(
|
|
5228
|
+
new Set(bag.agents.map((agent: DefinedAgent) => agent.name)),
|
|
5229
|
+
)
|
|
5230
|
+
const synthetic = createSyntheticSubagentTools(bag, {
|
|
5231
|
+
messages: turn?.before ?? messages,
|
|
5232
|
+
messagesFor: calls.messagesFor,
|
|
5233
|
+
threadId,
|
|
5234
|
+
runId,
|
|
5235
|
+
...(options.parentRunId !== undefined && {
|
|
5236
|
+
interruptedRunId: options.parentRunId,
|
|
5237
|
+
}),
|
|
5238
|
+
...(options.abortController && {
|
|
5239
|
+
abortSignal: options.abortController.signal,
|
|
5240
|
+
}),
|
|
5241
|
+
...(turn && { turn }),
|
|
5242
|
+
sink,
|
|
5243
|
+
})
|
|
5244
|
+
yield* streamWithChildUsage(
|
|
5245
|
+
runChatEngine(
|
|
5246
|
+
{
|
|
5247
|
+
...options,
|
|
5248
|
+
threadId,
|
|
5249
|
+
runId,
|
|
5250
|
+
...(turn && {
|
|
5251
|
+
[CHILD_RESUME_IDS]: new Set(
|
|
5252
|
+
turn.children.flatMap((child) =>
|
|
5253
|
+
child.resume.map((entry) => entry.interruptId),
|
|
5254
|
+
),
|
|
5255
|
+
),
|
|
5256
|
+
}),
|
|
5257
|
+
tools: [...(options.tools ?? []), ...synthetic],
|
|
5258
|
+
middleware: [...(options.middleware ?? []), calls.middleware],
|
|
5259
|
+
},
|
|
5260
|
+
engineRef,
|
|
5261
|
+
),
|
|
5262
|
+
sink,
|
|
5263
|
+
)
|
|
5264
|
+
return
|
|
5265
|
+
}
|
|
5266
|
+
yield* runChatEngine(options, engineRef)
|
|
5267
|
+
}
|
|
5268
|
+
|
|
5269
|
+
/** Add child usage to the next RUN_FINISHED of the parent. */
|
|
5270
|
+
async function* streamWithChildUsage(
|
|
5271
|
+
stream: AsyncIterable<StreamChunk>,
|
|
5272
|
+
sink: SubagentSink,
|
|
5273
|
+
): AsyncIterable<StreamChunk> {
|
|
5274
|
+
for await (const chunk of stream) {
|
|
5275
|
+
yield chunk.type === EventType.RUN_FINISHED
|
|
5276
|
+
? withChildUsage(chunk, sink)
|
|
5277
|
+
: chunk
|
|
5278
|
+
}
|
|
5279
|
+
}
|
|
5280
|
+
|
|
5281
|
+
async function* runRoutedSubagents(
|
|
5282
|
+
options: RuntimeTextActivityOptions<AnyTextAdapter, undefined, boolean>,
|
|
5283
|
+
engineRef: DeliveryEngineRef,
|
|
5284
|
+
): AsyncIterable<StreamChunk> {
|
|
5285
|
+
const bag = options.subagents
|
|
5286
|
+
if (!bag?.router) {
|
|
5287
|
+
yield* runChatEngine(options, engineRef)
|
|
5288
|
+
return
|
|
5289
|
+
}
|
|
5290
|
+
const threadId = options.threadId ?? `thread-${Date.now()}`
|
|
5291
|
+
const runId = options.runId ?? `run-${Date.now()}`
|
|
5292
|
+
const messages = options.messages ?? []
|
|
5293
|
+
const turn = readSubagentTurn(messages, options.resume)
|
|
5294
|
+
if (!turn && (options.resume?.length ?? 0) > 0) {
|
|
5295
|
+
// No child owns these answers. Main raised them, after a main pick or a
|
|
5296
|
+
// handoff. Run main alone.
|
|
5297
|
+
yield* runChatEngine(
|
|
5298
|
+
{ ...options, threadId, runId, subagents: undefined },
|
|
5299
|
+
engineRef,
|
|
5300
|
+
)
|
|
5301
|
+
return
|
|
5302
|
+
}
|
|
5303
|
+
const abortSignal = options.abortController?.signal
|
|
5304
|
+
const turnMessages = turn?.before ?? messages
|
|
5305
|
+
const subagentPersistence = readRoutedSubagentPersistence(options.middleware)
|
|
5306
|
+
await subagentPersistence?.start({
|
|
5307
|
+
threadId,
|
|
5308
|
+
runId,
|
|
5309
|
+
messages,
|
|
5310
|
+
...(options.resume && { resume: options.resume }),
|
|
5311
|
+
})
|
|
5312
|
+
try {
|
|
5313
|
+
// A resume reuses the plan that the first run put on each child's
|
|
5314
|
+
// SUBAGENT_STARTED metadata. Children that finished keep their result.
|
|
5315
|
+
// Suspended children continue.
|
|
5316
|
+
const plan =
|
|
5317
|
+
savedPlan(turn?.plan, bag.agents, {
|
|
5318
|
+
threadId,
|
|
5319
|
+
interruptedRunId: options.parentRunId ?? runId,
|
|
5320
|
+
interruptIds: options.resume?.map((entry) => entry.interruptId) ?? [],
|
|
5321
|
+
}) ??
|
|
5322
|
+
normalizeRouterPick(
|
|
5323
|
+
await bag.router({
|
|
5324
|
+
messages: turnMessages,
|
|
5325
|
+
agents: bag.agents,
|
|
5326
|
+
...(abortSignal && { abortSignal }),
|
|
5327
|
+
}),
|
|
5328
|
+
bag.agents,
|
|
5329
|
+
)
|
|
5330
|
+
// The run was stopped while the router decided. Start nothing. No
|
|
5331
|
+
// RUN_STARTED went out, so the stream ends without a terminal, but the
|
|
5332
|
+
// record start() opened still has to settle. Otherwise it stays `running`
|
|
5333
|
+
// and reconstructChat hands the client a run to tail that never emits.
|
|
5334
|
+
if (abortSignal?.aborted) {
|
|
5335
|
+
const error = new Error('Aborted')
|
|
5336
|
+
error.name = 'AbortError'
|
|
5337
|
+
await subagentPersistence?.abort({ threadId, runId, error })
|
|
5338
|
+
return
|
|
5339
|
+
}
|
|
5340
|
+
const onlyStep = plan.steps.length === 1 ? plan.steps[0] : undefined
|
|
5341
|
+
if (onlyStep?.names.length === 1 && onlyStep.names[0] === 'main') {
|
|
5342
|
+
// Earlier children own answers in this resume. Commit them first.
|
|
5343
|
+
if (turn) await subagentPersistence?.finish({ threadId, runId })
|
|
5344
|
+
yield* runChatEngine(
|
|
5345
|
+
{
|
|
5346
|
+
...options,
|
|
5347
|
+
threadId,
|
|
5348
|
+
runId,
|
|
5349
|
+
subagents: undefined,
|
|
5350
|
+
...(turn && { resume: turn.rest }),
|
|
5351
|
+
},
|
|
5352
|
+
engineRef,
|
|
5353
|
+
)
|
|
5354
|
+
return
|
|
5355
|
+
}
|
|
5356
|
+
|
|
5357
|
+
yield {
|
|
5358
|
+
type: EventType.RUN_STARTED,
|
|
5359
|
+
threadId,
|
|
5360
|
+
runId,
|
|
5361
|
+
timestamp: Date.now(),
|
|
5362
|
+
}
|
|
5363
|
+
|
|
5364
|
+
const earlier = [...(turn?.children ?? [])]
|
|
5365
|
+
const takeEarlier = (name: string) => {
|
|
5366
|
+
const index = earlier.findIndex((child) => child.name === name)
|
|
5367
|
+
return index === -1 ? undefined : earlier.splice(index, 1)[0]
|
|
5368
|
+
}
|
|
5369
|
+
const sink = createSubagentSink()
|
|
5370
|
+
const stepTexts: Array<string> = []
|
|
5371
|
+
let stepMessages = turnMessages
|
|
5372
|
+
let failure:
|
|
5373
|
+
| Extract<StreamChunk, { type: EventType.SUBAGENT_ERROR }>
|
|
5374
|
+
| undefined
|
|
5375
|
+
for (const step of plan.steps) {
|
|
5376
|
+
const entries: Array<SpawnEntry> = []
|
|
5377
|
+
const textByName = new Map<string, string>()
|
|
5378
|
+
for (const name of step.names) {
|
|
5379
|
+
const prior = takeEarlier(name)
|
|
5380
|
+
if (prior?.status === 'finished') {
|
|
5381
|
+
textByName.set(name, prior.text)
|
|
5382
|
+
continue
|
|
5383
|
+
}
|
|
5384
|
+
entries.push(
|
|
5385
|
+
prior?.status === 'suspended'
|
|
5386
|
+
? {
|
|
5387
|
+
name,
|
|
5388
|
+
resume: {
|
|
5389
|
+
subagentRunId: prior.subagentRunId,
|
|
5390
|
+
messages: prior.messages,
|
|
5391
|
+
entries: prior.resume,
|
|
5392
|
+
text: prior.text,
|
|
5393
|
+
},
|
|
5394
|
+
}
|
|
5395
|
+
: { name },
|
|
5396
|
+
)
|
|
5397
|
+
}
|
|
5398
|
+
const stepChunks: Array<StreamChunk> = []
|
|
5399
|
+
if (entries.length > 0) {
|
|
5400
|
+
for await (const chunk of spawnNamedAgents(
|
|
5401
|
+
entries,
|
|
5402
|
+
{ ...bag, order: step.order ?? bag.order },
|
|
5403
|
+
{
|
|
5404
|
+
messages: stepMessages,
|
|
5405
|
+
...(abortSignal && { abortSignal }),
|
|
5406
|
+
threadId,
|
|
5407
|
+
parentRunId: runId,
|
|
5408
|
+
...(options.parentRunId !== undefined && {
|
|
5409
|
+
interruptedRunId: options.parentRunId,
|
|
5410
|
+
}),
|
|
5411
|
+
},
|
|
5412
|
+
sink,
|
|
5413
|
+
)) {
|
|
5414
|
+
const tagged = withPlan(chunk, plan)
|
|
5415
|
+
stepChunks.push(tagged)
|
|
5416
|
+
await subagentPersistence?.chunk({ threadId, runId, chunk: tagged })
|
|
5417
|
+
yield tagged
|
|
5418
|
+
}
|
|
5419
|
+
}
|
|
5420
|
+
failure = stepChunks.find(
|
|
5421
|
+
(chunk) => chunk.type === EventType.SUBAGENT_ERROR,
|
|
5422
|
+
)
|
|
5423
|
+
if (failure) break
|
|
5424
|
+
if (sink.interrupts.length > 0) break
|
|
5425
|
+
for (const entry of entries) {
|
|
5426
|
+
const text = collectNamedText(stepChunks, [entry.name])
|
|
5427
|
+
textByName.set(
|
|
5428
|
+
entry.name,
|
|
5429
|
+
[entry.resume?.text, text].filter(Boolean).join('\n\n'),
|
|
5430
|
+
)
|
|
5431
|
+
}
|
|
5432
|
+
const text = step.names
|
|
5433
|
+
.map((name) => textByName.get(name)?.trim() ?? '')
|
|
5434
|
+
.filter((block) => block !== '')
|
|
5435
|
+
.join('\n\n')
|
|
5436
|
+
if (text) {
|
|
5437
|
+
stepTexts.push(text)
|
|
5438
|
+
stepMessages = [...stepMessages, { role: 'assistant', content: text }]
|
|
5439
|
+
}
|
|
5440
|
+
}
|
|
5441
|
+
|
|
5442
|
+
if (abortSignal?.aborted) {
|
|
5443
|
+
const error = new Error('Aborted')
|
|
5444
|
+
error.name = 'AbortError'
|
|
5445
|
+
await subagentPersistence?.abort({ threadId, runId, error })
|
|
5446
|
+
yield withChildUsage(
|
|
5447
|
+
{
|
|
5448
|
+
type: EventType.RUN_FINISHED,
|
|
5449
|
+
threadId,
|
|
5450
|
+
runId,
|
|
5451
|
+
outcome: { type: 'cancelled' },
|
|
5452
|
+
timestamp: Date.now(),
|
|
5453
|
+
},
|
|
5454
|
+
sink,
|
|
5455
|
+
)
|
|
5456
|
+
return
|
|
5457
|
+
}
|
|
5458
|
+
|
|
5459
|
+
if (failure) {
|
|
5460
|
+
// Carry the child's own cause, not a generic label, and keep the usage
|
|
5461
|
+
// the children already spent: the run failed, but the tokens were real.
|
|
5462
|
+
const message = failure.message || 'A subagent failed'
|
|
5463
|
+
await subagentPersistence?.abort({
|
|
5464
|
+
threadId,
|
|
5465
|
+
runId,
|
|
5466
|
+
error: new Error(message),
|
|
5467
|
+
})
|
|
5468
|
+
yield withChildUsage(
|
|
5469
|
+
{
|
|
5470
|
+
type: EventType.RUN_ERROR,
|
|
5471
|
+
threadId,
|
|
5472
|
+
runId,
|
|
5473
|
+
message,
|
|
5474
|
+
...(failure.code !== undefined ? { code: failure.code } : {}),
|
|
5475
|
+
timestamp: Date.now(),
|
|
5476
|
+
},
|
|
5477
|
+
sink,
|
|
5478
|
+
)
|
|
5479
|
+
return
|
|
5480
|
+
}
|
|
5481
|
+
|
|
5482
|
+
if (sink.interrupts.length > 0) {
|
|
5483
|
+
const interrupts = rebindInterrupts(sink.interrupts, runId)
|
|
5484
|
+
await subagentPersistence?.suspend?.({ threadId, runId, interrupts })
|
|
5485
|
+
yield withChildUsage(
|
|
5486
|
+
{
|
|
5487
|
+
type: EventType.RUN_FINISHED,
|
|
5488
|
+
threadId,
|
|
5489
|
+
runId,
|
|
5490
|
+
outcome: { type: 'interrupt', interrupts },
|
|
5491
|
+
timestamp: Date.now(),
|
|
5492
|
+
},
|
|
5493
|
+
sink,
|
|
5494
|
+
)
|
|
5495
|
+
return
|
|
5496
|
+
}
|
|
5497
|
+
|
|
5498
|
+
const strategy = bag.strategy ?? 'exclusive'
|
|
5499
|
+
if (strategy === 'handoff') {
|
|
5500
|
+
const childText = stepTexts.join('\n\n')
|
|
5501
|
+
// The children are done. Commit their answers and settle their records
|
|
5502
|
+
// before main runs. Main's own persistence takes over from here.
|
|
5503
|
+
await subagentPersistence?.finish({ threadId, runId })
|
|
5504
|
+
yield* streamWithChildUsage(
|
|
5505
|
+
runChatEngine(
|
|
5506
|
+
{
|
|
5507
|
+
...options,
|
|
5508
|
+
threadId,
|
|
5509
|
+
runId,
|
|
5510
|
+
subagents: undefined,
|
|
5511
|
+
...(turn && { resume: turn.rest }),
|
|
5512
|
+
messages: [
|
|
5513
|
+
...turnMessages,
|
|
5514
|
+
{
|
|
5515
|
+
// Same id as the recorder's parent row, so the stored thread
|
|
5516
|
+
// keeps one host message for the cards.
|
|
5517
|
+
id: subagentHostMessageId(runId),
|
|
5518
|
+
role: 'assistant',
|
|
5519
|
+
content: childText || 'Subagent finished.',
|
|
5520
|
+
metadata: { tanstack: { runId } },
|
|
5521
|
+
},
|
|
5522
|
+
],
|
|
5523
|
+
},
|
|
5524
|
+
engineRef,
|
|
5525
|
+
),
|
|
5526
|
+
sink,
|
|
5527
|
+
)
|
|
5528
|
+
return
|
|
5529
|
+
}
|
|
5530
|
+
|
|
5531
|
+
await subagentPersistence?.finish({ threadId, runId })
|
|
5532
|
+
yield withChildUsage(
|
|
5533
|
+
{
|
|
5534
|
+
type: EventType.RUN_FINISHED,
|
|
5535
|
+
threadId,
|
|
5536
|
+
runId,
|
|
5537
|
+
timestamp: Date.now(),
|
|
5538
|
+
},
|
|
5539
|
+
sink,
|
|
5540
|
+
)
|
|
5541
|
+
} catch (error) {
|
|
5542
|
+
await subagentPersistence?.abort({ threadId, runId, error })
|
|
5543
|
+
throw error
|
|
5544
|
+
}
|
|
5545
|
+
}
|
|
5546
|
+
|
|
5547
|
+
/**
|
|
5548
|
+
* The plan from a resumed turn, or undefined when it is absent. A plan that
|
|
5549
|
+
* names agents this chat does not have is stale: re-routing would strand the
|
|
5550
|
+
* suspended child that owns the answers, so fail the resume instead.
|
|
5551
|
+
*/
|
|
5552
|
+
function savedPlan(
|
|
5553
|
+
plan: unknown,
|
|
5554
|
+
agents: ReadonlyArray<DefinedAgent>,
|
|
5555
|
+
run: {
|
|
5556
|
+
threadId: string
|
|
5557
|
+
interruptedRunId: string
|
|
5558
|
+
interruptIds: ReadonlyArray<string>
|
|
5559
|
+
},
|
|
5560
|
+
): { steps: ReadonlyArray<SubagentStep> } | undefined {
|
|
5561
|
+
if (typeof plan !== 'object' || plan === null || !('steps' in plan)) {
|
|
5562
|
+
return undefined
|
|
5563
|
+
}
|
|
5564
|
+
try {
|
|
5565
|
+
// The plan comes back from the client. Check it like a router pick.
|
|
5566
|
+
return normalizeRouterPick(plan as SubagentStepsPlan, agents)
|
|
5567
|
+
} catch (error) {
|
|
5568
|
+
throw new InterruptResumeValidationError([
|
|
5569
|
+
{
|
|
5570
|
+
scope: 'batch',
|
|
5571
|
+
threadId: run.threadId,
|
|
5572
|
+
interruptedRunId: run.interruptedRunId,
|
|
5573
|
+
generation: 0,
|
|
5574
|
+
interruptIds: run.interruptIds,
|
|
5575
|
+
code: 'stale',
|
|
5576
|
+
message: `The saved subagent plan does not match the agents of this chat. ${
|
|
5577
|
+
error instanceof Error ? error.message : String(error)
|
|
5578
|
+
}`,
|
|
5579
|
+
source: 'server',
|
|
5580
|
+
retryable: false,
|
|
5581
|
+
},
|
|
5582
|
+
])
|
|
5583
|
+
}
|
|
5584
|
+
}
|
|
5585
|
+
|
|
5586
|
+
/** Put the router plan on a direct child's SUBAGENT_STARTED metadata. */
|
|
5587
|
+
function withPlan(
|
|
5588
|
+
chunk: StreamChunk,
|
|
5589
|
+
plan: { steps: ReadonlyArray<SubagentStep> },
|
|
5590
|
+
): StreamChunk {
|
|
5591
|
+
if (
|
|
5592
|
+
chunk.type !== EventType.SUBAGENT_STARTED ||
|
|
5593
|
+
chunk.parentSubagentRunId !== undefined
|
|
5594
|
+
) {
|
|
5595
|
+
return chunk
|
|
5596
|
+
}
|
|
5597
|
+
return withTanstackMetadata(chunk, { [SUBAGENT_PLAN_KEY]: plan })
|
|
5598
|
+
}
|
|
5599
|
+
|
|
5600
|
+
function readRoutedSubagentPersistence(
|
|
5601
|
+
middleware:
|
|
5602
|
+
| ReadonlyArray<{
|
|
5603
|
+
routedSubagentPersistence?: ChatMiddleware['routedSubagentPersistence']
|
|
5604
|
+
}>
|
|
5605
|
+
| undefined,
|
|
5606
|
+
) {
|
|
5607
|
+
for (const item of middleware ?? []) {
|
|
5608
|
+
if (item.routedSubagentPersistence) return item.routedSubagentPersistence
|
|
5609
|
+
}
|
|
5610
|
+
return undefined
|
|
5611
|
+
}
|
|
5612
|
+
|
|
4909
5613
|
/**
|
|
4910
5614
|
* Run non-streaming text - collects all content and returns as a string.
|
|
4911
5615
|
* Runs the full agentic loop (if tools are provided) but returns collected text.
|