agent-accelerator 0.5.7 → 0.5.9
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 +27 -8
- package/package.json +1 -1
- package/src/agent/agent.ts +84 -45
- package/src/agent/architecture.md +1 -0
- package/src/agent/context.ts +3 -3
- package/src/agent/delegation.ts +12 -12
- package/src/agent/loop.ts +50 -53
- package/src/agent/subagent.ts +4 -4
- package/src/agent/worker.ts +2 -1
- package/src/architecture.md +1 -1
- package/src/index.ts +7 -7
- package/src/models/architecture.md +1 -1
- package/src/models/catalog-cache.ts +7 -7
- package/src/models/catalog.ts +22 -34
- package/src/providers/google.ts +7 -7
- package/src/providers/openai-compat.ts +10 -16
- package/src/providers/openai.ts +8 -7
- package/src/providers/openrouter.ts +4 -4
- package/src/providers/registry.ts +6 -5
- package/src/providers/shared.ts +6 -10
- package/src/scheduler/architecture.md +6 -3
- package/src/scheduler/scheduler.ts +111 -57
- package/src/scheduler/store.ts +273 -59
- package/src/session/architecture.md +1 -1
- package/src/session/store.ts +22 -22
- package/src/streaming/event-stream.ts +11 -11
- package/src/telemetry-file.ts +74 -24
- package/src/telemetry.ts +2 -2
- package/src/tools/executor.ts +1 -1
- package/src/tools/tool.ts +8 -8
- package/src/types/agent.ts +22 -20
- package/src/types/response.ts +8 -8
- package/src/types/scheduler.ts +2 -2
- package/src/types/tool.ts +5 -5
- package/src/update-models.ts +2 -2
- package/src/utils/architecture.md +3 -3
- package/src/utils/cache.ts +4 -4
- package/src/utils/durable.ts +78 -3
- package/src/utils/env.ts +1 -1
- package/src/utils/errors.ts +45 -31
- package/src/utils/headers.ts +4 -4
- package/src/utils/home.ts +1 -1
- package/src/utils/retry.ts +2 -28
- package/src/utils/session.ts +5 -5
package/src/agent/loop.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
import { getModelFromCatalog, ensureModelCatalogFresh, validateModelThinking } from "../models/catalog.ts";
|
|
24
24
|
import { enforceFileAccess } from "../utils/safe-io.ts";
|
|
25
25
|
import { isSubagentResult, normalizeSubagentMetadata } from "../tools/subagent-result.ts";
|
|
26
|
-
import { BudgetExceededError } from "../types/errors.ts";
|
|
26
|
+
import { BudgetExceededError, TimeoutError } from "../types/errors.ts";
|
|
27
27
|
import { applyCaptureRaw, type CaptureRawMode } from "../utils/raw-capture.ts";
|
|
28
28
|
import { previewPrompt } from "../utils/session.ts";
|
|
29
29
|
import { noteProviderTurn, normalizeStructuredOutput, parseStructuredOutput } from "../providers.ts";
|
|
@@ -223,7 +223,7 @@ function normalizeMaxTurns(value?: number): number {
|
|
|
223
223
|
function warnMaxTurnsHit(agentName: string | undefined, maxTurns: number, pendingTools: string[]): void {
|
|
224
224
|
const who = agentName ? `"${agentName}"` : "Agent";
|
|
225
225
|
const tools = pendingTools.length > 0 ? ` Pending tool calls dropped: ${pendingTools.join(", ")}.` : "";
|
|
226
|
-
//
|
|
226
|
+
// No console.* in library code — route via the telemetry bus
|
|
227
227
|
// (default no-op; CLIs attach a pretty logger via telemetryBus.onEvent).
|
|
228
228
|
try {
|
|
229
229
|
telemetryBus.emitWarning(
|
|
@@ -237,11 +237,11 @@ function warnMaxTurnsHit(agentName: string | undefined, maxTurns: number, pendin
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
// ---------------------------------------------------------------------------
|
|
240
|
-
//
|
|
240
|
+
// Tool-call/result pairing repair.
|
|
241
241
|
// ---------------------------------------------------------------------------
|
|
242
242
|
|
|
243
243
|
/**
|
|
244
|
-
* Repairs tool call/result pairing before each provider request
|
|
244
|
+
* Repairs tool call/result pairing before each provider request.
|
|
245
245
|
*
|
|
246
246
|
* Providers reject a `tool_result` without its `tool_call`, a `tool_call`
|
|
247
247
|
* without its result, AND (Chat-Completions style APIs) results that are not
|
|
@@ -306,7 +306,7 @@ export function ensureToolPairing(context: AgentContext): void {
|
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
// ---------------------------------------------------------------------------
|
|
309
|
-
//
|
|
309
|
+
// Abort + limits helpers.
|
|
310
310
|
// ---------------------------------------------------------------------------
|
|
311
311
|
|
|
312
312
|
export interface LoopLimits {
|
|
@@ -319,7 +319,7 @@ export interface LoopLimits {
|
|
|
319
319
|
streamIdleTimeoutMs: number;
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
-
/** Resolves `(config as any).limits` with
|
|
322
|
+
/** Resolves `(config as any).limits` with defaults (timeouts tolerate reasoning). */
|
|
323
323
|
function resolveLoopLimits(config: AgentLoopConfig, fallbackMaxTurns: number): LoopLimits & { maxTurns: number; deadlineMs: number; maxToolCallsPerRun: number; maxTotalTokens: number } {
|
|
324
324
|
const raw = ((config as unknown as { limits?: Record<string, unknown> }).limits ?? {}) as Record<string, unknown>;
|
|
325
325
|
const num = (v: unknown): number | undefined =>
|
|
@@ -389,7 +389,7 @@ function checkTokenLimit(usage: TokenUsage, maxTotalTokens: number): void {
|
|
|
389
389
|
// Throws when the run has been going longer than limits.deadlineMs; in: both loops each turn, out: nothing or a thrown error.
|
|
390
390
|
function checkDeadline(startTime: number, deadlineMs: number): void {
|
|
391
391
|
if (Number.isFinite(deadlineMs) && deadlineMs !== Infinity && Date.now() - startTime > deadlineMs) {
|
|
392
|
-
throw new
|
|
392
|
+
throw new BudgetExceededError(`[Agent Accelerator] Run deadline exceeded after ${deadlineMs}ms.`);
|
|
393
393
|
}
|
|
394
394
|
}
|
|
395
395
|
|
|
@@ -400,9 +400,7 @@ async function generateWithTimeout<T>(task: () => Promise<T>, timeoutMs: number)
|
|
|
400
400
|
try {
|
|
401
401
|
const timeoutP = new Promise<never>((_, reject) => {
|
|
402
402
|
timer = setTimeout(() => {
|
|
403
|
-
|
|
404
|
-
e.name = "TimeoutError";
|
|
405
|
-
reject(e);
|
|
403
|
+
reject(new TimeoutError(`[Agent Accelerator] Request timed out after ${timeoutMs}ms.`));
|
|
406
404
|
}, timeoutMs);
|
|
407
405
|
try { (timer as unknown as { unref?: () => void }).unref?.(); } catch {}
|
|
408
406
|
});
|
|
@@ -436,7 +434,7 @@ export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec, serv
|
|
|
436
434
|
}
|
|
437
435
|
|
|
438
436
|
// ---------------------------------------------------------------------------
|
|
439
|
-
//
|
|
437
|
+
// Canonical usage accumulation (pure w.r.t. source).
|
|
440
438
|
// Invariants (see assertUsageInvariants in session/store.ts):
|
|
441
439
|
// - token counts are finite integers >= 0;
|
|
442
440
|
// - cachedTokens/cacheReadTokens are a SUBSET of inputTokens (clamped below
|
|
@@ -444,7 +442,7 @@ export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec, serv
|
|
|
444
442
|
// - thinkingTokens is a SUBSET of outputTokens when both are reported
|
|
445
443
|
// (reasoning is generated output; enforced in the test helper, documented
|
|
446
444
|
// here because providers vary on which side they omit).
|
|
447
|
-
// This function never mutates `source
|
|
445
|
+
// This function never mutates `source`: the priced cost is cloned
|
|
448
446
|
// into a local before folding into the target.
|
|
449
447
|
// ---------------------------------------------------------------------------
|
|
450
448
|
export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: ModelSpec, serviceTier?: string): void {
|
|
@@ -465,7 +463,7 @@ export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: M
|
|
|
465
463
|
target.cachedTokens = Math.min(preCached, target.inputTokens);
|
|
466
464
|
target.cacheReadTokens = Math.min(preRead, target.inputTokens);
|
|
467
465
|
if ((target.cachedTokens ?? 0) < preCached || (target.cacheReadTokens ?? 0) < preRead) {
|
|
468
|
-
//
|
|
466
|
+
// No console — report via the anomaly hook + telemetry bus.
|
|
469
467
|
reportUsageAnomaly({
|
|
470
468
|
type: "cache_clamp",
|
|
471
469
|
inputTokens: target.inputTokens,
|
|
@@ -475,7 +473,7 @@ export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: M
|
|
|
475
473
|
}
|
|
476
474
|
|
|
477
475
|
const priced = computeCostFromPricing(source, spec, serviceTier);
|
|
478
|
-
//
|
|
476
|
+
// Pure — read `source` only; never assign back to `source.cost`.
|
|
479
477
|
// Clone the priced cost so later target folds cannot alias source state.
|
|
480
478
|
const sourceCost = priced ? { ...priced } : undefined;
|
|
481
479
|
|
|
@@ -673,11 +671,11 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
673
671
|
const maxTurns = limits.maxTurns;
|
|
674
672
|
// Structured outputs: normalize once (throws ValidationError for bad shapes).
|
|
675
673
|
const loopOutput = resolveLoopOutput(config);
|
|
676
|
-
//
|
|
674
|
+
// Pre-run checkpoint for onFailure rollback.
|
|
677
675
|
const preRunCp = context.checkpoint();
|
|
678
676
|
const sessionIdForRollback = loopSessionId(config);
|
|
679
677
|
|
|
680
|
-
//
|
|
678
|
+
// Partial-run state hoisted outside try so the catch block can attach
|
|
681
679
|
// {usage,cost,turns,toolCalls,toolResults,text} to ANY thrown error.
|
|
682
680
|
const startTime = Date.now();
|
|
683
681
|
const accumulatedUsage: TokenUsage = {
|
|
@@ -717,11 +715,11 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
717
715
|
}
|
|
718
716
|
|
|
719
717
|
while (!stoppedByTool && turns < maxTurns) {
|
|
720
|
-
//
|
|
718
|
+
// Fail fast on abort (throws AbortError with partial).
|
|
721
719
|
throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: finalResult?.text });
|
|
722
720
|
checkDeadline(startTime, limits.deadlineMs);
|
|
723
721
|
config.enforceBudget?.();
|
|
724
|
-
//
|
|
722
|
+
// Repair orphan tool pairs before every request.
|
|
725
723
|
ensureToolPairing(context);
|
|
726
724
|
if (config.fileAccess?.roots) await enforceFileAccess(context.messages, config.fileAccess.roots);
|
|
727
725
|
turns++;
|
|
@@ -737,13 +735,13 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
737
735
|
toolChoice: toolChoiceForTurn(options?.toolChoice, turns),
|
|
738
736
|
...(loopOutput.spec ? { output: loopOutput.spec } : {}),
|
|
739
737
|
};
|
|
740
|
-
//
|
|
738
|
+
// Per-request timeout pass-through (providers honor timeoutMs).
|
|
741
739
|
try {
|
|
742
740
|
if (Number.isFinite(limits.requestTimeoutMs) && limits.requestTimeoutMs !== Infinity) {
|
|
743
741
|
(providerOptions as unknown as { timeoutMs?: number }).timeoutMs = limits.requestTimeoutMs;
|
|
744
742
|
}
|
|
745
743
|
} catch {}
|
|
746
|
-
//
|
|
744
|
+
// google:{store:false} opt-out pass-through (provider
|
|
747
745
|
// already respects body.store===false for chaining).
|
|
748
746
|
try {
|
|
749
747
|
const gStore =
|
|
@@ -784,8 +782,8 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
784
782
|
provider.id
|
|
785
783
|
);
|
|
786
784
|
|
|
787
|
-
// Thinking is exposed via events/traces and onTurn. Promoting it to answer text is gated (
|
|
788
|
-
//
|
|
785
|
+
// Thinking is exposed via events/traces and onTurn. Promoting it to answer text is gated (default
|
|
786
|
+
// OFF): without the opt-in the run returns the provider's empty text and finishReason.
|
|
789
787
|
if ((!genResult.text || genResult.text.trim() === "") && (!genResult.toolCalls || genResult.toolCalls.length === 0) && genResult.thinking) {
|
|
790
788
|
if (shouldPromoteThinkingToAnswer(config, runOptions)) {
|
|
791
789
|
if (genResult.thinking.includes("</think>")) {
|
|
@@ -853,13 +851,13 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
853
851
|
}
|
|
854
852
|
|
|
855
853
|
// Execute tool calls — always parallel (model-driven, bloatfree DX7)
|
|
856
|
-
//
|
|
854
|
+
// Check abort before tools (throws AbortError with partial).
|
|
857
855
|
throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: genResult.text });
|
|
858
856
|
emitProgress(config); // the calls are on record BEFORE they run, so a crash can finish them instead of forgetting them
|
|
859
857
|
allToolCalls.push(...genResult.toolCalls);
|
|
860
|
-
//
|
|
858
|
+
// Per-run tool-call budget.
|
|
861
859
|
if (allToolCalls.length > limits.maxToolCallsPerRun) {
|
|
862
|
-
throw new
|
|
860
|
+
throw new BudgetExceededError(
|
|
863
861
|
`[Agent Accelerator] maxToolCallsPerRun exceeded (${allToolCalls.length} > ${limits.maxToolCallsPerRun}).`
|
|
864
862
|
);
|
|
865
863
|
}
|
|
@@ -901,7 +899,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
901
899
|
allToolResults.push(...sanitizedResults);
|
|
902
900
|
context.addToolResults(sanitizedResults);
|
|
903
901
|
checkTokenLimit(accumulatedUsage, limits.maxTotalTokens);
|
|
904
|
-
//
|
|
902
|
+
// Results are stored BEFORE the cancellation check so an abort
|
|
905
903
|
// never drops executed tool work from history.
|
|
906
904
|
throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: genResult.text });
|
|
907
905
|
emitTurn(runOptions, {
|
|
@@ -926,7 +924,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
926
924
|
}
|
|
927
925
|
emitProgress(config);
|
|
928
926
|
|
|
929
|
-
//
|
|
927
|
+
// Abort always throws AbortError with partial attached (never
|
|
930
928
|
// silently breaks).
|
|
931
929
|
throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: finalResult?.text });
|
|
932
930
|
}
|
|
@@ -963,7 +961,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
963
961
|
responseId: finalResult?.responseId,
|
|
964
962
|
model: modelId,
|
|
965
963
|
provider: provider.id,
|
|
966
|
-
//
|
|
964
|
+
// Canonical finish reason + raw vendor passthrough.
|
|
967
965
|
...(stoppedByTool
|
|
968
966
|
? { finishReason: "stopped" as const, ...(finalResult?.finishReason ? { rawFinishReason: finalResult.finishReason } : {}) }
|
|
969
967
|
: canonicalFinishReason(finalResult?.finishReason, truncatedByMaxTurns)),
|
|
@@ -972,7 +970,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
972
970
|
turns,
|
|
973
971
|
});
|
|
974
972
|
} catch (err) {
|
|
975
|
-
//
|
|
973
|
+
// Run-level onFailure rollback (default restores pre-run
|
|
976
974
|
// checkpoint; opt out with (config as any).onFailure === "keep").
|
|
977
975
|
// Aborts are excluded so partial history survives interruption.
|
|
978
976
|
const isAbort =
|
|
@@ -983,14 +981,14 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
983
981
|
// why: rollback is best-effort — the original error takes precedence.
|
|
984
982
|
}
|
|
985
983
|
}
|
|
986
|
-
//
|
|
984
|
+
// Ensure aborts surface as AbortError with partial attached.
|
|
987
985
|
if (isAbort && (err as { name?: string } | null)?.name !== "AbortError") {
|
|
988
986
|
throw makeAbortError(
|
|
989
987
|
{ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }, turns: 0 },
|
|
990
988
|
err
|
|
991
989
|
);
|
|
992
990
|
}
|
|
993
|
-
//
|
|
991
|
+
// Every failure carries its partial progress for observability.
|
|
994
992
|
try {
|
|
995
993
|
(err as unknown as { partial?: unknown }).partial ??= {
|
|
996
994
|
usage: accumulatedUsage,
|
|
@@ -998,6 +996,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
998
996
|
turns,
|
|
999
997
|
toolCalls: allToolCalls,
|
|
1000
998
|
toolResults: allToolResults,
|
|
999
|
+
subagents: allSubagents,
|
|
1001
1000
|
text: finalResult?.text,
|
|
1002
1001
|
};
|
|
1003
1002
|
} catch {
|
|
@@ -1042,7 +1041,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1042
1041
|
}
|
|
1043
1042
|
};
|
|
1044
1043
|
|
|
1045
|
-
//
|
|
1044
|
+
// Per-run checkpoint + limits for the streaming loop.
|
|
1046
1045
|
const streamPreCp = config.context.checkpoint();
|
|
1047
1046
|
const streamSessionId = loopSessionId(config);
|
|
1048
1047
|
const streamFallbackMax = normalizeMaxTurns((config as { maxTurns?: number }).maxTurns);
|
|
@@ -1050,7 +1049,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1050
1049
|
const streamStartTime = startTime;
|
|
1051
1050
|
|
|
1052
1051
|
(async () => {
|
|
1053
|
-
// Hoisted for catch-block partial reporting
|
|
1052
|
+
// Hoisted for catch-block partial reporting.
|
|
1054
1053
|
let accumulatedUsage: TokenUsage = {
|
|
1055
1054
|
inputTokens: 0,
|
|
1056
1055
|
outputTokens: 0,
|
|
@@ -1062,7 +1061,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1062
1061
|
};
|
|
1063
1062
|
let lastResponse: AgentResponse | null = null;
|
|
1064
1063
|
let turns = 0;
|
|
1065
|
-
//
|
|
1064
|
+
// Tool progress hoisted so failures carry toolCalls/toolResults.
|
|
1066
1065
|
const allToolCalls: ToolCallRecord[] = [];
|
|
1067
1066
|
const allToolResults: ToolResultRecord[] = [];
|
|
1068
1067
|
const allSubagents: SubAgentExecutionMetadata[] = [];
|
|
@@ -1118,7 +1117,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1118
1117
|
thinkingTokens: 0,
|
|
1119
1118
|
};
|
|
1120
1119
|
|
|
1121
|
-
//
|
|
1120
|
+
// allToolCalls/allToolResults/allSubagents are hoisted above (fresh
|
|
1122
1121
|
// per stream run); just reset turn counters here.
|
|
1123
1122
|
lastResponse = null;
|
|
1124
1123
|
turns = 0;
|
|
@@ -1176,7 +1175,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1176
1175
|
);
|
|
1177
1176
|
currentInner = innerStream as AssistantMessageEventStream;
|
|
1178
1177
|
|
|
1179
|
-
//
|
|
1178
|
+
// First-byte + idle timeouts around SSE (tolerate reasoning).
|
|
1180
1179
|
// Implemented as races on the inner iterator so a hung provider
|
|
1181
1180
|
// surfaces as TimeoutError instead of hanging the run forever.
|
|
1182
1181
|
{
|
|
@@ -1191,11 +1190,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1191
1190
|
Number.isFinite(waitMs) && waitMs !== Infinity && waitMs > 0
|
|
1192
1191
|
? new Promise<never>((_, reject) => {
|
|
1193
1192
|
timer = setTimeout(() => {
|
|
1194
|
-
|
|
1195
|
-
`[Agent Accelerator] Stream ${first ? "first-byte" : "idle"} timed out after ${waitMs}ms.`
|
|
1196
|
-
);
|
|
1197
|
-
e.name = "TimeoutError";
|
|
1198
|
-
reject(e);
|
|
1193
|
+
reject(new TimeoutError(`[Agent Accelerator] Stream ${first ? "first-byte" : "idle"} timed out after ${waitMs}ms.`));
|
|
1199
1194
|
}, waitMs);
|
|
1200
1195
|
try { (timer as unknown as { unref?: () => void }).unref?.(); } catch {}
|
|
1201
1196
|
})
|
|
@@ -1238,7 +1233,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1238
1233
|
);
|
|
1239
1234
|
|
|
1240
1235
|
// Thinking stays on events/traces + onTurn (default exposed). Promoting
|
|
1241
|
-
// it to answer text is gated (
|
|
1236
|
+
// it to answer text is gated (default OFF): without the opt-in
|
|
1242
1237
|
// the turn keeps the provider's empty text and finishReason.
|
|
1243
1238
|
if ((!turnResponse.text || turnResponse.text.trim() === "") && (!turnResponse.toolCalls || turnResponse.toolCalls.length === 0) && turnResponse.thinking) {
|
|
1244
1239
|
if (shouldPromoteThinkingToAnswer(config, runOptions)) {
|
|
@@ -1311,11 +1306,11 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1311
1306
|
emitProgress(config); // the calls are on record BEFORE they run, so a crash can finish them instead of forgetting them
|
|
1312
1307
|
allToolCalls.push(...turnResponse.toolCalls);
|
|
1313
1308
|
if (allToolCalls.length > streamLimits.maxToolCallsPerRun) {
|
|
1314
|
-
throw new
|
|
1309
|
+
throw new BudgetExceededError(
|
|
1315
1310
|
`[Agent Accelerator] maxToolCallsPerRun exceeded (${allToolCalls.length} > ${streamLimits.maxToolCallsPerRun}).`
|
|
1316
1311
|
);
|
|
1317
1312
|
}
|
|
1318
|
-
//
|
|
1313
|
+
// Typed tool lifecycle for streaming consumers.
|
|
1319
1314
|
try {
|
|
1320
1315
|
outerStream.push({ type: "tool_start", toolCalls: turnResponse.toolCalls });
|
|
1321
1316
|
} catch {
|
|
@@ -1379,14 +1374,14 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1379
1374
|
toolResult: res,
|
|
1380
1375
|
});
|
|
1381
1376
|
}
|
|
1382
|
-
//
|
|
1377
|
+
// Close the tool lifecycle opened by `tool_start` above.
|
|
1383
1378
|
try {
|
|
1384
1379
|
outerStream.push({ type: "tool_end", toolResults: sanitizedResults });
|
|
1385
1380
|
} catch {
|
|
1386
1381
|
// why: stream push never throws, but guard anyway to protect the loop.
|
|
1387
1382
|
}
|
|
1388
1383
|
|
|
1389
|
-
//
|
|
1384
|
+
// Store tool results BEFORE the cancellation check.
|
|
1390
1385
|
allToolResults.push(...sanitizedResults);
|
|
1391
1386
|
context.addToolResults(sanitizedResults);
|
|
1392
1387
|
throwIfCancelled({ usage: accumulatedUsage, turns, text: turnResponse.text });
|
|
@@ -1444,6 +1439,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1444
1439
|
turns,
|
|
1445
1440
|
toolCalls: allToolCalls,
|
|
1446
1441
|
toolResults: allToolResults,
|
|
1442
|
+
subagents: allSubagents,
|
|
1447
1443
|
text: lastResponse?.text,
|
|
1448
1444
|
}; } catch {}
|
|
1449
1445
|
outerStream.fail(raw);
|
|
@@ -1463,7 +1459,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1463
1459
|
responseId: lastResponse?.responseId,
|
|
1464
1460
|
model: modelId,
|
|
1465
1461
|
provider: provider.id,
|
|
1466
|
-
//
|
|
1462
|
+
// Canonical finish reason + raw vendor passthrough.
|
|
1467
1463
|
...truncatedFinish,
|
|
1468
1464
|
durationMs: Date.now() - startTime,
|
|
1469
1465
|
raw: applyCaptureRaw(lastResponse?.raw, config.captureRaw) as ProviderRawData,
|
|
@@ -1486,7 +1482,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1486
1482
|
linked.signal.aborted ||
|
|
1487
1483
|
outerStream.isCancelled() ||
|
|
1488
1484
|
(raw as any)?.name === "AbortError";
|
|
1489
|
-
//
|
|
1485
|
+
// Streaming onFailure rollback (same policy as runAgentLoop;
|
|
1490
1486
|
// aborts excluded so partial history survives).
|
|
1491
1487
|
try {
|
|
1492
1488
|
if (shouldRollbackOnFailure(config.onFailure, isAbort, toolsExecuted)) {
|
|
@@ -1497,16 +1493,17 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1497
1493
|
} catch {
|
|
1498
1494
|
// why: rollback policy itself must never mask the original error.
|
|
1499
1495
|
}
|
|
1500
|
-
//
|
|
1496
|
+
// Every failure carries its partial progress for observability.
|
|
1501
1497
|
const partial = {
|
|
1502
1498
|
usage: accumulatedUsage,
|
|
1503
1499
|
cost: accumulatedUsage.cost,
|
|
1504
1500
|
turns,
|
|
1505
1501
|
toolCalls: allToolCalls,
|
|
1506
1502
|
toolResults: allToolResults,
|
|
1503
|
+
subagents: allSubagents,
|
|
1507
1504
|
text: lastResponse?.text,
|
|
1508
1505
|
};
|
|
1509
|
-
//
|
|
1506
|
+
// Aborts always surface as AbortError with partial attached.
|
|
1510
1507
|
if (isAbort) {
|
|
1511
1508
|
const abortErr =
|
|
1512
1509
|
raw.name === "AbortError" ? raw : makeAbortError(partial, raw);
|
|
@@ -1537,12 +1534,12 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1537
1534
|
}
|
|
1538
1535
|
|
|
1539
1536
|
// ---------------------------------------------------------------------------
|
|
1540
|
-
//
|
|
1537
|
+
// Thinking-promotion policy + canonical finish helper.
|
|
1541
1538
|
// (Appended here to avoid conflicts with other loop sections.)
|
|
1542
1539
|
// ---------------------------------------------------------------------------
|
|
1543
1540
|
|
|
1544
1541
|
/**
|
|
1545
|
-
* Whether an empty-text turn may promote `thinking` to answer `text
|
|
1542
|
+
* Whether an empty-text turn may promote `thinking` to answer `text`.
|
|
1546
1543
|
* Default false: the run returns the provider's empty text with its
|
|
1547
1544
|
* finishReason instead of fabricating an answer from reasoning. Opt in via
|
|
1548
1545
|
* `(runOptions as any).thinkingAsAnswerFallback === true` or
|
|
@@ -1567,7 +1564,7 @@ export function shouldPromoteThinkingToAnswer(config?: unknown, runOptions?: unk
|
|
|
1567
1564
|
}
|
|
1568
1565
|
|
|
1569
1566
|
/**
|
|
1570
|
-
* Canonicalizes a provider finish reason for AgentResponse
|
|
1567
|
+
* Canonicalizes a provider finish reason for AgentResponse.
|
|
1571
1568
|
* When `truncated` (maxTurns exhausted with pending tools), forces
|
|
1572
1569
|
* `"max_turns"` and preserves the provider's reason in `rawFinishReason`.
|
|
1573
1570
|
* Otherwise delegates to `normalizeFinishReason` (unknown vendor strings map
|
package/src/agent/subagent.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { getModel, getSubModel } from "../utils/env.ts";
|
|
|
9
9
|
export interface SubAgentConfig extends AgentConfig {}
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Canonical env-based construction
|
|
12
|
+
* Canonical env-based construction: builds an `Agent` reading
|
|
13
13
|
* `MODEL`/`SUB_AGENT_MODEL` only for the fields the caller omits, via
|
|
14
14
|
* `getModel()`/`getSubModel()`. Explicit config always wins; constructing an
|
|
15
15
|
* `Agent` with an explicit `model` reads no env for routing. The static
|
|
@@ -36,12 +36,12 @@ export function agentFromEnv(
|
|
|
36
36
|
* It inherits 100% of the capabilities of Agent, defaults to SUB_AGENT_MODEL,
|
|
37
37
|
* and provides .asTool() and .toTool() for seamless registration into parent agents.
|
|
38
38
|
*
|
|
39
|
-
* Model fallback chain (
|
|
39
|
+
* Model fallback chain (canonical): explicit `config.model` ??
|
|
40
40
|
* `SUB_AGENT_MODEL` (`getSubModel()`) ?? `MODEL` (`getModel()`). Missing
|
|
41
41
|
* everything throws. Constructing any `Agent`/`SubAgent` with an explicit
|
|
42
42
|
* `model` reads no env for routing.
|
|
43
43
|
*
|
|
44
|
-
*
|
|
44
|
+
* Decomposition boundary: this class stays a thin wrapper. The run/stream
|
|
45
45
|
* loops live in `agent.ts`/`loop.ts`, spawning in `delegation.ts`
|
|
46
46
|
* (`agentToTool`), and model parsing in `utils/session.ts` (`parseModelRef`).
|
|
47
47
|
* No loop, transport, or formatting logic is duplicated here.
|
|
@@ -104,7 +104,7 @@ export class SubAgent extends Agent {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
//
|
|
107
|
+
// `Agent.fromEnv()` static without touching `agent.ts` (owned by
|
|
108
108
|
// another lane). Wired at module load; typed via the declaration below so
|
|
109
109
|
// `Agent.fromEnv({...})` typechecks wherever this module is imported.
|
|
110
110
|
declare module "./agent.ts" {
|
package/src/agent/worker.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { Agent } from "./agent.ts";
|
|
|
5
5
|
import { normalizeToolName } from "../tools/executor.ts";
|
|
6
6
|
import { getModel, getSubModel } from "../utils/env.ts";
|
|
7
7
|
import { resolveModel } from "../providers/registry.ts";
|
|
8
|
+
import { ConfigError } from "../types/errors.ts";
|
|
8
9
|
|
|
9
10
|
/** Minimal live-trace shape shared by fixed + dynamic workers (structural subset of `SubAgentTrace`). */
|
|
10
11
|
export interface LiveWorkerTrace {
|
|
@@ -134,7 +135,7 @@ export function resolveDynamicModelInput(
|
|
|
134
135
|
export function resolveSubAgentModel(config: { name?: string; model?: AgentConfig["model"] }): NonNullable<AgentConfig["model"]> {
|
|
135
136
|
const resolvedModel = config.model ?? getSubModel() ?? getModel();
|
|
136
137
|
if (!resolvedModel) {
|
|
137
|
-
throw new
|
|
138
|
+
throw new ConfigError(
|
|
138
139
|
`[Agent Accelerator] SubAgent '${config.name || "subagent"}' requires a model. ` +
|
|
139
140
|
`Specify 'model' in SubAgent config, or set SUB_AGENT_MODEL or MODEL in environment variables.`
|
|
140
141
|
);
|
package/src/architecture.md
CHANGED
|
@@ -6,7 +6,7 @@ The whole SDK. `index.ts` is the only public entry (`main`/`module`/`types`); ev
|
|
|
6
6
|
| :-- | :-- |
|
|
7
7
|
| `index.ts` | The public barrel. Adding or removing an export is a public API change. |
|
|
8
8
|
| `telemetry.ts` | The in-process event bus (`telemetryBus`). Imports nothing from the SDK, so any module may use it. |
|
|
9
|
-
| `telemetry-file.ts` | Opt-in NDJSON sink for the bus
|
|
9
|
+
| `telemetry-file.ts` | Opt-in NDJSON sink for the bus: a persisted session's events go to `<session folder>/telemetry.ndjson`, the rest to `<home>/telemetry/<date>.ndjson`; never outside a home. |
|
|
10
10
|
| `update-models.ts` | CLI behind `bun run update-models` (refreshes the model catalog cache). |
|
|
11
11
|
| `providers.ts` | Provider-agnostic mappers (thinking, cache, tool choice, structured output, generation params). No HTTP. |
|
|
12
12
|
| `agent/` | `Agent`, the model-to-tools loop, workers, delegation. |
|
package/src/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ export { Agent, SubAgentModelError } from "./agent/agent.ts";
|
|
|
15
15
|
/**
|
|
16
16
|
* @deprecated Compat alias: prefer composing a plain `Agent` and exposing it
|
|
17
17
|
* via `agentToTool()`/`buildAgentTools()` (or `subagents: [...]`). `SubAgent`
|
|
18
|
-
* stays for compat and receives no new features
|
|
18
|
+
* stays for compat and receives no new features.
|
|
19
19
|
*/
|
|
20
20
|
export { SubAgent, agentFromEnv } from "./agent/subagent.ts";
|
|
21
21
|
export type { SubAgentConfig } from "./agent/subagent.ts";
|
|
@@ -33,7 +33,7 @@ export type { CreateToolOptions } from "./tools/tool.ts";
|
|
|
33
33
|
export { zodToJsonSchema, cleanJsonSchema } from "./tools/schema.ts";
|
|
34
34
|
/**
|
|
35
35
|
* @deprecated Internal executor surface. Prefer `Agent` runs; for direct
|
|
36
|
-
* execution import from the future `/internal` entry
|
|
36
|
+
* execution import from the future `/internal` entry.
|
|
37
37
|
*/
|
|
38
38
|
export { executeToolCalls } from "./tools/executor.ts";
|
|
39
39
|
|
|
@@ -135,7 +135,7 @@ export {
|
|
|
135
135
|
/**
|
|
136
136
|
* @deprecated Internal provider mappers/routing helpers. Prefer the
|
|
137
137
|
* provider-agnostic registry (`resolveModel`/`getProvider`); direct mapper
|
|
138
|
-
* use moves to the future `/internal` entry
|
|
138
|
+
* use moves to the future `/internal` entry.
|
|
139
139
|
*/
|
|
140
140
|
export {
|
|
141
141
|
emitProviderWarning,
|
|
@@ -204,7 +204,7 @@ export type { SteerEntry } from "./agent/loop.ts";
|
|
|
204
204
|
// Responses
|
|
205
205
|
export { AgentResponse } from "./types/response.ts";
|
|
206
206
|
|
|
207
|
-
// Core error taxonomy (
|
|
207
|
+
// Core error taxonomy (core surface)
|
|
208
208
|
export {
|
|
209
209
|
AgentAccelError,
|
|
210
210
|
ConfigError,
|
|
@@ -219,7 +219,7 @@ export {
|
|
|
219
219
|
formatErrorPlain,
|
|
220
220
|
} from "./types/errors.ts";
|
|
221
221
|
export type { AgentAccelErrorCode, AgentAccelErrorContext } from "./types/errors.ts";
|
|
222
|
-
// Normalized run limits (
|
|
222
|
+
// Normalized run limits (sub-agents inherit unless overridden)
|
|
223
223
|
export type { AgentRunLimits } from "./agent/agent.ts";
|
|
224
224
|
|
|
225
225
|
// Telemetry bus: warnings, anomalies and per-turn thinking traces (attach with `telemetryBus.onEvent`)
|
|
@@ -256,14 +256,14 @@ export type { SessionLimits, SessionPayloadStatus, PersistedAgentSession, Sessio
|
|
|
256
256
|
// Utilities
|
|
257
257
|
/**
|
|
258
258
|
* @deprecated Session-routing internals. Prefer agent-level `sessionId`;
|
|
259
|
-
* direct use moves to the future `/internal` entry
|
|
259
|
+
* direct use moves to the future `/internal` entry.
|
|
260
260
|
*/
|
|
261
261
|
export { createSessionId, hashSessionPart, createTrackingId, nowMs, newRunId, newTurnId, previewPrompt, parseModelRef } from "./utils/session.ts";
|
|
262
262
|
export type { ParsedModelRef } from "./utils/session.ts";
|
|
263
263
|
/**
|
|
264
264
|
* @deprecated Raw env access. Prefer explicit `Agent` config (which wins over
|
|
265
265
|
* env) and `Agent.fromEnv()`/`agentFromEnv()`; direct use moves to the future
|
|
266
|
-
* `/internal` entry
|
|
266
|
+
* `/internal` entry.
|
|
267
267
|
*/
|
|
268
268
|
export { getApiKey, getEnv } from "./utils/env.ts";
|
|
269
269
|
export { buildSessionHeaders, setAttribution, getAttribution } from "./utils/headers.ts";
|
|
@@ -5,7 +5,7 @@ The model catalog: context limits, prices, modalities and reasoning options come
|
|
|
5
5
|
| File | What it is |
|
|
6
6
|
| :-- | :-- |
|
|
7
7
|
| `catalog-cache.ts` | Downloads and caches `models.dev/api.json` in `<home>/catalog/` (or `AGENT_CACHE_DIR`). Stale-while-revalidate: an expired catalog is served at once and refreshed in the background (8 s timeout, 10 min backoff after a failure); only a cold start waits. `AGENT_CATALOG_REFRESH=off` stops automatic refreshes. |
|
|
8
|
-
| `catalog.ts` | Lookups (`getModelFromCatalog`), `registerModel`, thinking validation (`validateModelThinking`), and the exported `GOOGLE_/OPENAI_/OPENROUTER_MODELS` arrays (rebuilt in place when the catalog changes). |
|
|
8
|
+
| `catalog.ts` | Lookups (`getModelFromCatalog`), `registerModel`, thinking validation (`validateModelThinking`, which throws `ThinkingLevelError`, a one-line `ConfigError`), and the exported `GOOGLE_/OPENAI_/OPENROUTER_MODELS` arrays (rebuilt in place when the catalog changes). |
|
|
9
9
|
|
|
10
10
|
## Invariants
|
|
11
11
|
- Never hand-edit the cache file; register custom models with `registerModel()`. Verified modality fixes go in `MODALITY_OVERRIDES`.
|
|
@@ -9,13 +9,13 @@ import { writeFileDurable } from "../utils/durable.ts";
|
|
|
9
9
|
/** Default TTL: 12 Hours (in milliseconds). */
|
|
10
10
|
export const DEFAULT_CATALOG_TTL_MS = 12 * 60 * 60 * 1000;
|
|
11
11
|
|
|
12
|
-
/** Max accepted models.dev payload size: 15MB
|
|
12
|
+
/** Max accepted models.dev payload size: 15MB. Larger payloads are rejected before parse. */
|
|
13
13
|
export const MAX_CATALOG_PAYLOAD_BYTES = 15 * 1024 * 1024;
|
|
14
14
|
|
|
15
15
|
let globalCatalogTtlMs = DEFAULT_CATALOG_TTL_MS;
|
|
16
16
|
|
|
17
17
|
/** Sets the global model catalog cache TTL in milliseconds.
|
|
18
|
-
* Note
|
|
18
|
+
* Note: cache files carry their own `ttlMs`. A global `setCatalogTTL`
|
|
19
19
|
* does not rewrite in-flight file TTLs; it takes effect on the next refresh
|
|
20
20
|
* (fresh download) or when the cached file has no `ttlMs` of its own.
|
|
21
21
|
*/
|
|
@@ -90,7 +90,7 @@ export function getCacheDir(): string {
|
|
|
90
90
|
return path.join(resolveHome(), "catalog");
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
/** Cache file path: `<cacheDir>/models.dev.json`, honoring AGENT_CACHE_DIR
|
|
93
|
+
/** Cache file path: `<cacheDir>/models.dev.json`, honoring AGENT_CACHE_DIR. */
|
|
94
94
|
export function getCacheFilePath(): string {
|
|
95
95
|
return path.join(getCacheDir(), "models.dev.json");
|
|
96
96
|
}
|
|
@@ -111,7 +111,7 @@ function writeJsonFileSync(filePath: string, data: any): void {
|
|
|
111
111
|
writeFileDurable(filePath, JSON.stringify(data), { backup: false });
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
/** Verifies optional sha256 pin from MODELS_DEV_SHA256
|
|
114
|
+
/** Verifies optional sha256 pin from MODELS_DEV_SHA256. Returns false on mismatch. */
|
|
115
115
|
export function verifyCatalogSha256(text: string): boolean {
|
|
116
116
|
const pin = getEnv("MODELS_DEV_SHA256")?.trim().toLowerCase();
|
|
117
117
|
if (!pin) return true;
|
|
@@ -283,14 +283,14 @@ export async function refreshModelCatalog(options: RefreshCatalogOptions = {}):
|
|
|
283
283
|
throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
-
//
|
|
286
|
+
// Size-cap before parse (check text length, reject >15MB).
|
|
287
287
|
const text = await res.text();
|
|
288
288
|
if (text.length > MAX_CATALOG_PAYLOAD_BYTES) {
|
|
289
289
|
clearTimeout(timer);
|
|
290
290
|
fallbackToDiskCache();
|
|
291
291
|
return getCatalogStatus();
|
|
292
292
|
}
|
|
293
|
-
//
|
|
293
|
+
// Optional sha256 pin via MODELS_DEV_SHA256.
|
|
294
294
|
if (!verifyCatalogSha256(text)) {
|
|
295
295
|
clearTimeout(timer);
|
|
296
296
|
fallbackToDiskCache();
|
|
@@ -334,7 +334,7 @@ export async function refreshModelCatalog(options: RefreshCatalogOptions = {}):
|
|
|
334
334
|
|
|
335
335
|
// Update in-memory state FIRST so a read-only filesystem still serves the
|
|
336
336
|
// fresh catalog for this process; disk persistence below is best-effort.
|
|
337
|
-
//
|
|
337
|
+
// A real download is NOT from cache.
|
|
338
338
|
activeCatalog = freshData;
|
|
339
339
|
activeFetchedAt = fetchedAt;
|
|
340
340
|
activeTtlMs = effectiveTtl;
|