@framers/agentos 0.12.3 → 0.12.5

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 (119) hide show
  1. package/dist/api/agent.d.ts +7 -5
  2. package/dist/api/agent.d.ts.map +1 -1
  3. package/dist/api/agent.js.map +1 -1
  4. package/dist/api/generateObject.d.ts +5 -5
  5. package/dist/api/generateText.d.ts +173 -50
  6. package/dist/api/generateText.d.ts.map +1 -1
  7. package/dist/api/generateText.js +272 -81
  8. package/dist/api/generateText.js.map +1 -1
  9. package/dist/api/index.d.ts +6 -1
  10. package/dist/api/index.d.ts.map +1 -1
  11. package/dist/api/index.js +5 -1
  12. package/dist/api/index.js.map +1 -1
  13. package/dist/api/runtime/GMIChunkTransformer.d.ts +1 -1
  14. package/dist/api/runtime/GMIChunkTransformer.d.ts.map +1 -1
  15. package/dist/api/runtime/GMIChunkTransformer.js +7 -2
  16. package/dist/api/runtime/GMIChunkTransformer.js.map +1 -1
  17. package/dist/api/runtime/completionGateway.d.ts +98 -0
  18. package/dist/api/runtime/completionGateway.d.ts.map +1 -0
  19. package/dist/api/runtime/completionGateway.js +347 -0
  20. package/dist/api/runtime/completionGateway.js.map +1 -0
  21. package/dist/api/runtime/contextWindowFit.d.ts +49 -0
  22. package/dist/api/runtime/contextWindowFit.d.ts.map +1 -0
  23. package/dist/api/runtime/contextWindowFit.js +109 -0
  24. package/dist/api/runtime/contextWindowFit.js.map +1 -0
  25. package/dist/api/runtime/gatewayProviderManager.d.ts +37 -0
  26. package/dist/api/runtime/gatewayProviderManager.d.ts.map +1 -0
  27. package/dist/api/runtime/gatewayProviderManager.js +49 -0
  28. package/dist/api/runtime/gatewayProviderManager.js.map +1 -0
  29. package/dist/api/streamText.d.ts.map +1 -1
  30. package/dist/api/streamText.js +90 -6
  31. package/dist/api/streamText.js.map +1 -1
  32. package/dist/cognition/emergent/ComposableToolBuilder.d.ts.map +1 -1
  33. package/dist/cognition/emergent/ComposableToolBuilder.js +16 -8
  34. package/dist/cognition/emergent/ComposableToolBuilder.js.map +1 -1
  35. package/dist/cognition/emergent/EffectsStore.d.ts +58 -0
  36. package/dist/cognition/emergent/EffectsStore.d.ts.map +1 -0
  37. package/dist/cognition/emergent/EffectsStore.js +96 -0
  38. package/dist/cognition/emergent/EffectsStore.js.map +1 -0
  39. package/dist/cognition/emergent/EmergentCapabilityEngine.d.ts +22 -2
  40. package/dist/cognition/emergent/EmergentCapabilityEngine.d.ts.map +1 -1
  41. package/dist/cognition/emergent/EmergentCapabilityEngine.js +128 -19
  42. package/dist/cognition/emergent/EmergentCapabilityEngine.js.map +1 -1
  43. package/dist/cognition/emergent/EmergentJudge.d.ts +1 -1
  44. package/dist/cognition/emergent/EmergentJudge.d.ts.map +1 -1
  45. package/dist/cognition/emergent/EmergentToolRegistry.d.ts +12 -3
  46. package/dist/cognition/emergent/EmergentToolRegistry.d.ts.map +1 -1
  47. package/dist/cognition/emergent/EmergentToolRegistry.js +51 -4
  48. package/dist/cognition/emergent/EmergentToolRegistry.js.map +1 -1
  49. package/dist/cognition/emergent/ForgeToolMetaTool.d.ts.map +1 -1
  50. package/dist/cognition/emergent/ForgeToolMetaTool.js +4 -1
  51. package/dist/cognition/emergent/ForgeToolMetaTool.js.map +1 -1
  52. package/dist/cognition/emergent/SandboxedToolForge.d.ts +55 -51
  53. package/dist/cognition/emergent/SandboxedToolForge.d.ts.map +1 -1
  54. package/dist/cognition/emergent/SandboxedToolForge.js +103 -109
  55. package/dist/cognition/emergent/SandboxedToolForge.js.map +1 -1
  56. package/dist/cognition/emergent/ToolPackage.d.ts +2 -2
  57. package/dist/cognition/emergent/ToolPackage.d.ts.map +1 -1
  58. package/dist/cognition/emergent/broker/CapabilityBroker.d.ts +58 -0
  59. package/dist/cognition/emergent/broker/CapabilityBroker.d.ts.map +1 -0
  60. package/dist/cognition/emergent/broker/CapabilityBroker.js +288 -0
  61. package/dist/cognition/emergent/broker/CapabilityBroker.js.map +1 -0
  62. package/dist/cognition/emergent/broker/fetch.d.ts +44 -0
  63. package/dist/cognition/emergent/broker/fetch.d.ts.map +1 -0
  64. package/dist/cognition/emergent/broker/fetch.js +197 -0
  65. package/dist/cognition/emergent/broker/fetch.js.map +1 -0
  66. package/dist/cognition/emergent/broker/fs-read.d.ts +63 -0
  67. package/dist/cognition/emergent/broker/fs-read.d.ts.map +1 -0
  68. package/dist/cognition/emergent/broker/fs-read.js +133 -0
  69. package/dist/cognition/emergent/broker/fs-read.js.map +1 -0
  70. package/dist/cognition/emergent/broker/refusal.d.ts +10 -0
  71. package/dist/cognition/emergent/broker/refusal.d.ts.map +1 -0
  72. package/dist/cognition/emergent/broker/refusal.js +13 -0
  73. package/dist/cognition/emergent/broker/refusal.js.map +1 -0
  74. package/dist/cognition/emergent/ceiling.d.ts +75 -0
  75. package/dist/cognition/emergent/ceiling.d.ts.map +1 -0
  76. package/dist/cognition/emergent/ceiling.js +166 -0
  77. package/dist/cognition/emergent/ceiling.js.map +1 -0
  78. package/dist/cognition/emergent/index.d.ts +2 -0
  79. package/dist/cognition/emergent/index.d.ts.map +1 -1
  80. package/dist/cognition/emergent/index.js +1 -0
  81. package/dist/cognition/emergent/index.js.map +1 -1
  82. package/dist/cognition/emergent/types.d.ts +75 -2
  83. package/dist/cognition/emergent/types.d.ts.map +1 -1
  84. package/dist/cognition/emergent/types.js.map +1 -1
  85. package/dist/cognition/substrate/GMI.d.ts +21 -0
  86. package/dist/cognition/substrate/GMI.d.ts.map +1 -1
  87. package/dist/cognition/substrate/GMI.js +286 -88
  88. package/dist/cognition/substrate/GMI.js.map +1 -1
  89. package/dist/cognition/substrate/IGMI.d.ts +48 -1
  90. package/dist/cognition/substrate/IGMI.d.ts.map +1 -1
  91. package/dist/cognition/substrate/IGMI.js +4 -0
  92. package/dist/cognition/substrate/IGMI.js.map +1 -1
  93. package/dist/core/llm/providers/errors/ContextWindowExceededError.d.ts +22 -0
  94. package/dist/core/llm/providers/errors/ContextWindowExceededError.d.ts.map +1 -0
  95. package/dist/core/llm/providers/errors/ContextWindowExceededError.js +21 -0
  96. package/dist/core/llm/providers/errors/ContextWindowExceededError.js.map +1 -0
  97. package/dist/core/llm/providers/implementations/OpenRouterProvider.d.ts.map +1 -1
  98. package/dist/core/llm/providers/implementations/OpenRouterProvider.js +34 -13
  99. package/dist/core/llm/providers/implementations/OpenRouterProvider.js.map +1 -1
  100. package/dist/core/llm/routing/IModelRouter.d.ts +6 -0
  101. package/dist/core/llm/routing/IModelRouter.d.ts.map +1 -1
  102. package/dist/core/llm/routing/PolicyAwareRouter.d.ts +7 -0
  103. package/dist/core/llm/routing/PolicyAwareRouter.d.ts.map +1 -1
  104. package/dist/core/llm/routing/PolicyAwareRouter.js +12 -0
  105. package/dist/core/llm/routing/PolicyAwareRouter.js.map +1 -1
  106. package/dist/core/llm/routing/UncensoredModelCatalog.d.ts +37 -0
  107. package/dist/core/llm/routing/UncensoredModelCatalog.d.ts.map +1 -1
  108. package/dist/core/llm/routing/UncensoredModelCatalog.js +64 -74
  109. package/dist/core/llm/routing/UncensoredModelCatalog.js.map +1 -1
  110. package/dist/core/tools/ITool.d.ts +45 -0
  111. package/dist/core/tools/ITool.d.ts.map +1 -1
  112. package/dist/core/tools/ToolOrchestrator.d.ts.map +1 -1
  113. package/dist/core/tools/ToolOrchestrator.js +4 -8
  114. package/dist/core/tools/ToolOrchestrator.js.map +1 -1
  115. package/dist/index.d.ts +5 -0
  116. package/dist/index.d.ts.map +1 -1
  117. package/dist/index.js +4 -0
  118. package/dist/index.js.map +1 -1
  119. package/package.json +1 -1
