@framers/agentos 0.12.24 → 0.13.2

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 (109) hide show
  1. package/README.md +11 -11
  2. package/dist/api/AgentOS.d.ts +15 -0
  3. package/dist/api/AgentOS.d.ts.map +1 -1
  4. package/dist/api/AgentOS.js +82 -0
  5. package/dist/api/AgentOS.js.map +1 -1
  6. package/dist/api/agent.d.ts +7 -3
  7. package/dist/api/agent.d.ts.map +1 -1
  8. package/dist/api/agent.js.map +1 -1
  9. package/dist/api/embedText.d.ts.map +1 -1
  10. package/dist/api/embedText.js +1 -8
  11. package/dist/api/embedText.js.map +1 -1
  12. package/dist/api/gmi.d.ts.map +1 -1
  13. package/dist/api/gmi.js +38 -19
  14. package/dist/api/gmi.js.map +1 -1
  15. package/dist/api/runtime/AgentOSOrchestrator.d.ts.map +1 -1
  16. package/dist/api/runtime/AgentOSOrchestrator.js +23 -0
  17. package/dist/api/runtime/AgentOSOrchestrator.js.map +1 -1
  18. package/dist/api/runtime/agentCognitiveMemory.d.ts +12 -9
  19. package/dist/api/runtime/agentCognitiveMemory.d.ts.map +1 -1
  20. package/dist/api/runtime/agentCognitiveMemory.js +53 -17
  21. package/dist/api/runtime/agentCognitiveMemory.js.map +1 -1
  22. package/dist/api/runtime/completionGateway.d.ts.map +1 -1
  23. package/dist/api/runtime/completionGateway.js +7 -0
  24. package/dist/api/runtime/completionGateway.js.map +1 -1
  25. package/dist/api/runtime/gmiResults.d.ts +63 -6
  26. package/dist/api/runtime/gmiResults.d.ts.map +1 -1
  27. package/dist/api/runtime/gmiResults.js +83 -13
  28. package/dist/api/runtime/gmiResults.js.map +1 -1
  29. package/dist/api/runtime/gmiSession.d.ts +31 -6
  30. package/dist/api/runtime/gmiSession.d.ts.map +1 -1
  31. package/dist/api/runtime/gmiSession.js +144 -44
  32. package/dist/api/runtime/gmiSession.js.map +1 -1
  33. package/dist/api/runtime/gmiTranscript.d.ts +7 -6
  34. package/dist/api/runtime/gmiTranscript.d.ts.map +1 -1
  35. package/dist/api/runtime/gmiTranscript.js +6 -5
  36. package/dist/api/runtime/gmiTranscript.js.map +1 -1
  37. package/dist/api/runtime/provider-defaults.d.ts +9 -0
  38. package/dist/api/runtime/provider-defaults.d.ts.map +1 -1
  39. package/dist/api/runtime/provider-defaults.js +9 -0
  40. package/dist/api/runtime/provider-defaults.js.map +1 -1
  41. package/dist/api/types/AgentOSInput.d.ts +5 -0
  42. package/dist/api/types/AgentOSInput.d.ts.map +1 -1
  43. package/dist/api/types/OrchestratorConfig.d.ts +3 -0
  44. package/dist/api/types/OrchestratorConfig.d.ts.map +1 -1
  45. package/dist/cognition/emergent/executor/QuickJSExecutor.d.ts +8 -0
  46. package/dist/cognition/emergent/executor/QuickJSExecutor.d.ts.map +1 -1
  47. package/dist/cognition/emergent/executor/QuickJSExecutor.js +193 -36
  48. package/dist/cognition/emergent/executor/QuickJSExecutor.js.map +1 -1
  49. package/dist/cognition/emergent/executor/guest-prelude.d.ts +4 -0
  50. package/dist/cognition/emergent/executor/guest-prelude.d.ts.map +1 -1
  51. package/dist/cognition/emergent/executor/guest-prelude.js +21 -1
  52. package/dist/cognition/emergent/executor/guest-prelude.js.map +1 -1
  53. package/dist/cognition/emergent/executor/guest-surface.d.ts.map +1 -1
  54. package/dist/cognition/emergent/executor/guest-surface.js +7 -2
  55. package/dist/cognition/emergent/executor/guest-surface.js.map +1 -1
  56. package/dist/cognition/memory/CognitiveMemoryManager.d.ts +28 -21
  57. package/dist/cognition/memory/CognitiveMemoryManager.d.ts.map +1 -1
  58. package/dist/cognition/memory/CognitiveMemoryManager.js +76 -26
  59. package/dist/cognition/memory/CognitiveMemoryManager.js.map +1 -1
  60. package/dist/cognition/memory/retrieval/store/MemoryStore.d.ts +10 -2
  61. package/dist/cognition/memory/retrieval/store/MemoryStore.d.ts.map +1 -1
  62. package/dist/cognition/memory/retrieval/store/MemoryStore.js +11 -2
  63. package/dist/cognition/memory/retrieval/store/MemoryStore.js.map +1 -1
  64. package/dist/cognition/substrate/ConversationHistoryManager.d.ts +3 -1
  65. package/dist/cognition/substrate/ConversationHistoryManager.d.ts.map +1 -1
  66. package/dist/cognition/substrate/ConversationHistoryManager.js +9 -2
  67. package/dist/cognition/substrate/ConversationHistoryManager.js.map +1 -1
  68. package/dist/cognition/substrate/GMI.d.ts +33 -2
  69. package/dist/cognition/substrate/GMI.d.ts.map +1 -1
  70. package/dist/cognition/substrate/GMI.js +225 -71
  71. package/dist/cognition/substrate/GMI.js.map +1 -1
  72. package/dist/cognition/substrate/IGMI.d.ts +14 -3
  73. package/dist/cognition/substrate/IGMI.d.ts.map +1 -1
  74. package/dist/cognition/substrate/IGMI.js +2 -1
  75. package/dist/cognition/substrate/IGMI.js.map +1 -1
  76. package/dist/cognition/substrate/SentimentTracker.d.ts +8 -0
  77. package/dist/cognition/substrate/SentimentTracker.d.ts.map +1 -1
  78. package/dist/cognition/substrate/SentimentTracker.js +20 -0
  79. package/dist/cognition/substrate/SentimentTracker.js.map +1 -1
  80. package/dist/core/llm/providers/implementations/OpenAIProvider.d.ts.map +1 -1
  81. package/dist/core/llm/providers/implementations/OpenAIProvider.js +23 -4
  82. package/dist/core/llm/providers/implementations/OpenAIProvider.js.map +1 -1
  83. package/dist/core/utils/errors.d.ts +4 -0
  84. package/dist/core/utils/errors.d.ts.map +1 -1
  85. package/dist/core/utils/errors.js +8 -0
  86. package/dist/core/utils/errors.js.map +1 -1
  87. package/dist/extensions/ExtensionRegistry.d.ts.map +1 -1
  88. package/dist/extensions/ExtensionRegistry.js +4 -1
  89. package/dist/extensions/ExtensionRegistry.js.map +1 -1
  90. package/dist/io/speech/SpeechRuntime.d.ts.map +1 -1
  91. package/dist/io/speech/SpeechRuntime.js +5 -0
  92. package/dist/io/speech/SpeechRuntime.js.map +1 -1
  93. package/dist/io/speech/providers/MiniMaxTextToSpeechProvider.d.ts +34 -3
  94. package/dist/io/speech/providers/MiniMaxTextToSpeechProvider.d.ts.map +1 -1
  95. package/dist/io/speech/providers/MiniMaxTextToSpeechProvider.js +120 -11
  96. package/dist/io/speech/providers/MiniMaxTextToSpeechProvider.js.map +1 -1
  97. package/dist/safety/runtime/SpendMeter.d.ts +135 -0
  98. package/dist/safety/runtime/SpendMeter.d.ts.map +1 -0
  99. package/dist/safety/runtime/SpendMeter.js +61 -0
  100. package/dist/safety/runtime/SpendMeter.js.map +1 -0
  101. package/dist/safety/runtime/SqlSpendMeter.d.ts +78 -0
  102. package/dist/safety/runtime/SqlSpendMeter.d.ts.map +1 -0
  103. package/dist/safety/runtime/SqlSpendMeter.js +268 -0
  104. package/dist/safety/runtime/SqlSpendMeter.js.map +1 -0
  105. package/dist/safety/runtime/index.d.ts +4 -0
  106. package/dist/safety/runtime/index.d.ts.map +1 -1
  107. package/dist/safety/runtime/index.js +2 -0
  108. package/dist/safety/runtime/index.js.map +1 -1
  109. package/package.json +1 -1
