@tanstack/ai 0.44.0 → 0.45.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 +13 -1
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/index.js +126 -68
- 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 +80 -34
- 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 -49
- 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
|
|
|
@@ -729,6 +731,8 @@ class TextEngine<
|
|
|
729
731
|
private streamStartTime = 0
|
|
730
732
|
private totalChunkCount = 0
|
|
731
733
|
private currentMessageId: string | null = null
|
|
734
|
+
private currentMessageCreatedAt: Date | null = null
|
|
735
|
+
private streamIdentityCaptured = false
|
|
732
736
|
private accumulatedContent = ''
|
|
733
737
|
private accumulatedThinking: Array<{ content: string; signature?: string }> =
|
|
734
738
|
[]
|
|
@@ -799,12 +803,14 @@ class TextEngine<
|
|
|
799
803
|
code?: string
|
|
800
804
|
cause?: unknown
|
|
801
805
|
} | null = null
|
|
806
|
+
private combinedCompleteEmitted = false
|
|
802
807
|
private readonly finalStructuredOutput?: {
|
|
803
808
|
jsonSchema: JSONSchema
|
|
804
809
|
yieldChunks: boolean
|
|
805
810
|
normalize?: (data: unknown) => unknown
|
|
806
811
|
validate?: (data: unknown) => unknown
|
|
807
812
|
nativeCombined?: boolean
|
|
813
|
+
source?: 'text' | 'event'
|
|
808
814
|
}
|
|
809
815
|
|
|
810
816
|
constructor(
|
|
@@ -1108,7 +1114,8 @@ class TextEngine<
|
|
|
1108
1114
|
this.finalStructuredOutput &&
|
|
1109
1115
|
this.toolPhase !== 'wait' &&
|
|
1110
1116
|
!this.isCancelled() &&
|
|
1111
|
-
!this.finalizationError
|
|
1117
|
+
!this.finalizationError &&
|
|
1118
|
+
!this.earlyTermination
|
|
1112
1119
|
) {
|
|
1113
1120
|
if (this.finalStructuredOutput.nativeCombined === true) {
|
|
1114
1121
|
yield* this.harvestCombinedStructuredOutput()
|
|
@@ -1268,6 +1275,8 @@ class TextEngine<
|
|
|
1268
1275
|
|
|
1269
1276
|
private async beginIteration(): Promise<void> {
|
|
1270
1277
|
this.currentMessageId = this.createId('msg')
|
|
1278
|
+
this.currentMessageCreatedAt = new Date()
|
|
1279
|
+
this.streamIdentityCaptured = false
|
|
1271
1280
|
this.accumulatedContent = ''
|
|
1272
1281
|
this.accumulatedThinking = []
|
|
1273
1282
|
this.currentThinkingContent = ''
|
|
@@ -1370,9 +1379,46 @@ class TextEngine<
|
|
|
1370
1379
|
// text starting — intermediate tool-call iterations don't need it,
|
|
1371
1380
|
// and emitting at run-start would wrap tool-call commentary into a
|
|
1372
1381
|
// structured-output part too.
|
|
1382
|
+
if (
|
|
1383
|
+
chunk.type === EventType.CUSTOM &&
|
|
1384
|
+
chunk.name === 'structured-output.start'
|
|
1385
|
+
) {
|
|
1386
|
+
this.combinedStartEmitted = true
|
|
1387
|
+
const startValue = chunk.value
|
|
1388
|
+
if (
|
|
1389
|
+
startValue &&
|
|
1390
|
+
typeof startValue === 'object' &&
|
|
1391
|
+
'messageId' in startValue &&
|
|
1392
|
+
typeof startValue.messageId === 'string'
|
|
1393
|
+
) {
|
|
1394
|
+
this.combinedStructuredMessageId = startValue.messageId
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
let outboundChunk: StreamChunk = chunk
|
|
1399
|
+
if (
|
|
1400
|
+
this.finalStructuredOutput?.source === 'event' &&
|
|
1401
|
+
chunk.type === EventType.CUSTOM &&
|
|
1402
|
+
chunk.name === 'structured-output.complete'
|
|
1403
|
+
) {
|
|
1404
|
+
const parsed = readStructuredOutputCompleteValue(chunk.value)
|
|
1405
|
+
if (parsed) {
|
|
1406
|
+
const object = this.finalStructuredOutput.normalize
|
|
1407
|
+
? this.finalStructuredOutput.normalize(parsed.object)
|
|
1408
|
+
: parsed.object
|
|
1409
|
+
this.structuredOutputResult = { data: object, rawText: parsed.raw }
|
|
1410
|
+
this.combinedCompleteEmitted = true
|
|
1411
|
+
const value = chunk.value
|
|
1412
|
+
if (object !== parsed.object && value && typeof value === 'object') {
|
|
1413
|
+
outboundChunk = { ...chunk, value: { ...value, object } }
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1373
1418
|
if (
|
|
1374
1419
|
this.finalStructuredOutput?.nativeCombined === true &&
|
|
1375
1420
|
this.finalStructuredOutput.yieldChunks &&
|
|
1421
|
+
this.finalStructuredOutput.source !== 'event' &&
|
|
1376
1422
|
!this.combinedStartEmitted &&
|
|
1377
1423
|
chunk.type === EventType.TEXT_MESSAGE_START
|
|
1378
1424
|
) {
|
|
@@ -1404,7 +1450,7 @@ class TextEngine<
|
|
|
1404
1450
|
// Pipe chunk through middleware (devtools middleware observes; strip-to-spec cleans)
|
|
1405
1451
|
const outputChunks = await this.middlewareRunner.runOnChunk(
|
|
1406
1452
|
this.middlewareCtx,
|
|
1407
|
-
|
|
1453
|
+
outboundChunk,
|
|
1408
1454
|
)
|
|
1409
1455
|
// When a streaming structured-output finalization step will run after
|
|
1410
1456
|
// the agent loop, suppress the agent-loop's RUN_STARTED/RUN_FINISHED
|
|
@@ -1455,6 +1501,11 @@ class TextEngine<
|
|
|
1455
1501
|
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- AG-UI EventType enum members vs string-literal case labels; default branch handles untraced events.
|
|
1456
1502
|
switch (chunk.type) {
|
|
1457
1503
|
// AG-UI Events
|
|
1504
|
+
case 'TEXT_MESSAGE_START':
|
|
1505
|
+
if (typeof chunk.messageId === 'string' && chunk.messageId !== '') {
|
|
1506
|
+
this.captureStreamMessageIdentity(chunk.messageId)
|
|
1507
|
+
}
|
|
1508
|
+
break
|
|
1458
1509
|
case 'TEXT_MESSAGE_CONTENT':
|
|
1459
1510
|
this.handleTextMessageContentEvent(chunk)
|
|
1460
1511
|
break
|
|
@@ -1493,8 +1544,7 @@ class TextEngine<
|
|
|
1493
1544
|
break
|
|
1494
1545
|
|
|
1495
1546
|
default:
|
|
1496
|
-
// RUN_STARTED,
|
|
1497
|
-
// STATE_SNAPSHOT, STATE_DELTA, CUSTOM
|
|
1547
|
+
// RUN_STARTED, TEXT_MESSAGE_END, STATE_SNAPSHOT, STATE_DELTA, CUSTOM
|
|
1498
1548
|
// - no special handling needed in chat activity
|
|
1499
1549
|
break
|
|
1500
1550
|
}
|
|
@@ -1513,7 +1563,22 @@ class TextEngine<
|
|
|
1513
1563
|
this.middlewareCtx.accumulatedContent = this.accumulatedContent
|
|
1514
1564
|
}
|
|
1515
1565
|
|
|
1566
|
+
private captureStreamMessageIdentity(messageId: string): void {
|
|
1567
|
+
this.currentMessageId = messageId
|
|
1568
|
+
this.middlewareCtx.currentMessageId = messageId
|
|
1569
|
+
if (!this.streamIdentityCaptured) {
|
|
1570
|
+
this.currentMessageCreatedAt = new Date()
|
|
1571
|
+
this.streamIdentityCaptured = true
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1516
1575
|
private handleToolCallStartEvent(chunk: ToolCallStartEvent): void {
|
|
1576
|
+
if (
|
|
1577
|
+
typeof chunk.parentMessageId === 'string' &&
|
|
1578
|
+
chunk.parentMessageId !== ''
|
|
1579
|
+
) {
|
|
1580
|
+
this.captureStreamMessageIdentity(chunk.parentMessageId)
|
|
1581
|
+
}
|
|
1517
1582
|
this.toolCallManager.addToolCallStartEvent(chunk)
|
|
1518
1583
|
}
|
|
1519
1584
|
|
|
@@ -1531,9 +1596,23 @@ class TextEngine<
|
|
|
1531
1596
|
}
|
|
1532
1597
|
|
|
1533
1598
|
private handleRunErrorEvent(
|
|
1534
|
-
|
|
1599
|
+
chunk: Extract<StreamChunk, { type: 'RUN_ERROR' }>,
|
|
1535
1600
|
): void {
|
|
1536
1601
|
this.earlyTermination = true
|
|
1602
|
+
if (this.finalStructuredOutput && this.finalizationError === null) {
|
|
1603
|
+
const message =
|
|
1604
|
+
chunk.message ||
|
|
1605
|
+
chunk.error?.message ||
|
|
1606
|
+
'Run failed before structured output completed'
|
|
1607
|
+
this.finalizationError = {
|
|
1608
|
+
message,
|
|
1609
|
+
...(chunk.code !== undefined
|
|
1610
|
+
? { code: chunk.code }
|
|
1611
|
+
: chunk.error?.code !== undefined
|
|
1612
|
+
? { code: chunk.error.code }
|
|
1613
|
+
: {}),
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1537
1616
|
}
|
|
1538
1617
|
|
|
1539
1618
|
private finalizeCurrentThinkingStep(): void {
|
|
@@ -1954,6 +2033,8 @@ class TextEngine<
|
|
|
1954
2033
|
role: 'assistant',
|
|
1955
2034
|
content: this.accumulatedContent || null,
|
|
1956
2035
|
toolCalls,
|
|
2036
|
+
id: this.currentMessageId ?? undefined,
|
|
2037
|
+
createdAt: this.currentMessageCreatedAt ?? undefined,
|
|
1957
2038
|
...(this.accumulatedThinking.length > 0 && {
|
|
1958
2039
|
thinking: this.accumulatedThinking,
|
|
1959
2040
|
}),
|
|
@@ -2152,7 +2233,9 @@ class TextEngine<
|
|
|
2152
2233
|
? undefined
|
|
2153
2234
|
: JSON.stringify(message.content)
|
|
2154
2235
|
return {
|
|
2155
|
-
id:
|
|
2236
|
+
id:
|
|
2237
|
+
message.id ||
|
|
2238
|
+
`snapshot_${this.runIdOverride ?? this.requestId}_${index}`,
|
|
2156
2239
|
role: message.role,
|
|
2157
2240
|
...(content !== undefined ? { content } : {}),
|
|
2158
2241
|
...('toolCalls' in message && message.toolCalls
|
|
@@ -2830,7 +2913,9 @@ class TextEngine<
|
|
|
2830
2913
|
return null
|
|
2831
2914
|
}
|
|
2832
2915
|
|
|
2833
|
-
|
|
2916
|
+
// The synthetic event is inserted before its trigger, so share that
|
|
2917
|
+
// trigger's timestamp rather than making the earlier event sort later.
|
|
2918
|
+
const buildSynthesizedStart = (timestamp = Date.now()): StreamChunk => {
|
|
2834
2919
|
const idForStart = structuredMessageId ?? generateMessageId()
|
|
2835
2920
|
structuredMessageId = idForStart
|
|
2836
2921
|
return {
|
|
@@ -2838,7 +2923,7 @@ class TextEngine<
|
|
|
2838
2923
|
name: 'structured-output.start',
|
|
2839
2924
|
value: { messageId: idForStart },
|
|
2840
2925
|
model: this.params.model,
|
|
2841
|
-
timestamp
|
|
2926
|
+
timestamp,
|
|
2842
2927
|
threadId: this.threadId,
|
|
2843
2928
|
...(this.runIdOverride ? { runId: this.runIdOverride } : {}),
|
|
2844
2929
|
}
|
|
@@ -2888,7 +2973,7 @@ class TextEngine<
|
|
|
2888
2973
|
chunk.type === EventType.TEXT_MESSAGE_END)
|
|
2889
2974
|
) {
|
|
2890
2975
|
startEmitted = true
|
|
2891
|
-
const synthStart = buildSynthesizedStart()
|
|
2976
|
+
const synthStart = buildSynthesizedStart(chunk.timestamp)
|
|
2892
2977
|
const synthOutputs = await pipeThroughMiddleware(synthStart)
|
|
2893
2978
|
for (const outputChunk of synthOutputs) {
|
|
2894
2979
|
yield outputChunk
|
|
@@ -2901,7 +2986,7 @@ class TextEngine<
|
|
|
2901
2986
|
// of a silent UI.
|
|
2902
2987
|
if (!startEmitted && chunk.type === EventType.RUN_ERROR) {
|
|
2903
2988
|
startEmitted = true
|
|
2904
|
-
const synthStart = buildSynthesizedStart()
|
|
2989
|
+
const synthStart = buildSynthesizedStart(chunk.timestamp)
|
|
2905
2990
|
const synthOutputs = await pipeThroughMiddleware(synthStart)
|
|
2906
2991
|
for (const outputChunk of synthOutputs) {
|
|
2907
2992
|
yield outputChunk
|
|
@@ -3114,35 +3199,46 @@ class TextEngine<
|
|
|
3114
3199
|
}
|
|
3115
3200
|
|
|
3116
3201
|
const yieldChunks = this.finalStructuredOutput.yieldChunks
|
|
3117
|
-
const
|
|
3202
|
+
const source = this.finalStructuredOutput.source ?? 'text'
|
|
3118
3203
|
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
message: 'missing structured result',
|
|
3126
|
-
code: 'structured-output-missing-result',
|
|
3204
|
+
if (source === 'event') {
|
|
3205
|
+
if (!this.structuredOutputResult) {
|
|
3206
|
+
this.finalizationError = {
|
|
3207
|
+
message: 'missing structured result',
|
|
3208
|
+
code: 'structured-output-missing-result',
|
|
3209
|
+
}
|
|
3127
3210
|
}
|
|
3128
3211
|
} else {
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
? this.finalStructuredOutput.normalize(parsed)
|
|
3137
|
-
: parsed
|
|
3138
|
-
this.structuredOutputResult = { data, rawText }
|
|
3139
|
-
} catch (err: unknown) {
|
|
3140
|
-
const detail =
|
|
3141
|
-
rawText.slice(0, 200) + (rawText.length > 200 ? '...' : '')
|
|
3212
|
+
const rawText = this.accumulatedContent
|
|
3213
|
+
|
|
3214
|
+
// Empty final-turn text means the agent loop terminated without the
|
|
3215
|
+
// model emitting any assistant content (e.g. early termination after
|
|
3216
|
+
// tool calls). Mirror the fallback path's "missing structured result"
|
|
3217
|
+
// error rather than silently returning undefined.
|
|
3218
|
+
if (rawText.length === 0) {
|
|
3142
3219
|
this.finalizationError = {
|
|
3143
|
-
message:
|
|
3144
|
-
code: 'structured-output-
|
|
3145
|
-
|
|
3220
|
+
message: 'missing structured result',
|
|
3221
|
+
code: 'structured-output-missing-result',
|
|
3222
|
+
}
|
|
3223
|
+
} else {
|
|
3224
|
+
try {
|
|
3225
|
+
const parsed: unknown = JSON.parse(rawText)
|
|
3226
|
+
// Normalize (un-widen) before storing so the synthesized
|
|
3227
|
+
// structured-output.complete chunk and the Promise<T> result both
|
|
3228
|
+
// carry the cleaned payload. JSON.parse preserves provider nulls, so
|
|
3229
|
+
// this is where native-combined output gets its widening undone.
|
|
3230
|
+
const data = this.finalStructuredOutput.normalize
|
|
3231
|
+
? this.finalStructuredOutput.normalize(parsed)
|
|
3232
|
+
: parsed
|
|
3233
|
+
this.structuredOutputResult = { data, rawText }
|
|
3234
|
+
} catch (err: unknown) {
|
|
3235
|
+
const detail =
|
|
3236
|
+
rawText.slice(0, 200) + (rawText.length > 200 ? '...' : '')
|
|
3237
|
+
this.finalizationError = {
|
|
3238
|
+
message: `Failed to parse structured output as JSON. Content: ${detail}`,
|
|
3239
|
+
code: 'structured-output-parse-failed',
|
|
3240
|
+
cause: err,
|
|
3241
|
+
}
|
|
3146
3242
|
}
|
|
3147
3243
|
}
|
|
3148
3244
|
}
|
|
@@ -3212,7 +3308,11 @@ class TextEngine<
|
|
|
3212
3308
|
// complete event yields AFTER the loop ends, by which point
|
|
3213
3309
|
// `getActiveAssistantMessageId()` returns null and would otherwise drop
|
|
3214
3310
|
// the event silently).
|
|
3215
|
-
if (
|
|
3311
|
+
if (
|
|
3312
|
+
this.structuredOutputResult &&
|
|
3313
|
+
!this.finalizationError &&
|
|
3314
|
+
!this.combinedCompleteEmitted
|
|
3315
|
+
) {
|
|
3216
3316
|
const completeChunk: StreamChunk = {
|
|
3217
3317
|
type: EventType.CUSTOM,
|
|
3218
3318
|
name: 'structured-output.complete',
|
|
@@ -3865,6 +3965,8 @@ async function runAgenticStructuredOutput<
|
|
|
3865
3965
|
// agent loop's accumulated final-turn text.
|
|
3866
3966
|
const nativeCombined =
|
|
3867
3967
|
adapter.supportsCombinedToolsAndSchema?.(options.modelOptions) === true
|
|
3968
|
+
const source =
|
|
3969
|
+
adapter.combinedStructuredOutputSource?.(options.modelOptions) ?? 'text'
|
|
3868
3970
|
|
|
3869
3971
|
const mcpManager = MCPManager.from(mcp)
|
|
3870
3972
|
const mcpTools = await mcpManager.discover()
|
|
@@ -3888,6 +3990,7 @@ async function runAgenticStructuredOutput<
|
|
|
3888
3990
|
normalize,
|
|
3889
3991
|
...(validate ? { validate } : {}),
|
|
3890
3992
|
...(nativeCombined ? { nativeCombined: true } : {}),
|
|
3993
|
+
source,
|
|
3891
3994
|
},
|
|
3892
3995
|
},
|
|
3893
3996
|
logger,
|
|
@@ -3983,14 +4086,14 @@ async function* fallbackStructuredOutputStream(
|
|
|
3983
4086
|
chatOptions.threadId ?? `fallback-${Date.now()}-${fallbackRand}`
|
|
3984
4087
|
const messageId = `fallback-${Date.now()}-${fallbackRand}`
|
|
3985
4088
|
const model = chatOptions.model
|
|
3986
|
-
const
|
|
4089
|
+
const startedAt = Date.now()
|
|
3987
4090
|
|
|
3988
4091
|
yield {
|
|
3989
4092
|
type: EventType.RUN_STARTED,
|
|
3990
4093
|
runId,
|
|
3991
4094
|
threadId,
|
|
3992
4095
|
model,
|
|
3993
|
-
timestamp,
|
|
4096
|
+
timestamp: startedAt,
|
|
3994
4097
|
}
|
|
3995
4098
|
|
|
3996
4099
|
let result: StructuredOutputResult<unknown>
|
|
@@ -4004,7 +4107,7 @@ async function* fallbackStructuredOutputStream(
|
|
|
4004
4107
|
runId,
|
|
4005
4108
|
threadId,
|
|
4006
4109
|
model,
|
|
4007
|
-
timestamp,
|
|
4110
|
+
timestamp: Date.now(),
|
|
4008
4111
|
message,
|
|
4009
4112
|
error: { message },
|
|
4010
4113
|
}
|
|
@@ -4016,7 +4119,7 @@ async function* fallbackStructuredOutputStream(
|
|
|
4016
4119
|
messageId,
|
|
4017
4120
|
role: 'assistant',
|
|
4018
4121
|
model,
|
|
4019
|
-
timestamp,
|
|
4122
|
+
timestamp: Date.now(),
|
|
4020
4123
|
}
|
|
4021
4124
|
|
|
4022
4125
|
yield {
|
|
@@ -4024,14 +4127,14 @@ async function* fallbackStructuredOutputStream(
|
|
|
4024
4127
|
messageId,
|
|
4025
4128
|
delta: result.rawText,
|
|
4026
4129
|
model,
|
|
4027
|
-
timestamp,
|
|
4130
|
+
timestamp: Date.now(),
|
|
4028
4131
|
}
|
|
4029
4132
|
|
|
4030
4133
|
yield {
|
|
4031
4134
|
type: EventType.TEXT_MESSAGE_END,
|
|
4032
4135
|
messageId,
|
|
4033
4136
|
model,
|
|
4034
|
-
timestamp,
|
|
4137
|
+
timestamp: Date.now(),
|
|
4035
4138
|
}
|
|
4036
4139
|
|
|
4037
4140
|
yield {
|
|
@@ -4039,7 +4142,7 @@ async function* fallbackStructuredOutputStream(
|
|
|
4039
4142
|
name: 'structured-output.complete',
|
|
4040
4143
|
value: { object: result.data, raw: result.rawText },
|
|
4041
4144
|
model,
|
|
4042
|
-
timestamp,
|
|
4145
|
+
timestamp: Date.now(),
|
|
4043
4146
|
}
|
|
4044
4147
|
|
|
4045
4148
|
yield {
|
|
@@ -4047,7 +4150,7 @@ async function* fallbackStructuredOutputStream(
|
|
|
4047
4150
|
runId,
|
|
4048
4151
|
threadId,
|
|
4049
4152
|
model,
|
|
4050
|
-
timestamp,
|
|
4153
|
+
timestamp: Date.now(),
|
|
4051
4154
|
finishReason: 'stop',
|
|
4052
4155
|
// Forward adapter-reported token usage so consumers reading
|
|
4053
4156
|
// `RUN_FINISHED.usage` (and the engine's `runOnUsage` middleware hook) see
|
|
@@ -4180,6 +4283,8 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
4180
4283
|
// does not fire.
|
|
4181
4284
|
const nativeCombined =
|
|
4182
4285
|
adapter.supportsCombinedToolsAndSchema?.(options.modelOptions) === true
|
|
4286
|
+
const source =
|
|
4287
|
+
adapter.combinedStructuredOutputSource?.(options.modelOptions) ?? 'text'
|
|
4183
4288
|
|
|
4184
4289
|
const mcpManager = MCPManager.from(mcp)
|
|
4185
4290
|
const mcpTools = await mcpManager.discover()
|
|
@@ -4204,6 +4309,7 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
4204
4309
|
yieldChunks: true,
|
|
4205
4310
|
normalize,
|
|
4206
4311
|
...(nativeCombined ? { nativeCombined: true } : {}),
|
|
4312
|
+
source,
|
|
4207
4313
|
},
|
|
4208
4314
|
},
|
|
4209
4315
|
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
|
+
}
|