@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.
Files changed (54) hide show
  1. package/README.md +10 -1
  2. package/dist/esm/activities/chat/adapter.d.ts +13 -1
  3. package/dist/esm/activities/chat/adapter.js.map +1 -1
  4. package/dist/esm/activities/chat/index.js +122 -69
  5. package/dist/esm/activities/chat/index.js.map +1 -1
  6. package/dist/esm/activities/chat/messages.js +24 -27
  7. package/dist/esm/activities/chat/messages.js.map +1 -1
  8. package/dist/esm/activities/chat/stream/processor.js +14 -13
  9. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  10. package/dist/esm/activities/chat/tools/approval-schema.js +11 -8
  11. package/dist/esm/activities/chat/tools/approval-schema.js.map +1 -1
  12. package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
  13. package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
  14. package/dist/esm/activities/chat/tools/tool-calls.js +40 -31
  15. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  16. package/dist/esm/activities/generateVideo/index.js +3 -2
  17. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  18. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
  19. package/dist/esm/adapter-internals.d.ts +2 -0
  20. package/dist/esm/adapter-internals.js +3 -1
  21. package/dist/esm/interrupt-resume.js +24 -20
  22. package/dist/esm/interrupt-resume.js.map +1 -1
  23. package/dist/esm/interrupts.js +2 -1
  24. package/dist/esm/interrupts.js.map +1 -1
  25. package/dist/esm/logger/console-logger.js +1 -3
  26. package/dist/esm/logger/console-logger.js.map +1 -1
  27. package/dist/esm/logger/resolve.js +2 -1
  28. package/dist/esm/logger/resolve.js.map +1 -1
  29. package/dist/esm/stream-durability.js +1 -1
  30. package/dist/esm/stream-durability.js.map +1 -1
  31. package/dist/esm/types.d.ts +19 -16
  32. package/dist/esm/utilities/chat-params.js +1 -3
  33. package/dist/esm/utilities/chat-params.js.map +1 -1
  34. package/dist/esm/utilities/media-prompt.js +1 -3
  35. package/dist/esm/utilities/media-prompt.js.map +1 -1
  36. package/dist/esm/utilities/structured-output-events.d.ts +17 -0
  37. package/dist/esm/utilities/structured-output-events.js +32 -0
  38. package/dist/esm/utilities/structured-output-events.js.map +1 -0
  39. package/dist/esm/utilities/structured-output-text.d.ts +7 -0
  40. package/dist/esm/utilities/structured-output-text.js +50 -0
  41. package/dist/esm/utilities/structured-output-text.js.map +1 -0
  42. package/package.json +2 -2
  43. package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +6 -2
  44. package/skills/ai-core/media-generation/SKILL.md +33 -19
  45. package/skills/ai-core/structured-outputs/SKILL.md +73 -1
  46. package/skills/ai-core/tool-calling/SKILL.md +1 -1
  47. package/src/activities/chat/adapter.ts +16 -1
  48. package/src/activities/chat/index.ts +155 -52
  49. package/src/activities/chat/stream/processor.ts +6 -3
  50. package/src/activities/chat/tools/tool-calls.ts +12 -4
  51. package/src/adapter-internals.ts +8 -0
  52. package/src/types.ts +27 -19
  53. package/src/utilities/structured-output-events.ts +44 -0
  54. 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
 
@@ -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
- chunk,
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
- _chunk: Extract<StreamChunk, { type: 'RUN_ERROR' }>,
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 (undiscovered lazy). Per-turn skips come from
1807
- // middleware and appear in execution results.
1808
- const deferredErrorResults = [...undiscoveredLazyResults]
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 were undiscovered lazy tools — errors emitted, continue
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: `snapshot_${this.runIdOverride ?? this.requestId}_${index}`,
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
- const buildSynthesizedStart = (): StreamChunk => {
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: Date.now(),
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 rawText = this.accumulatedContent
3224
+ const source = this.finalStructuredOutput.source ?? 'text'
3143
3225
 
3144
- // Empty final-turn text means the agent loop terminated without the
3145
- // model emitting any assistant content (e.g. early termination after
3146
- // tool calls). Mirror the fallback path's "missing structured result"
3147
- // error rather than silently returning undefined.
3148
- if (rawText.length === 0) {
3149
- this.finalizationError = {
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
- try {
3155
- const parsed: unknown = JSON.parse(rawText)
3156
- // Normalize (un-widen) before storing so the synthesized
3157
- // structured-output.complete chunk and the Promise<T> result both
3158
- // carry the cleaned payload. JSON.parse preserves provider nulls, so
3159
- // this is where native-combined output gets its widening undone.
3160
- const data = this.finalStructuredOutput.normalize
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: `Failed to parse structured output as JSON. Content: ${detail}`,
3169
- code: 'structured-output-parse-failed',
3170
- cause: err,
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 (this.structuredOutputResult && !this.finalizationError) {
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 timestamp = Date.now()
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 = 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
+ }