@@ -25,6 +25,31 @@ import { feedbackTraceMessage } from './userFeedback.js';
25
25
  import { resolveReasoningTraceLimits } from './reasoningTraceLimits.js';
26
26
  const DEFAULT_MAX_CONVERSATION_HISTORY_TURNS = 20;
27
27
  const DEFAULT_SELF_REFLECTION_INTERVAL_TURNS = 5;
28
+ /** The completion options a persona's defaults and a turn's `metadata.options` may set (design D4b). */
29
+ const FORWARDED_COMPLETION_OPTION_KEYS = [
30
+ 'temperature', 'maxTokens', 'topP', 'frequencyPenalty', 'presencePenalty', 'thinking', 'effort', 'cache',
31
+ 'promptCacheKey', 'promptCacheRetention', 'serviceTier', 'requestTimeout', 'customModelParams',
32
+ 'stopSequences', 'responseFormat', 'toolChoice',
33
+ ];
34
+ /** The forwarded completion options `source` sets; unset keys are left out so a later layer can fill them. */
35
+ function pickCompletionOptions(source) {
36
+ const picked = {};
37
+ if (!source)
38
+ return picked;
39
+ for (const key of FORWARDED_COMPLETION_OPTION_KEYS) {
40
+ if (source[key] !== undefined)
41
+ picked[key] = source[key];
42
+ }
43
+ return picked;
44
+ }
45
+ /** Adds one provider usage report to the turn's total. */
46
+ function addUsage(total, usage) {
47
+ total.promptTokens += usage.promptTokens || 0;
48
+ total.completionTokens += usage.completionTokens || 0;
49
+ total.totalTokens = total.promptTokens + total.completionTokens;
50
+ if (usage.costUSD)
51
+ total.totalCostUSD = (total.totalCostUSD || 0) + usage.costUSD;
52
+ }
28
53
  /**
29
54
  * @class GMI
30
55
  * @implements {IGMI}
@@ -421,6 +446,21 @@ export class GMI {
421
446
  hydrateConversationHistory(conversationHistory) {
422
447
  this.conversationHistoryManager.hydrate(conversationHistory);
423
448
  }
449
+ /**
450
+ * Makes `messages` the whole conversation history. An empty array is
451
+ * authoritative: it empties the history (unlike
452
+ * `metadata.conversationHistoryForPrompt`, which a turn ignores when empty).
453
+ * The sentiment tracker and the metaprompts read the same history.
454
+ */
455
+ replaceHistory(messages) {
456
+ this.conversationHistoryManager.clear();
457
+ if (messages.length > 0)
458
+ this.conversationHistoryManager.hydrate(messages);
459
+ }
460
+ /** Empties the conversation history. */
461
+ clearHistory() {
462
+ this.conversationHistoryManager.clear();
463
+ }
424
464
  hydrateTurnContext(context) {
425
465
  if (typeof context.sessionId === 'string' && context.sessionId.trim()) {
426
466
  this.reasoningTrace.sessionId = context.sessionId.trim();
@@ -561,6 +601,25 @@ export class GMI {
561
601
  return 'google_function_calling';
562
602
  return 'openai_functions';
563
603
  }
604
+ /**
605
+ * The model target of a turn without a completion gateway: the runtime
606
+ * path's lookup through the provider manager, with the 8192-token window
607
+ * and no capabilities when the manager knows nothing about the model.
608
+ */
609
+ async legacyModelTargetInfo(modelId, providerId) {
610
+ const modelDetails = await this.llmProviderManager.getModelInfo(modelId, providerId);
611
+ return {
612
+ modelId,
613
+ providerId: modelDetails?.providerId || providerId || this.llmProviderManager.getProviderForModel(modelId)?.providerId || 'unknown',
614
+ maxContextTokens: modelDetails?.contextWindowSize || 8192,
615
+ capabilities: modelDetails?.capabilities || [],
616
+ promptFormatType: this.determinePromptFormat(),
617
+ toolSupport: {
618
+ supported: modelDetails?.capabilities.includes('tool_use') || false,
619
+ format: this.determineToolFormat(modelDetails, providerId),
620
+ },
621
+ };
622
+ }
564
623
  /** @inheritdoc */
565
624
  async *processTurnStream(turnInput) {
566
625
  const continuationAllowedStates = turnInput.metadata?.isToolContinuation === true
@@ -572,6 +631,10 @@ export class GMI {
572
631
  }
573
632
  this.state = GMIPrimeState.PROCESSING;
574
633
  const turnNumber = ++this.turnSequence;
634
+ // A user turn starts at the primary hop; a tool continuation stays on the
635
+ // hop that served the step it continues.
636
+ if (turnInput.metadata?.isToolContinuation !== true)
637
+ this.turnResolution = undefined;
575
638
  this.stateOwnerTurn = turnNumber;
576
639
  // False once a newer turn has started; lifecycle writes below check it.
577
640
  const ownsState = () => this.stateOwnerTurn === turnNumber;
@@ -787,41 +850,13 @@ export class GMI {
787
850
  assembledMemoryContext: assembledMemoryContext ?? undefined,
788
851
  // tools: this.activePersona.embeddedTools, // If ITool[] and PromptComponents.tools takes ITool[]
789
852
  };
853
+ const gateway = this.config.completionGateway;
790
854
  const preferredModelIdFromInput = turnInput.metadata?.options?.preferredModelId;
791
855
  const modelIdToUse = preferredModelIdFromInput || this.activePersona.defaultModelId || this.config.defaultLlmModelId;
792
856
  const providerIdForModel = this.activePersona.defaultProviderId || this.config.defaultLlmProviderId;
793
857
  if (!modelIdToUse) {
794
858
  throw new GMIError("Could not determine modelId for LLM call.", GMIErrorCode.CONFIGURATION_ERROR, { turnId });
795
859
  }
796
- const modelDetails = await this.llmProviderManager.getModelInfo(modelIdToUse, providerIdForModel);
797
- const modelTargetInfo = {
798
- modelId: modelIdToUse,
799
- providerId: modelDetails?.providerId || providerIdForModel || this.llmProviderManager.getProviderForModel(modelIdToUse)?.providerId || 'unknown',
800
- maxContextTokens: modelDetails?.contextWindowSize || 8192, // Default fallback
801
- capabilities: modelDetails?.capabilities || [],
802
- promptFormatType: this.determinePromptFormat(),
803
- toolSupport: {
804
- supported: modelDetails?.capabilities.includes('tool_use') || false,
805
- format: this.determineToolFormat(modelDetails, providerIdForModel),
806
- },
807
- };
808
- const promptEngineResult = await this.promptEngine.constructPrompt(promptComponents, modelTargetInfo, promptExecContext);
809
- promptEngineResult.issues?.forEach(issue => this.addTraceEntry(ReasoningEntryType.WARNING, `Prompt Engine Issue: ${issue.message}`, issue));
810
- // A failed template leaves the prompt empty, and every provider takes
811
- // a non-empty ChatMessage[]; fail the turn rather than send nothing.
812
- const promptMessages = promptEngineResult.prompt;
813
- if (!Array.isArray(promptMessages) || promptMessages.length === 0) {
814
- throw new GMIError(`Prompt construction produced no chat messages for model '${modelTargetInfo.modelId}' (template '${promptEngineResult.metadata?.templateUsed ?? 'unknown'}').`, GMIErrorCode.GMI_PROCESSING_ERROR, {
815
- turnId,
816
- templateUsed: promptEngineResult.metadata?.templateUsed,
817
- issues: promptEngineResult.issues?.filter((issue) => issue.type === 'error'),
818
- });
819
- }
820
- this.addTraceEntry(ReasoningEntryType.PROMPT_CONSTRUCTION_COMPLETE, `Prompt constructed for model ${modelTargetInfo.modelId}.`);
821
- const provider = this.llmProviderManager.getProvider(modelTargetInfo.providerId);
822
- if (!provider) {
823
- throw new GMIError(`LLM Provider '${modelTargetInfo.providerId}' not found or not initialized.`, GMIErrorCode.LLM_PROVIDER_UNAVAILABLE);
824
- }
825
860
  const plannedToolFailureMode = turnInput.metadata?.executionPolicy?.toolFailureMode === 'fail_closed'
826
861
  ? 'fail_closed'
827
862
  : 'fail_open';
@@ -846,77 +881,231 @@ export class GMI {
846
881
  toolsForLLM = discoveredTools;
847
882
  }
848
883
  }
884
+ // Every completion option the persona and the turn set (D4b); temperature and
885
+ // the output budget keep their defaults; tools, user and streaming are the GMI's.
886
+ const turnOptions = (turnInput.metadata?.options ?? {});
887
+ const personaOptions = (this.activePersona.defaultModelCompletionOptions ?? {});
849
888
  const llmOptions = {
850
- temperature: turnInput.metadata?.options?.temperature ?? this.activePersona.defaultModelCompletionOptions?.temperature ?? 0.7,
851
- maxTokens: turnInput.metadata?.options?.maxTokens ?? this.activePersona.defaultModelCompletionOptions?.maxTokens ?? 2048,
889
+ ...pickCompletionOptions(personaOptions),
890
+ ...pickCompletionOptions(turnOptions),
891
+ temperature: turnOptions.temperature ?? personaOptions.temperature ?? 0.7,
892
+ maxTokens: turnOptions.maxTokens ?? personaOptions.maxTokens ?? 2048,
852
893
  tools: toolsForLLM.length > 0 ? toolsForLLM.map(t => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.inputSchema } })) : undefined,