@@ -54,6 +54,26 @@ function cacheDiagnosticsSeed(value) {
54
54
  const id = typeof value === 'object' ? value.previousMessageId : undefined;
55
55
  return { previousMessageId: typeof id === 'string' && id.length > 0 ? id : null };
56
56
  }
57
+ /** `value` when it is a usage report as a provider gives one: an object with a numeric token count. */
58
+ function asUsageReport(value) {
59
+ if (!value || typeof value !== 'object')
60
+ return undefined;
61
+ const { promptTokens, completionTokens, totalTokens } = value;
62
+ return [promptTokens, completionTokens, totalTokens].some((count) => typeof count === 'number' && Number.isFinite(count))
63
+ ? value
64
+ : undefined;
65
+ }
66
+ /** A message's content as a string two equal contents share: the text itself, or the JSON of its parts. */
67
+ function contentKey(content) {
68
+ if (typeof content === 'string')
69
+ return content;
70
+ try {
71
+ return JSON.stringify(content) ?? '';
72
+ }
73
+ catch {
74
+ return '';
75
+ }
76
+ }
57
77
  /** Adds one provider usage report to the turn's total. */
58
78
  function addUsage(total, usage) {
59
79
  total.promptTokens += usage.promptTokens || 0;
@@ -86,6 +106,16 @@ export class GMI {
86
106
  */
87
107
  this.turnSequence = 0;
88
108
  this.stateOwnerTurn = 0;
109
+ /**
110
+ * What the GMI needs to forget a turn once the history no longer holds it
111
+ * (see {@link GMI.forgetTurnsOutside}): the turn each trace entry was recorded
112
+ * in (entries recorded outside a turn have none), and each user turn's input
113
+ * as a comparison key, by turn id, oldest first.
114
+ */
115
+ this.traceEntryTurn = new WeakMap();
116
+ this.turnInputs = new Map();
117
+ /** Settles once the latest replaceHistory() or clearHistory() has also cleaned the sentiment history. */
118
+ this.historyForgetting = Promise.resolve();
89
119
  // (Self-reflection state is owned by MetapromptExecutor)
90
120
  // Cognitive Memory Bridge
91
121
  this.memoryBridge = null;
@@ -355,6 +385,57 @@ export class GMI {
355
385
  details: details ? JSON.parse(JSON.stringify(details)) : {},
356
386
  };
357
387
  this.reasoningTrace.entries.push(entry);
388
+ if (this.reasoningTrace.turnId)
389
+ this.traceEntryTurn.set(entry, this.reasoningTrace.turnId);
390
+ }
391
+ /**
392
+ * Drops what the GMI recorded about the turns `history` no longer holds, so
393
+ * that a replaced or cleared fact reaches no later model call through the
394
+ * records kept beside the history: the reflection metaprompt sends recent
395
+ * trace entries to the model as evidence.
396
+ *
397
+ * A recorded turn is kept when `history` has a user message with that turn's
398
+ * input, each message keeping one turn, the newest first. For every other
399
+ * turn, the details of its trace entries are emptied (the first characters of
400
+ * its input, its tool calls and results; the entries stay, with their type and
401
+ * message), and the sentiment tracker drops the input excerpts of its events
402
+ * and sentiment trends. The tracker's part runs asynchronously and has
403
+ * finished before the next turn starts.
404
+ *
405
+ * @param history - The conversation history after the replacement.
406
+ */
407
+ forgetTurnsOutside(history) {
408
+ const unclaimed = new Map();
409
+ for (const message of history) {
410
+ if (message.role !== 'user')
411
+ continue;
412
+ const key = contentKey(message.content);
413
+ unclaimed.set(key, (unclaimed.get(key) ?? 0) + 1);
414
+ }
415
+ const kept = new Set();
416
+ for (const [turnId, key] of [...this.turnInputs].reverse()) {
417
+ const left = unclaimed.get(key) ?? 0;
418
+ if (left === 0) {
419
+ this.turnInputs.delete(turnId);
420
+ continue;
421
+ }
422
+ unclaimed.set(key, left - 1);
423
+ kept.add(turnId);
424
+ }
425
+ for (const entry of this.reasoningTrace.entries) {
426
+ const turnId = this.traceEntryTurn.get(entry);
427
+ if (turnId === undefined || kept.has(turnId))
428
+ continue;
429
+ entry.details = {};
430
+ this.traceEntryTurn.delete(entry);
431
+ }
432
+ this.historyForgetting = this.historyForgetting
433
+ .then(() => this.sentimentTracker?.forgetTurnsExcept(kept))
434
+ .catch((error) => {
435
+ this.addTraceEntry(ReasoningEntryType.WARNING, 'Could not drop the sentiment history of turns removed from the conversation history.', {
436
+ error: error instanceof Error ? error.message : String(error),
437
+ });
438
+ });
358
439
  }
359
440
  stringifyTurnContent(content) {
360
441
  if (typeof content === 'string') {
@@ -462,16 +543,22 @@ export class GMI {
462
543
  * Makes `messages` the whole conversation history. An empty array is
463
544
  * authoritative: it empties the history (unlike
464
545
  * `metadata.conversationHistoryForPrompt`, which a turn ignores when empty).
465
- * The sentiment tracker and the metaprompts read the same history.
546
+ * The sentiment tracker and the metaprompts read the same history. What the
547
+ * GMI recorded about turns the new history no longer holds is dropped too:
548
+ * the details of their reasoning-trace entries and the input excerpts of
549
+ * their sentiment records (a turn stays while a user message with its input
550
+ * is in the history).
466
551
  */
467
552
  replaceHistory(messages) {
468
553
  this.conversationHistoryManager.clear();
469
554
  if (messages.length > 0)
470
555
  this.conversationHistoryManager.hydrate(messages);
556
+ this.forgetTurnsOutside(this.conversationHistoryManager.history);
471
557
  }
472
- /** Empties the conversation history. */
558
+ /** Empties the conversation history, and drops what the GMI recorded about its turns, as {@link GMI.replaceHistory} does. */
473
559
  clearHistory() {
474
560
  this.conversationHistoryManager.clear();
561
+ this.forgetTurnsOutside([]);
475
562
  }
476
563
  hydrateTurnContext(context) {
477
564
  if (typeof context.sessionId === 'string' && context.sessionId.trim()) {
@@ -666,6 +753,9 @@ export class GMI {
666
753
  const aggregatedUsage = { totalTokens: 0, promptTokens: 0, completionTokens: 0, breakdown: [] };
667
754
  let lastErrorForOutput = undefined;
668
755
  try {
756
+ // A history replaced or cleared just before this turn has finished dropping
757
+ // the sentiment records of the turns it removed.
758
+ await this.historyForgetting;
669
759
  if (turnInput.userContextOverride) {
670
760
  const mergedPreferences = turnInput.userContextOverride.preferences &&
671
761
  typeof turnInput.userContextOverride.preferences === 'object'
@@ -700,6 +790,19 @@ export class GMI {
700
790
  // assistant replies and tool results the loop below adds.
701
791
  const historyAtTurnStart = [...this.conversationHistoryManager.history];
702
792
  const currentUserMessage = historyAtTurnStart.find((message) => message.role === 'user' && !messagesBeforeInput.has(message));
793
+ // The turn's input, so a later replaceHistory() or clearHistory() can tell
794
+ // whether the history still holds this turn. Bounded like the trace:
795
+ // every turn on the trace has at least one entry there.
796
+ if (currentUserMessage) {
797
+ this.turnInputs.delete(turnId);
798
+ this.turnInputs.set(turnId, contentKey(currentUserMessage.content));
799
+ while (this.turnInputs.size > this.traceLimits.maxEntries) {
800
+ const oldest = this.turnInputs.keys().next().value;
801
+ if (oldest === undefined)
802
+ break;
803
+ this.turnInputs.delete(oldest);
804
+ }
805
+ }
703
806
  const currentTurnText = currentUserMessage?.content
704
807
  ? (typeof currentUserMessage.content === 'string'
705
808
  ? currentUserMessage.content
@@ -900,34 +1003,51 @@ export class GMI {
900
1003
  toolsForLLM = discoveredTools;
901
1004
  }
902
1005
  }
903
- // Every completion option the persona and the turn set (D4b); temperature and
904
- // the output budget keep their defaults; tools, user and streaming are the GMI's.
1006
+ // Every completion option the persona and the turn set (D4b), the turn's
1007
+ // over the persona's; one neither sets is not sent, so the provider's
1008
+ // default applies. Tools, user and streaming are the GMI's.
905
1009
  const turnOptions = (turnInput.metadata?.options ?? {});
906
1010
  const personaOptions = (this.activePersona.defaultModelCompletionOptions ?? {});
1011
+ // The provider's end-user field: the host's choice when the turn names one
1012
+ // (null sends none), else the turn's user.
1013
+ const providerUserId = turnInput.metadata && 'providerUserId' in turnInput.metadata
1014
+ ? turnInput.metadata.providerUserId
1015
+ : this.currentUserContext.userId;
907
1016
  const llmOptions = {
908
1017
  ...pickCompletionOptions(personaOptions),
909
1018
  ...pickCompletionOptions(turnOptions),
910
1019
  ...(stepCacheDiagnostics ? { cacheDiagnostics: { ...stepCacheDiagnostics } } : {}),
911
- temperature: turnOptions.temperature ?? personaOptions.temperature ?? 0.7,
912
- maxTokens: turnOptions.maxTokens ?? personaOptions.maxTokens ?? 2048,
1020
+ // The turn's own abort signal: aborting it ends the model call in progress
1021
+ // with the provider's abort chunk, and every later call of the turn at once.
1022
+ ...(turnOptions.abortSignal ? { abortSignal: turnOptions.abortSignal } : {}),
913
1023
  tools: toolsForLLM.length > 0 ? toolsForLLM.map(t => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.inputSchema } })) : undefined,
914
- toolChoice: turnOptions.toolChoice ?? personaOptions.toolChoice ?? (toolsForLLM.length > 0 ? "auto" : undefined),
915
- userId: this.currentUserContext.userId,
1024
+ // Only with tools: OpenAI rejects a tool_choice on a request that offers none
1025
+ // (HTTP 400, "'tool_choice' is only allowed when 'tools' are specified").
1026
+ toolChoice: toolsForLLM.length > 0
1027
+ ? (turnOptions.toolChoice ?? personaOptions.toolChoice ?? "auto")
1028
+ : undefined,
1029
+ ...(providerUserId ? { userId: providerUserId } : {}),
916
1030
  stream: true,
917
1031
  };
918
1032
  // A schema travels to the gateway, which lowers it per hop (D9); it is not a provider option.
919
1033
  const responseSchema = turnOptions.responseSchema;
920
1034
  const schemaName = turnOptions.schemaName;
921
- // The gateway's route: the model asked for, the user's text as the router's
922
- // task hint, the tools, and the call options each fallback hop derives its
923
- // overrides from. Every hop that could not start is traced.
1035
+ // The gateway's route: the model asked for, the last user message (its text
1036
+ // is the router's task hint: the text parts of a multimodal message, and on
1037
+ // a continuation the message being answered, never the continuation's own
1038
+ // internal text), the tools, and the call options each fallback hop derives
1039
+ // its overrides from. Every hop that could not start is traced.
924
1040
  let lastHopError;
925
- const userText = this.stringifyTurnContent(turnInput.content) ?? '';
1041
+ let lastUserMessage;
1042
+ for (let i = historyAtTurnStart.length - 1; i >= 0 && !lastUserMessage; i--) {
1043
+ if (historyAtTurnStart[i].role === 'user')
1044
+ lastUserMessage = historyAtTurnStart[i];
1045
+ }
926
1046
  const route = gateway
927
1047
  ? {
928
1048
  modelId: modelIdToUse,
929
1049
  providerId: providerIdForModel,
930
- messages: userText ? [{ role: 'user', content: userText }] : [],
1050
+ messages: lastUserMessage ? [lastUserMessage] : [],
931
1051
  tools: llmOptions.tools,
932
1052
  callOptions: { maxTokens: llmOptions.maxTokens, effort: llmOptions.effort, cache: llmOptions.cache },
933
1053
  onFallback: ({ from, to, hop, error }) => this.addTraceEntry(ReasoningEntryType.WARNING, `Model fallback from '${from}' to '${to}' (hop ${hop}): ${error.message}`),
@@ -942,7 +1062,17 @@ export class GMI {
942
1062
  // prompt is built, so the prompt is budgeted for the serving model (D1).
943
1063
  let resolution = null;
944
1064
  if (gateway && route) {
945
- resolution = this.turnResolution ?? (await gateway.resolve(route));
1065
+ try {
1066
+ resolution = this.turnResolution ?? (await gateway.resolve(route));
1067
+ }
1068
+ catch (resolveError) {
1069
+ // resolve() throws only when the primary cannot be set up (no
1070
+ // credentials, a provider or model that does not resolve): a
1071
+ // configuration error, not a failure of the turn's processing.
1072
+ if (resolveError instanceof GMIError)
1073
+ throw resolveError;
1074
+ throw new GMIError(resolveError instanceof Error ? resolveError.message : String(resolveError), GMIErrorCode.CONFIGURATION_ERROR, { turnId, providerId: providerIdForModel, modelId: modelIdToUse });
1075
+ }
946
1076
  if (!resolution) {
947
1077
  throw new GMIError(`No model provider could serve the turn${lastHopError ? `: ${lastHopError.message}` : '.'}`, GMIErrorCode.LLM_PROVIDER_UNAVAILABLE, { turnId });
948
1078
  }
@@ -1032,65 +1162,89 @@ export class GMI {
1032
1162
  }
1033
1163
  this.addTraceEntry(ReasoningEntryType.LLM_CALL_START, `Streaming from ${modelTargetInfo.modelId}${resolution ? ` (hop ${resolution.hop})` : ''}. Tools: ${toolsForLLM.length}.`);
1034
1164
  let textDeltaEmitted = false;
1035
- for await (const chunk of attempt) {
1036
- if (chunk.error) {
1037
- throw new GMIError(`LLM stream error: ${chunk.error.message}`, GMIErrorCode.LLM_PROVIDER_ERROR, chunk.error.details);
1038
- }
1039
- if (chunk.responseTextDelta) {
1040
- currentIterationTextResponse += chunk.responseTextDelta;
1041
- aggregatedResponseText += chunk.responseTextDelta; // Aggregate for final output
1042
- yield this.createOutputChunk(turnInput.interactionId, GMIOutputChunkType.TEXT_DELTA, chunk.responseTextDelta, { usage: chunk.usage });
1043
- textDeltaEmitted = true;
1044
- }
1045
- // Handle fully formed tool_calls if present in the chunk's message
1046
- const choice = chunk.choices?.[0];
1047
- // Capture extended-thinking blocks from the final chunk so they ride
1048
- // the assistant turn into history (replayed verbatim next tool turn).
1049
- if (choice?.message?.thinkingBlocks?.length) {
1050
- currentIterationThinkingBlocks = choice.message.thinkingBlocks;
1051
- }
1052
- // The gateway lifts a streamed schema tool call into `structuredOutput` (D9).
1053
- const structured = chunk.structuredOutput;
1054
- if (structured !== undefined)
1055
- stepStructuredOutput = structured;
1056
- if (choice?.message?.tool_calls && choice.message.tool_calls.length > 0) {
1057
- currentIterationToolCallRequests = choice.message.tool_calls.map((tc) => ({
1058
- id: tc.id || `toolcall-${uuidv4()}`, // Ensure ID
1059
- name: tc.function.name,
1060
- arguments: typeof tc.function.arguments === 'string'
1061
- ? JSON.parse(tc.function.arguments)
1062
- : tc.function.arguments,
1063
- ...(tc.thoughtSignature ? { thoughtSignature: tc.thoughtSignature } : {}),
1064
- }));
1065
- aggregatedToolCalls.push(...currentIterationToolCallRequests); // Aggregate for final output
1066
- yield this.createOutputChunk(turnInput.interactionId, GMIOutputChunkType.TOOL_CALL_REQUEST, [...currentIterationToolCallRequests], {
1067
- metadata: {
1068
- executionMode: 'internal',
1069
- requiresExternalToolResult: false,
1070
- },
1165
+ // The usage on the error chunk that ended the step, when it carried one.
1166
+ let errorChunkUsage;
1167
+ try {
1168
+ for await (const chunk of attempt) {
1169
+ if (chunk.error) {
1170
+ errorChunkUsage = asUsageReport(chunk.usage);
1171
+ throw new GMIError(`LLM stream error: ${chunk.error.message}`, GMIErrorCode.LLM_PROVIDER_ERROR, chunk.error.details);
1172
+ }
1173
+ if (chunk.responseTextDelta) {
1174
+ currentIterationTextResponse += chunk.responseTextDelta;
1175
+ aggregatedResponseText += chunk.responseTextDelta; // Aggregate for final output
1176
+ yield this.createOutputChunk(turnInput.interactionId, GMIOutputChunkType.TEXT_DELTA, chunk.responseTextDelta, { usage: chunk.usage });
1177
+ textDeltaEmitted = true;
1178
+ }
1179
+ // Handle fully formed tool_calls if present in the chunk's message
1180
+ const choice = chunk.choices?.[0];
1181
+ // Capture extended-thinking blocks from the final chunk so they ride
1182
+ // the assistant turn into history (replayed verbatim next tool turn).
1183
+ if (choice?.message?.thinkingBlocks?.length) {
1184
+ currentIterationThinkingBlocks = choice.message.thinkingBlocks;
1185
+ }
1186
+ // The gateway lifts a streamed schema tool call into `structuredOutput` (D9).
1187
+ const structured = chunk.structuredOutput;
1188
+ if (structured !== undefined)
1189
+ stepStructuredOutput = structured;
1190
+ if (choice?.message?.tool_calls && choice.message.tool_calls.length > 0) {
1191
+ currentIterationToolCallRequests = choice.message.tool_calls.map((tc) => ({
1192
+ id: tc.id || `toolcall-${uuidv4()}`, // Ensure ID
1193
+ name: tc.function.name,
1194
+ arguments: typeof tc.function.arguments === 'string'
1195
+ ? JSON.parse(tc.function.arguments)
1196
+ : tc.function.arguments,
1197
+ ...(tc.thoughtSignature ? { thoughtSignature: tc.thoughtSignature } : {}),
1198
+ }));
1199
+ aggregatedToolCalls.push(...currentIterationToolCallRequests); // Aggregate for final output
1200
+ yield this.createOutputChunk(turnInput.interactionId, GMIOutputChunkType.TOOL_CALL_REQUEST, [...currentIterationToolCallRequests], {
1201
+ metadata: {
1202
+ executionMode: 'internal',
1203
+ requiresExternalToolResult: false,
1204
+ },
1205
+ });
1206
+ this.addTraceEntry(ReasoningEntryType.TOOL_CALL_REQUESTED, `LLM requested tool(s).`, { requests: currentIterationToolCallRequests });
1207
+ }
1208
+ // A provider that sends no deltas: its final content is the step's text, emitted once (D4d).
1209
+ if (chunk.isFinal && !textDeltaEmitted && typeof choice?.message?.content === 'string' && choice.message.content.length > 0) {
1210
+ currentIterationTextResponse = choice.message.content;
1211
+ aggregatedResponseText += choice.message.content;
1212
+ yield this.createOutputChunk(turnInput.interactionId, GMIOutputChunkType.TEXT_DELTA, choice.message.content, { usage: chunk.usage });
1213
+ textDeltaEmitted = true;
1214
+ }
1215
+ if (chunk.isFinal && choice?.finishReason) {
1216
+ stepFinishReason = choice.finishReason;
1217
+ this.addTraceEntry(ReasoningEntryType.LLM_CALL_COMPLETE, `LLM stream part finished. Reason: ${choice.finishReason}`, { usage: chunk.usage });
1218
+ }
1219
+ if (chunk.isFinal)
1220
+ stepFinalChunk = chunk;
1221
+ // Usage on every chunk that carries it, choice or not (D4c). Providers report
1222
+ // the request's running total, so the turn total takes the step's last value once.
1223
+ if (chunk.usage) {
1224
+ stepUsage = chunk.usage;
1225
+ yield this.createOutputChunk(turnInput.interactionId, GMIOutputChunkType.USAGE_UPDATE, chunk.usage);
1226
+ }
1227
+ } // End LLM stream
1228
+ }
1229
+ catch (stepError) {
1230
+ // A step that fails once its attempt has started was still billed.
1231
+ // Providers report the request's running total, so the bill is the
1232
+ // latest report: the error chunk's own usage, else the usage the
1233
+ // error carries (a refused turn reports it), else the last usage the
1234
+ // step reported. It counts toward the turn once and is reported on a
1235
+ // USAGE_UPDATE of its own, marked as a failed attempt's, because no
1236
+ // STEP_FINISHED will carry it.
1237
+ const billed = errorChunkUsage ??
1238
+ asUsageReport(stepError?.details?.usage) ??
1239
+ stepUsage;
1240
+ if (billed) {
1241
+ addUsage(aggregatedUsage, billed);
1242
+ yield this.createOutputChunk(turnInput.interactionId, GMIOutputChunkType.USAGE_UPDATE, billed, {
1243
+ metadata: { attemptFailed: true, hop: resolution?.hop ?? 0, providerId: modelTargetInfo.providerId, modelId: modelTargetInfo.modelId },
1071
1244
  });
1072
- this.addTraceEntry(ReasoningEntryType.TOOL_CALL_REQUESTED, `LLM requested tool(s).`, { requests: currentIterationToolCallRequests });
1073
- }
1074
- // A provider that sends no deltas: its final content is the step's text, emitted once (D4d).
1075
- if (chunk.isFinal && !textDeltaEmitted && typeof choice?.message?.content === 'string' && choice.message.content.length > 0) {
1076
- currentIterationTextResponse = choice.message.content;
1077
- aggregatedResponseText += choice.message.content;
1078
- yield this.createOutputChunk(turnInput.interactionId, GMIOutputChunkType.TEXT_DELTA, choice.message.content, { usage: chunk.usage });
1079
- textDeltaEmitted = true;
1080
- }
1081
- if (chunk.isFinal && choice?.finishReason) {
1082
- stepFinishReason = choice.finishReason;
1083
- this.addTraceEntry(ReasoningEntryType.LLM_CALL_COMPLETE, `LLM stream part finished. Reason: ${choice.finishReason}`, { usage: chunk.usage });
1084
1245
  }
1085
- if (chunk.isFinal)
1086
- stepFinalChunk = chunk;
1087
- // Usage on every chunk that carries it, choice or not (D4c). Providers report
1088
- // the request's running total, so the turn total takes the step's last value once.
1089
- if (chunk.usage) {
1090
- stepUsage = chunk.usage;
1091
- yield this.createOutputChunk(turnInput.interactionId, GMIOutputChunkType.USAGE_UPDATE, chunk.usage);
1092
- }
1093
- } // End LLM stream
1246
+ throw stepError;
1247
+ }
1094
1248
  if (gateway && route && resolution && attemptOutcome) {
1095
1249
  const outcome = await attemptOutcome;
1096
1250
  if (outcome.kind === 'hopFailed') {