@tanstack/ai 0.59.0 → 0.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- 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 +17 -5
- package/dist/esm/activities/chat/agents/define-agent.js.map +1 -1
- package/dist/esm/activities/chat/agents/spawn.d.ts +2 -0
- package/dist/esm/activities/chat/agents/spawn.js +8 -5
- package/dist/esm/activities/chat/agents/spawn.js.map +1 -1
- package/dist/esm/activities/chat/index.js +289 -78
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +35 -19
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +1 -0
- package/dist/esm/activities/chat/middleware/types.js.map +1 -1
- package/dist/esm/activities/chat/stream/message-updaters.d.ts +2 -2
- package/dist/esm/activities/chat/stream/message-updaters.js +11 -3
- package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +11 -10
- package/dist/esm/activities/chat/stream/processor.js +51 -28
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +16 -2
- package/dist/esm/activities/chat/tools/tool-calls.js +57 -16
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-definition.d.ts +4 -0
- package/dist/esm/activities/chat/tools/tool-definition.js +4 -0
- package/dist/esm/activities/chat/tools/tool-definition.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/evaluate/adapter.d.ts +4 -0
- package/dist/esm/activities/evaluate/adapter.js.map +1 -1
- package/dist/esm/activities/evaluate/index.d.ts +4 -0
- package/dist/esm/activities/evaluate/index.js +3 -1
- package/dist/esm/activities/evaluate/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/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 +2 -0
- package/dist/esm/activities/generateImage/index.js.map +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/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 +5 -4
- package/dist/esm/activities/generateWorld/index.js.map +1 -1
- package/dist/esm/activities/index.d.ts +6 -3
- package/dist/esm/activities/index.js +13 -11
- 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/client.d.ts +3 -1
- package/dist/esm/client.js +2 -1
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +4 -3
- package/dist/esm/index.js +5 -3
- package/dist/esm/interrupt-resume.js +29 -4
- package/dist/esm/interrupt-resume.js.map +1 -1
- package/dist/esm/middlewares/otel.d.ts +5 -2
- package/dist/esm/middlewares/otel.js +114 -0
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/types.d.ts +114 -14
- package/dist/esm/utilities/ag-ui-wire.js +39 -16
- 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/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/tool-result.d.ts +14 -3
- package/dist/esm/utilities/tool-result.js +26 -3
- package/dist/esm/utilities/tool-result.js.map +1 -1
- package/package.json +4 -4
- package/skills/ai-core/adapter-configuration/SKILL.md +62 -0
- package/skills/ai-core/chat-experience/SKILL.md +134 -0
- package/skills/ai-core/media-generation/SKILL.md +8 -0
- package/skills/ai-core/tool-calling/SKILL.md +103 -0
- package/src/activities/chat/adapter.ts +10 -0
- package/src/activities/chat/agents/define-agent.ts +20 -3
- package/src/activities/chat/agents/spawn.ts +20 -12
- package/src/activities/chat/index.ts +458 -99
- package/src/activities/chat/messages.ts +52 -6
- package/src/activities/chat/middleware/types.ts +1 -0
- package/src/activities/chat/stream/message-updaters.ts +27 -2
- package/src/activities/chat/stream/processor.ts +81 -49
- package/src/activities/chat/tools/tool-calls.ts +104 -9
- package/src/activities/chat/tools/tool-definition.ts +8 -0
- package/src/activities/embed/adapter.ts +7 -0
- package/src/activities/embed/index.ts +5 -0
- package/src/activities/evaluate/adapter.ts +4 -0
- package/src/activities/evaluate/index.ts +6 -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 +25 -1
- package/src/activities/summarize/chat-stream-summarize.ts +22 -12
- package/src/client.ts +8 -0
- package/src/index.ts +17 -0
- package/src/interrupt-resume.ts +55 -4
- package/src/middlewares/otel.ts +161 -3
- package/src/types.ts +114 -14
- package/src/utilities/ag-ui-wire.ts +72 -17
- package/src/utilities/content-source.ts +138 -0
- package/src/utilities/provider-executed.ts +13 -0
- package/src/utilities/tool-result.ts +45 -3
|
@@ -43,8 +43,13 @@ import { withDurabilityBatchHint } from '../../utilities/durability-batch'
|
|
|
43
43
|
import { normalizeStreamChunk } from '../../utilities/normalize-stream-chunk'
|
|
44
44
|
import { restorePublicUsage } from '../../utilities/restore-inbound-chunk'
|
|
45
45
|
import type { AdapterYieldChunk } from '../../utilities/adapter-yield-chunk'
|
|
46
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
normalizeToolResult,
|
|
48
|
+
parseToolOutput,
|
|
49
|
+
toolResultErrorText,
|
|
50
|
+
} from '../../utilities/tool-result'
|
|
47
51
|
import { isProviderExecutedToolCall } from '../../utilities/provider-executed'
|
|
52
|
+
import { assertMessagesFileSourceSupport } from '../../utilities/content-source'
|
|
48
53
|
import { LazyToolManager } from './tools/lazy-tool-manager'
|
|
49
54
|
import { assertUniqueToolNames } from './tools/unique-tool-names'
|
|
50
55
|
import type { DefinedAgent } from './agents/define-agent'
|
|
@@ -108,6 +113,7 @@ import type {
|
|
|
108
113
|
import type {
|
|
109
114
|
ApprovalRequest,
|
|
110
115
|
ClientToolRequest,
|
|
116
|
+
McpInputRequest,
|
|
111
117
|
ToolResult,
|
|
112
118
|
} from './tools/tool-calls'
|
|
113
119
|
import type { ApprovalSchemaConfig } from './tools/tool-definition'
|
|
@@ -143,6 +149,7 @@ import type {
|
|
|
143
149
|
ToolCallEndEvent,
|
|
144
150
|
ToolCallResultEvent,
|
|
145
151
|
ToolCallStartEvent,
|
|
152
|
+
ToolInputResponse,
|
|
146
153
|
UIMessage,
|
|
147
154
|
} from '../../types'
|
|
148
155
|
import type {
|
|
@@ -170,6 +177,12 @@ import type {
|
|
|
170
177
|
} from './runtime-context-types'
|
|
171
178
|
import type { ChatMCPOptions } from './mcp/types'
|
|
172
179
|
|
|
180
|
+
/** One entry of the per-iteration arrival order (see `turnParts`). */
|
|
181
|
+
type TurnPart =
|
|
182
|
+
| { type: 'thinking'; index: number }
|
|
183
|
+
| { type: 'text'; content: string }
|
|
184
|
+
| { type: 'call'; id: string; providerExecuted: boolean }
|
|
185
|
+
|
|
173
186
|
// ===========================
|
|
174
187
|
// Activity Kind
|
|
175
188
|
// ===========================
|
|
@@ -186,25 +199,6 @@ const interruptBindingMetadataKey = INTERRUPT_BINDING_METADATA_KEY
|
|
|
186
199
|
/** Resume entries a subagent tool call owns. The parent run skips them. */
|
|
187
200
|
const CHILD_RESUME_IDS = Symbol('tanstack.ai.childResumeIds')
|
|
188
201
|
|
|
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
|
-
|
|
208
202
|
interface StructuralInterruptFailure {
|
|
209
203
|
error: Error
|
|
210
204
|
errors: ReadonlyArray<InterruptSubmissionError>
|
|
@@ -861,6 +855,15 @@ class TextEngine<
|
|
|
861
855
|
private accumulatedContent = ''
|
|
862
856
|
private accumulatedThinking: Array<{ content: string; signature?: string }> =
|
|
863
857
|
[]
|
|
858
|
+
/**
|
|
859
|
+
* Arrival order of this iteration's thinking steps, text and tool calls.
|
|
860
|
+
* A ModelMessage keeps `thinking` apart from `content`/`toolCalls`, so a
|
|
861
|
+
* provider turn that thinks between provider-executed tools would otherwise
|
|
862
|
+
* be recorded as "all thinking, then text, then tools" and the provider
|
|
863
|
+
* rejects the replay (signed thinking must keep its position). `null` once
|
|
864
|
+
* the order could no longer be tracked (callers fall back to one message).
|
|
865
|
+
*/
|
|
866
|
+
private turnParts: Array<TurnPart> | null = []
|
|
864
867
|
private currentThinkingContent = ''
|
|
865
868
|
private currentThinkingSignature = ''
|
|
866
869
|
private eventOptions?: Record<string, unknown> | undefined
|
|
@@ -879,8 +882,10 @@ class TextEngine<
|
|
|
879
882
|
private readonly initialClientToolResults: Map<string, any>
|
|
880
883
|
private readonly resumeApprovals = new Map<string, ToolApprovalResolution>()
|
|
881
884
|
private readonly resumeClientToolResults = new Map<string, any>()
|
|
885
|
+
private readonly resumeClientToolErrors = new Map<string, string>()
|
|
882
886
|
private readonly resumeDeniedToolResults = new Map<string, unknown>()
|
|
883
887
|
private readonly resumeCancelledToolCallIds = new Set<string>()
|
|
888
|
+
private readonly resumeInputResponses = new Map<string, ToolInputResponse>()
|
|
884
889
|
private readonly resumeGenericInterrupts = new Map<
|
|
885
890
|
string,
|
|
886
891
|
ChatResumeGenericResolution
|
|
@@ -951,6 +956,8 @@ class TextEngine<
|
|
|
951
956
|
message: string
|
|
952
957
|
code?: string
|
|
953
958
|
cause?: unknown
|
|
959
|
+
/** Full model text, when a structured-output parse failed (#1485). */
|
|
960
|
+
rawText?: string
|
|
954
961
|
} | null = null
|
|
955
962
|
private combinedCompleteEmitted = false
|
|
956
963
|
private readonly finalStructuredOutput?: {
|
|
@@ -1188,10 +1195,54 @@ class TextEngine<
|
|
|
1188
1195
|
message: string
|
|
1189
1196
|
code?: string
|
|
1190
1197
|
cause?: unknown
|
|
1198
|
+
rawText?: string
|
|
1191
1199
|
} | null {
|
|
1192
1200
|
return this.finalizationError
|
|
1193
1201
|
}
|
|
1194
1202
|
|
|
1203
|
+
private async runTerminalHook(): Promise<void> {
|
|
1204
|
+
if (this.terminalHookCalled || this.isCancelled()) return
|
|
1205
|
+
|
|
1206
|
+
this.terminalHookCalled = true
|
|
1207
|
+
|
|
1208
|
+
if (this.finalizationError) {
|
|
1209
|
+
const errForHook = new Error(
|
|
1210
|
+
this.finalizationError.message,
|
|
1211
|
+
this.finalizationError.cause !== undefined
|
|
1212
|
+
? { cause: this.finalizationError.cause }
|
|
1213
|
+
: undefined,
|
|
1214
|
+
)
|
|
1215
|
+
if (this.finalizationError.code !== undefined) {
|
|
1216
|
+
Object.defineProperty(errForHook, 'code', {
|
|
1217
|
+
value: this.finalizationError.code,
|
|
1218
|
+
enumerable: true,
|
|
1219
|
+
})
|
|
1220
|
+
}
|
|
1221
|
+
if (this.finalizationError.rawText !== undefined) {
|
|
1222
|
+
Object.defineProperty(errForHook, 'rawText', {
|
|
1223
|
+
value: this.finalizationError.rawText,
|
|
1224
|
+
enumerable: true,
|
|
1225
|
+
})
|
|
1226
|
+
}
|
|
1227
|
+
await this.middlewareRunner.runOnError(this.middlewareCtx, {
|
|
1228
|
+
error: errForHook,
|
|
1229
|
+
duration: Date.now() - this.streamStartTime,
|
|
1230
|
+
})
|
|
1231
|
+
return
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
this.addTerminalAssistantMessages()
|
|
1235
|
+
await this.middlewareRunner.runOnFinish(this.middlewareCtx, {
|
|
1236
|
+
finishReason: this.lastFinishReason,
|
|
1237
|
+
duration: Date.now() - this.streamStartTime,
|
|
1238
|
+
content: this.accumulatedContent,
|
|
1239
|
+
usage: rebuildTokenUsage(
|
|
1240
|
+
this.finishedEvent?.usage,
|
|
1241
|
+
tanstackMetadata(this.finishedEvent ?? undefined)?.usage,
|
|
1242
|
+
),
|
|
1243
|
+
})
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1195
1246
|
async *run(): AsyncGenerator<StreamChunk> {
|
|
1196
1247
|
this.beforeRun()
|
|
1197
1248
|
this.logger.agentLoop('run started', {
|
|
@@ -1237,7 +1288,8 @@ class TextEngine<
|
|
|
1237
1288
|
}
|
|
1238
1289
|
|
|
1239
1290
|
const pendingPhase = yield* this.checkForPendingToolCalls()
|
|
1240
|
-
if (pendingPhase === 'wait') {
|
|
1291
|
+
if (pendingPhase === 'stop' || pendingPhase === 'wait') {
|
|
1292
|
+
await this.runTerminalHook()
|
|
1241
1293
|
return
|
|
1242
1294
|
}
|
|
1243
1295
|
|
|
@@ -1346,46 +1398,11 @@ class TextEngine<
|
|
|
1346
1398
|
}
|
|
1347
1399
|
}
|
|
1348
1400
|
|
|
1349
|
-
//
|
|
1350
|
-
//
|
|
1351
|
-
//
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
this.toolPhase !== 'wait' &&
|
|
1355
|
-
!this.isCancelled()
|
|
1356
|
-
) {
|
|
1357
|
-
if (this.finalizationError) {
|
|
1358
|
-
this.terminalHookCalled = true
|
|
1359
|
-
const errForHook = new Error(
|
|
1360
|
-
this.finalizationError.message,
|
|
1361
|
-
this.finalizationError.cause !== undefined
|
|
1362
|
-
? { cause: this.finalizationError.cause }
|
|
1363
|
-
: undefined,
|
|
1364
|
-
)
|
|
1365
|
-
if (this.finalizationError.code !== undefined) {
|
|
1366
|
-
Object.defineProperty(errForHook, 'code', {
|
|
1367
|
-
value: this.finalizationError.code,
|
|
1368
|
-
enumerable: true,
|
|
1369
|
-
})
|
|
1370
|
-
}
|
|
1371
|
-
await this.middlewareRunner.runOnError(this.middlewareCtx, {
|
|
1372
|
-
error: errForHook,
|
|
1373
|
-
duration: Date.now() - this.streamStartTime,
|
|
1374
|
-
})
|
|
1375
|
-
} else {
|
|
1376
|
-
this.addTerminalAssistantMessages()
|
|
1377
|
-
this.terminalHookCalled = true
|
|
1378
|
-
await this.middlewareRunner.runOnFinish(this.middlewareCtx, {
|
|
1379
|
-
finishReason: this.lastFinishReason,
|
|
1380
|
-
duration: Date.now() - this.streamStartTime,
|
|
1381
|
-
content: this.accumulatedContent,
|
|
1382
|
-
usage: rebuildTokenUsage(
|
|
1383
|
-
this.finishedEvent?.usage,
|
|
1384
|
-
tanstackMetadata(this.finishedEvent ?? undefined)?.usage,
|
|
1385
|
-
),
|
|
1386
|
-
})
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1401
|
+
// A client-tool or approval wait ends this server-side chat invocation.
|
|
1402
|
+
// Structured-output finalization stays paused until the caller submits
|
|
1403
|
+
// the result in a new invocation, but middleware must still observe one
|
|
1404
|
+
// terminal hook for the current invocation.
|
|
1405
|
+
await this.runTerminalHook()
|
|
1389
1406
|
} catch (error: unknown) {
|
|
1390
1407
|
if (
|
|
1391
1408
|
error instanceof Error &&
|
|
@@ -1504,6 +1521,7 @@ class TextEngine<
|
|
|
1504
1521
|
this.streamIdentityCaptured = false
|
|
1505
1522
|
this.accumulatedContent = ''
|
|
1506
1523
|
this.accumulatedThinking = []
|
|
1524
|
+
this.turnParts = []
|
|
1507
1525
|
this.currentThinkingContent = ''
|
|
1508
1526
|
this.currentThinkingSignature = ''
|
|
1509
1527
|
|
|
@@ -1571,7 +1589,12 @@ class TextEngine<
|
|
|
1571
1589
|
)
|
|
1572
1590
|
}
|
|
1573
1591
|
|
|
1574
|
-
|
|
1592
|
+
// Fail closed on `{ type: 'file' }` sources for adapters that haven't
|
|
1593
|
+
// declared support — an adapter written before the file arm existed would
|
|
1594
|
+
// otherwise fall through to its URL/data branch and silently mis-map the
|
|
1595
|
+
// reference. Checked per model call so tool results added mid-loop are
|
|
1596
|
+
// covered too.
|
|
1597
|
+
assertMessagesFileSourceSupport(this.adapter, this.messages)
|
|
1575
1598
|
|
|
1576
1599
|
for await (const raw of this.adapter.chatStream({
|
|
1577
1600
|
model: this.params.model,
|
|
@@ -1805,12 +1828,27 @@ class TextEngine<
|
|
|
1805
1828
|
// Adapters still emit leftover cumulative `content` on RAW yields.
|
|
1806
1829
|
// Alignment suppresses already-delivered deltas, so this snapshot is
|
|
1807
1830
|
// what a takeover saves as the full assistant text.
|
|
1831
|
+
const before = this.accumulatedContent
|
|
1808
1832
|
if (typeof extra.content === 'string' && extra.content !== '') {
|
|
1809
1833
|
this.accumulatedContent = extra.content
|
|
1810
1834
|
} else {
|
|
1811
1835
|
this.accumulatedContent += chunk.delta
|
|
1812
1836
|
}
|
|
1813
1837
|
this.middlewareCtx.accumulatedContent = this.accumulatedContent
|
|
1838
|
+
if (!this.turnParts) return
|
|
1839
|
+
if (!this.accumulatedContent.startsWith(before)) {
|
|
1840
|
+
// A cumulative `content` snapshot rewrote earlier text; order unknown.
|
|
1841
|
+
this.turnParts = null
|
|
1842
|
+
return
|
|
1843
|
+
}
|
|
1844
|
+
const delta = this.accumulatedContent.slice(before.length)
|
|
1845
|
+
if (delta === '') return
|
|
1846
|
+
const last = this.turnParts[this.turnParts.length - 1]
|
|
1847
|
+
if (last && last.type === 'text') {
|
|
1848
|
+
last.content += delta
|
|
1849
|
+
} else {
|
|
1850
|
+
this.turnParts.push({ type: 'text', content: delta })
|
|
1851
|
+
}
|
|
1814
1852
|
}
|
|
1815
1853
|
|
|
1816
1854
|
private captureStreamMessageIdentity(messageId: string): void {
|
|
@@ -1835,6 +1873,20 @@ class TextEngine<
|
|
|
1835
1873
|
this.captureStreamMessageIdentity(chunk.parentMessageId)
|
|
1836
1874
|
}
|
|
1837
1875
|
this.toolCallManager.addToolCallStartEvent(chunk)
|
|
1876
|
+
if (
|
|
1877
|
+
this.turnParts &&
|
|
1878
|
+
!this.turnParts.some(
|
|
1879
|
+
(part) => part.type === 'call' && part.id === chunk.toolCallId,
|
|
1880
|
+
)
|
|
1881
|
+
) {
|
|
1882
|
+
this.turnParts.push({
|
|
1883
|
+
type: 'call',
|
|
1884
|
+
id: chunk.toolCallId,
|
|
1885
|
+
providerExecuted: isProviderExecutedToolCall({
|
|
1886
|
+
metadata: chunk.metadata,
|
|
1887
|
+
}),
|
|
1888
|
+
})
|
|
1889
|
+
}
|
|
1838
1890
|
const metadata = chunk.metadata
|
|
1839
1891
|
const thoughtSignature =
|
|
1840
1892
|
metadata != null &&
|
|
@@ -1941,17 +1993,48 @@ class TextEngine<
|
|
|
1941
1993
|
signature: this.currentThinkingSignature,
|
|
1942
1994
|
}),
|
|
1943
1995
|
})
|
|
1996
|
+
if (this.turnParts) {
|
|
1997
|
+
const placeholder = [...this.turnParts]
|
|
1998
|
+
.reverse()
|
|
1999
|
+
.find(
|
|
2000
|
+
(part): part is Extract<TurnPart, { type: 'thinking' }> =>
|
|
2001
|
+
part.type === 'thinking' && part.index === -1,
|
|
2002
|
+
)
|
|
2003
|
+
const index = this.accumulatedThinking.length - 1
|
|
2004
|
+
if (placeholder) {
|
|
2005
|
+
placeholder.index = index
|
|
2006
|
+
} else {
|
|
2007
|
+
this.turnParts.push({ type: 'thinking', index })
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
1944
2010
|
this.currentThinkingContent = ''
|
|
1945
2011
|
this.currentThinkingSignature = ''
|
|
1946
2012
|
}
|
|
1947
2013
|
}
|
|
1948
2014
|
|
|
2015
|
+
/**
|
|
2016
|
+
* Record where the current thinking step sits among this turn's parts. A
|
|
2017
|
+
* step is finalized only when the next step starts (or the turn ends), by
|
|
2018
|
+
* which time later tool calls have already arrived, so the position has to
|
|
2019
|
+
* be noted when the step's first content or signature shows up.
|
|
2020
|
+
*/
|
|
2021
|
+
private noteThinkingStepPosition(): void {
|
|
2022
|
+
if (
|
|
2023
|
+
this.turnParts &&
|
|
2024
|
+
this.currentThinkingContent === '' &&
|
|
2025
|
+
this.currentThinkingSignature === ''
|
|
2026
|
+
) {
|
|
2027
|
+
this.turnParts.push({ type: 'thinking', index: -1 })
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
1949
2031
|
private handleStepStartedEvent(): void {
|
|
1950
2032
|
this.finalizeCurrentThinkingStep()
|
|
1951
2033
|
}
|
|
1952
2034
|
|
|
1953
2035
|
private handleStepFinishedEvent(chunk: AdapterYieldChunk): void {
|
|
1954
2036
|
if (typeof chunk.signature === 'string' && chunk.signature !== '') {
|
|
2037
|
+
this.noteThinkingStepPosition()
|
|
1955
2038
|
this.currentThinkingSignature = chunk.signature
|
|
1956
2039
|
}
|
|
1957
2040
|
}
|
|
@@ -1959,6 +2042,7 @@ class TextEngine<
|
|
|
1959
2042
|
private handleReasoningMessageContentEvent(
|
|
1960
2043
|
chunk: Extract<StreamChunk, { type: 'REASONING_MESSAGE_CONTENT' }>,
|
|
1961
2044
|
): void {
|
|
2045
|
+
this.noteThinkingStepPosition()
|
|
1962
2046
|
this.currentThinkingContent += chunk.delta
|
|
1963
2047
|
}
|
|
1964
2048
|
|
|
@@ -1979,6 +2063,7 @@ class TextEngine<
|
|
|
1979
2063
|
}
|
|
1980
2064
|
return
|
|
1981
2065
|
}
|
|
2066
|
+
this.noteThinkingStepPosition()
|
|
1982
2067
|
this.currentThinkingSignature = chunk.encryptedValue
|
|
1983
2068
|
}
|
|
1984
2069
|
|
|
@@ -2112,8 +2197,10 @@ class TextEngine<
|
|
|
2112
2197
|
this.middlewareCtx.context,
|
|
2113
2198
|
this.toolAbortSignal,
|
|
2114
2199
|
{
|
|
2200
|
+
clientToolErrors: this.resumeClientToolErrors,
|
|
2115
2201
|
deniedToolResults: this.resumeDeniedToolResults,
|
|
2116
2202
|
cancelledToolCallIds: this.resumeCancelledToolCallIds,
|
|
2203
|
+
inputResponses: this.resumeInputResponses,
|
|
2117
2204
|
},
|
|
2118
2205
|
)
|
|
2119
2206
|
|
|
@@ -2138,11 +2225,12 @@ class TextEngine<
|
|
|
2138
2225
|
}),
|
|
2139
2226
|
)
|
|
2140
2227
|
|
|
2141
|
-
|
|
2228
|
+
const pauseForDecision =
|
|
2142
2229
|
executionResult.needsApproval.length > 0 ||
|
|
2143
2230
|
executionResult.needsClientExecution.length > 0 ||
|
|
2231
|
+
executionResult.inputRequired.length > 0 ||
|
|
2144
2232
|
executionResult.subagentInterrupts.length > 0
|
|
2145
|
-
) {
|
|
2233
|
+
if (pauseForDecision) {
|
|
2146
2234
|
this.discardDeferredToolCallRunFinishedChunks()
|
|
2147
2235
|
|
|
2148
2236
|
if (allResults.length > 0) {
|
|
@@ -2159,6 +2247,7 @@ class TextEngine<
|
|
|
2159
2247
|
executionResult.needsApproval,
|
|
2160
2248
|
executionResult.needsClientExecution,
|
|
2161
2249
|
[],
|
|
2250
|
+
executionResult.inputRequired,
|
|
2162
2251
|
executionResult.subagentInterrupts,
|
|
2163
2252
|
)
|
|
2164
2253
|
this.setToolPhase(emitted ? 'wait' : 'stop')
|
|
@@ -2296,8 +2385,10 @@ class TextEngine<
|
|
|
2296
2385
|
this.middlewareCtx.context,
|
|
2297
2386
|
this.toolAbortSignal,
|
|
2298
2387
|
{
|
|
2388
|
+
clientToolErrors: this.resumeClientToolErrors,
|
|
2299
2389
|
deniedToolResults: this.resumeDeniedToolResults,
|
|
2300
2390
|
cancelledToolCallIds: this.resumeCancelledToolCallIds,
|
|
2391
|
+
inputResponses: this.resumeInputResponses,
|
|
2301
2392
|
},
|
|
2302
2393
|
)
|
|
2303
2394
|
|
|
@@ -2345,16 +2436,18 @@ class TextEngine<
|
|
|
2345
2436
|
finishEvent,
|
|
2346
2437
|
toolCalls,
|
|
2347
2438
|
afterToolRequests,
|
|
2439
|
+
executionResult.inputRequired,
|
|
2348
2440
|
)
|
|
2349
2441
|
this.setToolPhase('wait')
|
|
2350
2442
|
return
|
|
2351
2443
|
}
|
|
2352
2444
|
|
|
2353
|
-
|
|
2445
|
+
const pauseForDecision =
|
|
2354
2446
|
executionResult.needsApproval.length > 0 ||
|
|
2355
2447
|
executionResult.needsClientExecution.length > 0 ||
|
|
2448
|
+
executionResult.inputRequired.length > 0 ||
|
|
2356
2449
|
executionResult.subagentInterrupts.length > 0
|
|
2357
|
-
) {
|
|
2450
|
+
if (pauseForDecision) {
|
|
2358
2451
|
if (allResults.length > 0) {
|
|
2359
2452
|
for (const chunk of afterToolBoundaryChunks) {
|
|
2360
2453
|
yield* this.pipeThroughMiddleware(chunk)
|
|
@@ -2366,6 +2459,7 @@ class TextEngine<
|
|
|
2366
2459
|
executionResult.needsApproval,
|
|
2367
2460
|
executionResult.needsClientExecution,
|
|
2368
2461
|
[],
|
|
2462
|
+
executionResult.inputRequired,
|
|
2369
2463
|
executionResult.subagentInterrupts,
|
|
2370
2464
|
)
|
|
2371
2465
|
this.setToolPhase(emitted ? 'wait' : 'stop')
|
|
@@ -2464,21 +2558,110 @@ class TextEngine<
|
|
|
2464
2558
|
)
|
|
2465
2559
|
}
|
|
2466
2560
|
|
|
2561
|
+
/**
|
|
2562
|
+
* Split this iteration into assistant ModelMessages that keep the provider's
|
|
2563
|
+
* block order: a new segment starts at every thinking step that follows a
|
|
2564
|
+
* provider-executed tool call (the rule buildAssistantMessages applies to
|
|
2565
|
+
* UIMessages). Segments after the first get `${id}-segment-${n}` ids.
|
|
2566
|
+
* Returns null when no split is needed or the order could not be tracked,
|
|
2567
|
+
* so callers fall back to the single-message shape.
|
|
2568
|
+
*/
|
|
2569
|
+
private buildOrderedAssistantSegments(
|
|
2570
|
+
toolCalls: ReadonlyArray<ToolCall>,
|
|
2571
|
+
id: string | undefined,
|
|
2572
|
+
createdAt: Date | undefined,
|
|
2573
|
+
): Array<ModelMessage> | null {
|
|
2574
|
+
const parts = this.turnParts
|
|
2575
|
+
if (!parts) return null
|
|
2576
|
+
const providerCallIds = new Set(
|
|
2577
|
+
parts.flatMap((part) =>
|
|
2578
|
+
part.type === 'call' && part.providerExecuted ? [part.id] : [],
|
|
2579
|
+
),
|
|
2580
|
+
)
|
|
2581
|
+
type Segment = {
|
|
2582
|
+
thinking: Array<{ content: string; signature?: string }>
|
|
2583
|
+
text: string
|
|
2584
|
+
callIds: Array<string>
|
|
2585
|
+
}
|
|
2586
|
+
let current: Segment = { thinking: [], text: '', callIds: [] }
|
|
2587
|
+
const segments: Array<Segment> = [current]
|
|
2588
|
+
let split = false
|
|
2589
|
+
for (const part of parts) {
|
|
2590
|
+
if (part.type === 'thinking') {
|
|
2591
|
+
const thinking = this.accumulatedThinking[part.index]
|
|
2592
|
+
if (!thinking) return null
|
|
2593
|
+
if (current.callIds.some((callId) => providerCallIds.has(callId))) {
|
|
2594
|
+
current = { thinking: [thinking], text: '', callIds: [] }
|
|
2595
|
+
segments.push(current)
|
|
2596
|
+
split = true
|
|
2597
|
+
} else {
|
|
2598
|
+
current.thinking.push(thinking)
|
|
2599
|
+
}
|
|
2600
|
+
} else if (part.type === 'text') {
|
|
2601
|
+
current.text += part.content
|
|
2602
|
+
} else {
|
|
2603
|
+
current.callIds.push(part.id)
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
if (!split) return null
|
|
2607
|
+
if (
|
|
2608
|
+
segments.map((segment) => segment.text).join('') !==
|
|
2609
|
+
this.accumulatedContent
|
|
2610
|
+
) {
|
|
2611
|
+
return null
|
|
2612
|
+
}
|
|
2613
|
+
if (
|
|
2614
|
+
segments.reduce((n, segment) => n + segment.thinking.length, 0) !==
|
|
2615
|
+
this.accumulatedThinking.length
|
|
2616
|
+
) {
|
|
2617
|
+
return null
|
|
2618
|
+
}
|
|
2619
|
+
const placed = new Set(segments.flatMap((segment) => segment.callIds))
|
|
2620
|
+
for (const toolCall of toolCalls) {
|
|
2621
|
+
if (!placed.has(toolCall.id)) current.callIds.push(toolCall.id)
|
|
2622
|
+
}
|
|
2623
|
+
return segments.map((segment, index) => {
|
|
2624
|
+
const segmentCalls = toolCalls.filter((toolCall) =>
|
|
2625
|
+
segment.callIds.includes(toolCall.id),
|
|
2626
|
+
)
|
|
2627
|
+
return {
|
|
2628
|
+
role: 'assistant',
|
|
2629
|
+
content: segment.text || null,
|
|
2630
|
+
...(segmentCalls.length > 0 && { toolCalls: segmentCalls }),
|
|
2631
|
+
id:
|
|
2632
|
+
id === undefined
|
|
2633
|
+
? undefined
|
|
2634
|
+
: index === 0
|
|
2635
|
+
? id
|
|
2636
|
+
: `${id}-segment-${index}`,
|
|
2637
|
+
createdAt,
|
|
2638
|
+
...(segment.thinking.length > 0 && { thinking: segment.thinking }),
|
|
2639
|
+
}
|
|
2640
|
+
})
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2467
2643
|
private addAssistantToolCallMessage(toolCalls: Array<ToolCall>): void {
|
|
2468
2644
|
this.finalizeCurrentThinkingStep()
|
|
2469
2645
|
|
|
2646
|
+
const segments = this.buildOrderedAssistantSegments(
|
|
2647
|
+
toolCalls,
|
|
2648
|
+
this.currentMessageId ?? undefined,
|
|
2649
|
+
this.currentMessageCreatedAt ?? undefined,
|
|
2650
|
+
)
|
|
2470
2651
|
this.messages = [
|
|
2471
2652
|
...this.messages,
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2653
|
+
...(segments ?? [
|
|
2654
|
+
{
|
|
2655
|
+
role: 'assistant' as const,
|
|
2656
|
+
content: this.accumulatedContent || null,
|
|
2657
|
+
toolCalls,
|
|
2658
|
+
id: this.currentMessageId ?? undefined,
|
|
2659
|
+
createdAt: this.currentMessageCreatedAt ?? undefined,
|
|
2660
|
+
...(this.accumulatedThinking.length > 0 && {
|
|
2661
|
+
thinking: this.accumulatedThinking,
|
|
2662
|
+
}),
|
|
2663
|
+
},
|
|
2664
|
+
]),
|
|
2482
2665
|
]
|
|
2483
2666
|
this.middlewareCtx.messages = this.messages
|
|
2484
2667
|
}
|
|
@@ -2557,13 +2740,23 @@ class TextEngine<
|
|
|
2557
2740
|
!currentTurnAlreadyRecorded &&
|
|
2558
2741
|
(this.accumulatedContent !== '' || thinking)
|
|
2559
2742
|
) {
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2743
|
+
const id = this.currentMessageId ?? this.createId('msg')
|
|
2744
|
+
const createdAt = this.currentMessageCreatedAt ?? new Date()
|
|
2745
|
+
messages.push(
|
|
2746
|
+
...(this.buildOrderedAssistantSegments(
|
|
2747
|
+
this.toolCallManager.getToolCalls(),
|
|
2748
|
+
id,
|
|
2749
|
+
createdAt,
|
|
2750
|
+
) ?? [
|
|
2751
|
+
{
|
|
2752
|
+
role: 'assistant',
|
|
2753
|
+
content: this.accumulatedContent || null,
|
|
2754
|
+
id,
|
|
2755
|
+
createdAt,
|
|
2756
|
+
...(thinking ? { thinking } : {}),
|
|
2757
|
+
},
|
|
2758
|
+
]),
|
|
2759
|
+
)
|
|
2567
2760
|
}
|
|
2568
2761
|
if (structuredOutput) {
|
|
2569
2762
|
messages.push({
|
|
@@ -2685,6 +2878,7 @@ class TextEngine<
|
|
|
2685
2878
|
GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
|
|
2686
2879
|
> = [],
|
|
2687
2880
|
genericInterruptIds: ReadonlyArray<string> = [],
|
|
2881
|
+
inputRequired: ReadonlyArray<McpInputRequest> = [],
|
|
2688
2882
|
childInterrupts: ReadonlyArray<Interrupt> = [],
|
|
2689
2883
|
): Array<Interrupt> {
|
|
2690
2884
|
const interrupts: Array<Interrupt> = []
|
|
@@ -2803,6 +2997,30 @@ class TextEngine<
|
|
|
2803
2997
|
})
|
|
2804
2998
|
}
|
|
2805
2999
|
|
|
3000
|
+
for (const pendingInput of inputRequired) {
|
|
3001
|
+
const id = `mcp_input_${pendingInput.toolCallId}`
|
|
3002
|
+
interrupts.push({
|
|
3003
|
+
id,
|
|
3004
|
+
reason: 'mcp_input',
|
|
3005
|
+
message: `Input required to run ${pendingInput.toolName}`,
|
|
3006
|
+
toolCallId: pendingInput.toolCallId,
|
|
3007
|
+
metadata: {
|
|
3008
|
+
toolName: pendingInput.toolName,
|
|
3009
|
+
// A generic binding with no definition id. `useChat` shows it as a
|
|
3010
|
+
// generic interrupt with `resolveInterrupt` and `cancel`. The answer
|
|
3011
|
+
// is not validated here. The MCP server checks it.
|
|
3012
|
+
[interruptBindingMetadataKey]: {
|
|
3013
|
+
v: INTERRUPT_BINDING_VERSION,
|
|
3014
|
+
kind: 'generic',
|
|
3015
|
+
interruptId: id,
|
|
3016
|
+
},
|
|
3017
|
+
[INTERRUPT_PAYLOAD_METADATA_KEY]: {
|
|
3018
|
+
kind: pendingInput.kind,
|
|
3019
|
+
request: pendingInput.request,
|
|
3020
|
+
},
|
|
3021
|
+
},
|
|
3022
|
+
})
|
|
3023
|
+
}
|
|
2806
3024
|
// A subagent tool call raised these. They keep their `subagentRunId`.
|
|
2807
3025
|
interrupts.push(...childInterrupts)
|
|
2808
3026
|
|
|
@@ -2827,6 +3045,7 @@ class TextEngine<
|
|
|
2827
3045
|
GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
|
|
2828
3046
|
> = [],
|
|
2829
3047
|
genericInterruptIds?: ReadonlyArray<string>,
|
|
3048
|
+
inputRequired: ReadonlyArray<McpInputRequest> = [],
|
|
2830
3049
|
childInterrupts?: ReadonlyArray<Interrupt>,
|
|
2831
3050
|
): StreamChunk {
|
|
2832
3051
|
return {
|
|
@@ -2839,6 +3058,7 @@ class TextEngine<
|
|
|
2839
3058
|
clientRequests,
|
|
2840
3059
|
genericRequests,
|
|
2841
3060
|
genericInterruptIds,
|
|
3061
|
+
inputRequired,
|
|
2842
3062
|
childInterrupts,
|
|
2843
3063
|
),
|
|
2844
3064
|
},
|
|
@@ -2989,6 +3209,7 @@ class TextEngine<
|
|
|
2989
3209
|
genericRequests: ReadonlyArray<
|
|
2990
3210
|
GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
|
|
2991
3211
|
> = [],
|
|
3212
|
+
inputRequired: ReadonlyArray<McpInputRequest> = [],
|
|
2992
3213
|
childInterrupts: ReadonlyArray<Interrupt> = [],
|
|
2993
3214
|
): AsyncGenerator<StreamChunk, boolean, void> {
|
|
2994
3215
|
yield* this.emitSyntheticRunStarted(finishEvent)
|
|
@@ -3003,6 +3224,7 @@ class TextEngine<
|
|
|
3003
3224
|
clientRequests,
|
|
3004
3225
|
genericRequests,
|
|
3005
3226
|
genericInterruptIds,
|
|
3227
|
+
inputRequired,
|
|
3006
3228
|
childInterrupts,
|
|
3007
3229
|
),
|
|
3008
3230
|
)
|
|
@@ -3039,6 +3261,7 @@ class TextEngine<
|
|
|
3039
3261
|
requests?: ReadonlyArray<
|
|
3040
3262
|
GenericInterruptRequest<InterruptDefinition<any, any, any, any>>
|
|
3041
3263
|
>,
|
|
3264
|
+
inputRequired: ReadonlyArray<McpInputRequest> = [],
|
|
3042
3265
|
): AsyncGenerator<StreamChunk, boolean, void> {
|
|
3043
3266
|
this.middlewareCtx.phase = phase
|
|
3044
3267
|
const boundaryRequests =
|
|
@@ -3074,6 +3297,7 @@ class TextEngine<
|
|
|
3074
3297
|
actionable.approvals,
|
|
3075
3298
|
actionable.clientRequests,
|
|
3076
3299
|
boundaryRequests,
|
|
3300
|
+
inputRequired,
|
|
3077
3301
|
)
|
|
3078
3302
|
return true
|
|
3079
3303
|
}
|
|
@@ -3097,6 +3321,8 @@ class TextEngine<
|
|
|
3097
3321
|
const approvalRequests: Array<ApprovalRequest> = []
|
|
3098
3322
|
const clientRequests: Array<ClientToolRequest> = []
|
|
3099
3323
|
for (const toolCall of toolCalls) {
|
|
3324
|
+
// Provider-executed calls are complete; never surface them as client work.
|
|
3325
|
+
if (isProviderExecutedToolCall(toolCall)) continue
|
|
3100
3326
|
const tool = this.resolveExecutableTools([toolCall]).find(
|
|
3101
3327
|
(candidate) => candidate.name === toolCall.function.name,
|
|
3102
3328
|
) as RuntimeToolWithApproval | undefined
|
|
@@ -3119,6 +3345,7 @@ class TextEngine<
|
|
|
3119
3345
|
} else if (
|
|
3120
3346
|
!tool.execute &&
|
|
3121
3347
|
!clientToolResults.has(toolCall.id) &&
|
|
3348
|
+
!this.resumeClientToolErrors.has(toolCall.id) &&
|
|
3122
3349
|
!this.resumeCancelledToolCallIds.has(toolCall.id)
|
|
3123
3350
|
) {
|
|
3124
3351
|
clientRequests.push({
|
|
@@ -3226,9 +3453,15 @@ class TextEngine<
|
|
|
3226
3453
|
content: wireContent,
|
|
3227
3454
|
role: 'tool' as const,
|
|
3228
3455
|
}
|
|
3456
|
+
// An outcome always comes with output-error.
|
|
3229
3457
|
chunks.push(
|
|
3230
3458
|
(result.state === 'output-error'
|
|
3231
|
-
? withTanstackMetadata(resultChunk, {
|
|
3459
|
+
? withTanstackMetadata(resultChunk, {
|
|
3460
|
+
state: result.state,
|
|
3461
|
+
...(result.outcome !== undefined && {
|
|
3462
|
+
toolResultOutcome: result.outcome,
|
|
3463
|
+
}),
|
|
3464
|
+
})
|
|
3232
3465
|
: resultChunk) as StreamChunk,
|
|
3233
3466
|
)
|
|
3234
3467
|
|
|
@@ -3249,19 +3482,63 @@ class TextEngine<
|
|
|
3249
3482
|
return false
|
|
3250
3483
|
}
|
|
3251
3484
|
})
|
|
3252
|
-
|
|
3253
|
-
const
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3485
|
+
const isDeniedResult = result.outcome === 'denied'
|
|
3486
|
+
const existingToolResultIdx = isDeniedResult
|
|
3487
|
+
? this.messages.findIndex(
|
|
3488
|
+
(message) =>
|
|
3489
|
+
message.role === 'tool' &&
|
|
3490
|
+
message.toolCallId === result.toolCallId,
|
|
3491
|
+
)
|
|
3492
|
+
: -1
|
|
3493
|
+
const resultMessageIdx =
|
|
3494
|
+
existingToolResultIdx >= 0 ? existingToolResultIdx : placeholderIdx
|
|
3495
|
+
|
|
3496
|
+
// Only a denied result keeps the old message's fields. A replaced
|
|
3497
|
+
// pendingExecution placeholder must not leak into the real result.
|
|
3498
|
+
const existingToolMessage =
|
|
3499
|
+
existingToolResultIdx >= 0
|
|
3500
|
+
? this.messages[existingToolResultIdx]
|
|
3501
|
+
: undefined
|
|
3502
|
+
const errorField = result.state === 'output-error' && {
|
|
3503
|
+
error: toolResultErrorText(parseToolOutput(wireContent)),
|
|
3257
3504
|
}
|
|
3505
|
+
const replacementMessage: ModelMessage =
|
|
3506
|
+
existingToolMessage?.role === 'tool'
|
|
3507
|
+
? {
|
|
3508
|
+
...existingToolMessage,
|
|
3509
|
+
content,
|
|
3510
|
+
toolCallId: result.toolCallId,
|
|
3511
|
+
...errorField,
|
|
3512
|
+
}
|
|
3513
|
+
: {
|
|
3514
|
+
role: 'tool',
|
|
3515
|
+
content,
|
|
3516
|
+
toolCallId: result.toolCallId,
|
|
3517
|
+
...errorField,
|
|
3518
|
+
}
|
|
3519
|
+
const newToolMessage: ModelMessage =
|
|
3520
|
+
result.outcome !== undefined
|
|
3521
|
+
? withTanstackMetadata(replacementMessage, {
|
|
3522
|
+
toolResultOutcome: result.outcome,
|
|
3523
|
+
})
|
|
3524
|
+
: replacementMessage
|
|
3258
3525
|
|
|
3259
|
-
if (
|
|
3260
|
-
|
|
3261
|
-
...this.messages.slice(0,
|
|
3526
|
+
if (resultMessageIdx >= 0) {
|
|
3527
|
+
const replacedMessages = [
|
|
3528
|
+
...this.messages.slice(0, resultMessageIdx),
|
|
3262
3529
|
newToolMessage,
|
|
3263
|
-
...this.messages.slice(
|
|
3530
|
+
...this.messages.slice(resultMessageIdx + 1),
|
|
3264
3531
|
]
|
|
3532
|
+
this.messages = isDeniedResult
|
|
3533
|
+
? replacedMessages.filter(
|
|
3534
|
+
(message, index) =>
|
|
3535
|
+
index === resultMessageIdx ||
|
|
3536
|
+
!(
|
|
3537
|
+
message.role === 'tool' &&
|
|
3538
|
+
message.toolCallId === result.toolCallId
|
|
3539
|
+
),
|
|
3540
|
+
)
|
|
3541
|
+
: replacedMessages
|
|
3265
3542
|
} else {
|
|
3266
3543
|
this.messages = [...this.messages, newToolMessage]
|
|
3267
3544
|
}
|
|
@@ -3292,7 +3569,10 @@ class TextEngine<
|
|
|
3292
3569
|
}
|
|
3293
3570
|
|
|
3294
3571
|
// Only mark as complete if NOT pending execution
|
|
3295
|
-
if (
|
|
3572
|
+
if (
|
|
3573
|
+
!hasPendingExecution &&
|
|
3574
|
+
!this.resumeDeniedToolResults.has(message.toolCallId)
|
|
3575
|
+
) {
|
|
3296
3576
|
completedToolIds.add(message.toolCallId)
|
|
3297
3577
|
}
|
|
3298
3578
|
}
|
|
@@ -3589,7 +3869,11 @@ class TextEngine<
|
|
|
3589
3869
|
// Apply merged config back to engine state
|
|
3590
3870
|
this.applyMiddlewareConfig(postOnConfig)
|
|
3591
3871
|
|
|
3592
|
-
|
|
3872
|
+
// Schema-only structured output with no tools skips the agent loop, so
|
|
3873
|
+
// `streamModelResponse` never runs this check. Middleware can also
|
|
3874
|
+
// replace `this.messages` above. Fail closed here before the
|
|
3875
|
+
// structured-output adapter call.
|
|
3876
|
+
assertMessagesFileSourceSupport(this.adapter, this.messages)
|
|
3593
3877
|
|
|
3594
3878
|
// Build the StructuredOutputOptions the adapter expects.
|
|
3595
3879
|
// `this.adapter` is already `TAdapter extends AnyTextAdapter` per the
|
|
@@ -3676,6 +3960,9 @@ class TextEngine<
|
|
|
3676
3960
|
// Track whether a RUN_ERROR has been yielded to streaming consumers so
|
|
3677
3961
|
// we don't emit a duplicate synthetic one at the end.
|
|
3678
3962
|
let runErrorYielded = false
|
|
3963
|
+
// The full model text. Adapter errors carry only a 200-character preview,
|
|
3964
|
+
// so a Promise caller could not recover the answer (#1485).
|
|
3965
|
+
let rawText = ''
|
|
3679
3966
|
|
|
3680
3967
|
// Pipe chunks through middleware; yield to consumer only when yieldChunks=true
|
|
3681
3968
|
for await (const raw of providerStream) {
|
|
@@ -3787,14 +4074,20 @@ class TextEngine<
|
|
|
3787
4074
|
await this.runOnUsageFromChunk(chunk)
|
|
3788
4075
|
}
|
|
3789
4076
|
|
|
4077
|
+
if (chunk.type === EventType.TEXT_MESSAGE_CONTENT) {
|
|
4078
|
+
rawText += chunk.delta
|
|
4079
|
+
}
|
|
4080
|
+
|
|
3790
4081
|
if (chunk.type === EventType.RUN_ERROR) {
|
|
3791
4082
|
// RunErrorEvent already exposes `message` and `code` after narrowing.
|
|
4083
|
+
// A native stream has no adapter error to capture, but its RUN_ERROR
|
|
4084
|
+
// can carry the provider's error body as `rawEvent` (#1005).
|
|
4085
|
+
const cause = fallbackAdapterError ?? chunk.rawEvent
|
|
3792
4086
|
this.finalizationError = {
|
|
3793
4087
|
message: chunk.message,
|
|
3794
4088
|
...(chunk.code ? { code: chunk.code } : {}),
|
|
3795
|
-
...(
|
|
3796
|
-
|
|
3797
|
-
: {}),
|
|
4089
|
+
...(cause !== undefined ? { cause } : {}),
|
|
4090
|
+
...(rawText ? { rawText } : {}),
|
|
3798
4091
|
}
|
|
3799
4092
|
}
|
|
3800
4093
|
|
|
@@ -3932,7 +4225,16 @@ class TextEngine<
|
|
|
3932
4225
|
const yieldChunks = this.finalStructuredOutput.yieldChunks
|
|
3933
4226
|
const source = this.finalStructuredOutput.source ?? 'text'
|
|
3934
4227
|
|
|
3935
|
-
|
|
4228
|
+
// A final turn cut off at the output cap holds truncated JSON, or none
|
|
4229
|
+
// for a reasoning model that spent the budget. Report the token limit
|
|
4230
|
+
// instead of a parse or missing-result error (#1426).
|
|
4231
|
+
if (this.lastFinishReason === 'length') {
|
|
4232
|
+
this.finalizationError = {
|
|
4233
|
+
message:
|
|
4234
|
+
'The response was cut off because the maximum token limit was reached (finish_reason=length); raise the output token limit.',
|
|
4235
|
+
code: 'max_tokens',
|
|
4236
|
+
}
|
|
4237
|
+
} else if (source === 'event') {
|
|
3936
4238
|
if (!this.structuredOutputResult) {
|
|
3937
4239
|
this.finalizationError = {
|
|
3938
4240
|
message: 'missing structured result',
|
|
@@ -3969,6 +4271,7 @@ class TextEngine<
|
|
|
3969
4271
|
message: `Failed to parse structured output as JSON. Content: ${detail}`,
|
|
3970
4272
|
code: 'structured-output-parse-failed',
|
|
3971
4273
|
cause: err,
|
|
4274
|
+
rawText,
|
|
3972
4275
|
}
|
|
3973
4276
|
}
|
|
3974
4277
|
}
|
|
@@ -4079,6 +4382,7 @@ class TextEngine<
|
|
|
4079
4382
|
resumeToolState: {
|
|
4080
4383
|
approvals: this.resumeApprovals,
|
|
4081
4384
|
clientToolResults: this.resumeClientToolResults,
|
|
4385
|
+
clientToolErrors: this.resumeClientToolErrors,
|
|
4082
4386
|
deniedToolResults: this.resumeDeniedToolResults,
|
|
4083
4387
|
cancelledToolCallIds: this.resumeCancelledToolCallIds,
|
|
4084
4388
|
},
|
|
@@ -4221,6 +4525,26 @@ class TextEngine<
|
|
|
4221
4525
|
: []
|
|
4222
4526
|
})
|
|
4223
4527
|
pending.push(...genericPending)
|
|
4528
|
+
// An `mcp_input_*` answer is valid only for a server tool call that is
|
|
4529
|
+
// still pending in history.
|
|
4530
|
+
const mcpInputCallIds = new Map<string, string>()
|
|
4531
|
+
for (const toolCall of pendingToolCalls) {
|
|
4532
|
+
const interruptId = `mcp_input_${toolCall.id}`
|
|
4533
|
+
if (!resumeInterruptIds.has(interruptId)) continue
|
|
4534
|
+
if (!toolsByCallId.get(toolCall.id)?.execute) continue
|
|
4535
|
+
mcpInputCallIds.set(interruptId, toolCall.id)
|
|
4536
|
+
pending.push({
|
|
4537
|
+
interruptId,
|
|
4538
|
+
payload: { id: interruptId },
|
|
4539
|
+
binding: {
|
|
4540
|
+
v: INTERRUPT_BINDING_VERSION,
|
|
4541
|
+
kind: 'generic',
|
|
4542
|
+
interruptId,
|
|
4543
|
+
interruptedRunId,
|
|
4544
|
+
generation: 0,
|
|
4545
|
+
},
|
|
4546
|
+
})
|
|
4547
|
+
}
|
|
4224
4548
|
const validated = await validateInterruptResumeBatch({
|
|
4225
4549
|
threadId: this.threadId,
|
|
4226
4550
|
interruptedRunId,
|
|
@@ -4250,6 +4574,16 @@ class TextEngine<
|
|
|
4250
4574
|
})
|
|
4251
4575
|
|
|
4252
4576
|
const genericResolutions = validated.resumeToolState.genericInterrupts
|
|
4577
|
+
for (const [interruptId, toolCallId] of mcpInputCallIds) {
|
|
4578
|
+
const resolution = genericResolutions?.get(interruptId)
|
|
4579
|
+
if (!resolution) continue
|
|
4580
|
+
this.resumeInputResponses.set(
|
|
4581
|
+
toolCallId,
|
|
4582
|
+
resolution.status === 'resolved'
|
|
4583
|
+
? { status: 'resolved', payload: resolution.payload }
|
|
4584
|
+
: { status: 'cancelled' },
|
|
4585
|
+
)
|
|
4586
|
+
}
|
|
4253
4587
|
if (genericPending.length > 0 && genericResolutions) {
|
|
4254
4588
|
const resolutions = genericPending
|
|
4255
4589
|
.sort((left, right) => {
|
|
@@ -4299,6 +4633,7 @@ class TextEngine<
|
|
|
4299
4633
|
this.earlyTermination = true
|
|
4300
4634
|
} else if (policy.toolResume === 'cancel') {
|
|
4301
4635
|
for (const request of pendingToolCalls) {
|
|
4636
|
+
if (this.resumeDeniedToolResults.has(request.id)) continue
|
|
4302
4637
|
this.resumeCancelledToolCallIds.add(request.id)
|
|
4303
4638
|
}
|
|
4304
4639
|
}
|
|
@@ -4440,6 +4775,11 @@ class TextEngine<
|
|
|
4440
4775
|
this.resumeClientToolResults.set(toolCallId, result)
|
|
4441
4776
|
}
|
|
4442
4777
|
}
|
|
4778
|
+
if (state?.clientToolErrors) {
|
|
4779
|
+
for (const [toolCallId, errorText] of state.clientToolErrors) {
|
|
4780
|
+
this.resumeClientToolErrors.set(toolCallId, errorText)
|
|
4781
|
+
}
|
|
4782
|
+
}
|
|
4443
4783
|
if (state?.deniedToolResults) {
|
|
4444
4784
|
for (const [toolCallId, result] of state.deniedToolResults) {
|
|
4445
4785
|
this.resumeDeniedToolResults.set(toolCallId, result)
|
|
@@ -4499,6 +4839,7 @@ class TextEngine<
|
|
|
4499
4839
|
this.earlyTermination = true
|
|
4500
4840
|
} else if (policy.toolResume === 'cancel') {
|
|
4501
4841
|
for (const toolCall of this.getPendingToolCallsFromMessages()) {
|
|
4842
|
+
if (this.resumeDeniedToolResults.has(toolCall.id)) continue
|
|
4502
4843
|
this.resumeCancelledToolCallIds.add(toolCall.id)
|
|
4503
4844
|
}
|
|
4504
4845
|
}
|
|
@@ -4653,6 +4994,7 @@ class TextEngine<
|
|
|
4653
4994
|
results: Array<ToolResult>
|
|
4654
4995
|
needsApproval: Array<ApprovalRequest>
|
|
4655
4996
|
needsClientExecution: Array<ClientToolRequest>
|
|
4997
|
+
inputRequired: Array<McpInputRequest>
|
|
4656
4998
|
subagentInterrupts: Array<Interrupt>
|
|
4657
4999
|
},
|
|
4658
5000
|
void
|
|
@@ -4663,6 +5005,7 @@ class TextEngine<
|
|
|
4663
5005
|
results: Array<ToolResult>
|
|
4664
5006
|
needsApproval: Array<ApprovalRequest>
|
|
4665
5007
|
needsClientExecution: Array<ClientToolRequest>
|
|
5008
|
+
inputRequired: Array<McpInputRequest>
|
|
4666
5009
|
subagentInterrupts: Array<Interrupt>
|
|
4667
5010
|
},
|
|
4668
5011
|
void
|
|
@@ -5101,6 +5444,16 @@ async function* runRoutedSubagents(
|
|
|
5101
5444
|
yield* runChatEngine(options, engineRef)
|
|
5102
5445
|
return
|
|
5103
5446
|
}
|
|
5447
|
+
// ponytail: a router cannot write input yet. Add it to the router pick if
|
|
5448
|
+
// someone needs it.
|
|
5449
|
+
const withInput = bag.agents.find(
|
|
5450
|
+
(agent: DefinedAgent) => agent.inputSchema !== undefined,
|
|
5451
|
+
)
|
|
5452
|
+
if (withInput) {
|
|
5453
|
+
throw new Error(
|
|
5454
|
+
`Subagent "${withInput.name}" has an inputSchema. inputSchema needs tool mode: remove subagents.router so the model writes the input.`,
|
|
5455
|
+
)
|
|
5456
|
+
}
|
|
5104
5457
|
const threadId = options.threadId ?? `thread-${Date.now()}`
|
|
5105
5458
|
const runId = options.runId ?? `run-${Date.now()}`
|
|
5106
5459
|
const messages = options.messages ?? []
|
|
@@ -5556,6 +5909,12 @@ async function runAgenticStructuredOutput<TSchema extends SchemaInput>(
|
|
|
5556
5909
|
enumerable: true,
|
|
5557
5910
|
})
|
|
5558
5911
|
}
|
|
5912
|
+
if (finalizationError.rawText !== undefined) {
|
|
5913
|
+
Object.defineProperty(err, 'rawText', {
|
|
5914
|
+
value: finalizationError.rawText,
|
|
5915
|
+
enumerable: true,
|
|
5916
|
+
})
|
|
5917
|
+
}
|
|
5559
5918
|
throw err
|
|
5560
5919
|
}
|
|
5561
5920
|
|