853
- toolChoice: turnInput.metadata?.options?.toolChoice || (toolsForLLM.length > 0 ? "auto" : undefined),
894
+ toolChoice: turnOptions.toolChoice ?? personaOptions.toolChoice ?? (toolsForLLM.length > 0 ? "auto" : undefined),
854
895
  userId: this.currentUserContext.userId,
855
- stream: true, // For generateCompletionStream
856
- responseFormat: turnInput.metadata?.options?.responseFormat,
896
+ stream: true,
857
897
  };
858
- this.addTraceEntry(ReasoningEntryType.LLM_CALL_START, `Streaming from ${modelTargetInfo.modelId}. Tools: ${toolsForLLM.length}.`);
898
+ // A schema travels to the gateway, which lowers it per hop (D9); it is not a provider option.
899
+ const responseSchema = turnOptions.responseSchema;
900
+ const schemaName = turnOptions.schemaName;
901
+ // The gateway's route: the model asked for, the user's text as the router's
902
+ // task hint, the tools, and the call options each fallback hop derives its
903
+ // overrides from. Every hop that could not start is traced.
904
+ let lastHopError;
905
+ const userText = this.stringifyTurnContent(turnInput.content) ?? '';
906
+ const route = gateway
907
+ ? {
908
+ modelId: modelIdToUse,
909
+ providerId: providerIdForModel,
910
+ messages: userText ? [{ role: 'user', content: userText }] : [],
911
+ tools: llmOptions.tools,
912
+ callOptions: { maxTokens: llmOptions.maxTokens, effort: llmOptions.effort, cache: llmOptions.cache },
913
+ onFallback: ({ from, to, hop, error }) => this.addTraceEntry(ReasoningEntryType.WARNING, `Model fallback from '${from}' to '${to}' (hop ${hop}): ${error.message}`),
914
+ onHopFailure: ({ providerId, hop, error }) => {
915
+ lastHopError = error;
916
+ this.addTraceEntry(ReasoningEntryType.WARNING, `Provider '${providerId}' (hop ${hop}) could not start: ${error.message}`);
917
+ },
918
+ }
919
+ : undefined;
920
+ // The hop that serves this step: the one that served the turn's last
921
+ // step, else the route's first hop that can start. Resolved before the
922
+ // prompt is built, so the prompt is budgeted for the serving model (D1).
923
+ let resolution = null;
924
+ if (gateway && route) {
925
+ resolution = this.turnResolution ?? (await gateway.resolve(route));
926
+ if (!resolution) {
927
+ throw new GMIError(`No model provider could serve the turn${lastHopError ? `: ${lastHopError.message}` : '.'}`, GMIErrorCode.LLM_PROVIDER_UNAVAILABLE, { turnId });
928
+ }
929
+ }
930
+ // What the serving attempt produced; read after the hop loop.
859
931
  let currentIterationTextResponse = "";
