@tanstack/ai 0.44.1 → 0.45.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- package/dist/esm/activities/chat/adapter.d.ts +13 -1
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/index.js +122 -69
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +24 -27
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.js +14 -13
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/approval-schema.js +11 -8
- package/dist/esm/activities/chat/tools/approval-schema.js.map +1 -1
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
- package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.js +40 -31
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.js +3 -2
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
- package/dist/esm/adapter-internals.d.ts +2 -0
- package/dist/esm/adapter-internals.js +3 -1
- package/dist/esm/interrupt-resume.js +24 -20
- package/dist/esm/interrupt-resume.js.map +1 -1
- package/dist/esm/interrupts.js +2 -1
- package/dist/esm/interrupts.js.map +1 -1
- package/dist/esm/logger/console-logger.js +1 -3
- package/dist/esm/logger/console-logger.js.map +1 -1
- package/dist/esm/logger/resolve.js +2 -1
- package/dist/esm/logger/resolve.js.map +1 -1
- package/dist/esm/stream-durability.js +1 -1
- package/dist/esm/stream-durability.js.map +1 -1
- package/dist/esm/types.d.ts +19 -16
- package/dist/esm/utilities/chat-params.js +1 -3
- package/dist/esm/utilities/chat-params.js.map +1 -1
- package/dist/esm/utilities/media-prompt.js +1 -3
- package/dist/esm/utilities/media-prompt.js.map +1 -1
- package/dist/esm/utilities/structured-output-events.d.ts +17 -0
- package/dist/esm/utilities/structured-output-events.js +32 -0
- package/dist/esm/utilities/structured-output-events.js.map +1 -0
- package/dist/esm/utilities/structured-output-text.d.ts +7 -0
- package/dist/esm/utilities/structured-output-text.js +50 -0
- package/dist/esm/utilities/structured-output-text.js.map +1 -0
- package/package.json +2 -2
- package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +6 -2
- package/skills/ai-core/media-generation/SKILL.md +33 -19
- package/skills/ai-core/structured-outputs/SKILL.md +73 -1
- package/skills/ai-core/tool-calling/SKILL.md +1 -1
- package/src/activities/chat/adapter.ts +16 -1
- package/src/activities/chat/index.ts +155 -52
- package/src/activities/chat/stream/processor.ts +6 -3
- package/src/activities/chat/tools/tool-calls.ts +12 -4
- package/src/adapter-internals.ts +8 -0
- package/src/types.ts +27 -19
- package/src/utilities/structured-output-events.ts +44 -0
- package/src/utilities/structured-output-text.ts +63 -0
|
@@ -655,11 +655,12 @@ interface TextEngineConfig<
|
|
|
655
655
|
* - nativeCombined: when true, the adapter declared
|
|
656
656
|
* `supportsCombinedToolsAndSchema()` and the engine wires `jsonSchema`
|
|
657
657
|
* into the regular `chatStream` call instead of running a separate
|
|
658
|
-
* finalization round-trip. The
|
|
659
|
-
*
|
|
660
|
-
*
|
|
661
|
-
*
|
|
662
|
-
* `
|
|
658
|
+
* finalization round-trip. The `'structuredOutput'` middleware phase
|
|
659
|
+
* does NOT fire on this path — middleware sees the run through
|
|
660
|
+
* `beforeModel` / `modelStream` as usual.
|
|
661
|
+
* - source: how to take the combined object. `'text'` (default) parses
|
|
662
|
+
* accumulated assistant text. `'event'` reads an adapter-emitted
|
|
663
|
+
* `structured-output.complete` and does not parse prose.
|
|
663
664
|
*/
|
|
664
665
|
finalStructuredOutput?: {
|
|
665
666
|
jsonSchema: JSONSchema
|
|
@@ -667,6 +668,7 @@ interface TextEngineConfig<
|
|
|
667
668
|
normalize?: (data: unknown) => unknown
|
|
668
669
|
validate?: (data: unknown) => unknown
|
|
669
670
|
nativeCombined?: boolean
|
|
671
|
+
source?: 'text' | 'event'
|
|
670
672
|
}
|
|
671
673
|
}
|
|
672
674
|
|
|
@@ -739,6 +741,7 @@ class TextEngine<
|
|
|
739
741
|
private eventOptions?: Record<string, unknown> | undefined
|
|
740
742
|
private eventToolNames?: Array<string>
|
|
741
743
|
private finishedEvent: RunFinishedEvent | null = null
|
|
744
|
+
private readonly streamedToolErrorResults = new Map<string, ToolResult>()
|
|
742
745
|
private deferredToolCallRunFinishedChunks: Array<StreamChunk> = []
|
|
743
746
|
private earlyTermination = false
|
|
744
747
|
private toolPhase: ToolPhaseResult = 'continue'
|
|
@@ -801,12 +804,14 @@ class TextEngine<
|
|
|
801
804
|
code?: string
|
|
802
805
|
cause?: unknown
|
|
803
806
|
} | null = null
|
|
807
|
+
private combinedCompleteEmitted = false
|
|
804
808
|
private readonly finalStructuredOutput?: {
|
|
805
809
|
jsonSchema: JSONSchema
|
|
806
810
|
yieldChunks: boolean
|
|
807
811
|
normalize?: (data: unknown) => unknown
|
|
808
812
|
validate?: (data: unknown) => unknown
|
|
809
813
|
nativeCombined?: boolean
|
|
814
|
+
source?: 'text' | 'event'
|
|
810
815
|
}
|
|
811
816
|
|
|
812
817
|
constructor(
|
|
@@ -1110,7 +1115,8 @@ class TextEngine<
|
|
|
1110
1115
|
this.finalStructuredOutput &&
|
|
1111
1116
|
this.toolPhase !== 'wait' &&
|
|
1112
1117
|
!this.isCancelled() &&
|
|
1113
|
-
!this.finalizationError
|
|
1118
|
+
!this.finalizationError &&
|
|
1119
|
+
!this.earlyTermination
|
|
1114
1120
|
) {
|
|
1115
1121
|
if (this.finalStructuredOutput.nativeCombined === true) {
|
|
1116
1122
|
yield* this.harvestCombinedStructuredOutput()
|
|
@@ -1277,6 +1283,7 @@ class TextEngine<
|
|
|
1277
1283
|
this.currentThinkingContent = ''
|
|
1278
1284
|
this.currentThinkingSignature = ''
|
|
1279
1285
|
this.finishedEvent = null
|
|
1286
|
+
this.streamedToolErrorResults.clear()
|
|
1280
1287
|
|
|
1281
1288
|
// Update mutable context fields
|
|
1282
1289
|
this.middlewareCtx.currentMessageId = this.currentMessageId
|
|
@@ -1374,9 +1381,46 @@ class TextEngine<
|
|
|
1374
1381
|
// text starting — intermediate tool-call iterations don't need it,
|
|
1375
1382
|
// and emitting at run-start would wrap tool-call commentary into a
|
|
1376
1383
|
// structured-output part too.
|
|
1384
|
+
if (
|
|
1385
|
+
chunk.type === EventType.CUSTOM &&
|
|
1386
|
+
chunk.name === 'structured-output.start'
|
|
1387
|
+
) {
|
|
1388
|
+
this.combinedStartEmitted = true
|
|
1389
|
+
const startValue = chunk.value
|
|
1390
|
+
if (
|
|
1391
|
+
startValue &&
|
|
1392
|
+
typeof startValue === 'object' &&
|
|
1393
|
+
'messageId' in startValue &&
|
|
1394
|
+
typeof startValue.messageId === 'string'
|
|
1395
|
+
) {
|
|
1396
|
+
this.combinedStructuredMessageId = startValue.messageId
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
let outboundChunk: StreamChunk = chunk
|
|
1401
|
+
if (
|
|
1402
|
+
this.finalStructuredOutput?.source === 'event' &&
|
|
1403
|
+
chunk.type === EventType.CUSTOM &&
|
|
1404
|
+
chunk.name === 'structured-output.complete'
|
|
1405
|
+
) {
|
|
1406
|
+
const parsed = readStructuredOutputCompleteValue(chunk.value)
|
|
1407
|
+
if (parsed) {
|
|
1408
|
+
const object = this.finalStructuredOutput.normalize
|
|
1409
|
+
? this.finalStructuredOutput.normalize(parsed.object)
|
|
1410
|
+
: parsed.object
|
|
1411
|
+
this.structuredOutputResult = { data: object, rawText: parsed.raw }
|
|
1412
|
+
this.combinedCompleteEmitted = true
|
|
1413
|
+
const value = chunk.value
|
|
1414
|
+
if (object !== parsed.object && value && typeof value === 'object') {
|
|
1415
|
+
outboundChunk = { ...chunk, value: { ...value, object } }
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1377
1420
|
if (
|
|
1378
1421
|
this.finalStructuredOutput?.nativeCombined === true &&
|
|
1379
1422
|
this.finalStructuredOutput.yieldChunks &&
|
|
1423
|
+
this.finalStructuredOutput.source !== 'event' &&
|
|
1380
1424
|
!this.combinedStartEmitted &&
|
|
1381
1425
|
chunk.type === EventType.TEXT_MESSAGE_START
|
|
1382
1426
|
) {
|
|
@@ -1408,7 +1452,7 @@ class TextEngine<
|
|
|
1408
1452
|
// Pipe chunk through middleware (devtools middleware observes; strip-to-spec cleans)
|
|
1409
1453
|
const outputChunks = await this.middlewareRunner.runOnChunk(
|
|
1410
1454
|
this.middlewareCtx,
|
|
1411
|
-
|
|
1455
|
+
outboundChunk,
|
|
1412
1456
|
)
|
|
1413
1457
|
// When a streaming structured-output finalization step will run after
|
|
1414
1458
|
// the agent loop, suppress the agent-loop's RUN_STARTED/RUN_FINISHED
|
|
@@ -1546,6 +1590,20 @@ class TextEngine<
|
|
|
1546
1590
|
|
|
1547
1591
|
private handleToolCallEndEvent(chunk: ToolCallEndEvent): void {
|
|
1548
1592
|
this.toolCallManager.completeToolCall(chunk)
|
|
1593
|
+
if (chunk.state !== 'output-error' || chunk.result === undefined) return
|
|
1594
|
+
|
|
1595
|
+
const toolCall = this.toolCallManager
|
|
1596
|
+
.getToolCalls()
|
|
1597
|
+
.find((candidate) => candidate.id === chunk.toolCallId)
|
|
1598
|
+
if (!toolCall) return
|
|
1599
|
+
|
|
1600
|
+
this.streamedToolErrorResults.set(chunk.toolCallId, {
|
|
1601
|
+
toolCallId: chunk.toolCallId,
|
|
1602
|
+
toolName: toolCall.function.name,
|
|
1603
|
+
result: chunk.result,
|
|
1604
|
+
...(chunk.input !== undefined && { input: chunk.input }),
|
|
1605
|
+
state: 'output-error',
|
|
1606
|
+
})
|
|
1549
1607
|
}
|
|
1550
1608
|
|
|
1551
1609
|
private handleRunFinishedEvent(chunk: RunFinishedEvent): void {
|
|
@@ -1554,9 +1612,23 @@ class TextEngine<
|
|
|
1554
1612
|
}
|
|
1555
1613
|
|
|
1556
1614
|
private handleRunErrorEvent(
|
|
1557
|
-
|
|
1615
|
+
chunk: Extract<StreamChunk, { type: 'RUN_ERROR' }>,
|
|
1558
1616
|
): void {
|
|
1559
1617
|
this.earlyTermination = true
|
|
1618
|
+
if (this.finalStructuredOutput && this.finalizationError === null) {
|
|
1619
|
+
const message =
|
|
1620
|
+
chunk.message ||
|
|
1621
|
+
chunk.error?.message ||
|
|
1622
|
+
'Run failed before structured output completed'
|
|
1623
|
+
this.finalizationError = {
|
|
1624
|
+
message,
|
|
1625
|
+
...(chunk.code !== undefined
|
|
1626
|
+
? { code: chunk.code }
|
|
1627
|
+
: chunk.error?.code !== undefined
|
|
1628
|
+
? { code: chunk.error.code }
|
|
1629
|
+
: {}),
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1560
1632
|
}
|
|
1561
1633
|
|
|
1562
1634
|
private finalizeCurrentThinkingStep(): void {
|
|
@@ -1787,6 +1859,9 @@ class TextEngine<
|
|
|
1787
1859
|
// Handle undiscovered lazy tool calls with self-correcting error messages
|
|
1788
1860
|
const undiscoveredLazyResults: Array<ToolResult> = []
|
|
1789
1861
|
const executableToolCalls = toolCalls.filter((tc) => {
|
|
1862
|
+
if (this.streamedToolErrorResults.has(tc.id)) {
|
|
1863
|
+
return false
|
|
1864
|
+
}
|
|
1790
1865
|
if (this.lazyToolManager.isUndiscoveredLazyTool(tc.function.name)) {
|
|
1791
1866
|
undiscoveredLazyResults.push({
|
|
1792
1867
|
toolCallId: tc.id,
|
|
@@ -1803,14 +1878,17 @@ class TextEngine<
|
|
|
1803
1878
|
return true
|
|
1804
1879
|
})
|
|
1805
1880
|
|
|
1806
|
-
// Non-executed outcomes
|
|
1807
|
-
//
|
|
1808
|
-
const deferredErrorResults = [
|
|
1881
|
+
// Non-executed outcomes. Per-turn skips come from middleware and appear in
|
|
1882
|
+
// execution results.
|
|
1883
|
+
const deferredErrorResults = [
|
|
1884
|
+
...this.streamedToolErrorResults.values(),
|
|
1885
|
+
...undiscoveredLazyResults,
|
|
1886
|
+
]
|
|
1809
1887
|
|
|
1810
1888
|
if (executableToolCalls.length === 0) {
|
|
1811
1889
|
yield* this.flushDeferredToolCallRunFinishedChunks()
|
|
1812
|
-
// All tool calls
|
|
1813
|
-
// loop (strategy / onShouldContinue may stop).
|
|
1890
|
+
// All tool calls already have error results — emit them, then continue
|
|
1891
|
+
// the loop (strategy / onShouldContinue may stop).
|
|
1814
1892
|
if (deferredErrorResults.length > 0) {
|
|
1815
1893
|
for (const chunk of this.buildToolResultChunks(
|
|
1816
1894
|
deferredErrorResults,
|
|
@@ -2177,7 +2255,9 @@ class TextEngine<
|
|
|
2177
2255
|
? undefined
|
|
2178
2256
|
: JSON.stringify(message.content)
|
|
2179
2257
|
return {
|
|
2180
|
-
id:
|
|
2258
|
+
id:
|
|
2259
|
+
message.id ||
|
|
2260
|
+
`snapshot_${this.runIdOverride ?? this.requestId}_${index}`,
|
|
2181
2261
|
role: message.role,
|
|
2182
2262
|
...(content !== undefined ? { content } : {}),
|
|
2183
2263
|
...('toolCalls' in message && message.toolCalls
|
|
@@ -2855,7 +2935,9 @@ class TextEngine<
|
|
|
2855
2935
|
return null
|
|
2856
2936
|
}
|
|
2857
2937
|
|
|
2858
|
-
|
|
2938
|
+
// The synthetic event is inserted before its trigger, so share that
|
|
2939
|
+
// trigger's timestamp rather than making the earlier event sort later.
|
|
2940
|
+
const buildSynthesizedStart = (timestamp = Date.now()): StreamChunk => {
|
|
2859
2941
|
const idForStart = structuredMessageId ?? generateMessageId()
|
|
2860
2942
|
structuredMessageId = idForStart
|
|
2861
2943
|
return {
|
|
@@ -2863,7 +2945,7 @@ class TextEngine<
|
|
|
2863
2945
|
name: 'structured-output.start',
|
|
2864
2946
|
value: { messageId: idForStart },
|
|
2865
2947
|
model: this.params.model,
|
|
2866
|
-
timestamp
|
|
2948
|
+
timestamp,
|
|
2867
2949
|
threadId: this.threadId,
|
|
2868
2950
|
...(this.runIdOverride ? { runId: this.runIdOverride } : {}),
|
|
2869
2951
|
}
|
|
@@ -2913,7 +2995,7 @@ class TextEngine<
|
|
|
2913
2995
|
chunk.type === EventType.TEXT_MESSAGE_END)
|
|
2914
2996
|
) {
|
|
2915
2997
|
startEmitted = true
|
|
2916
|
-
const synthStart = buildSynthesizedStart()
|
|
2998
|
+
const synthStart = buildSynthesizedStart(chunk.timestamp)
|
|
2917
2999
|
const synthOutputs = await pipeThroughMiddleware(synthStart)
|
|
2918
3000
|
for (const outputChunk of synthOutputs) {
|
|
2919
3001
|
yield outputChunk
|
|
@@ -2926,7 +3008,7 @@ class TextEngine<
|
|
|
2926
3008
|
// of a silent UI.
|
|
2927
3009
|
if (!startEmitted && chunk.type === EventType.RUN_ERROR) {
|
|
2928
3010
|
startEmitted = true
|
|
2929
|
-
const synthStart = buildSynthesizedStart()
|
|
3011
|
+
const synthStart = buildSynthesizedStart(chunk.timestamp)
|
|
2930
3012
|
const synthOutputs = await pipeThroughMiddleware(synthStart)
|
|
2931
3013
|
for (const outputChunk of synthOutputs) {
|
|
2932
3014
|
yield outputChunk
|
|
@@ -3139,35 +3221,46 @@ class TextEngine<
|
|
|
3139
3221
|
}
|
|
3140
3222
|
|
|
3141
3223
|
const yieldChunks = this.finalStructuredOutput.yieldChunks
|
|
3142
|
-
const
|
|
3224
|
+
const source = this.finalStructuredOutput.source ?? 'text'
|
|
3143
3225
|
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
message: 'missing structured result',
|
|
3151
|
-
code: 'structured-output-missing-result',
|
|
3226
|
+
if (source === 'event') {
|
|
3227
|
+
if (!this.structuredOutputResult) {
|
|
3228
|
+
this.finalizationError = {
|
|
3229
|
+
message: 'missing structured result',
|
|
3230
|
+
code: 'structured-output-missing-result',
|
|
3231
|
+
}
|
|
3152
3232
|
}
|
|
3153
3233
|
} else {
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
? this.finalStructuredOutput.normalize(parsed)
|
|
3162
|
-
: parsed
|
|
3163
|
-
this.structuredOutputResult = { data, rawText }
|
|
3164
|
-
} catch (err: unknown) {
|
|
3165
|
-
const detail =
|
|
3166
|
-
rawText.slice(0, 200) + (rawText.length > 200 ? '...' : '')
|
|
3234
|
+
const rawText = this.accumulatedContent
|
|
3235
|
+
|
|
3236
|
+
// Empty final-turn text means the agent loop terminated without the
|
|
3237
|
+
// model emitting any assistant content (e.g. early termination after
|
|
3238
|
+
// tool calls). Mirror the fallback path's "missing structured result"
|
|
3239
|
+
// error rather than silently returning undefined.
|
|
3240
|
+
if (rawText.length === 0) {
|
|
3167
3241
|
this.finalizationError = {
|
|
3168
|
-
message:
|
|
3169
|
-
code: 'structured-output-
|
|
3170
|
-
|
|
3242
|
+
message: 'missing structured result',
|
|
3243
|
+
code: 'structured-output-missing-result',
|
|
3244
|
+
}
|
|
3245
|
+
} else {
|
|
3246
|
+
try {
|
|
3247
|
+
const parsed: unknown = JSON.parse(rawText)
|
|
3248
|
+
// Normalize (un-widen) before storing so the synthesized
|
|
3249
|
+
// structured-output.complete chunk and the Promise<T> result both
|
|
3250
|
+
// carry the cleaned payload. JSON.parse preserves provider nulls, so
|
|
3251
|
+
// this is where native-combined output gets its widening undone.
|
|
3252
|
+
const data = this.finalStructuredOutput.normalize
|
|
3253
|
+
? this.finalStructuredOutput.normalize(parsed)
|
|
3254
|
+
: parsed
|
|
3255
|
+
this.structuredOutputResult = { data, rawText }
|
|
3256
|
+
} catch (err: unknown) {
|
|
3257
|
+
const detail =
|
|
3258
|
+
rawText.slice(0, 200) + (rawText.length > 200 ? '...' : '')
|
|
3259
|
+
this.finalizationError = {
|
|
3260
|
+
message: `Failed to parse structured output as JSON. Content: ${detail}`,
|
|
3261
|
+
code: 'structured-output-parse-failed',
|
|
3262
|
+
cause: err,
|
|
3263
|
+
}
|
|
3171
3264
|
}
|
|
3172
3265
|
}
|
|
3173
3266
|
}
|
|
@@ -3237,7 +3330,11 @@ class TextEngine<
|
|
|
3237
3330
|
// complete event yields AFTER the loop ends, by which point
|
|
3238
3331
|
// `getActiveAssistantMessageId()` returns null and would otherwise drop
|
|
3239
3332
|
// the event silently).
|
|
3240
|
-
if (
|
|
3333
|
+
if (
|
|
3334
|
+
this.structuredOutputResult &&
|
|
3335
|
+
!this.finalizationError &&
|
|
3336
|
+
!this.combinedCompleteEmitted
|
|
3337
|
+
) {
|
|
3241
3338
|
const completeChunk: StreamChunk = {
|
|
3242
3339
|
type: EventType.CUSTOM,
|
|
3243
3340
|
name: 'structured-output.complete',
|
|
@@ -3890,6 +3987,8 @@ async function runAgenticStructuredOutput<
|
|
|
3890
3987
|
// agent loop's accumulated final-turn text.
|
|
3891
3988
|
const nativeCombined =
|
|
3892
3989
|
adapter.supportsCombinedToolsAndSchema?.(options.modelOptions) === true
|
|
3990
|
+
const source =
|
|
3991
|
+
adapter.combinedStructuredOutputSource?.(options.modelOptions) ?? 'text'
|
|
3893
3992
|
|
|
3894
3993
|
const mcpManager = MCPManager.from(mcp)
|
|
3895
3994
|
const mcpTools = await mcpManager.discover()
|
|
@@ -3913,6 +4012,7 @@ async function runAgenticStructuredOutput<
|
|
|
3913
4012
|
normalize,
|
|
3914
4013
|
...(validate ? { validate } : {}),
|
|
3915
4014
|
...(nativeCombined ? { nativeCombined: true } : {}),
|
|
4015
|
+
source,
|
|
3916
4016
|
},
|
|
3917
4017
|
},
|
|
3918
4018
|
logger,
|
|
@@ -4008,14 +4108,14 @@ async function* fallbackStructuredOutputStream(
|
|
|
4008
4108
|
chatOptions.threadId ?? `fallback-${Date.now()}-${fallbackRand}`
|
|
4009
4109
|
const messageId = `fallback-${Date.now()}-${fallbackRand}`
|
|
4010
4110
|
const model = chatOptions.model
|
|
4011
|
-
const
|
|
4111
|
+
const startedAt = Date.now()
|
|
4012
4112
|
|
|
4013
4113
|
yield {
|
|
4014
4114
|
type: EventType.RUN_STARTED,
|
|
4015
4115
|
runId,
|
|
4016
4116
|
threadId,
|
|
4017
4117
|
model,
|
|
4018
|
-
timestamp,
|
|
4118
|
+
timestamp: startedAt,
|
|
4019
4119
|
}
|
|
4020
4120
|
|
|
4021
4121
|
let result: StructuredOutputResult<unknown>
|
|
@@ -4029,7 +4129,7 @@ async function* fallbackStructuredOutputStream(
|
|
|
4029
4129
|
runId,
|
|
4030
4130
|
threadId,
|
|
4031
4131
|
model,
|
|
4032
|
-
timestamp,
|
|
4132
|
+
timestamp: Date.now(),
|
|
4033
4133
|
message,
|
|
4034
4134
|
error: { message },
|
|
4035
4135
|
}
|
|
@@ -4041,7 +4141,7 @@ async function* fallbackStructuredOutputStream(
|
|
|
4041
4141
|
messageId,
|
|
4042
4142
|
role: 'assistant',
|
|
4043
4143
|
model,
|
|
4044
|
-
timestamp,
|
|
4144
|
+
timestamp: Date.now(),
|
|
4045
4145
|
}
|
|
4046
4146
|
|
|
4047
4147
|
yield {
|
|
@@ -4049,14 +4149,14 @@ async function* fallbackStructuredOutputStream(
|
|
|
4049
4149
|
messageId,
|
|
4050
4150
|
delta: result.rawText,
|
|
4051
4151
|
model,
|
|
4052
|
-
timestamp,
|
|
4152
|
+
timestamp: Date.now(),
|
|
4053
4153
|
}
|
|
4054
4154
|
|
|
4055
4155
|
yield {
|
|
4056
4156
|
type: EventType.TEXT_MESSAGE_END,
|
|
4057
4157
|
messageId,
|
|
4058
4158
|
model,
|
|
4059
|
-
timestamp,
|
|
4159
|
+
timestamp: Date.now(),
|
|
4060
4160
|
}
|
|
4061
4161
|
|
|
4062
4162
|
yield {
|
|
@@ -4064,7 +4164,7 @@ async function* fallbackStructuredOutputStream(
|
|
|
4064
4164
|
name: 'structured-output.complete',
|
|
4065
4165
|
value: { object: result.data, raw: result.rawText },
|
|
4066
4166
|
model,
|
|
4067
|
-
timestamp,
|
|
4167
|
+
timestamp: Date.now(),
|
|
4068
4168
|
}
|
|
4069
4169
|
|
|
4070
4170
|
yield {
|
|
@@ -4072,7 +4172,7 @@ async function* fallbackStructuredOutputStream(
|
|
|
4072
4172
|
runId,
|
|
4073
4173
|
threadId,
|
|
4074
4174
|
model,
|
|
4075
|
-
timestamp,
|
|
4175
|
+
timestamp: Date.now(),
|
|
4076
4176
|
finishReason: 'stop',
|
|
4077
4177
|
// Forward adapter-reported token usage so consumers reading
|
|
4078
4178
|
// `RUN_FINISHED.usage` (and the engine's `runOnUsage` middleware hook) see
|
|
@@ -4205,6 +4305,8 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
4205
4305
|
// does not fire.
|
|
4206
4306
|
const nativeCombined =
|
|
4207
4307
|
adapter.supportsCombinedToolsAndSchema?.(options.modelOptions) === true
|
|
4308
|
+
const source =
|
|
4309
|
+
adapter.combinedStructuredOutputSource?.(options.modelOptions) ?? 'text'
|
|
4208
4310
|
|
|
4209
4311
|
const mcpManager = MCPManager.from(mcp)
|
|
4210
4312
|
const mcpTools = await mcpManager.discover()
|
|
@@ -4229,6 +4331,7 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
4229
4331
|
yieldChunks: true,
|
|
4230
4332
|
normalize,
|
|
4231
4333
|
...(nativeCombined ? { nativeCombined: true } : {}),
|
|
4334
|
+
source,
|
|
4232
4335
|
},
|
|
4233
4336
|
},
|
|
4234
4337
|
logger,
|
|
@@ -1854,9 +1854,10 @@ export class StreamProcessor {
|
|
|
1854
1854
|
|
|
1855
1855
|
if (chunk.name === 'structured-output.start' && chunk.value) {
|
|
1856
1856
|
const v = chunk.value as { messageId?: string }
|
|
1857
|
-
const targetId =
|
|
1857
|
+
const { messageId: targetId } = this.ensureAssistantMessage(
|
|
1858
|
+
v.messageId ?? messageId ?? undefined,
|
|
1859
|
+
)
|
|
1858
1860
|
if (targetId) {
|
|
1859
|
-
this.ensureAssistantMessage(targetId)
|
|
1860
1861
|
this.structuredMessageIds.add(targetId)
|
|
1861
1862
|
this.structuredOutputUpdateBatches.delete(targetId)
|
|
1862
1863
|
this.events.onStructuredOutputChange?.({
|
|
@@ -1876,7 +1877,9 @@ export class StreamProcessor {
|
|
|
1876
1877
|
reasoning?: string
|
|
1877
1878
|
messageId?: string
|
|
1878
1879
|
}
|
|
1879
|
-
const targetId =
|
|
1880
|
+
const { messageId: targetId } = this.ensureAssistantMessage(
|
|
1881
|
+
v.messageId ?? messageId ?? undefined,
|
|
1882
|
+
)
|
|
1880
1883
|
if (targetId) {
|
|
1881
1884
|
this.flushStructuredOutputUpdate(targetId)
|
|
1882
1885
|
this.messages = completeStructuredOutputPart(
|
|
@@ -813,7 +813,7 @@ export async function* executeToolCalls<TContext = unknown>(
|
|
|
813
813
|
continue
|
|
814
814
|
}
|
|
815
815
|
|
|
816
|
-
// Parse arguments
|
|
816
|
+
// Parse arguments
|
|
817
817
|
let input: unknown = {}
|
|
818
818
|
const argsStr = toolCall.function.arguments.trim() || '{}'
|
|
819
819
|
if (argsStr) {
|
|
@@ -821,9 +821,17 @@ export async function* executeToolCalls<TContext = unknown>(
|
|
|
821
821
|
const parsed = JSON.parse(argsStr)
|
|
822
822
|
// Normalize null/non-object to {} (e.g. Anthropic empty tool_use blocks)
|
|
823
823
|
input = parsed && typeof parsed === 'object' ? parsed : {}
|
|
824
|
-
} catch
|
|
825
|
-
|
|
826
|
-
|
|
824
|
+
} catch {
|
|
825
|
+
results.push({
|
|
826
|
+
toolCallId: toolCall.id,
|
|
827
|
+
toolName,
|
|
828
|
+
result: {
|
|
829
|
+
error: `Failed to parse tool arguments as JSON: ${argsStr}`,
|
|
830
|
+
},
|
|
831
|
+
input,
|
|
832
|
+
state: 'output-error',
|
|
833
|
+
})
|
|
834
|
+
continue
|
|
827
835
|
}
|
|
828
836
|
}
|
|
829
837
|
|
package/src/adapter-internals.ts
CHANGED
|
@@ -25,3 +25,11 @@ export {
|
|
|
25
25
|
PendingTurnCapability,
|
|
26
26
|
providePendingTurn,
|
|
27
27
|
} from './activities/chat/middleware/pending-turn'
|
|
28
|
+
export {
|
|
29
|
+
appendOutputSchemaInstruction,
|
|
30
|
+
parseJsonFromAssistantText,
|
|
31
|
+
} from './utilities/structured-output-text'
|
|
32
|
+
export {
|
|
33
|
+
structuredOutputCompleteChunk,
|
|
34
|
+
structuredOutputStartChunk,
|
|
35
|
+
} from './utilities/structured-output-events'
|
package/src/types.ts
CHANGED
|
@@ -958,10 +958,13 @@ export interface TextOptions<
|
|
|
958
958
|
* `supportsCombinedToolsAndSchema(modelOptions) === true`. The adapter
|
|
959
959
|
* should then wire the schema into the upstream request (e.g.
|
|
960
960
|
* `response_format: { type: 'json_schema', ... }`, `text.format`,
|
|
961
|
-
* `output_format`) alongside any `tools`.
|
|
962
|
-
*
|
|
963
|
-
*
|
|
964
|
-
*
|
|
961
|
+
* `output_format`, `--json-schema`) alongside any `tools`.
|
|
962
|
+
*
|
|
963
|
+
* How the engine then takes the object depends on
|
|
964
|
+
* `combinedStructuredOutputSource()`:
|
|
965
|
+
* - `'text'` (default): the final-turn assistant text is the JSON.
|
|
966
|
+
* - `'event'`: the adapter emits `structured-output.complete` during
|
|
967
|
+
* `chatStream`. Accumulated prose is not parsed.
|
|
965
968
|
*
|
|
966
969
|
* Adapters that did NOT declare the capability never see this field
|
|
967
970
|
* populated — the engine instead invokes `structuredOutput` /
|
|
@@ -1555,20 +1558,25 @@ export interface CodeModeExternalErrorEvent extends CustomEvent {
|
|
|
1555
1558
|
name: 'code_mode:external_error'
|
|
1556
1559
|
value: { function: string; error: string; duration: number }
|
|
1557
1560
|
}
|
|
1558
|
-
export interface
|
|
1559
|
-
name: 'code_mode:
|
|
1560
|
-
value: {
|
|
1561
|
+
export interface CodeModeSnippetCallEvent extends CustomEvent {
|
|
1562
|
+
name: 'code_mode:snippet_call'
|
|
1563
|
+
value: { snippet: string; input: unknown; timestamp: number }
|
|
1561
1564
|
}
|
|
1562
|
-
export interface
|
|
1563
|
-
name: 'code_mode:
|
|
1564
|
-
value: {
|
|
1565
|
+
export interface CodeModeSnippetResultEvent extends CustomEvent {
|
|
1566
|
+
name: 'code_mode:snippet_result'
|
|
1567
|
+
value: {
|
|
1568
|
+
snippet: string
|
|
1569
|
+
result: unknown
|
|
1570
|
+
duration: number
|
|
1571
|
+
timestamp: number
|
|
1572
|
+
}
|
|
1565
1573
|
}
|
|
1566
|
-
export interface
|
|
1567
|
-
name: 'code_mode:
|
|
1568
|
-
value: {
|
|
1574
|
+
export interface CodeModeSnippetErrorEvent extends CustomEvent {
|
|
1575
|
+
name: 'code_mode:snippet_error'
|
|
1576
|
+
value: { snippet: string; error: string; duration: number; timestamp: number }
|
|
1569
1577
|
}
|
|
1570
|
-
export interface
|
|
1571
|
-
name: '
|
|
1578
|
+
export interface SnippetRegisteredEvent extends CustomEvent {
|
|
1579
|
+
name: 'snippet:registered'
|
|
1572
1580
|
value: { id: string; name: string; description: string; timestamp: number }
|
|
1573
1581
|
}
|
|
1574
1582
|
|
|
@@ -1587,10 +1595,10 @@ export type KnownCustomEvent =
|
|
|
1587
1595
|
| CodeModeExternalCallEvent
|
|
1588
1596
|
| CodeModeExternalResultEvent
|
|
1589
1597
|
| CodeModeExternalErrorEvent
|
|
1590
|
-
|
|
|
1591
|
-
|
|
|
1592
|
-
|
|
|
1593
|
-
|
|
|
1598
|
+
| CodeModeSnippetCallEvent
|
|
1599
|
+
| CodeModeSnippetResultEvent
|
|
1600
|
+
| CodeModeSnippetErrorEvent
|
|
1601
|
+
| SnippetRegisteredEvent
|
|
1594
1602
|
| StructuredOutputStartEvent
|
|
1595
1603
|
| StructuredOutputCompleteEvent
|
|
1596
1604
|
| ApprovalRequestedEvent
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { EventType } from '../types'
|
|
2
|
+
import type { StreamChunk } from '../types'
|
|
3
|
+
|
|
4
|
+
export function structuredOutputStartChunk(args: {
|
|
5
|
+
messageId: string
|
|
6
|
+
model: string
|
|
7
|
+
threadId: string
|
|
8
|
+
runId: string
|
|
9
|
+
timestamp?: number
|
|
10
|
+
}): StreamChunk {
|
|
11
|
+
return {
|
|
12
|
+
type: EventType.CUSTOM,
|
|
13
|
+
name: 'structured-output.start',
|
|
14
|
+
value: { messageId: args.messageId },
|
|
15
|
+
model: args.model,
|
|
16
|
+
timestamp: args.timestamp ?? Date.now(),
|
|
17
|
+
threadId: args.threadId,
|
|
18
|
+
runId: args.runId,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function structuredOutputCompleteChunk(args: {
|
|
23
|
+
messageId: string
|
|
24
|
+
model: string
|
|
25
|
+
threadId: string
|
|
26
|
+
runId: string
|
|
27
|
+
object: unknown
|
|
28
|
+
raw: string
|
|
29
|
+
timestamp?: number
|
|
30
|
+
}): StreamChunk {
|
|
31
|
+
return {
|
|
32
|
+
type: EventType.CUSTOM,
|
|
33
|
+
name: 'structured-output.complete',
|
|
34
|
+
value: {
|
|
35
|
+
object: args.object,
|
|
36
|
+
raw: args.raw,
|
|
37
|
+
messageId: args.messageId,
|
|
38
|
+
},
|
|
39
|
+
model: args.model,
|
|
40
|
+
timestamp: args.timestamp ?? Date.now(),
|
|
41
|
+
threadId: args.threadId,
|
|
42
|
+
runId: args.runId,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse JSON from a model/harness assistant string.
|
|
3
|
+
* Strips a wrapping markdown fence when the whole payload is fenced.
|
|
4
|
+
* If the model wrote prose first, take the last JSON object or array.
|
|
5
|
+
*/
|
|
6
|
+
export function parseJsonFromAssistantText(raw: string): unknown {
|
|
7
|
+
const trimmed = raw.trim()
|
|
8
|
+
if (trimmed === '') {
|
|
9
|
+
throw new SyntaxError('Assistant text is empty')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const candidates: Array<string> = []
|
|
13
|
+
const wholeFence = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/)
|
|
14
|
+
if (wholeFence?.[1]) candidates.push(wholeFence[1].trim())
|
|
15
|
+
candidates.push(trimmed)
|
|
16
|
+
const lastFence = [
|
|
17
|
+
...trimmed.matchAll(/```(?:json)?\s*([\s\S]*?)\s*```/g),
|
|
18
|
+
].at(-1)
|
|
19
|
+
if (lastFence?.[1]) candidates.push(lastFence[1].trim())
|
|
20
|
+
const extracted = extractLastJsonSlice(trimmed)
|
|
21
|
+
if (extracted !== undefined) candidates.push(extracted)
|
|
22
|
+
|
|
23
|
+
let lastError: unknown
|
|
24
|
+
for (const candidate of candidates) {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(candidate)
|
|
27
|
+
} catch (error) {
|
|
28
|
+
lastError = error
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
throw lastError instanceof Error
|
|
32
|
+
? lastError
|
|
33
|
+
: new SyntaxError('No JSON object found in assistant text')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function extractLastJsonSlice(text: string): string | undefined {
|
|
37
|
+
for (let end = text.length - 1; end >= 0; end--) {
|
|
38
|
+
if (text[end] !== '}' && text[end] !== ']') continue
|
|
39
|
+
for (let start = end; start >= 0; start--) {
|
|
40
|
+
const opener = text[start]
|
|
41
|
+
if (opener !== '{' && opener !== '[') continue
|
|
42
|
+
const slice = text.slice(start, end + 1)
|
|
43
|
+
try {
|
|
44
|
+
JSON.parse(slice)
|
|
45
|
+
return slice
|
|
46
|
+
} catch {
|
|
47
|
+
// Try an earlier opener, then an earlier closer.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return undefined
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function appendOutputSchemaInstruction(
|
|
55
|
+
prompt: string,
|
|
56
|
+
schema: unknown,
|
|
57
|
+
): string {
|
|
58
|
+
return `${prompt}
|
|
59
|
+
|
|
60
|
+
Respond with a single JSON object that matches this JSON Schema. Do not wrap the object in markdown unless you must.
|
|
61
|
+
|
|
62
|
+
${JSON.stringify(schema)}`
|
|
63
|
+
}
|