@framers/agentos 0.13.1 → 0.13.3

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