agent-accelerator 0.5.7 → 0.5.8
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 +4 -4
- package/package.json +1 -1
- package/src/agent/agent.ts +76 -39
- package/src/agent/context.ts +3 -3
- package/src/agent/delegation.ts +12 -12
- package/src/agent/loop.ts +41 -41
- package/src/agent/subagent.ts +4 -4
- package/src/architecture.md +1 -1
- package/src/index.ts +7 -7
- package/src/models/catalog-cache.ts +7 -7
- package/src/models/catalog.ts +13 -13
- package/src/providers/google.ts +4 -4
- package/src/providers/openai-compat.ts +7 -7
- package/src/providers/openai.ts +5 -5
- package/src/providers/openrouter.ts +2 -2
- package/src/providers/shared.ts +4 -4
- package/src/scheduler/architecture.md +5 -2
- 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 +1 -1
- package/src/utils/cache.ts +4 -4
- package/src/utils/durable.ts +78 -3
- package/src/utils/env.ts +1 -1
- package/src/utils/headers.ts +4 -4
- package/src/utils/home.ts +1 -1
- package/src/utils/retry.ts +1 -1
- package/src/utils/session.ts +5 -5
package/src/agent/loop.ts
CHANGED
|
@@ -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 =>
|
|
@@ -436,7 +436,7 @@ export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec, serv
|
|
|
436
436
|
}
|
|
437
437
|
|
|
438
438
|
// ---------------------------------------------------------------------------
|
|
439
|
-
//
|
|
439
|
+
// Canonical usage accumulation (pure w.r.t. source).
|
|
440
440
|
// Invariants (see assertUsageInvariants in session/store.ts):
|
|
441
441
|
// - token counts are finite integers >= 0;
|
|
442
442
|
// - cachedTokens/cacheReadTokens are a SUBSET of inputTokens (clamped below
|
|
@@ -444,7 +444,7 @@ export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec, serv
|
|
|
444
444
|
// - thinkingTokens is a SUBSET of outputTokens when both are reported
|
|
445
445
|
// (reasoning is generated output; enforced in the test helper, documented
|
|
446
446
|
// here because providers vary on which side they omit).
|
|
447
|
-
// This function never mutates `source
|
|
447
|
+
// This function never mutates `source`: the priced cost is cloned
|
|
448
448
|
// into a local before folding into the target.
|
|
449
449
|
// ---------------------------------------------------------------------------
|
|
450
450
|
export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: ModelSpec, serviceTier?: string): void {
|
|
@@ -465,7 +465,7 @@ export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: M
|
|
|
465
465
|
target.cachedTokens = Math.min(preCached, target.inputTokens);
|
|
466
466
|
target.cacheReadTokens = Math.min(preRead, target.inputTokens);
|
|
467
467
|
if ((target.cachedTokens ?? 0) < preCached || (target.cacheReadTokens ?? 0) < preRead) {
|
|
468
|
-
//
|
|
468
|
+
// No console — report via the anomaly hook + telemetry bus.
|
|
469
469
|
reportUsageAnomaly({
|
|
470
470
|
type: "cache_clamp",
|
|
471
471
|
inputTokens: target.inputTokens,
|
|
@@ -475,7 +475,7 @@ export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: M
|
|
|
475
475
|
}
|
|
476
476
|
|
|
477
477
|
const priced = computeCostFromPricing(source, spec, serviceTier);
|
|
478
|
-
//
|
|
478
|
+
// Pure — read `source` only; never assign back to `source.cost`.
|
|
479
479
|
// Clone the priced cost so later target folds cannot alias source state.
|
|
480
480
|
const sourceCost = priced ? { ...priced } : undefined;
|
|
481
481
|
|
|
@@ -673,11 +673,11 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
673
673
|
const maxTurns = limits.maxTurns;
|
|
674
674
|
// Structured outputs: normalize once (throws ValidationError for bad shapes).
|
|
675
675
|
const loopOutput = resolveLoopOutput(config);
|
|
676
|
-
//
|
|
676
|
+
// Pre-run checkpoint for onFailure rollback.
|
|
677
677
|
const preRunCp = context.checkpoint();
|
|
678
678
|
const sessionIdForRollback = loopSessionId(config);
|
|
679
679
|
|
|
680
|
-
//
|
|
680
|
+
// Partial-run state hoisted outside try so the catch block can attach
|
|
681
681
|
// {usage,cost,turns,toolCalls,toolResults,text} to ANY thrown error.
|
|
682
682
|
const startTime = Date.now();
|
|
683
683
|
const accumulatedUsage: TokenUsage = {
|
|
@@ -717,11 +717,11 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
717
717
|
}
|
|
718
718
|
|
|
719
719
|
while (!stoppedByTool && turns < maxTurns) {
|
|
720
|
-
//
|
|
720
|
+
// Fail fast on abort (throws AbortError with partial).
|
|
721
721
|
throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: finalResult?.text });
|
|
722
722
|
checkDeadline(startTime, limits.deadlineMs);
|
|
723
723
|
config.enforceBudget?.();
|
|
724
|
-
//
|
|
724
|
+
// Repair orphan tool pairs before every request.
|
|
725
725
|
ensureToolPairing(context);
|
|
726
726
|
if (config.fileAccess?.roots) await enforceFileAccess(context.messages, config.fileAccess.roots);
|
|
727
727
|
turns++;
|
|
@@ -737,13 +737,13 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
737
737
|
toolChoice: toolChoiceForTurn(options?.toolChoice, turns),
|
|
738
738
|
...(loopOutput.spec ? { output: loopOutput.spec } : {}),
|
|
739
739
|
};
|
|
740
|
-
//
|
|
740
|
+
// Per-request timeout pass-through (providers honor timeoutMs).
|
|
741
741
|
try {
|
|
742
742
|
if (Number.isFinite(limits.requestTimeoutMs) && limits.requestTimeoutMs !== Infinity) {
|
|
743
743
|
(providerOptions as unknown as { timeoutMs?: number }).timeoutMs = limits.requestTimeoutMs;
|
|
744
744
|
}
|
|
745
745
|
} catch {}
|
|
746
|
-
//
|
|
746
|
+
// google:{store:false} opt-out pass-through (provider
|
|
747
747
|
// already respects body.store===false for chaining).
|
|
748
748
|
try {
|
|
749
749
|
const gStore =
|
|
@@ -784,8 +784,8 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
784
784
|
provider.id
|
|
785
785
|
);
|
|
786
786
|
|
|
787
|
-
// Thinking is exposed via events/traces and onTurn. Promoting it to answer text is gated (
|
|
788
|
-
//
|
|
787
|
+
// Thinking is exposed via events/traces and onTurn. Promoting it to answer text is gated (default
|
|
788
|
+
// OFF): without the opt-in the run returns the provider's empty text and finishReason.
|
|
789
789
|
if ((!genResult.text || genResult.text.trim() === "") && (!genResult.toolCalls || genResult.toolCalls.length === 0) && genResult.thinking) {
|
|
790
790
|
if (shouldPromoteThinkingToAnswer(config, runOptions)) {
|
|
791
791
|
if (genResult.thinking.includes("</think>")) {
|
|
@@ -853,11 +853,11 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
853
853
|
}
|
|
854
854
|
|
|
855
855
|
// Execute tool calls — always parallel (model-driven, bloatfree DX7)
|
|
856
|
-
//
|
|
856
|
+
// Check abort before tools (throws AbortError with partial).
|
|
857
857
|
throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: genResult.text });
|
|
858
858
|
emitProgress(config); // the calls are on record BEFORE they run, so a crash can finish them instead of forgetting them
|
|
859
859
|
allToolCalls.push(...genResult.toolCalls);
|
|
860
|
-
//
|
|
860
|
+
// Per-run tool-call budget.
|
|
861
861
|
if (allToolCalls.length > limits.maxToolCallsPerRun) {
|
|
862
862
|
throw new Error(
|
|
863
863
|
`[Agent Accelerator] maxToolCallsPerRun exceeded (${allToolCalls.length} > ${limits.maxToolCallsPerRun}).`
|
|
@@ -901,7 +901,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
901
901
|
allToolResults.push(...sanitizedResults);
|
|
902
902
|
context.addToolResults(sanitizedResults);
|
|
903
903
|
checkTokenLimit(accumulatedUsage, limits.maxTotalTokens);
|
|
904
|
-
//
|
|
904
|
+
// Results are stored BEFORE the cancellation check so an abort
|
|
905
905
|
// never drops executed tool work from history.
|
|
906
906
|
throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: genResult.text });
|
|
907
907
|
emitTurn(runOptions, {
|
|
@@ -926,7 +926,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
926
926
|
}
|
|
927
927
|
emitProgress(config);
|
|
928
928
|
|
|
929
|
-
//
|
|
929
|
+
// Abort always throws AbortError with partial attached (never
|
|
930
930
|
// silently breaks).
|
|
931
931
|
throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: finalResult?.text });
|
|
932
932
|
}
|
|
@@ -963,7 +963,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
963
963
|
responseId: finalResult?.responseId,
|
|
964
964
|
model: modelId,
|
|
965
965
|
provider: provider.id,
|
|
966
|
-
//
|
|
966
|
+
// Canonical finish reason + raw vendor passthrough.
|
|
967
967
|
...(stoppedByTool
|
|
968
968
|
? { finishReason: "stopped" as const, ...(finalResult?.finishReason ? { rawFinishReason: finalResult.finishReason } : {}) }
|
|
969
969
|
: canonicalFinishReason(finalResult?.finishReason, truncatedByMaxTurns)),
|
|
@@ -972,7 +972,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
972
972
|
turns,
|
|
973
973
|
});
|
|
974
974
|
} catch (err) {
|
|
975
|
-
//
|
|
975
|
+
// Run-level onFailure rollback (default restores pre-run
|
|
976
976
|
// checkpoint; opt out with (config as any).onFailure === "keep").
|
|
977
977
|
// Aborts are excluded so partial history survives interruption.
|
|
978
978
|
const isAbort =
|
|
@@ -983,14 +983,14 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
983
983
|
// why: rollback is best-effort — the original error takes precedence.
|
|
984
984
|
}
|
|
985
985
|
}
|
|
986
|
-
//
|
|
986
|
+
// Ensure aborts surface as AbortError with partial attached.
|
|
987
987
|
if (isAbort && (err as { name?: string } | null)?.name !== "AbortError") {
|
|
988
988
|
throw makeAbortError(
|
|
989
989
|
{ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }, turns: 0 },
|
|
990
990
|
err
|
|
991
991
|
);
|
|
992
992
|
}
|
|
993
|
-
//
|
|
993
|
+
// Every failure carries its partial progress for observability.
|
|
994
994
|
try {
|
|
995
995
|
(err as unknown as { partial?: unknown }).partial ??= {
|
|
996
996
|
usage: accumulatedUsage,
|
|
@@ -1042,7 +1042,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1042
1042
|
}
|
|
1043
1043
|
};
|
|
1044
1044
|
|
|
1045
|
-
//
|
|
1045
|
+
// Per-run checkpoint + limits for the streaming loop.
|
|
1046
1046
|
const streamPreCp = config.context.checkpoint();
|
|
1047
1047
|
const streamSessionId = loopSessionId(config);
|
|
1048
1048
|
const streamFallbackMax = normalizeMaxTurns((config as { maxTurns?: number }).maxTurns);
|
|
@@ -1050,7 +1050,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1050
1050
|
const streamStartTime = startTime;
|
|
1051
1051
|
|
|
1052
1052
|
(async () => {
|
|
1053
|
-
// Hoisted for catch-block partial reporting
|
|
1053
|
+
// Hoisted for catch-block partial reporting.
|
|
1054
1054
|
let accumulatedUsage: TokenUsage = {
|
|
1055
1055
|
inputTokens: 0,
|
|
1056
1056
|
outputTokens: 0,
|
|
@@ -1062,7 +1062,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1062
1062
|
};
|
|
1063
1063
|
let lastResponse: AgentResponse | null = null;
|
|
1064
1064
|
let turns = 0;
|
|
1065
|
-
//
|
|
1065
|
+
// Tool progress hoisted so failures carry toolCalls/toolResults.
|
|
1066
1066
|
const allToolCalls: ToolCallRecord[] = [];
|
|
1067
1067
|
const allToolResults: ToolResultRecord[] = [];
|
|
1068
1068
|
const allSubagents: SubAgentExecutionMetadata[] = [];
|
|
@@ -1118,7 +1118,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1118
1118
|
thinkingTokens: 0,
|
|
1119
1119
|
};
|
|
1120
1120
|
|
|
1121
|
-
//
|
|
1121
|
+
// allToolCalls/allToolResults/allSubagents are hoisted above (fresh
|
|
1122
1122
|
// per stream run); just reset turn counters here.
|
|
1123
1123
|
lastResponse = null;
|
|
1124
1124
|
turns = 0;
|
|
@@ -1176,7 +1176,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1176
1176
|
);
|
|
1177
1177
|
currentInner = innerStream as AssistantMessageEventStream;
|
|
1178
1178
|
|
|
1179
|
-
//
|
|
1179
|
+
// First-byte + idle timeouts around SSE (tolerate reasoning).
|
|
1180
1180
|
// Implemented as races on the inner iterator so a hung provider
|
|
1181
1181
|
// surfaces as TimeoutError instead of hanging the run forever.
|
|
1182
1182
|
{
|
|
@@ -1238,7 +1238,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1238
1238
|
);
|
|
1239
1239
|
|
|
1240
1240
|
// Thinking stays on events/traces + onTurn (default exposed). Promoting
|
|
1241
|
-
// it to answer text is gated (
|
|
1241
|
+
// it to answer text is gated (default OFF): without the opt-in
|
|
1242
1242
|
// the turn keeps the provider's empty text and finishReason.
|
|
1243
1243
|
if ((!turnResponse.text || turnResponse.text.trim() === "") && (!turnResponse.toolCalls || turnResponse.toolCalls.length === 0) && turnResponse.thinking) {
|
|
1244
1244
|
if (shouldPromoteThinkingToAnswer(config, runOptions)) {
|
|
@@ -1315,7 +1315,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1315
1315
|
`[Agent Accelerator] maxToolCallsPerRun exceeded (${allToolCalls.length} > ${streamLimits.maxToolCallsPerRun}).`
|
|
1316
1316
|
);
|
|
1317
1317
|
}
|
|
1318
|
-
//
|
|
1318
|
+
// Typed tool lifecycle for streaming consumers.
|
|
1319
1319
|
try {
|
|
1320
1320
|
outerStream.push({ type: "tool_start", toolCalls: turnResponse.toolCalls });
|
|
1321
1321
|
} catch {
|
|
@@ -1379,14 +1379,14 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1379
1379
|
toolResult: res,
|
|
1380
1380
|
});
|
|
1381
1381
|
}
|
|
1382
|
-
//
|
|
1382
|
+
// Close the tool lifecycle opened by `tool_start` above.
|
|
1383
1383
|
try {
|
|
1384
1384
|
outerStream.push({ type: "tool_end", toolResults: sanitizedResults });
|
|
1385
1385
|
} catch {
|
|
1386
1386
|
// why: stream push never throws, but guard anyway to protect the loop.
|
|
1387
1387
|
}
|
|
1388
1388
|
|
|
1389
|
-
//
|
|
1389
|
+
// Store tool results BEFORE the cancellation check.
|
|
1390
1390
|
allToolResults.push(...sanitizedResults);
|
|
1391
1391
|
context.addToolResults(sanitizedResults);
|
|
1392
1392
|
throwIfCancelled({ usage: accumulatedUsage, turns, text: turnResponse.text });
|
|
@@ -1463,7 +1463,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1463
1463
|
responseId: lastResponse?.responseId,
|
|
1464
1464
|
model: modelId,
|
|
1465
1465
|
provider: provider.id,
|
|
1466
|
-
//
|
|
1466
|
+
// Canonical finish reason + raw vendor passthrough.
|
|
1467
1467
|
...truncatedFinish,
|
|
1468
1468
|
durationMs: Date.now() - startTime,
|
|
1469
1469
|
raw: applyCaptureRaw(lastResponse?.raw, config.captureRaw) as ProviderRawData,
|
|
@@ -1486,7 +1486,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1486
1486
|
linked.signal.aborted ||
|
|
1487
1487
|
outerStream.isCancelled() ||
|
|
1488
1488
|
(raw as any)?.name === "AbortError";
|
|
1489
|
-
//
|
|
1489
|
+
// Streaming onFailure rollback (same policy as runAgentLoop;
|
|
1490
1490
|
// aborts excluded so partial history survives).
|
|
1491
1491
|
try {
|
|
1492
1492
|
if (shouldRollbackOnFailure(config.onFailure, isAbort, toolsExecuted)) {
|
|
@@ -1497,7 +1497,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1497
1497
|
} catch {
|
|
1498
1498
|
// why: rollback policy itself must never mask the original error.
|
|
1499
1499
|
}
|
|
1500
|
-
//
|
|
1500
|
+
// Every failure carries its partial progress for observability.
|
|
1501
1501
|
const partial = {
|
|
1502
1502
|
usage: accumulatedUsage,
|
|
1503
1503
|
cost: accumulatedUsage.cost,
|
|
@@ -1506,7 +1506,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1506
1506
|
toolResults: allToolResults,
|
|
1507
1507
|
text: lastResponse?.text,
|
|
1508
1508
|
};
|
|
1509
|
-
//
|
|
1509
|
+
// Aborts always surface as AbortError with partial attached.
|
|
1510
1510
|
if (isAbort) {
|
|
1511
1511
|
const abortErr =
|
|
1512
1512
|
raw.name === "AbortError" ? raw : makeAbortError(partial, raw);
|
|
@@ -1537,12 +1537,12 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1537
1537
|
}
|
|
1538
1538
|
|
|
1539
1539
|
// ---------------------------------------------------------------------------
|
|
1540
|
-
//
|
|
1540
|
+
// Thinking-promotion policy + canonical finish helper.
|
|
1541
1541
|
// (Appended here to avoid conflicts with other loop sections.)
|
|
1542
1542
|
// ---------------------------------------------------------------------------
|
|
1543
1543
|
|
|
1544
1544
|
/**
|
|
1545
|
-
* Whether an empty-text turn may promote `thinking` to answer `text
|
|
1545
|
+
* Whether an empty-text turn may promote `thinking` to answer `text`.
|
|
1546
1546
|
* Default false: the run returns the provider's empty text with its
|
|
1547
1547
|
* finishReason instead of fabricating an answer from reasoning. Opt in via
|
|
1548
1548
|
* `(runOptions as any).thinkingAsAnswerFallback === true` or
|
|
@@ -1567,7 +1567,7 @@ export function shouldPromoteThinkingToAnswer(config?: unknown, runOptions?: unk
|
|
|
1567
1567
|
}
|
|
1568
1568
|
|
|
1569
1569
|
/**
|
|
1570
|
-
* Canonicalizes a provider finish reason for AgentResponse
|
|
1570
|
+
* Canonicalizes a provider finish reason for AgentResponse.
|
|
1571
1571
|
* When `truncated` (maxTurns exhausted with pending tools), forces
|
|
1572
1572
|
* `"max_turns"` and preserves the provider's reason in `rawFinishReason`.
|
|
1573
1573
|
* 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/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";
|
|
@@ -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;
|
package/src/models/catalog.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Model catalog
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Model catalog: the single source of truth (models.dev) for context limits, prices, modalities and reasoning options.
|
|
3
|
+
* The data is cached in `<home>/catalog/models.dev.json` (or `AGENT_CACHE_DIR`) with a 12 hour TTL and refreshed in the
|
|
4
|
+
* background (see `catalog-cache.ts`). `GOOGLE_MODELS`, `OPENAI_MODELS` and `OPENROUTER_MODELS` are views of it,
|
|
5
|
+
* rebuilt in place whenever it changes.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {
|
|
@@ -77,12 +77,12 @@ function normalizeModelIdForLookup(modelId: string): string {
|
|
|
77
77
|
|
|
78
78
|
// ---------------------------------------------------------------------------
|
|
79
79
|
// Internal cache — battle-tested: avoid repeated global scans (7487 models)
|
|
80
|
-
// Bound lookupCache to 1000 entries LRU
|
|
80
|
+
// Bound lookupCache to 1000 entries LRU: evict oldest on overflow.
|
|
81
81
|
// ---------------------------------------------------------------------------
|
|
82
82
|
const lookupCache = new Map<string, ModelSpec | undefined>();
|
|
83
83
|
const providerModelsCache = new Map<string, ModelSpec[]>();
|
|
84
84
|
|
|
85
|
-
/** Bounded LRU set for lookupCache
|
|
85
|
+
/** Bounded LRU set for lookupCache: keeps at most 1000 entries. */
|
|
86
86
|
function setLookupCache(key: string, value: ModelSpec | undefined): void {
|
|
87
87
|
if (lookupCache.has(key)) lookupCache.delete(key);
|
|
88
88
|
else if (lookupCache.size >= 1000) {
|
|
@@ -92,7 +92,7 @@ function setLookupCache(key: string, value: ModelSpec | undefined): void {
|
|
|
92
92
|
lookupCache.set(key, value);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
// Custom-registered models
|
|
95
|
+
// Custom-registered models: checked first in getModelFromCatalog.
|
|
96
96
|
const customModelRegistry = new Map<string, ModelSpec>();
|
|
97
97
|
|
|
98
98
|
// Case-insensitive key of a registered custom model; in: registerModel and lookups, out: a string.
|
|
@@ -101,7 +101,7 @@ function customRegistryKey(provider: string, modelId: string): string {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
/**
|
|
104
|
-
* Registers a custom model spec that takes precedence over the catalog
|
|
104
|
+
* Registers a custom model spec that takes precedence over the catalog.
|
|
105
105
|
* Stored in a separate map, checked first in `getModelFromCatalog`.
|
|
106
106
|
*/
|
|
107
107
|
export function registerModel(spec: ModelSpec): void {
|
|
@@ -124,7 +124,7 @@ export function clearCustomModels(): void {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
/**
|
|
127
|
-
* Returns where a model resolves from without touching frozen types
|
|
127
|
+
* Returns where a model resolves from without touching frozen types:
|
|
128
128
|
* `"override"` for custom-registered or MODALITY_OVERRIDES-covered models,
|
|
129
129
|
* `"catalog"` for plain catalog hits, `"unknown"` when unresolvable.
|
|
130
130
|
*/
|
|
@@ -256,7 +256,7 @@ export function getModelFromCatalog(providerInput: string, modelIdInput: string)
|
|
|
256
256
|
const modelId = modelIdInput.trim();
|
|
257
257
|
let result: ModelSpec | undefined;
|
|
258
258
|
|
|
259
|
-
//
|
|
259
|
+
// Custom-registered models win over the catalog.
|
|
260
260
|
const customHit =
|
|
261
261
|
customModelRegistry.get(customRegistryKey(provider, modelId)) ??
|
|
262
262
|
customModelRegistry.get(customRegistryKey(provider, normalizeModelIdForLookup(modelId)));
|
|
@@ -287,7 +287,7 @@ export function getModelFromCatalog(providerInput: string, modelIdInput: string)
|
|
|
287
287
|
return result;
|
|
288
288
|
}
|
|
289
289
|
}
|
|
290
|
-
//
|
|
290
|
+
// Tightened fuzzy match — exact, `endsWith("/"+modelId)` boundary,
|
|
291
291
|
// or normalized equality. The old loose `modelId.endsWith(key)` is removed
|
|
292
292
|
// (e.g. "gpt-4o-mini-extra" must not match "mini").
|
|
293
293
|
for (const [key, raw] of Object.entries(providerData.models as Record<string, any>)) {
|
|
@@ -563,7 +563,7 @@ export function getModelsForProvider(provider: string): ModelSpec[] {
|
|
|
563
563
|
out.push(mapModelSpec(alias as ProviderId, key, raw));
|
|
564
564
|
}
|
|
565
565
|
}
|
|
566
|
-
//
|
|
566
|
+
// Include custom-registered models for this provider/alias set.
|
|
567
567
|
for (const spec of customModelRegistry.values()) {
|
|
568
568
|
if (aliases.includes(String(spec.provider)) || String(spec.provider) === provider) {
|
|
569
569
|
if (!seen.has(spec.id)) {
|
|
@@ -573,7 +573,7 @@ export function getModelsForProvider(provider: string): ModelSpec[] {
|
|
|
573
573
|
}
|
|
574
574
|
}
|
|
575
575
|
// Also include our internal provider id mapping
|
|
576
|
-
//
|
|
576
|
+
// Return copies — cache the canonical array, hand out a fresh copy.
|
|
577
577
|
providerModelsCache.set(provider, out);
|
|
578
578
|
return [...out];
|
|
579
579
|
}
|
package/src/providers/google.ts
CHANGED
|
@@ -609,7 +609,7 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
|
|
|
609
609
|
const turnStart = steps.length;
|
|
610
610
|
for (const part of m.content) {
|
|
611
611
|
if (part.type === "thinking" && part.thinking) {
|
|
612
|
-
//
|
|
612
|
+
// Preserve namespaced signatures only — foreign-provider
|
|
613
613
|
// signatures never echo here; untagged legacy echoes (compat).
|
|
614
614
|
// Absent signatures are omitted (never sent as empty strings).
|
|
615
615
|
const partProvider = (part as { thoughtSignatureProvider?: unknown }).thoughtSignatureProvider;
|
|
@@ -642,7 +642,7 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
|
|
|
642
642
|
} else if (m.thoughtSignature && !steps.slice(turnStart).some((s) => s.type === "thought")) {
|
|
643
643
|
// Preserve a signature-only thought so stateless chaining keeps
|
|
644
644
|
// reasoning continuity even when no summary text was retained.
|
|
645
|
-
//
|
|
645
|
+
// Only for google/untagged signatures.
|
|
646
646
|
const msgProvider = (m as { thoughtSignatureProvider?: unknown }).thoughtSignatureProvider;
|
|
647
647
|
if (shouldEchoSignature(msgProvider, "google")) {
|
|
648
648
|
steps.push({ type: "thought", signature: m.thoughtSignature });
|
|
@@ -910,7 +910,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
910
910
|
signal?: AbortSignal
|
|
911
911
|
): RequestInit {
|
|
912
912
|
const headers: Record<string, string> = { "Content-Type": "application/json", ...options?.headers };
|
|
913
|
-
//
|
|
913
|
+
// Key travels as the `x-goog-api-key` header (never `?key=` in the
|
|
914
914
|
// URL, which would leak into logs). Case-insensitive merge so a custom
|
|
915
915
|
// `X-Goog-Api-Key` never duplicates the canonical header. Proxy flows
|
|
916
916
|
// (`apiKey === "unused"`) preserve custom auth instead of overwriting.
|
|
@@ -1392,7 +1392,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1392
1392
|
noteProviderTurn(sessionId, "google");
|
|
1393
1393
|
checkTurnTokens(sessionId, usage);
|
|
1394
1394
|
|
|
1395
|
-
//
|
|
1395
|
+
// Terminal-state guard — fail `incomplete_stream` (retryable)
|
|
1396
1396
|
// instead of resolving empty success. Skipped for non-SSE payloads
|
|
1397
1397
|
// (JSON mocks) where no SSE frames were ever seen.
|
|
1398
1398
|
const hasTerminal = completedBody !== undefined || sawFinishReason || sawUsage;
|