@tanstack/ai 0.20.0 → 0.21.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 (98) hide show
  1. package/dist/esm/activities/chat/adapter.js +3 -1
  2. package/dist/esm/activities/chat/adapter.js.map +1 -1
  3. package/dist/esm/activities/chat/index.d.ts +4 -4
  4. package/dist/esm/activities/chat/index.js +403 -276
  5. package/dist/esm/activities/chat/index.js.map +1 -1
  6. package/dist/esm/activities/chat/messages.js +1 -1
  7. package/dist/esm/activities/chat/messages.js.map +1 -1
  8. package/dist/esm/activities/chat/middleware/compose.d.ts +10 -1
  9. package/dist/esm/activities/chat/middleware/compose.js +57 -0
  10. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  11. package/dist/esm/activities/chat/middleware/index.d.ts +1 -1
  12. package/dist/esm/activities/chat/middleware/types.d.ts +40 -8
  13. package/dist/esm/activities/chat/stream/processor.d.ts +8 -8
  14. package/dist/esm/activities/chat/stream/processor.js +29 -21
  15. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  16. package/dist/esm/activities/chat/stream/strategies.d.ts +3 -3
  17. package/dist/esm/activities/chat/stream/strategies.js +4 -4
  18. package/dist/esm/activities/chat/stream/strategies.js.map +1 -1
  19. package/dist/esm/activities/chat/tools/lazy-tool-manager.js +5 -0
  20. package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
  21. package/dist/esm/activities/chat/tools/schema-converter.d.ts +13 -2
  22. package/dist/esm/activities/chat/tools/schema-converter.js +47 -37
  23. package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
  24. package/dist/esm/activities/chat/tools/tool-calls.d.ts +2 -2
  25. package/dist/esm/activities/chat/tools/tool-calls.js +17 -9
  26. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  27. package/dist/esm/activities/chat/tools/tool-definition.js +1 -1
  28. package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
  29. package/dist/esm/activities/generateAudio/adapter.js +3 -1
  30. package/dist/esm/activities/generateAudio/adapter.js.map +1 -1
  31. package/dist/esm/activities/generateAudio/index.d.ts +5 -1
  32. package/dist/esm/activities/generateAudio/index.js.map +1 -1
  33. package/dist/esm/activities/generateImage/adapter.js +3 -1
  34. package/dist/esm/activities/generateImage/adapter.js.map +1 -1
  35. package/dist/esm/activities/generateImage/index.js +5 -0
  36. package/dist/esm/activities/generateImage/index.js.map +1 -1
  37. package/dist/esm/activities/generateSpeech/adapter.js +3 -1
  38. package/dist/esm/activities/generateSpeech/adapter.js.map +1 -1
  39. package/dist/esm/activities/generateTranscription/adapter.js +3 -1
  40. package/dist/esm/activities/generateTranscription/adapter.js.map +1 -1
  41. package/dist/esm/activities/generateVideo/adapter.js +3 -1
  42. package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
  43. package/dist/esm/activities/stream-generation-result.js +6 -2
  44. package/dist/esm/activities/stream-generation-result.js.map +1 -1
  45. package/dist/esm/activities/summarize/adapter.js +3 -1
  46. package/dist/esm/activities/summarize/adapter.js.map +1 -1
  47. package/dist/esm/activities/summarize/chat-stream-summarize.d.ts +1 -1
  48. package/dist/esm/activities/summarize/chat-stream-summarize.js +5 -0
  49. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
  50. package/dist/esm/activities/summarize/index.js.map +1 -1
  51. package/dist/esm/extend-adapter.js.map +1 -1
  52. package/dist/esm/index.d.ts +3 -2
  53. package/dist/esm/index.js +4 -1
  54. package/dist/esm/index.js.map +1 -1
  55. package/dist/esm/logger/internal-logger.js +2 -0
  56. package/dist/esm/logger/internal-logger.js.map +1 -1
  57. package/dist/esm/middlewares/content-guard.js +5 -4
  58. package/dist/esm/middlewares/content-guard.js.map +1 -1
  59. package/dist/esm/middlewares/otel.js +25 -18
  60. package/dist/esm/middlewares/otel.js.map +1 -1
  61. package/dist/esm/realtime/index.d.ts +1 -1
  62. package/dist/esm/realtime/index.js.map +1 -1
  63. package/dist/esm/strip-to-spec-middleware.js.map +1 -1
  64. package/dist/esm/tools/provider-tool.d.ts +9 -0
  65. package/dist/esm/tools/provider-tool.js +7 -0
  66. package/dist/esm/tools/provider-tool.js.map +1 -0
  67. package/dist/esm/types.d.ts +6 -6
  68. package/dist/esm/utilities/ag-ui-wire.js +4 -1
  69. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  70. package/dist/esm/utilities/chat-params.js.map +1 -1
  71. package/package.json +2 -2
  72. package/skills/ai-core/middleware/SKILL.md +124 -18
  73. package/skills/ai-core/structured-outputs/SKILL.md +13 -0
  74. package/src/activities/chat/index.ts +685 -394
  75. package/src/activities/chat/messages.ts +3 -2
  76. package/src/activities/chat/middleware/compose.ts +65 -5
  77. package/src/activities/chat/middleware/index.ts +1 -0
  78. package/src/activities/chat/middleware/types.ts +64 -11
  79. package/src/activities/chat/stream/processor.ts +21 -21
  80. package/src/activities/chat/stream/strategies.ts +3 -3
  81. package/src/activities/chat/tools/schema-converter.ts +98 -58
  82. package/src/activities/chat/tools/tool-calls.ts +18 -11
  83. package/src/activities/chat/tools/tool-definition.ts +6 -1
  84. package/src/activities/generateAudio/index.ts +5 -4
  85. package/src/activities/generateImage/index.ts +8 -3
  86. package/src/activities/stream-generation-result.ts +11 -2
  87. package/src/activities/summarize/chat-stream-summarize.ts +5 -2
  88. package/src/activities/summarize/index.ts +2 -2
  89. package/src/extend-adapter.ts +1 -1
  90. package/src/index.ts +6 -1
  91. package/src/middlewares/content-guard.ts +12 -4
  92. package/src/middlewares/otel.ts +32 -24
  93. package/src/realtime/index.ts +1 -1
  94. package/src/strip-to-spec-middleware.ts +1 -4
  95. package/src/tools/provider-tool.ts +14 -0
  96. package/src/types.ts +12 -8
  97. package/src/utilities/ag-ui-wire.ts +4 -3
  98. package/src/utilities/chat-params.ts +2 -2