860
932
  let currentIterationToolCallRequests = [];
861
- // Extended-thinking blocks emitted this iteration (Opus 4.7/4.8 with
862
- // thinking enabled). Captured from the final chunk + stored on the
863
- // assistant turn so the next tool turn replays them verbatim, which
864
- // Anthropic requires. Empty for every non-thinking turn, so the stored
865
- // turn is unchanged on the normal path.
933
+ // Extended-thinking blocks emitted this step (Anthropic with thinking
934
+ // enabled). Captured from the final chunk and stored on the assistant
935
+ // turn so the next tool turn replays them verbatim, which Anthropic
936
+ // requires. Empty for every non-thinking step.
866
937
  let currentIterationThinkingBlocks = [];
867
- let textDeltaEmitted = false;
868
- for await (const chunk of provider.generateCompletionStream(modelTargetInfo.modelId, promptMessages, llmOptions)) {
869
- if (chunk.error) {
870
- throw new GMIError(`LLM stream error: ${chunk.error.message}`, GMIErrorCode.LLM_PROVIDER_ERROR, chunk.error.details);
938
+ let stepUsage;
939
+ let stepFinishReason = null;
940
+ let stepFinalChunk;
941
+ let stepStructuredOutput;
942
+ let modelTargetInfo;
943
+ // One attempt per hop. A failure before any output moves to the next
944
+ // hop and rebuilds the prompt for that hop's model (D3); after output
945
+ // there is no fallback for the step. Without a gateway the loop runs once.
946
+ hopLoop: for (;;) {
947
+ if (resolution) {
948
+ this.llmProviderManager.setResolution?.(resolution);
871
949
  }
872
- if (chunk.responseTextDelta) {
873
- currentIterationTextResponse += chunk.responseTextDelta;
874
- aggregatedResponseText += chunk.responseTextDelta; // Aggregate for final output
875
- yield this.createOutputChunk(turnInput.interactionId, GMIOutputChunkType.TEXT_DELTA, chunk.responseTextDelta, { usage: chunk.usage });
876
- textDeltaEmitted = true;
950
+ modelTargetInfo = resolution
951
+ ? {
952
+ modelId: resolution.modelId,
953
+ providerId: resolution.providerId,
954
+ maxContextTokens: resolution.maxContextTokens,
955
+ capabilities: resolution.capabilities,
956
+ promptFormatType: this.determinePromptFormat(),
957
+ toolSupport: { supported: resolution.capabilities.includes('tool_use'), format: resolution.toolFormat },
958
+ }
959
+ : await this.legacyModelTargetInfo(modelIdToUse, providerIdForModel);
960
+ const promptEngineResult = await this.promptEngine.constructPrompt(promptComponents, modelTargetInfo, promptExecContext);
961
+ promptEngineResult.issues?.forEach(issue => this.addTraceEntry(ReasoningEntryType.WARNING, `Prompt Engine Issue: ${issue.message}`, issue));
962
+ // A failed template leaves the prompt empty, and every provider takes
963
+ // a non-empty ChatMessage[]; fail the turn rather than send nothing.
964
+ const promptMessages = promptEngineResult.prompt;
965
+ if (!Array.isArray(promptMessages) || promptMessages.length === 0) {
966
+ throw new GMIError(`Prompt construction produced no chat messages for model '${modelTargetInfo.modelId}' (template '${promptEngineResult.metadata?.templateUsed ?? 'unknown'}').`, GMIErrorCode.GMI_PROCESSING_ERROR, {
967
+ turnId,
968
+ templateUsed: promptEngineResult.metadata?.templateUsed,
969
+ issues: promptEngineResult.issues?.filter((issue) => issue.type === 'error'),
970
+ });
877
971
  }
878
- // Handle fully formed tool_calls if present in the chunk's message
879
- const choice = chunk.choices?.[0];
880
- // Capture extended-thinking blocks from the final chunk so they ride
881
- // the assistant turn into history (replayed verbatim next tool turn).
882
- if (choice?.message?.thinkingBlocks?.length) {
883
- currentIterationThinkingBlocks = choice.message.thinkingBlocks;
972
+ this.addTraceEntry(ReasoningEntryType.PROMPT_CONSTRUCTION_COMPLETE, `Prompt constructed for model ${modelTargetInfo.modelId}.`);
973
+ let attempt;
974
+ let attemptOutcome;
975
+ if (gateway && resolution) {
976
+ const gatewayAttempt = gateway.stream(resolution, promptMessages, llmOptions, responseSchema, schemaName);
977
+ attempt = gatewayAttempt;
978
+ attemptOutcome = gatewayAttempt.outcome;
884
979
  }
885
- if (choice?.message?.tool_calls && choice.message.tool_calls.length > 0) {
886
- currentIterationToolCallRequests = choice.message.tool_calls.map((tc) => ({
887
- id: tc.id || `toolcall-${uuidv4()}`, // Ensure ID
888
- name: tc.function.name,
889
- arguments: typeof tc.function.arguments === 'string'
890
- ? JSON.parse(tc.function.arguments)
891
- : tc.function.arguments,
892
- ...(tc.thoughtSignature ? { thoughtSignature: tc.thoughtSignature } : {}),
893
- }));
894
- aggregatedToolCalls.push(...currentIterationToolCallRequests); // Aggregate for final output
895
- yield this.createOutputChunk(turnInput.interactionId, GMIOutputChunkType.TOOL_CALL_REQUEST, [...currentIterationToolCallRequests], {
896
- metadata: {
897
- executionMode: 'internal',
898
- requiresExternalToolResult: false,
899
- },
900
- });
901
- this.addTraceEntry(ReasoningEntryType.TOOL_CALL_REQUESTED, `LLM requested tool(s).`, { requests: currentIterationToolCallRequests });
980
+ else {
981
+ const provider = this.llmProviderManager.getProvider(modelTargetInfo.providerId);
982
+ if (!provider) {
983
+ throw new GMIError(`LLM Provider '${modelTargetInfo.providerId}' not found or not initialized.`, GMIErrorCode.LLM_PROVIDER_UNAVAILABLE);
984
+ }
985
+ attempt = provider.generateCompletionStream(modelTargetInfo.modelId, promptMessages, llmOptions);
902
986
  }
903
- if (chunk.isFinal && choice?.finishReason) {
904
- this.addTraceEntry(ReasoningEntryType.LLM_CALL_COMPLETE, `LLM stream part finished. Reason: ${choice.finishReason}`, { usage: chunk.usage });
987
+ this.addTraceEntry(ReasoningEntryType.LLM_CALL_START, `Streaming from ${modelTargetInfo.modelId}${resolution ? ` (hop ${resolution.hop})` : ''}. Tools: ${toolsForLLM.length}.`);
988
+ let textDeltaEmitted = false;
989
+ for await (const chunk of attempt) {
990
+ if (chunk.error) {
991
+ throw new GMIError(`LLM stream error: ${chunk.error.message}`, GMIErrorCode.LLM_PROVIDER_ERROR, chunk.error.details);
992
+ }
993
+ if (chunk.responseTextDelta) {
994
+ currentIterationTextResponse += chunk.responseTextDelta;
995
+ aggregatedResponseText += chunk.responseTextDelta; // Aggregate for final output
996
+ yield this.createOutputChunk(turnInput.interactionId, GMIOutputChunkType.TEXT_DELTA, chunk.responseTextDelta, { usage: chunk.usage });
997
+ textDeltaEmitted = true;
998
+ }
999
+ // Handle fully formed tool_calls if present in the chunk's message
1000
+ const choice = chunk.choices?.[0];
1001
+ // Capture extended-thinking blocks from the final chunk so they ride
1002
+ // the assistant turn into history (replayed verbatim next tool turn).
1003
+ if (choice?.message?.thinkingBlocks?.length) {
1004
+ currentIterationThinkingBlocks = choice.message.thinkingBlocks;
1005
+ }
1006
+ // The gateway lifts a streamed schema tool call into `structuredOutput` (D9).
1007
+ const structured = chunk.structuredOutput;
1008
+ if (structured !== undefined)
1009
+ stepStructuredOutput = structured;
1010
+ if (choice?.message?.tool_calls && choice.message.tool_calls.length > 0) {
1011
+ currentIterationToolCallRequests = choice.message.tool_calls.map((tc) => ({
1012
+ id: tc.id || `toolcall-${uuidv4()}`, // Ensure ID
1013
+ name: tc.function.name,
1014
+ arguments: typeof tc.function.arguments === 'string'
1015
+ ? JSON.parse(tc.function.arguments)
1016
+ : tc.function.arguments,
1017
+ ...(tc.thoughtSignature ? { thoughtSignature: tc.thoughtSignature } : {}),
1018
+ }));
1019
+ aggregatedToolCalls.push(...currentIterationToolCallRequests); // Aggregate for final output
1020
+ yield this.createOutputChunk(turnInput.interactionId, GMIOutputChunkType.TOOL_CALL_REQUEST, [...currentIterationToolCallRequests], {
1021
+ metadata: {
1022
+ executionMode: 'internal',
1023
+ requiresExternalToolResult: false,
1024
+ },
1025
+ });
1026
+ this.addTraceEntry(ReasoningEntryType.TOOL_CALL_REQUESTED, `LLM requested tool(s).`, { requests: currentIterationToolCallRequests });
1027
+ }
1028
+ // A provider that sends no deltas: its final content is the step's text, emitted once (D4d).
1029
+ if (chunk.isFinal && !textDeltaEmitted && typeof choice?.message?.content === 'string' && choice.message.content.length > 0) {
1030
+ currentIterationTextResponse = choice.message.content;
1031
+ aggregatedResponseText += choice.message.content;
1032
+ yield this.createOutputChunk(turnInput.interactionId, GMIOutputChunkType.TEXT_DELTA, choice.message.content, { usage: chunk.usage });
1033
+ textDeltaEmitted = true;
1034
+ }
1035
+ if (chunk.isFinal && choice?.finishReason) {
1036
+ stepFinishReason = choice.finishReason;
1037
+ this.addTraceEntry(ReasoningEntryType.LLM_CALL_COMPLETE, `LLM stream part finished. Reason: ${choice.finishReason}`, { usage: chunk.usage });
1038
+ }
1039
+ if (chunk.isFinal)
1040
+ stepFinalChunk = chunk;
1041
+ // Usage on every chunk that carries it, choice or not (D4c). Providers report
1042
+ // the request's running total, so the turn total takes the step's last value once.
905
1043
  if (chunk.usage) {
1044
+ stepUsage = chunk.usage;
906
1045
  yield this.createOutputChunk(turnInput.interactionId, GMIOutputChunkType.USAGE_UPDATE, chunk.usage);
907
- // Aggregate usage
908
- aggregatedUsage.promptTokens += chunk.usage.promptTokens || 0;
909
- aggregatedUsage.completionTokens += chunk.usage.completionTokens || 0;
910
- aggregatedUsage.totalTokens = aggregatedUsage.promptTokens + aggregatedUsage.completionTokens;
911
- if (chunk.usage.costUSD)
912
- aggregatedUsage.totalCostUSD = (aggregatedUsage.totalCostUSD || 0) + chunk.usage.costUSD;
1046
+ }
1047
+ } // End LLM stream
1048
+ if (gateway && route && resolution && attemptOutcome) {
1049
+ const outcome = await attemptOutcome;
1050
+ if (outcome.kind === 'hopFailed') {
1051
+ const failedHop = resolution;
1052
+ lastHopError = outcome.error;
1053
+ this.addTraceEntry(ReasoningEntryType.WARNING, `Provider '${failedHop.providerId}' (hop ${failedHop.hop}) failed before any output: ${outcome.error.message}`);
1054
+ // A failed attempt can still be billed (a refused turn reports its
1055
+ // usage). It counts toward the turn once, whether or not the turn
1056
+ // goes on, and is reported on its own USAGE_UPDATE: no STEP_FINISHED
1057
+ // carries it.
1058
+ if (outcome.usage) {
1059
+ addUsage(aggregatedUsage, outcome.usage);
1060
+ yield this.createOutputChunk(turnInput.interactionId, GMIOutputChunkType.USAGE_UPDATE, outcome.usage, {
1061
+ metadata: { attemptFailed: true, hop: failedHop.hop, providerId: failedHop.providerId, modelId: failedHop.modelId },
1062
+ });
1063
+ }
1064
+ if (!outcome.retryable) {
1065
+ throw new GMIError(`LLM provider error: ${outcome.error.message}`, GMIErrorCode.LLM_PROVIDER_ERROR, { turnId, providerId: failedHop.providerId, hop: failedHop.hop });
1066
+ }
1067
+ const next = await gateway.resolve(route, failedHop);
1068
+ if (!next) {
1069
+ throw new GMIError(`LLM provider error after ${failedHop.hop + 1} hop(s): ${lastHopError.message}`, GMIErrorCode.LLM_PROVIDER_ERROR, { turnId, providerId: failedHop.providerId, hop: failedHop.hop });
1070
+ }
1071
+ // Nothing of the failed attempt carries over to the next hop.
1072
+ currentIterationTextResponse = "";
1073
+ currentIterationToolCallRequests = [];
1074
+ currentIterationThinkingBlocks = [];
1075
+ stepUsage = undefined;
1076
+ stepFinishReason = null;
1077
+ stepFinalChunk = undefined;
1078
+ stepStructuredOutput = undefined;
1079
+ resolution = next;
1080
+ continue hopLoop;
913
1081
  }
914
1082
  }
915
- } // End LLM stream
916
- // Ensure at least one TEXT_DELTA is emitted for this turn if text was produced
917
- if (!textDeltaEmitted && aggregatedResponseText) {
918
- yield this.createOutputChunk(turnInput.interactionId, GMIOutputChunkType.TEXT_DELTA, aggregatedResponseText);
1083
+ break hopLoop;
919
1084
  }
1085
+ if (resolution)
1086
+ this.turnResolution = resolution;
1087
+ // The step's usage, counted once (D4c).
1088
+ if (stepUsage)
1089
+ addUsage(aggregatedUsage, stepUsage);
1090
+ // The step boundary (D4d): the step's own text, finish reason, hop and usage.
1091
+ const stepPayload = {
1092
+ stepIndex: safetyBreak - 1,
1093
+ text: currentIterationTextResponse,
1094
+ finishReason: stepFinishReason,
1095
+ providerId: modelTargetInfo.providerId,
1096
+ modelId: modelTargetInfo.modelId,
1097
+ hop: resolution?.hop ?? 0,
1098
+ ...(stepUsage ? { usage: stepUsage } : {}),
1099
+ ...(stepFinalChunk?.modelId ? { responseModel: stepFinalChunk.modelId } : {}),
1100
+ ...(stepFinalChunk?.serviceTier ? { serviceTier: stepFinalChunk.serviceTier } : {}),
1101
+ ...(stepFinalChunk?.id ? { providerMessageId: stepFinalChunk.id } : {}),
1102
+ ...(stepFinalChunk?.cacheDiagnostics !== undefined ? { cacheDiagnostics: stepFinalChunk.cacheDiagnostics } : {}),
1103
+ ...(stepStructuredOutput !== undefined ? { structuredOutput: stepStructuredOutput } : {}),
1104
+ };
1105
+ yield this.createOutputChunk(turnInput.interactionId, GMIOutputChunkType.STEP_FINISHED, stepPayload, {
1106
+ ...(stepFinishReason ? { finishReason: stepFinishReason } : {}),
1107
+ ...(stepUsage ? { usage: stepUsage } : {}),
1108
+ });
920
1109
  const assistantMessage = {
921
1110
  role: 'assistant',
922
1111
  content: currentIterationTextResponse || null,
@@ -968,14 +1157,21 @@ export class GMI {
968
1157
  toolExecutionResults.push(...this.resultsForUnfinishedToolCalls(currentIterationToolCallRequests, toolExecutionResults.length, callInFlight, error));
969
1158
  }
970
1159
  // Recorded on success and failure alike, so the history this turn
971
- // leaves answers every call its assistant message declared.
1160
+ // leaves answers every call its assistant message declared. Each
1161
+ // recorded result is also published as a TOOL_RESULT chunk (D4e).
972
1162
  for (const tcResult of toolExecutionResults) {
973
1163
  turnMessages.push(this.conversationHistoryManager.updateWithToolResult(tcResult));
1164
+ const toolResultPayload = {
1165
+ toolCallId: tcResult.toolCallId,
1166
+ name: tcResult.toolName,
1167
+ result: tcResult.output,
1168
+ isError: tcResult.isError === true,
1169
+ ...(tcResult.errorDetails !== undefined ? { errorDetails: tcResult.errorDetails } : {}),
1170
+ };
1171
+ yield this.createOutputChunk(turnInput.interactionId, GMIOutputChunkType.TOOL_RESULT, toolResultPayload);
974
1172
  }
975
1173
  if (toolRoundFailure)
976
1174
  throw toolRoundFailure.error;
977
- currentIterationTextResponse = ""; // Reset for next iteration if any
978
- currentIterationToolCallRequests = []; // Reset
979
1175
  if (ownsState())
980
1176
  this.state = GMIPrimeState.PROCESSING;
981
1177
  continue main_processing_loop;
@@ -1004,7 +1200,9 @@ export class GMI {
1004
1200
  return finalTurnOutput; // Return the aggregated output
1005
1201
  }
1006
1202
  catch (error) {
1007
- const gmiError = createGMIErrorFromError(error, GMIErrorCode.GMI_PROCESSING_ERROR, { turnId }, `Error in GMI turn '${turnId}'.`);
1203
+ // A GMIError keeps its code (an in-band provider error stays
1204
+ // LLM_PROVIDER_ERROR); anything else is a processing error (D4f).
1205
+ const gmiError = createGMIErrorFromError(error, error instanceof GMIError ? error.code : GMIErrorCode.GMI_PROCESSING_ERROR, { turnId }, `Error in GMI turn '${turnId}'.`);
1008
1206
  if (ownsState())
1009
1207
  this.state = GMIPrimeState.ERRORED;
1010
1208
  lastErrorForOutput = { code: gmiError.code, message: gmiError.message, details: gmiError.details };