@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.
- package/README.md +11 -11
- package/dist/api/AgentOS.d.ts +29 -0
- package/dist/api/AgentOS.d.ts.map +1 -1
- package/dist/api/AgentOS.js +137 -10
- package/dist/api/AgentOS.js.map +1 -1
- package/dist/api/agent.d.ts +7 -3
- package/dist/api/agent.d.ts.map +1 -1
- package/dist/api/agent.js.map +1 -1
- package/dist/api/embedText.d.ts.map +1 -1
- package/dist/api/embedText.js +1 -8
- package/dist/api/embedText.js.map +1 -1
- package/dist/api/gmi.d.ts.map +1 -1
- package/dist/api/gmi.js +24 -19
- package/dist/api/gmi.js.map +1 -1
- package/dist/api/runtime/AgentOSOrchestrator.d.ts +7 -0
- package/dist/api/runtime/AgentOSOrchestrator.d.ts.map +1 -1
- package/dist/api/runtime/AgentOSOrchestrator.js +7 -0
- package/dist/api/runtime/AgentOSOrchestrator.js.map +1 -1
- package/dist/api/runtime/agentCognitiveMemory.d.ts +12 -9
- package/dist/api/runtime/agentCognitiveMemory.d.ts.map +1 -1
- package/dist/api/runtime/agentCognitiveMemory.js +53 -17
- package/dist/api/runtime/agentCognitiveMemory.js.map +1 -1
- package/dist/api/runtime/completionGateway.d.ts.map +1 -1
- package/dist/api/runtime/completionGateway.js +7 -0
- package/dist/api/runtime/completionGateway.js.map +1 -1
- package/dist/api/runtime/gmiResults.d.ts +63 -6
- package/dist/api/runtime/gmiResults.d.ts.map +1 -1
- package/dist/api/runtime/gmiResults.js +83 -13
- package/dist/api/runtime/gmiResults.js.map +1 -1
- package/dist/api/runtime/gmiSession.d.ts +17 -2
- package/dist/api/runtime/gmiSession.d.ts.map +1 -1
- package/dist/api/runtime/gmiSession.js +32 -7
- package/dist/api/runtime/gmiSession.js.map +1 -1
- package/dist/api/runtime/provider-defaults.d.ts +9 -0
- package/dist/api/runtime/provider-defaults.d.ts.map +1 -1
- package/dist/api/runtime/provider-defaults.js +9 -0
- package/dist/api/runtime/provider-defaults.js.map +1 -1
- package/dist/cognition/emergent/executor/QuickJSExecutor.d.ts.map +1 -1
- package/dist/cognition/emergent/executor/QuickJSExecutor.js +28 -7
- package/dist/cognition/emergent/executor/QuickJSExecutor.js.map +1 -1
- package/dist/cognition/emergent/executor/guest-prelude.d.ts.map +1 -1
- package/dist/cognition/emergent/executor/guest-prelude.js +6 -0
- package/dist/cognition/emergent/executor/guest-prelude.js.map +1 -1
- package/dist/cognition/emergent/executor/guest-surface.d.ts.map +1 -1
- package/dist/cognition/emergent/executor/guest-surface.js +10 -0
- package/dist/cognition/emergent/executor/guest-surface.js.map +1 -1
- package/dist/cognition/memory/CognitiveMemoryManager.d.ts +28 -21
- package/dist/cognition/memory/CognitiveMemoryManager.d.ts.map +1 -1
- package/dist/cognition/memory/CognitiveMemoryManager.js +76 -26
- package/dist/cognition/memory/CognitiveMemoryManager.js.map +1 -1
- package/dist/cognition/memory/retrieval/store/MemoryStore.d.ts +10 -2
- package/dist/cognition/memory/retrieval/store/MemoryStore.d.ts.map +1 -1
- package/dist/cognition/memory/retrieval/store/MemoryStore.js +11 -2
- package/dist/cognition/memory/retrieval/store/MemoryStore.js.map +1 -1
- package/dist/cognition/substrate/ConversationHistoryManager.d.ts +3 -1
- package/dist/cognition/substrate/ConversationHistoryManager.d.ts.map +1 -1
- package/dist/cognition/substrate/ConversationHistoryManager.js +9 -2
- package/dist/cognition/substrate/ConversationHistoryManager.js.map +1 -1
- package/dist/cognition/substrate/GMI.d.ts +33 -2
- package/dist/cognition/substrate/GMI.d.ts.map +1 -1
- package/dist/cognition/substrate/GMI.js +231 -71
- package/dist/cognition/substrate/GMI.js.map +1 -1
- package/dist/cognition/substrate/IGMI.d.ts +14 -3
- package/dist/cognition/substrate/IGMI.d.ts.map +1 -1
- package/dist/cognition/substrate/IGMI.js +2 -1
- package/dist/cognition/substrate/IGMI.js.map +1 -1
- package/dist/cognition/substrate/SentimentTracker.d.ts +8 -0
- package/dist/cognition/substrate/SentimentTracker.d.ts.map +1 -1
- package/dist/cognition/substrate/SentimentTracker.js +20 -0
- package/dist/cognition/substrate/SentimentTracker.js.map +1 -1
- package/dist/cognition/substrate/personas/IPersonaDefinition.d.ts +6 -0
- package/dist/cognition/substrate/personas/IPersonaDefinition.d.ts.map +1 -1
- package/dist/cognition/substrate/personas/SoulLoader.js.map +1 -1
- package/dist/cognition/substrate/personas/hardLimits.d.ts +16 -0
- package/dist/cognition/substrate/personas/hardLimits.d.ts.map +1 -0
- package/dist/cognition/substrate/personas/hardLimits.js +21 -0
- package/dist/cognition/substrate/personas/hardLimits.js.map +1 -0
- package/dist/core/conversation/ConversationContext.d.ts +8 -0
- package/dist/core/conversation/ConversationContext.d.ts.map +1 -1
- package/dist/core/conversation/ConversationContext.js +17 -0
- package/dist/core/conversation/ConversationContext.js.map +1 -1
- package/dist/core/llm/providers/implementations/OpenAIProvider.d.ts.map +1 -1
- package/dist/core/llm/providers/implementations/OpenAIProvider.js +23 -4
- package/dist/core/llm/providers/implementations/OpenAIProvider.js.map +1 -1
- package/dist/core/utils/errors.d.ts +2 -0
- package/dist/core/utils/errors.d.ts.map +1 -1
- package/dist/core/utils/errors.js +3 -0
- package/dist/core/utils/errors.js.map +1 -1
- package/dist/extensions/ExtensionRegistry.d.ts.map +1 -1
- package/dist/extensions/ExtensionRegistry.js +4 -1
- package/dist/extensions/ExtensionRegistry.js.map +1 -1
- package/dist/safety/guardrails/IGuardrailService.d.ts +11 -0
- package/dist/safety/guardrails/IGuardrailService.d.ts.map +1 -1
- package/dist/safety/guardrails/ParallelGuardrailDispatcher.d.ts.map +1 -1
- package/dist/safety/guardrails/ParallelGuardrailDispatcher.js +206 -80
- package/dist/safety/guardrails/ParallelGuardrailDispatcher.js.map +1 -1
- package/dist/safety/guardrails/builtin/PhraseListGuardrail.d.ts +109 -0
- package/dist/safety/guardrails/builtin/PhraseListGuardrail.d.ts.map +1 -0
- package/dist/safety/guardrails/builtin/PhraseListGuardrail.js +180 -0
- package/dist/safety/guardrails/builtin/PhraseListGuardrail.js.map +1 -0
- package/dist/safety/guardrails/guardrailDispatcher.d.ts +20 -0
- package/dist/safety/guardrails/guardrailDispatcher.d.ts.map +1 -1
- package/dist/safety/guardrails/guardrailDispatcher.js.map +1 -1
- package/dist/safety/guardrails/index.d.ts +3 -1
- package/dist/safety/guardrails/index.d.ts.map +1 -1
- package/dist/safety/guardrails/index.js +4 -0
- package/dist/safety/guardrails/index.js.map +1 -1
- package/dist/safety/guardrails/requiredGuardrails.d.ts +34 -0
- package/dist/safety/guardrails/requiredGuardrails.d.ts.map +1 -0
- package/dist/safety/guardrails/requiredGuardrails.js +68 -0
- package/dist/safety/guardrails/requiredGuardrails.js.map +1 -0
- 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)
|
|
904
|
-
// the
|
|
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
|
-
|
|
912
|
-
|
|
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
|
-
|
|
915
|
-
|
|
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
|
|
922
|
-
// task hint
|
|
923
|
-
//
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
: tc.
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
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
|
-
|
|
1086
|
-
|
|
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') {
|