agent-accelerator 0.6.5 → 0.6.6
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 +0 -2
- package/package.json +1 -1
- package/src/agent/agent.ts +97 -670
- package/src/agent/architecture.md +4 -1
- package/src/agent/loop.ts +208 -213
- package/src/agent/named-sessions.ts +93 -0
- package/src/agent/options.ts +166 -0
- package/src/agent/run-control.ts +248 -0
- package/src/index.ts +2 -2
- package/src/providers/google.ts +1 -2
- package/src/streaming/event-stream.ts +1 -1
- package/src/types/agent.ts +0 -2
- package/src/utils/headers.ts +1 -1
|
@@ -5,6 +5,9 @@ The agent and everything that runs under it.
|
|
|
5
5
|
| File | What it is | Used by |
|
|
6
6
|
| :------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------- |
|
|
7
7
|
| `agent.ts` | `Agent`: `run`/`stream`/`ask`, `steer`/`queue`/`interrupt`, persistence hooks, `resume()`/`interrupted()`, `usageReport()`, `fork()`, `toConfig()`/`fromConfig()`, the `workers` getter. | callers, workers, scheduler |
|
|
8
|
+
| `run-control.ts` | `RunControl`: the active run, the waiting queue and the machinery behind `steer`/`queue`/`interrupt` (`pumpQueue`, `finishCurrentRun`, urgent queueing). `Agent` keeps the public methods and calls it. | `Agent` |
|
|
9
|
+
| `options.ts` | `buildProviderOptions` (agent config + per-run options in one place), `resolveAgentLimits`, `normalizeThinking`, `resolveEffectiveThinking`, `normalizeToolChoiceOption`. | `Agent`, barrel |
|
|
10
|
+
| `named-sessions.ts` | `NamedSessions`: the in-memory contexts of `run(..., { sessionId })` (LRU 100, 1 h idle TTL). | `Agent` |
|
|
8
11
|
| `loop.ts` | `runAgentLoop` / `streamAgentLoop`: per turn budget, pairing repair, file access, provider call, tool execution, repeat guard. Also `ensureToolPairing`, `accumulateUsage`. | `Agent` |
|
|
9
12
|
| `context.ts` | `AgentContext`: the validated message list with checkpoint/rollback. | `Agent`, loops |
|
|
10
13
|
| `delegation.ts` | Fixed workers (`agentToTool`, `buildAgentTools`), the live worker trace store, session-id helpers, budget helpers, `openWorkerTrace`/`workerMetadata`. | `Agent`, `workers.ts` |
|
|
@@ -19,7 +22,7 @@ The agent and everything that runs under it.
|
|
|
19
22
|
|
|
20
23
|
## Invariants
|
|
21
24
|
|
|
22
|
-
- `runAgentLoop` and `streamAgentLoop` duplicate
|
|
25
|
+
- `runAgentLoop` and `streamAgentLoop` still duplicate the shape of a turn, but the repeated parts live once in helpers at the top of `loop.ts` (`buildTurnOptions`, `promoteThinkingIfAllowed`, `recordModelTurn`, `emitThinkingTrace`, `rollUpToolResults`, `partialProgress`, `newTurnUsage`): change those there, and any change to the turn order itself goes into both loops.
|
|
23
26
|
- An `Agent` runs one thing at a time; delegation forks a copy per call so parallel calls never queue.
|
|
24
27
|
- A worker is a plain `Agent` with a parent link and presets. Never duplicate logic between Agent and worker.
|
|
25
28
|
- A worker is never less constrained than its parent (`inheritableConfig()`); its children draw only from the tools it was granted.
|
package/src/agent/loop.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Provider, ProviderRequestOptions, ModelSpec, ProviderRawData } from "../types/model.ts";
|
|
2
|
-
import type { ToolDefinition, ToolCallRecord, ToolResultRecord } from "../types/tool.ts";
|
|
2
|
+
import type { ToolDefinition, ToolCallRecord, ToolResultRecord, StandardToolDeclaration } from "../types/tool.ts";
|
|
3
3
|
import type { TokenUsage } from "../types/core.ts";
|
|
4
4
|
import type { AgentRunOptions } from "../types/agent.ts";
|
|
5
5
|
import type { ContentPart } from "../types/message.ts";
|
|
@@ -556,6 +556,163 @@ function sanitizeToolResult(r: ToolResultRecord): { sanitized: ToolResultRecord;
|
|
|
556
556
|
return { sanitized: r, metas: [] };
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
+
// ---------------------------------------------------------------------------
|
|
560
|
+
// Turn helpers shared by runAgentLoop and streamAgentLoop (one copy of the logic that both loops need).
|
|
561
|
+
// ---------------------------------------------------------------------------
|
|
562
|
+
|
|
563
|
+
// A zeroed usage total for a new run; in: both loops, out: the run's accumulatedUsage.
|
|
564
|
+
function newTurnUsage(): TokenUsage {
|
|
565
|
+
return {
|
|
566
|
+
inputTokens: 0,
|
|
567
|
+
outputTokens: 0,
|
|
568
|
+
totalTokens: 0,
|
|
569
|
+
cachedTokens: 0,
|
|
570
|
+
cacheReadTokens: 0,
|
|
571
|
+
cacheWriteTokens: 0,
|
|
572
|
+
thinkingTokens: 0,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// What a failed run had done so far (the `partial` attached to its error); in: both loops' catch blocks, out: err.partial.
|
|
577
|
+
function partialProgress(
|
|
578
|
+
usage: TokenUsage,
|
|
579
|
+
turns: number,
|
|
580
|
+
toolCalls: ToolCallRecord[],
|
|
581
|
+
toolResults: ToolResultRecord[],
|
|
582
|
+
subagents: SubAgentExecutionMetadata[],
|
|
583
|
+
text: string | undefined
|
|
584
|
+
): {
|
|
585
|
+
usage: TokenUsage;
|
|
586
|
+
cost: TokenUsage["cost"];
|
|
587
|
+
turns: number;
|
|
588
|
+
toolCalls: ToolCallRecord[];
|
|
589
|
+
toolResults: ToolResultRecord[];
|
|
590
|
+
subagents: SubAgentExecutionMetadata[];
|
|
591
|
+
text: string | undefined;
|
|
592
|
+
} {
|
|
593
|
+
return { usage, cost: usage.cost, turns, toolCalls, toolResults, subagents, text };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// The provider options of one turn: the run's options plus this turn's tools, signal, forced tool choice, structured output and request timeout; in: both loops before each model call, out: provider.generate/stream options.
|
|
597
|
+
function buildTurnOptions(
|
|
598
|
+
config: AgentLoopConfig,
|
|
599
|
+
standardTools: StandardToolDeclaration[],
|
|
600
|
+
signal: AbortSignal | undefined,
|
|
601
|
+
turn: number,
|
|
602
|
+
outputSpec: import("../types/model.ts").StructuredOutputSpec | undefined,
|
|
603
|
+
requestTimeoutMs: number
|
|
604
|
+
): ProviderRequestOptions {
|
|
605
|
+
const { options, runOptions } = config;
|
|
606
|
+
const providerOptions: ProviderRequestOptions = {
|
|
607
|
+
...options,
|
|
608
|
+
sessionId: runOptions?.sessionId || options?.sessionId,
|
|
609
|
+
cache: options?.cache,
|
|
610
|
+
tools: standardTools.length > 0 ? standardTools : undefined,
|
|
611
|
+
signal,
|
|
612
|
+
toolChoice: toolChoiceForTurn(options?.toolChoice, turn),
|
|
613
|
+
...(outputSpec ? { output: outputSpec } : {}),
|
|
614
|
+
};
|
|
615
|
+
// Per-request timeout pass-through (providers honor timeoutMs).
|
|
616
|
+
try {
|
|
617
|
+
if (Number.isFinite(requestTimeoutMs) && requestTimeoutMs !== Infinity) {
|
|
618
|
+
(providerOptions as unknown as { timeoutMs?: number }).timeoutMs = requestTimeoutMs;
|
|
619
|
+
}
|
|
620
|
+
} catch {}
|
|
621
|
+
return providerOptions;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Moves an empty-text turn's reasoning into its answer, only when the caller opted in; in: a finished model turn, out: that turn's text and thinking.
|
|
625
|
+
function promoteThinkingIfAllowed(
|
|
626
|
+
turn: { text: string; thinking?: string; toolCalls?: unknown[] },
|
|
627
|
+
config: AgentLoopConfig,
|
|
628
|
+
runOptions: AgentRunOptions | undefined
|
|
629
|
+
): void {
|
|
630
|
+
if ((turn.text && turn.text.trim() !== "") || (turn.toolCalls && turn.toolCalls.length > 0) || !turn.thinking) return;
|
|
631
|
+
// Thinking is exposed via events/traces and onTurn. Promoting it to answer text is gated (default OFF): without the
|
|
632
|
+
// opt-in the run returns the provider's empty text and finishReason.
|
|
633
|
+
if (!shouldPromoteThinkingToAnswer(config, runOptions)) return;
|
|
634
|
+
const mutable = turn as { text: string; thinking?: string };
|
|
635
|
+
if (turn.thinking.includes("</think>")) {
|
|
636
|
+
const parts = turn.thinking.split(/<\/(?:think|thought)>/i);
|
|
637
|
+
mutable.thinking = parts[0]!.replace(/<(?:think|thought)>/i, "").trim() || undefined;
|
|
638
|
+
mutable.text = parts.slice(1).join("").trim();
|
|
639
|
+
} else {
|
|
640
|
+
mutable.text = turn.thinking;
|
|
641
|
+
mutable.thinking = undefined;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Counts one finished model turn: adds it to the run's usage, reports it to the agent's own spending, then checks the budgets and the token limit; in: both loops after each provider call, out: accumulatedUsage and a thrown BudgetExceededError when a cap is passed.
|
|
646
|
+
function recordModelTurn(
|
|
647
|
+
config: AgentLoopConfig,
|
|
648
|
+
accumulated: TokenUsage,
|
|
649
|
+
usage: TokenUsage,
|
|
650
|
+
spec: ModelSpec | undefined,
|
|
651
|
+
maxTotalTokens: number
|
|
652
|
+
): void {
|
|
653
|
+
const serviceTier = config.options?.serviceTier;
|
|
654
|
+
accumulateUsage(accumulated, usage, spec, serviceTier);
|
|
655
|
+
try {
|
|
656
|
+
config.onTurnUsage?.(usage, spec);
|
|
657
|
+
} catch {}
|
|
658
|
+
config.enforceBudget?.({ ...usage, cost: computeCostFromPricing(usage, spec, serviceTier) });
|
|
659
|
+
checkTokenLimit(accumulated, maxTotalTokens);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Emits the full reasoning text of a turn on the telemetry bus (observability only: never in wire bodies, history or files); in: both loops after each model turn, out: a `turn.thinking` event.
|
|
663
|
+
function emitThinkingTrace(
|
|
664
|
+
turn: { thinking?: string; thoughtSignature?: string },
|
|
665
|
+
turns: number,
|
|
666
|
+
providerId: string,
|
|
667
|
+
modelId: string,
|
|
668
|
+
sessionId: string | undefined
|
|
669
|
+
): void {
|
|
670
|
+
if (!turn.thinking) return;
|
|
671
|
+
try {
|
|
672
|
+
telemetryBus.emit({
|
|
673
|
+
code: "turn.thinking",
|
|
674
|
+
message: `[Agent Accelerator] turn ${turns} thinking (${providerId}/${modelId})`,
|
|
675
|
+
data: {
|
|
676
|
+
turns,
|
|
677
|
+
model: modelId,
|
|
678
|
+
provider: providerId,
|
|
679
|
+
sessionId,
|
|
680
|
+
thinking: turn.thinking,
|
|
681
|
+
...(turn.thoughtSignature ? { thoughtSignature: turn.thoughtSignature } : {}),
|
|
682
|
+
},
|
|
683
|
+
});
|
|
684
|
+
} catch {}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Turns executed tool results into what goes into history: unwraps delegation envelopes, rolls each worker's usage into the run's total and budget (calling `onWorker` for each), and returns the sanitized results; in: both loops after a tool batch, out: the results for the context and `allSubagents`.
|
|
688
|
+
function rollUpToolResults(
|
|
689
|
+
results: ToolResultRecord[],
|
|
690
|
+
config: AgentLoopConfig,
|
|
691
|
+
accumulated: TokenUsage,
|
|
692
|
+
allSubagents: SubAgentExecutionMetadata[],
|
|
693
|
+
onWorker?: (worker: SubAgentExecutionMetadata) => void
|
|
694
|
+
): ToolResultRecord[] {
|
|
695
|
+
const sanitizedResults: ToolResultRecord[] = [];
|
|
696
|
+
for (const r of results) {
|
|
697
|
+
const { sanitized, metas } = sanitizeToolResult(r);
|
|
698
|
+
if (metas.length > 0) {
|
|
699
|
+
allSubagents.push(...metas);
|
|
700
|
+
for (const s of metas) {
|
|
701
|
+
if (s.raw) s.raw = applyCaptureRaw(s.raw, config.captureRaw);
|
|
702
|
+
const subagentSpec = getModelFromCatalog(s.provider, s.model);
|
|
703
|
+
if (subagentSpec && (!s.usage?.cost || !s.usage.cost.totalCost)) {
|
|
704
|
+
s.usage.cost = computeCostFromPricing(s.usage, subagentSpec);
|
|
705
|
+
}
|
|
706
|
+
accumulateUsage(accumulated, s.usage, subagentSpec);
|
|
707
|
+
config.enforceBudget?.(s.usage);
|
|
708
|
+
onWorker?.(s);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
sanitizedResults.push(sanitized);
|
|
712
|
+
}
|
|
713
|
+
return sanitizedResults;
|
|
714
|
+
}
|
|
715
|
+
|
|
559
716
|
/**
|
|
560
717
|
* Stops a model from hammering the same call. Per fingerprint (name + stable
|
|
561
718
|
* args) the guard counts CONSECUTIVE turns: the first two identical calls run,
|
|
@@ -751,15 +908,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
751
908
|
// Partial-run state hoisted outside try so the catch block can attach
|
|
752
909
|
// {usage,cost,turns,toolCalls,toolResults,text} to ANY thrown error.
|
|
753
910
|
const startTime = Date.now();
|
|
754
|
-
const accumulatedUsage: TokenUsage =
|
|
755
|
-
inputTokens: 0,
|
|
756
|
-
outputTokens: 0,
|
|
757
|
-
totalTokens: 0,
|
|
758
|
-
cachedTokens: 0,
|
|
759
|
-
cacheReadTokens: 0,
|
|
760
|
-
cacheWriteTokens: 0,
|
|
761
|
-
thinkingTokens: 0,
|
|
762
|
-
};
|
|
911
|
+
const accumulatedUsage: TokenUsage = newTurnUsage();
|
|
763
912
|
const allToolCalls: ToolCallRecord[] = [];
|
|
764
913
|
const allToolResults: ToolResultRecord[] = [];
|
|
765
914
|
const allSubagents: SubAgentExecutionMetadata[] = [];
|
|
@@ -799,21 +948,14 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
799
948
|
|
|
800
949
|
const spec = getModelFromCatalog(provider.id, modelId);
|
|
801
950
|
|
|
802
|
-
const providerOptions
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
};
|
|
811
|
-
// Per-request timeout pass-through (providers honor timeoutMs).
|
|
812
|
-
try {
|
|
813
|
-
if (Number.isFinite(limits.requestTimeoutMs) && limits.requestTimeoutMs !== Infinity) {
|
|
814
|
-
(providerOptions as unknown as { timeoutMs?: number }).timeoutMs = limits.requestTimeoutMs;
|
|
815
|
-
}
|
|
816
|
-
} catch {}
|
|
951
|
+
const providerOptions = buildTurnOptions(
|
|
952
|
+
config,
|
|
953
|
+
standardTools,
|
|
954
|
+
runOptions?.signal,
|
|
955
|
+
turns,
|
|
956
|
+
loopOutput.spec,
|
|
957
|
+
limits.requestTimeoutMs
|
|
958
|
+
);
|
|
817
959
|
|
|
818
960
|
// A long-running call (e.g. Google long decode) times each HTTP request itself: no whole-call race.
|
|
819
961
|
const longRunning = provider.isLongRunning?.(modelId, providerOptions) === true;
|
|
@@ -835,34 +977,9 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
835
977
|
// Canonical session routing: lets adapters detect provider switches.
|
|
836
978
|
noteProviderTurn(runOptions?.sessionId || options?.sessionId, provider.id);
|
|
837
979
|
|
|
838
|
-
|
|
839
|
-
// OFF): without the opt-in the run returns the provider's empty text and finishReason.
|
|
840
|
-
if (
|
|
841
|
-
(!genResult.text || genResult.text.trim() === "") &&
|
|
842
|
-
(!genResult.toolCalls || genResult.toolCalls.length === 0) &&
|
|
843
|
-
genResult.thinking
|
|
844
|
-
) {
|
|
845
|
-
if (shouldPromoteThinkingToAnswer(config, runOptions)) {
|
|
846
|
-
if (genResult.thinking.includes("</think>")) {
|
|
847
|
-
const parts = genResult.thinking.split(/<\/(?:think|thought)>/i);
|
|
848
|
-
genResult.thinking = parts[0]!.replace(/<(?:think|thought)>/i, "").trim() || undefined;
|
|
849
|
-
genResult.text = parts.slice(1).join("").trim();
|
|
850
|
-
} else {
|
|
851
|
-
genResult.text = genResult.thinking;
|
|
852
|
-
genResult.thinking = undefined;
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
}
|
|
980
|
+
promoteThinkingIfAllowed(genResult, config, runOptions);
|
|
856
981
|
|
|
857
|
-
|
|
858
|
-
try {
|
|
859
|
-
config.onTurnUsage?.(genResult.usage, spec);
|
|
860
|
-
} catch {}
|
|
861
|
-
config.enforceBudget?.({
|
|
862
|
-
...genResult.usage,
|
|
863
|
-
cost: computeCostFromPricing(genResult.usage, spec, options?.serviceTier),
|
|
864
|
-
});
|
|
865
|
-
checkTokenLimit(accumulatedUsage, limits.maxTotalTokens);
|
|
982
|
+
recordModelTurn(config, accumulatedUsage, genResult.usage, spec, limits.maxTotalTokens);
|
|
866
983
|
|
|
867
984
|
// Record assistant turn in context
|
|
868
985
|
context.addAssistantMessage(
|
|
@@ -873,25 +990,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
873
990
|
genResult.providerSteps
|
|
874
991
|
);
|
|
875
992
|
|
|
876
|
-
|
|
877
|
-
// one bus event per thinking turn carrying the text + thought signature.
|
|
878
|
-
// Never touches provider payloads, history, or persistence formats.
|
|
879
|
-
if (genResult.thinking) {
|
|
880
|
-
try {
|
|
881
|
-
telemetryBus.emit({
|
|
882
|
-
code: "turn.thinking",
|
|
883
|
-
message: `[Agent Accelerator] turn ${turns} thinking (${provider.id}/${modelId})`,
|
|
884
|
-
data: {
|
|
885
|
-
turns,
|
|
886
|
-
model: modelId,
|
|
887
|
-
provider: provider.id,
|
|
888
|
-
sessionId: runOptions?.sessionId || options?.sessionId,
|
|
889
|
-
thinking: genResult.thinking,
|
|
890
|
-
...(genResult.thoughtSignature ? { thoughtSignature: genResult.thoughtSignature } : {}),
|
|
891
|
-
},
|
|
892
|
-
});
|
|
893
|
-
} catch {}
|
|
894
|
-
}
|
|
993
|
+
emitThinkingTrace(genResult, turns, provider.id, modelId, runOptions?.sessionId || options?.sessionId);
|
|
895
994
|
|
|
896
995
|
// Mid-session steer: if new queries arrived during this provider turn,
|
|
897
996
|
// inject them now. Tool turns still execute first (see below) so the
|
|
@@ -943,24 +1042,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
943
1042
|
previousToolCallFingerprints = guarded.fingerprints;
|
|
944
1043
|
|
|
945
1044
|
// Process results and extract any sub-agent execution metadata
|
|
946
|
-
const sanitizedResults
|
|
947
|
-
|
|
948
|
-
for (const r of results) {
|
|
949
|
-
const { sanitized, metas } = sanitizeToolResult(r);
|
|
950
|
-
if (metas.length > 0) {
|
|
951
|
-
allSubagents.push(...metas);
|
|
952
|
-
for (const s of metas) {
|
|
953
|
-
if (s.raw) s.raw = applyCaptureRaw(s.raw, config.captureRaw);
|
|
954
|
-
const subagentSpec = getModelFromCatalog(s.provider, s.model);
|
|
955
|
-
if (subagentSpec && (!s.usage?.cost || !s.usage.cost.totalCost)) {
|
|
956
|
-
s.usage.cost = computeCostFromPricing(s.usage, subagentSpec);
|
|
957
|
-
}
|
|
958
|
-
accumulateUsage(accumulatedUsage, s.usage, subagentSpec);
|
|
959
|
-
config.enforceBudget?.(s.usage);
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
sanitizedResults.push(sanitized);
|
|
963
|
-
}
|
|
1045
|
+
const sanitizedResults = rollUpToolResults(results, config, accumulatedUsage, allSubagents);
|
|
964
1046
|
|
|
965
1047
|
allToolResults.push(...sanitizedResults);
|
|
966
1048
|
context.addToolResults(sanitizedResults);
|
|
@@ -1058,15 +1140,14 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
1058
1140
|
}
|
|
1059
1141
|
// Every failure carries its partial progress for observability.
|
|
1060
1142
|
try {
|
|
1061
|
-
(err as unknown as { partial?: unknown }).partial ??=
|
|
1062
|
-
|
|
1063
|
-
cost: accumulatedUsage.cost,
|
|
1143
|
+
(err as unknown as { partial?: unknown }).partial ??= partialProgress(
|
|
1144
|
+
accumulatedUsage,
|
|
1064
1145
|
turns,
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1146
|
+
allToolCalls,
|
|
1147
|
+
allToolResults,
|
|
1148
|
+
allSubagents,
|
|
1149
|
+
finalResult?.text
|
|
1150
|
+
);
|
|
1070
1151
|
} catch {
|
|
1071
1152
|
// why: partial attachment is observability-only and must never mask the error.
|
|
1072
1153
|
}
|
|
@@ -1130,15 +1211,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1130
1211
|
|
|
1131
1212
|
(async () => {
|
|
1132
1213
|
// Hoisted for catch-block partial reporting.
|
|
1133
|
-
let accumulatedUsage: TokenUsage =
|
|
1134
|
-
inputTokens: 0,
|
|
1135
|
-
outputTokens: 0,
|
|
1136
|
-
totalTokens: 0,
|
|
1137
|
-
cachedTokens: 0,
|
|
1138
|
-
cacheReadTokens: 0,
|
|
1139
|
-
cacheWriteTokens: 0,
|
|
1140
|
-
thinkingTokens: 0,
|
|
1141
|
-
};
|
|
1214
|
+
let accumulatedUsage: TokenUsage = newTurnUsage();
|
|
1142
1215
|
let lastResponse: AgentResponse | null = null;
|
|
1143
1216
|
let turns = 0;
|
|
1144
1217
|
// Tool progress hoisted so failures carry toolCalls/toolResults.
|
|
@@ -1178,15 +1251,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1178
1251
|
return;
|
|
1179
1252
|
}
|
|
1180
1253
|
|
|
1181
|
-
accumulatedUsage =
|
|
1182
|
-
inputTokens: 0,
|
|
1183
|
-
outputTokens: 0,
|
|
1184
|
-
totalTokens: 0,
|
|
1185
|
-
cachedTokens: 0,
|
|
1186
|
-
cacheReadTokens: 0,
|
|
1187
|
-
cacheWriteTokens: 0,
|
|
1188
|
-
thinkingTokens: 0,
|
|
1189
|
-
};
|
|
1254
|
+
accumulatedUsage = newTurnUsage();
|
|
1190
1255
|
|
|
1191
1256
|
// allToolCalls/allToolResults/allSubagents are hoisted above (fresh
|
|
1192
1257
|
// per stream run); just reset turn counters here.
|
|
@@ -1206,20 +1271,14 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1206
1271
|
|
|
1207
1272
|
const spec2 = getModelFromCatalog(provider.id, modelId);
|
|
1208
1273
|
|
|
1209
|
-
const providerOptions
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
};
|
|
1218
|
-
try {
|
|
1219
|
-
if (Number.isFinite(streamLimits.requestTimeoutMs) && streamLimits.requestTimeoutMs !== Infinity) {
|
|
1220
|
-
(providerOptions as unknown as { timeoutMs?: number }).timeoutMs = streamLimits.requestTimeoutMs;
|
|
1221
|
-
}
|
|
1222
|
-
} catch {}
|
|
1274
|
+
const providerOptions = buildTurnOptions(
|
|
1275
|
+
config,
|
|
1276
|
+
standardTools,
|
|
1277
|
+
linked.signal,
|
|
1278
|
+
turns,
|
|
1279
|
+
streamOutputSpec,
|
|
1280
|
+
streamLimits.requestTimeoutMs
|
|
1281
|
+
);
|
|
1223
1282
|
|
|
1224
1283
|
const longRunning = provider.isLongRunning?.(modelId, providerOptions) === true;
|
|
1225
1284
|
const innerStream = provider.stream(
|
|
@@ -1301,35 +1360,9 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1301
1360
|
// Canonical session routing: lets adapters detect provider switches.
|
|
1302
1361
|
noteProviderTurn(runOptions?.sessionId || options?.sessionId, provider.id);
|
|
1303
1362
|
|
|
1304
|
-
|
|
1305
|
-
// it to answer text is gated (default OFF): without the opt-in
|
|
1306
|
-
// the turn keeps the provider's empty text and finishReason.
|
|
1307
|
-
if (
|
|
1308
|
-
(!turnResponse.text || turnResponse.text.trim() === "") &&
|
|
1309
|
-
(!turnResponse.toolCalls || turnResponse.toolCalls.length === 0) &&
|
|
1310
|
-
turnResponse.thinking
|
|
1311
|
-
) {
|
|
1312
|
-
if (shouldPromoteThinkingToAnswer(config, runOptions)) {
|
|
1313
|
-
if (turnResponse.thinking.includes("</think>")) {
|
|
1314
|
-
const parts = turnResponse.thinking.split(/<\/(?:think|thought)>/i);
|
|
1315
|
-
(turnResponse as any).thinking = parts[0]!.replace(/<(?:think|thought)>/i, "").trim() || undefined;
|
|
1316
|
-
(turnResponse as any).text = parts.slice(1).join("").trim();
|
|
1317
|
-
} else {
|
|
1318
|
-
(turnResponse as any).text = turnResponse.thinking;
|
|
1319
|
-
(turnResponse as any).thinking = undefined;
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1363
|
+
promoteThinkingIfAllowed(turnResponse, config, runOptions);
|
|
1323
1364
|
|
|
1324
|
-
|
|
1325
|
-
try {
|
|
1326
|
-
config.onTurnUsage?.(turnResponse.usage, spec2);
|
|
1327
|
-
} catch {}
|
|
1328
|
-
config.enforceBudget?.({
|
|
1329
|
-
...turnResponse.usage,
|
|
1330
|
-
cost: computeCostFromPricing(turnResponse.usage, spec2, options?.serviceTier),
|
|
1331
|
-
});
|
|
1332
|
-
checkTokenLimit(accumulatedUsage, streamLimits.maxTotalTokens);
|
|
1365
|
+
recordModelTurn(config, accumulatedUsage, turnResponse.usage, spec2, streamLimits.maxTotalTokens);
|
|
1333
1366
|
|
|
1334
1367
|
context.addAssistantMessage(
|
|
1335
1368
|
turnResponse.text,
|
|
@@ -1339,24 +1372,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1339
1372
|
turnResponse.providerSteps
|
|
1340
1373
|
);
|
|
1341
1374
|
|
|
1342
|
-
|
|
1343
|
-
// mirrors the non-streaming loop above; bus-only, never wire/history.
|
|
1344
|
-
if (turnResponse.thinking) {
|
|
1345
|
-
try {
|
|
1346
|
-
telemetryBus.emit({
|
|
1347
|
-
code: "turn.thinking",
|
|
1348
|
-
message: `[Agent Accelerator] turn ${turns} thinking (${provider.id}/${modelId})`,
|
|
1349
|
-
data: {
|
|
1350
|
-
turns,
|
|
1351
|
-
model: modelId,
|
|
1352
|
-
provider: provider.id,
|
|
1353
|
-
sessionId: runOptions?.sessionId || options?.sessionId,
|
|
1354
|
-
thinking: turnResponse.thinking,
|
|
1355
|
-
...(turnResponse.thoughtSignature ? { thoughtSignature: turnResponse.thoughtSignature } : {}),
|
|
1356
|
-
},
|
|
1357
|
-
});
|
|
1358
|
-
} catch {}
|
|
1359
|
-
}
|
|
1375
|
+
emitThinkingTrace(turnResponse, turns, provider.id, modelId, runOptions?.sessionId || options?.sessionId);
|
|
1360
1376
|
|
|
1361
1377
|
const noToolSteered =
|
|
1362
1378
|
!turnResponse.toolCalls || turnResponse.toolCalls.length === 0
|
|
@@ -1427,28 +1443,9 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1427
1443
|
const results = guarded.results;
|
|
1428
1444
|
previousToolCallFingerprints = guarded.fingerprints;
|
|
1429
1445
|
|
|
1430
|
-
const sanitizedResults
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
const { sanitized, metas } = sanitizeToolResult(r);
|
|
1434
|
-
if (metas.length > 0) {
|
|
1435
|
-
allSubagents.push(...metas);
|
|
1436
|
-
for (const s of metas) {
|
|
1437
|
-
if (s.raw) s.raw = applyCaptureRaw(s.raw, config.captureRaw);
|
|
1438
|
-
const subagentSpec = getModelFromCatalog(s.provider, s.model);
|
|
1439
|
-
if (subagentSpec && (!s.usage?.cost || !s.usage.cost.totalCost)) {
|
|
1440
|
-
s.usage.cost = computeCostFromPricing(s.usage, subagentSpec);
|
|
1441
|
-
}
|
|
1442
|
-
accumulateUsage(accumulatedUsage, s.usage, subagentSpec);
|
|
1443
|
-
config.enforceBudget?.(s.usage);
|
|
1444
|
-
outerStream.push({
|
|
1445
|
-
type: "subagent_complete",
|
|
1446
|
-
subagent: s,
|
|
1447
|
-
});
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
sanitizedResults.push(sanitized);
|
|
1451
|
-
}
|
|
1446
|
+
const sanitizedResults = rollUpToolResults(results, config, accumulatedUsage, allSubagents, (s) =>
|
|
1447
|
+
outerStream.push({ type: "subagent_complete", subagent: s })
|
|
1448
|
+
);
|
|
1452
1449
|
|
|
1453
1450
|
for (const res of sanitizedResults) {
|
|
1454
1451
|
outerStream.push({
|
|
@@ -1521,15 +1518,14 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1521
1518
|
} catch (err) {
|
|
1522
1519
|
const raw = err instanceof Error ? err : new Error(String(err));
|
|
1523
1520
|
try {
|
|
1524
|
-
(raw as unknown as { partial?: unknown }).partial ??=
|
|
1525
|
-
|
|
1526
|
-
cost: accumulatedUsage.cost,
|
|
1521
|
+
(raw as unknown as { partial?: unknown }).partial ??= partialProgress(
|
|
1522
|
+
accumulatedUsage,
|
|
1527
1523
|
turns,
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1524
|
+
allToolCalls,
|
|
1525
|
+
allToolResults,
|
|
1526
|
+
allSubagents,
|
|
1527
|
+
lastResponse?.text
|
|
1528
|
+
);
|
|
1533
1529
|
} catch {}
|
|
1534
1530
|
outerStream.fail(raw);
|
|
1535
1531
|
return;
|
|
@@ -1582,15 +1578,14 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1582
1578
|
// why: rollback policy itself must never mask the original error.
|
|
1583
1579
|
}
|
|
1584
1580
|
// Every failure carries its partial progress for observability.
|
|
1585
|
-
const partial =
|
|
1586
|
-
|
|
1587
|
-
cost: accumulatedUsage.cost,
|
|
1581
|
+
const partial = partialProgress(
|
|
1582
|
+
accumulatedUsage,
|
|
1588
1583
|
turns,
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1584
|
+
allToolCalls,
|
|
1585
|
+
allToolResults,
|
|
1586
|
+
allSubagents,
|
|
1587
|
+
lastResponse?.text
|
|
1588
|
+
);
|
|
1594
1589
|
// Aborts always surface as AbortError with partial attached.
|
|
1595
1590
|
if (isAbort) {
|
|
1596
1591
|
const abortErr = raw.name === "AbortError" ? raw : makeAbortError(partial, raw);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The named in-memory sessions of one Agent (`run(prompt, { sessionId })`, `agent.session(id)`): one `AgentContext` per
|
|
3
|
+
* id, at most 100 kept (least recently used goes first) and dropped after an hour idle. The Agent's own session is
|
|
4
|
+
* never kept here: it is its default context.
|
|
5
|
+
*/
|
|
6
|
+
import { AgentContext } from "./context.ts";
|
|
7
|
+
import { clearSessionRouting } from "../providers.ts";
|
|
8
|
+
import { clearInteractionChains } from "../providers/google.ts";
|
|
9
|
+
|
|
10
|
+
/** Max named sessions kept per Agent (LRU). */
|
|
11
|
+
const SESSION_LRU_MAX = 100;
|
|
12
|
+
/** Idle TTL for named sessions. */
|
|
13
|
+
const SESSION_TTL_MS = 60 * 60 * 1000;
|
|
14
|
+
|
|
15
|
+
/** The named sessions of one Agent. */
|
|
16
|
+
export class NamedSessions {
|
|
17
|
+
private readonly entries = new Map<string, { context: AgentContext; lastAccess: number }>();
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
private readonly defaultId: () => string,
|
|
21
|
+
private readonly defaultContext: () => AgentContext,
|
|
22
|
+
private readonly create: () => AgentContext
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
// Forgets a session's provider chain and routing state; in: every removal below, out: nothing.
|
|
26
|
+
private forget(id: string): void {
|
|
27
|
+
try {
|
|
28
|
+
clearInteractionChains(id);
|
|
29
|
+
} catch {}
|
|
30
|
+
try {
|
|
31
|
+
clearSessionRouting(id);
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The context for `sessionId` (the default session gives the Agent's own context); expired ones are dropped and the oldest is evicted past the limit. */
|
|
36
|
+
contextFor(sessionId?: string): AgentContext {
|
|
37
|
+
const sid = sessionId || this.defaultId();
|
|
38
|
+
if (sid === this.defaultId()) return this.defaultContext();
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
try {
|
|
41
|
+
for (const [key, entry] of [...this.entries]) {
|
|
42
|
+
if (now - entry.lastAccess > SESSION_TTL_MS) {
|
|
43
|
+
this.entries.delete(key);
|
|
44
|
+
this.forget(key);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch {}
|
|
48
|
+
const existing = this.entries.get(sid);
|
|
49
|
+
if (existing) {
|
|
50
|
+
existing.lastAccess = now;
|
|
51
|
+
return existing.context;
|
|
52
|
+
}
|
|
53
|
+
const fresh = this.create();
|
|
54
|
+
this.entries.set(sid, { context: fresh, lastAccess: now });
|
|
55
|
+
try {
|
|
56
|
+
while (this.entries.size > SESSION_LRU_MAX) {
|
|
57
|
+
let oldest: string | undefined;
|
|
58
|
+
let oldestTs = Infinity;
|
|
59
|
+
for (const [key, entry] of this.entries) {
|
|
60
|
+
if (entry.lastAccess < oldestTs) {
|
|
61
|
+
oldestTs = entry.lastAccess;
|
|
62
|
+
oldest = key;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!oldest) break;
|
|
66
|
+
this.entries.delete(oldest);
|
|
67
|
+
this.forget(oldest);
|
|
68
|
+
}
|
|
69
|
+
} catch {}
|
|
70
|
+
return fresh;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Drops one session and its provider state; no-op for an unknown id. */
|
|
74
|
+
dispose(id: string): void {
|
|
75
|
+
try {
|
|
76
|
+
this.entries.delete(id);
|
|
77
|
+
} catch {}
|
|
78
|
+
this.forget(id);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Drops every named session. */
|
|
82
|
+
clear(): void {
|
|
83
|
+
try {
|
|
84
|
+
for (const id of [...this.entries.keys()]) this.forget(id);
|
|
85
|
+
this.entries.clear();
|
|
86
|
+
} catch {}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The ids of the named sessions held now. */
|
|
90
|
+
ids(): string[] {
|
|
91
|
+
return [...this.entries.keys()];
|
|
92
|
+
}
|
|
93
|
+
}
|