@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.
Files changed (53) hide show
  1. package/dist/esm/activities/chat/adapter.d.ts +13 -1
  2. package/dist/esm/activities/chat/adapter.js.map +1 -1
  3. package/dist/esm/activities/chat/index.js +126 -68
  4. package/dist/esm/activities/chat/index.js.map +1 -1
  5. package/dist/esm/activities/chat/messages.js +24 -27
  6. package/dist/esm/activities/chat/messages.js.map +1 -1
  7. package/dist/esm/activities/chat/stream/processor.js +14 -13
  8. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  9. package/dist/esm/activities/chat/tools/approval-schema.js +11 -8
  10. package/dist/esm/activities/chat/tools/approval-schema.js.map +1 -1
  11. package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
  12. package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
  13. package/dist/esm/activities/chat/tools/tool-calls.js +40 -31
  14. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  15. package/dist/esm/activities/generateVideo/index.js +3 -2
  16. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  17. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
  18. package/dist/esm/adapter-internals.d.ts +2 -0
  19. package/dist/esm/adapter-internals.js +3 -1
  20. package/dist/esm/interrupt-resume.js +24 -20
  21. package/dist/esm/interrupt-resume.js.map +1 -1
  22. package/dist/esm/interrupts.js +2 -1
  23. package/dist/esm/interrupts.js.map +1 -1
  24. package/dist/esm/logger/console-logger.js +1 -3
  25. package/dist/esm/logger/console-logger.js.map +1 -1
  26. package/dist/esm/logger/resolve.js +2 -1
  27. package/dist/esm/logger/resolve.js.map +1 -1
  28. package/dist/esm/stream-durability.js +1 -1
  29. package/dist/esm/stream-durability.js.map +1 -1
  30. package/dist/esm/types.d.ts +19 -16
  31. package/dist/esm/utilities/chat-params.js +1 -3
  32. package/dist/esm/utilities/chat-params.js.map +1 -1
  33. package/dist/esm/utilities/media-prompt.js +1 -3
  34. package/dist/esm/utilities/media-prompt.js.map +1 -1
  35. package/dist/esm/utilities/structured-output-events.d.ts +17 -0
  36. package/dist/esm/utilities/structured-output-events.js +32 -0
  37. package/dist/esm/utilities/structured-output-events.js.map +1 -0
  38. package/dist/esm/utilities/structured-output-text.d.ts +7 -0
  39. package/dist/esm/utilities/structured-output-text.js +50 -0
  40. package/dist/esm/utilities/structured-output-text.js.map +1 -0
  41. package/package.json +2 -2
  42. package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +6 -2
  43. package/skills/ai-core/media-generation/SKILL.md +80 -34
  44. package/skills/ai-core/structured-outputs/SKILL.md +73 -1
  45. package/skills/ai-core/tool-calling/SKILL.md +1 -1
  46. package/src/activities/chat/adapter.ts +16 -1
  47. package/src/activities/chat/index.ts +155 -49
  48. package/src/activities/chat/stream/processor.ts +6 -3
  49. package/src/activities/chat/tools/tool-calls.ts +12 -4
  50. package/src/adapter-internals.ts +8 -0
  51. package/src/types.ts +27 -19
  52. package/src/utilities/structured-output-events.ts +44 -0
  53. 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 agent loop's final-turn text is the
659
- * schema-constrained JSON; the engine parses it from accumulated
660
- * content. The `'structuredOutput'` middleware phase does NOT fire on
661
- * this path — middleware sees the run through `beforeModel` /
662
- * `modelStream` as usual.
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
- chunk,
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, TEXT_MESSAGE_START, TEXT_MESSAGE_END,
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
- _chunk: Extract<StreamChunk, { type: 'RUN_ERROR' }>,
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: `snapshot_${this.runIdOverride ?? this.requestId}_${index}`,
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
- const buildSynthesizedStart = (): StreamChunk => {
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: Date.now(),
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 rawText = this.accumulatedContent
3202
+ const source = this.finalStructuredOutput.source ?? 'text'
3118
3203
 
3119
- // Empty final-turn text means the agent loop terminated without the
3120
- // model emitting any assistant content (e.g. early termination after
3121
- // tool calls). Mirror the fallback path's "missing structured result"
3122
- // error rather than silently returning undefined.
3123
- if (rawText.length === 0) {
3124
- this.finalizationError = {
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
- try {
3130
- const parsed: unknown = JSON.parse(rawText)
3131
- // Normalize (un-widen) before storing so the synthesized
3132
- // structured-output.complete chunk and the Promise<T> result both
3133
- // carry the cleaned payload. JSON.parse preserves provider nulls, so
3134
- // this is where native-combined output gets its widening undone.
3135
- const data = this.finalStructuredOutput.normalize
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: `Failed to parse structured output as JSON. Content: ${detail}`,
3144
- code: 'structured-output-parse-failed',
3145
- cause: err,
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 (this.structuredOutputResult && !this.finalizationError) {
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 timestamp = Date.now()
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 = v.messageId ?? messageId
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 = v.messageId ?? messageId
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, throwing error if invalid JSON
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 (parseError) {
825
- // If parsing fails, throw error to fail fast
826
- throw new Error(`Failed to parse tool arguments as JSON: ${argsStr}`)
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
 
@@ -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`. The model's natural final
962
- * turn carries the schema-constrained JSON text and the engine
963
- * harvests it from the agent loop without a separate finalization
964
- * round-trip.
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 CodeModeSkillCallEvent extends CustomEvent {
1559
- name: 'code_mode:skill_call'
1560
- value: { skill: string; input: unknown; timestamp: number }
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 CodeModeSkillResultEvent extends CustomEvent {
1563
- name: 'code_mode:skill_result'
1564
- value: { skill: string; result: unknown; duration: number; timestamp: number }
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 CodeModeSkillErrorEvent extends CustomEvent {
1567
- name: 'code_mode:skill_error'
1568
- value: { skill: string; error: string; duration: number; timestamp: number }
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 SkillRegisteredEvent extends CustomEvent {
1571
- name: 'skill:registered'
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
- | CodeModeSkillCallEvent
1591
- | CodeModeSkillResultEvent
1592
- | CodeModeSkillErrorEvent
1593
- | SkillRegisteredEvent
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
+ }