@@ -14,24 +14,62 @@ function createChatOptions(options) {
14
14
  return options;
15
15
  }
16
16
  class TextEngine {
17
+ adapter;
18
+ params;
19
+ systemPrompts;
20
+ tools;
21
+ loopStrategy;
22
+ toolCallManager;
23
+ lazyToolManager;
24
+ initialMessageCount;
25
+ requestId;
26
+ streamId;
27
+ effectiveRequest;
28
+ effectiveSignal;
29
+ messages;
30
+ iterationCount = 0;
31
+ lastFinishReason = null;
32
+ streamStartTime = 0;
33
+ totalChunkCount = 0;
34
+ currentMessageId = null;
35
+ accumulatedContent = "";
36
+ accumulatedThinking = [];
37
+ currentThinkingContent = "";
38
+ currentThinkingSignature = "";
39
+ eventOptions;
40
+ eventToolNames;
41
+ finishedEvent = null;
42
+ earlyTermination = false;
43
+ toolPhase = "continue";
44
+ cyclePhase = "processText";
45
+ // Client state extracted from initial messages (before conversion to ModelMessage)
46
+ initialApprovals;
47
+ initialClientToolResults;
48
+ // AG-UI protocol IDs
49
+ threadId;
50
+ runIdOverride;
51
+ parentRunIdOverride;
52
+ // Middleware support
53
+ middlewareRunner;
54
+ middlewareCtx;
55
+ deferredPromises = [];
56
+ abortReason;
57
+ middlewareAbortController;
58
+ terminalHookCalled = false;
59
+ logger;
60
+ // Structured-output finalization state (populated by runStructuredFinalization)
61
+ structuredOutputResult = null;
62
+ // Holds the validated value when `finalStructuredOutput.validate` is provided
63
+ // and succeeds. Distinct from `structuredOutputResult.data` (the raw,
64
+ // unvalidated payload from the structured-output.complete chunk).
65
+ validatedStructuredOutput = void 0;
66
+ hasValidatedStructuredOutput = false;
67
+ finalizationError = null;
68
+ finalStructuredOutput;
17
69
  constructor(config, logger) {
18
- this.iterationCount = 0;
19
- this.lastFinishReason = null;
20
- this.streamStartTime = 0;
21
- this.totalChunkCount = 0;
22
- this.currentMessageId = null;
23
- this.accumulatedContent = "";
24
- this.accumulatedThinking = [];
25
- this.currentThinkingContent = "";
26
- this.currentThinkingSignature = "";
27
- this.finishedEvent = null;
28
- this.earlyTermination = false;
29
- this.toolPhase = "continue";
30
- this.cyclePhase = "processText";
31
- this.deferredPromises = [];
32
- this.terminalHookCalled = false;
33
70
  this.logger = logger;
34
71
  this.adapter = config.adapter;
72
+ this.finalStructuredOutput = config.finalStructuredOutput;
35
73
  this.params = config.params;
36
74
  this.systemPrompts = config.params.systemPrompts || [];
37
75
  this.loopStrategy = config.params.agentLoopStrategy || maxIterations(5);
@@ -41,9 +79,7 @@ class TextEngine {
41
79
  );
42
80
  this.initialApprovals = approvals;
43
81
  this.initialClientToolResults = clientToolResults;
44
- this.messages = convertMessagesToModelMessages(
45
- config.params.messages
46
- );
82
+ this.messages = convertMessagesToModelMessages(config.params.messages);
47
83
  this.lazyToolManager = new LazyToolManager(
48
84
  config.params.tools || [],
49
85
  this.messages
@@ -112,6 +148,24 @@ class TextEngine {
112
148
  getMessages() {
113
149
  return this.messages;
114
150
  }
151
+ /** Returns the structured-output result if finalization ran successfully. */
152
+ getStructuredOutputResult() {
153
+ return this.structuredOutputResult;
154
+ }
155
+ /**
156
+ * Returns the validated structured-output value (the result of running
157
+ * `finalStructuredOutput.validate` against the raw structured-output data)
158
+ * wrapped in a `{ value }` object so callers can distinguish "no validation
159
+ * happened" from "validation produced undefined". Returns `null` when no
160
+ * validator was configured or validation hasn't been performed yet.
161
+ */
162
+ getValidatedStructuredOutput() {
163
+ return this.hasValidatedStructuredOutput ? { value: this.validatedStructuredOutput } : null;
164
+ }
165
+ /** Returns the recorded finalization error, if any. */
166
+ getFinalizationError() {
167
+ return this.finalizationError;
168
+ }
115
169
  async *run() {
116
170
  this.beforeRun();
117
171
  this.logger.agentLoop("run started", {
@@ -130,40 +184,64 @@ class TextEngine {
130
184
  if (pendingPhase === "wait") {
131
185
  return;
132
186
  }
133
- do {
134
- if (this.earlyTermination || this.isCancelled()) {
135
- return;
136
- }
137
- this.logger.agentLoop(`iteration=${this.middlewareCtx.iteration}`, {
138
- iteration: this.middlewareCtx.iteration
139
- });
140
- await this.beginCycle();
141
- if (this.cyclePhase === "processText") {
142
- this.middlewareCtx.phase = "beforeModel";
143
- this.middlewareCtx.iteration = this.iterationCount;
144
- const iterConfig = this.buildMiddlewareConfig();
145
- const transformedConfig2 = await this.middlewareRunner.runOnConfig(
146
- this.middlewareCtx,
147
- iterConfig
148
- );
149
- this.applyMiddlewareConfig(transformedConfig2);
150
- yield* this.streamModelResponse();
151
- } else {
152
- yield* this.processToolCalls();
153
- }
154
- this.endCycle();
155
- } while (this.shouldContinue());
187
+ const skipAgentLoop = !!this.finalStructuredOutput && this.tools.length === 0;
188
+ if (!skipAgentLoop) {
189
+ do {
190
+ if (this.earlyTermination || this.isCancelled()) {
191
+ return;
192
+ }
193
+ this.logger.agentLoop(`iteration=${this.middlewareCtx.iteration}`, {
194
+ iteration: this.middlewareCtx.iteration
195
+ });
196
+ await this.beginCycle();
197
+ if (this.cyclePhase === "processText") {
198
+ this.middlewareCtx.phase = "beforeModel";
199
+ this.middlewareCtx.iteration = this.iterationCount;
200
+ const iterConfig = this.buildMiddlewareConfig();
201
+ const transformedConfig2 = await this.middlewareRunner.runOnConfig(
202
+ this.middlewareCtx,
203
+ iterConfig
204
+ );
205
+ this.applyMiddlewareConfig(transformedConfig2);
206
+ yield* this.streamModelResponse();
207
+ } else {
208
+ yield* this.processToolCalls();
209
+ }
210
+ this.endCycle();
211
+ } while (this.shouldContinue());
212
+ }
156
213
  this.logger.agentLoop("run finished", {
157
214
  finishReason: this.lastFinishReason
158
215
  });
159
- if (!this.terminalHookCalled && this.toolPhase !== "wait") {
160
- this.terminalHookCalled = true;
161
- await this.middlewareRunner.runOnFinish(this.middlewareCtx, {
162
- finishReason: this.lastFinishReason,
163
- duration: Date.now() - this.streamStartTime,
164
- content: this.accumulatedContent,
165
- usage: this.finishedEvent?.usage
166
- });
216
+ if (this.finalStructuredOutput && !this.isCancelled() && !this.finalizationError) {
217
+ yield* this.runStructuredFinalization();
218
+ }
219
+ if (!this.terminalHookCalled && this.toolPhase !== "wait" && !this.isCancelled()) {
220
+ if (this.finalizationError) {
221
+ this.terminalHookCalled = true;
222
+ const errForHook = new Error(
223
+ this.finalizationError.message,
224
+ this.finalizationError.cause !== void 0 ? { cause: this.finalizationError.cause } : void 0
225
+ );
226
+ if (this.finalizationError.code !== void 0) {
227
+ Object.defineProperty(errForHook, "code", {
228
+ value: this.finalizationError.code,
229
+ enumerable: true
230
+ });
231
+ }
232
+ await this.middlewareRunner.runOnError(this.middlewareCtx, {
233
+ error: errForHook,
234
+ duration: Date.now() - this.streamStartTime
235
+ });
236
+ } else {
237
+ this.terminalHookCalled = true;
238
+ await this.middlewareRunner.runOnFinish(this.middlewareCtx, {
239
+ finishReason: this.lastFinishReason,
240
+ duration: Date.now() - this.streamStartTime,
241
+ content: this.accumulatedContent,
242
+ usage: this.finishedEvent?.usage
243
+ });
244
+ }
167
245
  }
168
246
  } catch (error) {
169
247
  if (!this.terminalHookCalled) {
@@ -285,7 +363,11 @@ class TextEngine {
285
363
  this.middlewareCtx,
286
364
  chunk
287
365
  );
366
+ const suppressAgentLifecycle = !!this.finalStructuredOutput && this.finalStructuredOutput.yieldChunks;
288
367
  for (const outputChunk of outputChunks) {
368
+ if (suppressAgentLifecycle && (outputChunk.type === EventType.RUN_STARTED || outputChunk.type === EventType.RUN_FINISHED)) {
369
+ continue;
370
+ }
289
371
  this.logger.output(`type=${outputChunk.type}`, { chunk: outputChunk });
290
372
  yield outputChunk;
291
373
  this.middlewareCtx.chunkIndex++;
@@ -527,11 +609,10 @@ class TextEngine {
527
609
  }
528
610
  return true;
529
611
  });
530
- if (undiscoveredLazyResults.length > 0) {
531
- const finishEvt = this.finishedEvent;
612
+ if (undiscoveredLazyResults.length > 0 && this.finishedEvent) {
532
613
  for (const chunk of this.buildToolResultChunks(
533
614
  undiscoveredLazyResults,
534
- finishEvt
615
+ this.finishedEvent
535
616
  )) {
536
617
  yield* this.pipeThroughMiddleware(chunk);
537
618
  }
@@ -858,6 +939,212 @@ class TextEngine {
858
939
  isCancelled() {
859
940
  return this.isAborted() || this.isMiddlewareAborted();
860
941
  }
942
+ /**
943
+ * Run the final structured-output adapter call through the middleware
944
+ * pipeline. Yields chunks to the caller only when
945
+ * `this.finalStructuredOutput.yieldChunks` is true; otherwise consumes
946
+ * silently while still piping through middleware.
947
+ *
948
+ * On success, populates this.structuredOutputResult.
949
+ * On failure, populates this.finalizationError.
950
+ */
951
+ async *runStructuredFinalization() {
952
+ if (!this.finalStructuredOutput) {
953
+ throw new Error(
954
+ "runStructuredFinalization called without finalStructuredOutput config"
955
+ );
956
+ }
957
+ this.middlewareCtx.phase = "structuredOutput";
958
+ const baseConfig = this.buildMiddlewareConfig();
959
+ const { tools: _omitTools, ...baseWithoutTools } = baseConfig;
960
+ let structuredConfig = {
961
+ ...baseWithoutTools,
962
+ outputSchema: this.finalStructuredOutput.jsonSchema
963
+ };
964
+ structuredConfig = await this.middlewareRunner.runOnStructuredOutputConfig(
965
+ this.middlewareCtx,
966
+ structuredConfig
967
+ );
968
+ const { outputSchema: pinnedSchema, ...chatConfigSlice } = structuredConfig;
969
+ const postOnConfig = await this.middlewareRunner.runOnConfig(
970
+ this.middlewareCtx,
971
+ { ...chatConfigSlice, tools: baseConfig.tools }
972
+ );
973
+ this.applyMiddlewareConfig(postOnConfig);
974
+ const structuredCallOptions = {
975
+ chatOptions: {
976
+ model: this.params.model,
977
+ messages: this.messages,
978
+ temperature: postOnConfig.temperature,
979
+ topP: postOnConfig.topP,
980
+ maxTokens: postOnConfig.maxTokens,
981
+ metadata: postOnConfig.metadata,
982
+ modelOptions: postOnConfig.modelOptions,
983
+ systemPrompts: postOnConfig.systemPrompts,
984
+ logger: this.logger,
985
+ threadId: this.threadId,
986
+ runId: this.runIdOverride,
987
+ parentRunId: this.parentRunIdOverride,
988
+ ...this.effectiveRequest ? { request: this.effectiveRequest } : {}
989
+ },
990
+ outputSchema: pinnedSchema
991
+ };
992
+ let fallbackAdapterError = void 0;
993
+ const providerStream = this.adapter.structuredOutputStream ? this.adapter.structuredOutputStream(structuredCallOptions) : fallbackStructuredOutputStream(
994
+ this.adapter,
995
+ structuredCallOptions,
996
+ (err) => {
997
+ fallbackAdapterError = err;
998
+ }
999
+ );
1000
+ let startEmitted = false;
1001
+ let structuredMessageId = null;
1002
+ const extractMessageId = (c) => {
1003
+ if (c.type === EventType.TEXT_MESSAGE_START || c.type === EventType.TEXT_MESSAGE_CONTENT || c.type === EventType.TEXT_MESSAGE_END) {
1004
+ return typeof c.messageId === "string" && c.messageId !== "" ? c.messageId : null;
1005
+ }
1006
+ return null;
1007
+ };
1008
+ const buildSynthesizedStart = () => {
1009
+ const idForStart = structuredMessageId ?? generateMessageId();
1010
+ structuredMessageId = idForStart;
1011
+ return {
1012
+ type: EventType.CUSTOM,
1013
+ name: "structured-output.start",
1014
+ value: { messageId: idForStart },
1015
+ model: this.params.model,
1016
+ timestamp: Date.now(),
1017
+ threadId: this.threadId,
1018
+ ...this.runIdOverride ? { runId: this.runIdOverride } : {}
1019
+ };
1020
+ };
1021
+ const pipeThroughMiddleware = async (synthChunk) => this.middlewareRunner.runOnChunk(this.middlewareCtx, synthChunk);
1022
+ let runErrorYielded = false;
1023
+ for await (const chunk of providerStream) {
1024
+ if (this.isCancelled()) {
1025
+ break;
1026
+ }
1027
+ if (!startEmitted && chunk.type === EventType.CUSTOM && chunk.name === "structured-output.start") {
1028
+ startEmitted = true;
1029
+ }
1030
+ if (!structuredMessageId) {
1031
+ const extracted = extractMessageId(chunk);
1032
+ if (extracted) structuredMessageId = extracted;
1033
+ }
1034
+ if (this.finalStructuredOutput.yieldChunks) {
1035
+ if (!startEmitted && (chunk.type === EventType.TEXT_MESSAGE_START || chunk.type === EventType.TEXT_MESSAGE_CONTENT || chunk.type === EventType.TEXT_MESSAGE_END)) {
1036
+ startEmitted = true;
1037
+ const synthStart = buildSynthesizedStart();
1038
+ const synthOutputs = await pipeThroughMiddleware(synthStart);
1039
+ for (const outputChunk of synthOutputs) {
1040
+ yield outputChunk;
1041
+ this.middlewareCtx.chunkIndex++;
1042
+ }
1043
+ }
1044
+ if (!startEmitted && chunk.type === EventType.RUN_ERROR) {
1045
+ startEmitted = true;
1046
+ const synthStart = buildSynthesizedStart();
1047
+ const synthOutputs = await pipeThroughMiddleware(synthStart);
1048
+ for (const outputChunk of synthOutputs) {
1049
+ yield outputChunk;
1050
+ this.middlewareCtx.chunkIndex++;
1051
+ }
1052
+ }
1053
+ }
1054
+ if (chunk.type === EventType.CUSTOM && chunk.name === "structured-output.complete") {
1055
+ const parsed = readStructuredOutputCompleteValue(chunk.value);
1056
+ if (parsed) {
1057
+ this.structuredOutputResult = {
1058
+ data: parsed.object,
1059
+ rawText: parsed.raw
1060
+ };
1061
+ }
1062
+ }
1063
+ if (chunk.type === EventType.RUN_FINISHED && chunk.usage) {
1064
+ await this.middlewareRunner.runOnUsage(this.middlewareCtx, chunk.usage);
1065
+ }
1066
+ if (chunk.type === EventType.RUN_ERROR) {
1067
+ this.finalizationError = {
1068
+ message: chunk.message,
1069
+ ...chunk.code ? { code: chunk.code } : {},
1070
+ ...fallbackAdapterError !== void 0 ? { cause: fallbackAdapterError } : {}
1071
+ };
1072
+ }
1073
+ const outputChunks = await this.middlewareRunner.runOnChunk(
1074
+ this.middlewareCtx,
1075
+ chunk
1076
+ );
1077
+ if (this.finalStructuredOutput.yieldChunks) {
1078
+ for (const outputChunk of outputChunks) {
1079
+ if (outputChunk.type === EventType.RUN_ERROR) {
1080
+ runErrorYielded = true;
1081
+ }
1082
+ yield outputChunk;
1083
+ this.middlewareCtx.chunkIndex++;
1084
+ }
1085
+ }
1086
+ if (this.finalizationError) {
1087
+ break;
1088
+ }
1089
+ }
1090
+ if (this.isCancelled()) {
1091
+ return;
1092
+ }
1093
+ if (!this.structuredOutputResult && !this.finalizationError) {
1094
+ this.finalizationError = {
1095
+ message: "missing structured result",
1096
+ code: "structured-output-missing-result"
1097
+ };
1098
+ }
1099
+ if (this.structuredOutputResult && !this.finalizationError && this.finalStructuredOutput.validate) {
1100
+ try {
1101
+ const validated = this.finalStructuredOutput.validate(
1102
+ this.structuredOutputResult.data
1103
+ );
1104
+ this.validatedStructuredOutput = validated;
1105
+ this.hasValidatedStructuredOutput = true;
1106
+ } catch (err) {
1107
+ const message = err instanceof Error ? err.message : String(err);
1108
+ this.finalizationError = {
1109
+ message,
1110
+ code: "structured-output-validation-failed",
1111
+ cause: err
1112
+ };
1113
+ }
1114
+ }
1115
+ if (this.finalizationError && this.finalStructuredOutput.yieldChunks && !runErrorYielded) {
1116
+ if (!startEmitted) {
1117
+ const synthStart = buildSynthesizedStart();
1118
+ const startOutputs = await pipeThroughMiddleware(synthStart);
1119
+ for (const outputChunk of startOutputs) {
1120
+ yield outputChunk;
1121
+ this.middlewareCtx.chunkIndex++;
1122
+ }
1123
+ startEmitted = true;
1124
+ }
1125
+ const errChunk = {
1126
+ type: EventType.RUN_ERROR,
1127
+ runId: this.runIdOverride ?? this.requestId,
1128
+ model: this.params.model,
1129
+ timestamp: Date.now(),
1130
+ threadId: this.threadId,
1131
+ message: this.finalizationError.message,
1132
+ ...this.finalizationError.code ? { code: this.finalizationError.code } : {},
1133
+ error: {
1134
+ message: this.finalizationError.message,
1135
+ ...this.finalizationError.code ? { code: this.finalizationError.code } : {}
1136
+ }
1137
+ };
1138
+ const outputChunks = await this.middlewareRunner.runOnChunk(
1139
+ this.middlewareCtx,
1140
+ errChunk
1141
+ );
1142
+ for (const outputChunk of outputChunks) {
1143
+ yield outputChunk;
1144
+ this.middlewareCtx.chunkIndex++;
1145
+ }
1146
+ }
1147
+ }
861
1148
  buildMiddlewareConfig() {
862
1149
  return {
863
1150
  messages: this.messages,
@@ -977,6 +1264,7 @@ async function* runStreamingText(options) {
977
1264
  }
978
1265
  function runNonStreamingText(options) {
979
1266
  const stream = runStreamingText(
1267
+ // eslint-disable-next-line no-restricted-syntax -- generic-stream remap: caller is non-streaming (false), but runStreamingText is invoked internally to collect text; concrete `false`→`true` literals don't structurally overlap.
980
1268
  options
981
1269
  );
982
1270
  return streamToText(stream);
@@ -988,58 +1276,72 @@ async function runAgenticStructuredOutput(options) {
988
1276
  if (!outputSchema) {
989
1277
  throw new Error("outputSchema is required for structured output");
990
1278
  }
1279
+ const jsonSchema = convertSchemaToJsonSchema(outputSchema, {
1280
+ forStructuredOutput: true
1281
+ });
1282
+ if (!jsonSchema) {
1283
+ throw new Error("Failed to convert output schema to JSON Schema");
1284
+ }
1285
+ const validate = isStandardSchema(outputSchema) ? (data) => parseWithStandardSchema(outputSchema, data) : void 0;
991
1286
  const engine = new TextEngine(
992
1287
  {
993
1288
  adapter,
994
1289
  params: { ...textOptions, model, logger },
995
1290
  middleware,
996
- context
1291
+ context,
1292
+ finalStructuredOutput: {
1293
+ jsonSchema,
1294
+ yieldChunks: false,
1295
+ ...validate ? { validate } : {}
1296
+ }
997
1297
  },
998
1298
  logger
999
1299
  );
1000
1300
  for await (const _chunk of engine.run()) {
1001
1301
  }
1002
- const finalMessages = engine.getMessages();
1003
- const {
1004
- tools: _tools,
1005
- agentLoopStrategy: _als,
1006
- ...structuredTextOptions
1007
- } = textOptions;
1008
- const jsonSchema = convertSchemaToJsonSchema(outputSchema);
1009
- if (!jsonSchema) {
1010
- throw new Error("Failed to convert output schema to JSON Schema");
1011
- }
1012
- const providerName = adapter.provider ?? adapter.name;
1013
- logger.request(
1014
- `activity=chat-structured provider=${providerName} model=${model} messages=${finalMessages.length}`,
1015
- {
1016
- provider: providerName,
1017
- model,
1018
- messageCount: finalMessages.length
1019
- }
1020
- );
1021
- const result = await adapter.structuredOutput({
1022
- chatOptions: {
1023
- ...structuredTextOptions,
1024
- model,
1025
- messages: finalMessages,
1026
- logger
1027
- },
1028
- outputSchema: jsonSchema
1029
- });
1030
- if (isStandardSchema(outputSchema)) {
1031
- return parseWithStandardSchema(
1032
- outputSchema,
1033
- result.data
1302
+ const finalizationError = engine.getFinalizationError();
1303
+ if (finalizationError) {
1304
+ const err = new Error(
1305
+ finalizationError.message,
1306
+ finalizationError.cause !== void 0 ? { cause: finalizationError.cause } : void 0
1034
1307
  );
1308
+ if (finalizationError.code !== void 0) {
1309
+ Object.defineProperty(err, "code", {
1310
+ value: finalizationError.code,
1311
+ enumerable: true
1312
+ });
1313
+ }
1314
+ throw err;
1315
+ }
1316
+ const validated = engine.getValidatedStructuredOutput();
1317
+ if (validated) {
1318
+ return validated.value;
1319
+ }
1320
+ const result = engine.getStructuredOutputResult();
1321
+ if (!result) {
1322
+ throw new Error("structured output finalization produced no result");
1035
1323
  }
1036
1324
  return result.data;
1037
1325
  }
1038
- async function* fallbackStructuredOutputStream(adapter, options) {
1326
+ function readStructuredOutputCompleteValue(value) {
1327
+ if (typeof value !== "object" || value === null) return null;
1328
+ if (!("object" in value) || !("raw" in value)) return null;
1329
+ const raw = value.raw;
1330
+ if (typeof raw !== "string") return null;
1331
+ const reasoningField = value.reasoning;
1332
+ const reasoning = typeof reasoningField === "string" ? reasoningField : void 0;
1333
+ return {
1334
+ object: value.object,
1335
+ raw,
1336
+ ...reasoning !== void 0 ? { reasoning } : {}
1337
+ };
1338
+ }
1339
+ async function* fallbackStructuredOutputStream(adapter, options, onAdapterError) {
1039
1340
  const { chatOptions } = options;
1040
- const runId = chatOptions.runId ?? `mock-${Date.now()}`;
1041
- const threadId = chatOptions.threadId ?? `mock-${Date.now()}`;
1042
- const messageId = `mock-${Date.now()}-${Math.random().toString(36).slice(2)}`;
1341
+ const fallbackRand = Math.random().toString(36).slice(2);
1342
+ const runId = chatOptions.runId ?? `fallback-${Date.now()}-${fallbackRand}`;
1343
+ const threadId = chatOptions.threadId ?? `fallback-${Date.now()}-${fallbackRand}`;
1344
+ const messageId = `fallback-${Date.now()}-${fallbackRand}`;
1043
1345
  const model = chatOptions.model;
1044
1346
  const timestamp = Date.now();
1045
1347
  yield {
@@ -1053,10 +1355,12 @@ async function* fallbackStructuredOutputStream(adapter, options) {
1053
1355
  try {
1054
1356
  result = await adapter.structuredOutput(options);
1055
1357
  } catch (error) {
1056
- const message = error instanceof Error ? error.message : "Unknown error";
1358
+ onAdapterError?.(error);
1359
+ const message = error instanceof Error ? error.message : String(error);
1057
1360
  yield {
1058
1361
  type: EventType.RUN_ERROR,
1059
1362
  runId,
1363
+ threadId,
1060
1364
  model,
1061
1365
  timestamp,
1062
1366
  message,
@@ -1120,194 +1424,17 @@ async function* runStreamingStructuredOutputImpl(options, jsonSchema) {
1120
1424
  const { adapter, outputSchema, middleware, context, debug, ...textOptions } = options;
1121
1425
  const model = adapter.model;
1122
1426
  const logger = resolveDebugOption(debug);
1123
- const runId = textOptions.runId;
1124
- let finalMessages = convertMessagesToModelMessages(textOptions.messages ?? []);
1125
- if (textOptions.tools?.length) {
1126
- const engine = new TextEngine(
1127
- {
1128
- adapter,
1129
- params: { ...textOptions, model, logger, messages: finalMessages },
1130
- middleware,
1131
- context
1132
- },
1133
- logger
1134
- );
1135
- let agentLoopErrored = false;
1136
- try {
1137
- for await (const chunk of engine.run()) {
1138
- if (chunk.type === "RUN_STARTED" || chunk.type === "RUN_FINISHED") {
1139
- continue;
1140
- }
1141
- if (chunk.type === "RUN_ERROR") {
1142
- agentLoopErrored = true;
1143
- yield chunk;
1144
- continue;
1145
- }
1146
- yield chunk;
1147
- }
1148
- } catch (engineError) {
1149
- const message = engineError.message || "Agent loop failed";
1150
- logger.errors("runStreamingStructuredOutput agent loop failed", {
1151
- error: engineError,
1152
- source: "runStreamingStructuredOutput"
1153
- });
1154
- yield {
1155
- type: EventType.RUN_ERROR,
1156
- runId,
1157
- model,
1158
- timestamp: Date.now(),
1159
- message,
1160
- code: "agent-loop-failed",
1161
- error: { message, code: "agent-loop-failed" }
1162
- };
1163
- return;
1164
- }
1165
- if (agentLoopErrored) {
1166
- return;
1167
- }
1168
- finalMessages = engine.getMessages();
1169
- }
1170
- const {
1171
- tools: _tools,
1172
- agentLoopStrategy: _als,
1173
- ...structuredTextOptions
1174
- } = textOptions;
1175
- logger.request(
1176
- `activity=chat-structured-stream provider=${adapter.name} model=${model} messages=${finalMessages.length}`,
1427
+ const engine = new TextEngine(
1177
1428
  {
1178
- provider: adapter.name,
1179
- model,
1180
- messageCount: finalMessages.length
1181
- }
1429
+ adapter,
1430
+ params: { ...textOptions, model, logger },
1431
+ middleware,
1432
+ context,
1433
+ finalStructuredOutput: { jsonSchema, yieldChunks: true }
1434
+ },
1435
+ logger
1182
1436
  );
1183
- const structuredChatOptions = {
1184
- ...structuredTextOptions,
1185
- model,
1186
- messages: finalMessages,
1187
- logger,
1188
- request: textOptions.abortController ? { signal: textOptions.abortController.signal } : void 0
1189
- };
1190
- const stream = adapter.structuredOutputStream ? adapter.structuredOutputStream({
1191
- chatOptions: structuredChatOptions,
1192
- outputSchema: jsonSchema
1193
- }) : fallbackStructuredOutputStream(adapter, {
1194
- chatOptions: structuredChatOptions,
1195
- outputSchema: jsonSchema
1196
- });
1197
- let structuredMessageId = null;
1198
- let startEmitted = false;
1199
- const extractMessageId = (c) => {
1200
- const id = c.messageId;
1201
- return typeof id === "string" && id !== "" ? id : null;
1202
- };
1203
- const emitStartIfNeeded = function* (referenceChunk) {
1204
- if (startEmitted) return;
1205
- const idForStart = structuredMessageId ?? generateMessageId();
1206
- structuredMessageId = idForStart;
1207
- startEmitted = true;
1208
- yield {
1209
- type: EventType.CUSTOM,
1210
- name: "structured-output.start",
1211
- value: { messageId: idForStart },
1212
- model: "model" in referenceChunk ? referenceChunk.model ?? model : model,
1213
- timestamp: "timestamp" in referenceChunk ? referenceChunk.timestamp ?? Date.now() : Date.now(),
1214
- runId
1215
- };
1216
- };
1217
- for await (const chunk of stream) {
1218
- if (!structuredMessageId) {
1219
- if (chunk.type === EventType.TEXT_MESSAGE_START || chunk.type === EventType.TEXT_MESSAGE_CONTENT) {
1220
- structuredMessageId = extractMessageId(chunk);
1221
- }
1222
- }
1223
- if (chunk.type === EventType.RUN_ERROR && !startEmitted) {
1224
- yield* emitStartIfNeeded(chunk);
1225
- }
1226
- if (!structuredMessageId && chunk.type === EventType.TEXT_MESSAGE_CONTENT) {
1227
- yield {
1228
- type: EventType.RUN_ERROR,
1229
- runId,
1230
- model,
1231
- timestamp: Date.now(),
1232
- message: "Structured-output stream produced text content without a messageId; adapter is not honoring the AG-UI contract.",
1233
- code: "structured-output-missing-message-id"
1234
- };
1235
- return;
1236
- }
1237
- if (!startEmitted && structuredMessageId && (chunk.type === EventType.TEXT_MESSAGE_START || chunk.type === EventType.TEXT_MESSAGE_CONTENT)) {
1238
- startEmitted = true;
1239
- yield {
1240
- type: EventType.CUSTOM,
1241
- name: "structured-output.start",
1242
- value: { messageId: structuredMessageId },
1243
- model: "model" in chunk ? chunk.model ?? model : model,
1244
- timestamp: "timestamp" in chunk ? chunk.timestamp ?? Date.now() : Date.now(),
1245
- runId
1246
- };
1247
- }
1248
- if (chunk.type === EventType.CUSTOM && chunk.name === "structured-output.complete") {
1249
- const value = chunk.value;
1250
- if (isStandardSchema(outputSchema)) {
1251
- try {
1252
- const validated = parseWithStandardSchema(
1253
- outputSchema,
1254
- value.object
1255
- );
1256
- yield {
1257
- ...chunk,
1258
- // Forward `reasoning` through schema validation so consumers that
1259
- // only listen for the terminal event don't lose chain-of-thought.
1260
- // Tag with messageId so the client processor can snap the right
1261
- // assistant message's structured-output part.
1262
- value: {
1263
- object: validated,
1264
- raw: value.raw,
1265
- ...value.reasoning ? { reasoning: value.reasoning } : {},
1266
- ...structuredMessageId ? { messageId: structuredMessageId } : {}
1267
- }
1268
- };
1269
- continue;
1270
- } catch (err) {
1271
- const message = err.message || "Schema validation failed";
1272
- logger.errors(
1273
- "runStreamingStructuredOutput schema validation failed",
1274
- {
1275
- error: err,
1276
- source: "runStreamingStructuredOutput",
1277
- // Include reasoning in error meta so post-mortems can recover
1278
- // what the model thought through before producing invalid JSON.
1279
- ...value.reasoning ? { reasoning: value.reasoning } : {}
1280
- }
1281
- );
1282
- yield {
1283
- type: EventType.RUN_ERROR,
1284
- runId,
1285
- model: chunk.model ?? model,
1286
- timestamp: chunk.timestamp ?? Date.now(),
1287
- message,
1288
- code: "schema-validation",
1289
- error: {
1290
- message,
1291
- code: "schema-validation",
1292
- ...value.reasoning ? { reasoning: value.reasoning } : {}
1293
- }
1294
- };
1295
- return;
1296
- }
1297
- }
1298
- if (structuredMessageId) {
1299
- yield {
1300
- ...chunk,
1301
- value: {
1302
- ...chunk.value,
1303
- messageId: structuredMessageId
1304
- }
1305
- };
1306
- continue;
1307
- }
1308
- yield chunk;
1309
- continue;
1310
- }
1437
+ for await (const chunk of engine.run()) {
1311
1438
  yield chunk;
1312
1439
  }
1313
1440
  }