agent-accelerator 0.5.6 → 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 +10 -17
- package/package.json +1 -1
- package/src/agent/agent.ts +92 -57
- package/src/agent/architecture.md +25 -0
- package/src/agent/context.ts +7 -3
- package/src/agent/delegation.ts +30 -96
- package/src/agent/loop.ts +54 -56
- package/src/agent/subagent.ts +4 -4
- package/src/agent/worker.ts +1 -3
- package/src/architecture.md +28 -0
- package/src/index.ts +13 -80
- package/src/models/architecture.md +14 -0
- package/src/models/catalog-cache.ts +10 -7
- package/src/models/catalog.ts +20 -13
- package/src/providers/architecture.md +24 -0
- package/src/providers/google.ts +24 -4
- package/src/providers/openai-compat.ts +29 -7
- package/src/providers/openai.ts +22 -6
- package/src/providers/openrouter.ts +14 -2
- package/src/providers/registry.ts +1 -0
- package/src/providers/shared.ts +4 -4
- package/src/providers.ts +9 -53
- package/src/scheduler/architecture.md +22 -0
- package/src/scheduler/scheduler.ts +131 -56
- package/src/scheduler/store.ts +274 -54
- package/src/scheduler/time.ts +2 -0
- package/src/session/architecture.md +19 -0
- package/src/session/store.ts +37 -22
- package/src/session/usage.ts +1 -0
- package/src/streaming/architecture.md +13 -0
- package/src/streaming/event-stream.ts +16 -11
- package/src/telemetry-file.ts +74 -24
- package/src/telemetry.ts +3 -2
- package/src/tools/architecture.md +19 -0
- package/src/tools/executor.ts +14 -1
- package/src/tools/schema.ts +1 -0
- package/src/tools/subagent-result.ts +1 -0
- package/src/tools/tool.ts +8 -8
- package/src/types/agent.ts +22 -20
- package/src/types/architecture.md +20 -0
- 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 +25 -0
- package/src/utils/base64.ts +2 -0
- package/src/utils/cache.ts +6 -4
- package/src/utils/durable.ts +78 -3
- package/src/utils/env.ts +1 -1
- package/src/utils/errors.ts +4 -0
- package/src/utils/headers.ts +7 -4
- package/src/utils/home.ts +1 -1
- package/src/utils/media.ts +1 -0
- package/src/utils/raw-capture.ts +3 -0
- package/src/utils/retry.ts +2 -1
- package/src/utils/safe-io.ts +4 -0
- package/src/utils/session.ts +10 -166
package/src/agent/loop.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { enforceFileAccess } from "../utils/safe-io.ts";
|
|
|
25
25
|
import { isSubagentResult, normalizeSubagentMetadata } from "../tools/subagent-result.ts";
|
|
26
26
|
import { BudgetExceededError } from "../types/errors.ts";
|
|
27
27
|
import { applyCaptureRaw, type CaptureRawMode } from "../utils/raw-capture.ts";
|
|
28
|
+
import { previewPrompt } from "../utils/session.ts";
|
|
28
29
|
import { noteProviderTurn, normalizeStructuredOutput, parseStructuredOutput } from "../providers.ts";
|
|
29
30
|
|
|
30
31
|
/** One mid-session steer request buffered while a run is active. */
|
|
@@ -99,20 +100,6 @@ export interface AgentLoopConfig {
|
|
|
99
100
|
output?: unknown;
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
/** Short preview for `steer_injected` / `queued` event payloads (bounded, display-only). */
|
|
103
|
-
function steerPreview(prompt: string | ContentPart[]): string {
|
|
104
|
-
if (typeof prompt === "string") return prompt.slice(0, 500);
|
|
105
|
-
try {
|
|
106
|
-
const texts = prompt
|
|
107
|
-
.filter((p) => (p as { type?: unknown }).type === "text")
|
|
108
|
-
.map((p) => (p as { text?: string }).text ?? "")
|
|
109
|
-
.join("\n");
|
|
110
|
-
return (texts || "[multipart]").slice(0, 500);
|
|
111
|
-
} catch {
|
|
112
|
-
return "[multipart]";
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
103
|
/**
|
|
117
104
|
* Drains pending steer entries into the conversation as new user messages.
|
|
118
105
|
* Returns display previews for events/telemetry. Never throws: invalid
|
|
@@ -133,7 +120,7 @@ function drainSteerInbox(context: AgentContext, inbox?: SteerEntry[]): string[]
|
|
|
133
120
|
else prompt = [{ type: "text", text: prefix } as ContentPart, ...prompt];
|
|
134
121
|
}
|
|
135
122
|
context.addUserMessage(prompt);
|
|
136
|
-
injected.push(
|
|
123
|
+
injected.push(previewPrompt(entry.prompt));
|
|
137
124
|
} catch {
|
|
138
125
|
continue;
|
|
139
126
|
}
|
|
@@ -141,6 +128,7 @@ function drainSteerInbox(context: AgentContext, inbox?: SteerEntry[]): string[]
|
|
|
141
128
|
return injected;
|
|
142
129
|
}
|
|
143
130
|
|
|
131
|
+
// Reports one finished turn to the caller's onTurn hook (ids only), never throwing; in: both loops, out: AgentRunOptions.onTurn.
|
|
144
132
|
function emitTurn(
|
|
145
133
|
runOptions: AgentRunOptions | undefined,
|
|
146
134
|
turn: { turns: number; text?: string; thinking?: string; toolCalls?: ToolCallRecord[]; toolResults?: ToolResultRecord[] }
|
|
@@ -183,6 +171,7 @@ function shouldRollbackOnFailure(
|
|
|
183
171
|
return !toolsExecuted;
|
|
184
172
|
}
|
|
185
173
|
|
|
174
|
+
// Calls the persistence checkpoint hook, never throwing; in: both loops after each step, out: Agent.persistNow.
|
|
186
175
|
function emitProgress(config: { onProgress?: () => void }): void {
|
|
187
176
|
try { config.onProgress?.(); } catch {}
|
|
188
177
|
}
|
|
@@ -191,6 +180,7 @@ function emitProgress(config: { onProgress?: () => void }): void {
|
|
|
191
180
|
// Structured outputs (canonical output wiring for the loop).
|
|
192
181
|
// ---------------------------------------------------------------------------
|
|
193
182
|
|
|
183
|
+
// Normalizes the structured-output option once per run (raw value + provider spec); in: AgentLoopConfig.output, out: the final-answer validation.
|
|
194
184
|
function resolveLoopOutput(config: AgentLoopConfig): { raw: unknown; spec: import("../types/model.ts").StructuredOutputSpec | undefined } {
|
|
195
185
|
const raw = (config as { output?: unknown }).output;
|
|
196
186
|
const fromOptions = (config.options as { output?: import("../types/model.ts").StructuredOutputSpec } | undefined)?.output;
|
|
@@ -204,6 +194,7 @@ function resolveLoopOutput(config: AgentLoopConfig): { raw: unknown; spec: impor
|
|
|
204
194
|
return { raw: undefined, spec: undefined };
|
|
205
195
|
}
|
|
206
196
|
|
|
197
|
+
// Validates the final text against the structured-output option, throwing ValidationError on mismatch; in: the last model text, out: AgentResponse.parsed.
|
|
207
198
|
function parseFinalStructuredOutput(text: string, raw: unknown, spec: import("../types/model.ts").StructuredOutputSpec | undefined): unknown | undefined {
|
|
208
199
|
if (raw === undefined && spec === undefined) return undefined;
|
|
209
200
|
// Strict-fail when opted in: invalid JSON/schema throws ValidationError.
|
|
@@ -232,7 +223,7 @@ function normalizeMaxTurns(value?: number): number {
|
|
|
232
223
|
function warnMaxTurnsHit(agentName: string | undefined, maxTurns: number, pendingTools: string[]): void {
|
|
233
224
|
const who = agentName ? `"${agentName}"` : "Agent";
|
|
234
225
|
const tools = pendingTools.length > 0 ? ` Pending tool calls dropped: ${pendingTools.join(", ")}.` : "";
|
|
235
|
-
//
|
|
226
|
+
// No console.* in library code — route via the telemetry bus
|
|
236
227
|
// (default no-op; CLIs attach a pretty logger via telemetryBus.onEvent).
|
|
237
228
|
try {
|
|
238
229
|
telemetryBus.emitWarning(
|
|
@@ -246,11 +237,11 @@ function warnMaxTurnsHit(agentName: string | undefined, maxTurns: number, pendin
|
|
|
246
237
|
}
|
|
247
238
|
|
|
248
239
|
// ---------------------------------------------------------------------------
|
|
249
|
-
//
|
|
240
|
+
// Tool-call/result pairing repair.
|
|
250
241
|
// ---------------------------------------------------------------------------
|
|
251
242
|
|
|
252
243
|
/**
|
|
253
|
-
* Repairs tool call/result pairing before each provider request
|
|
244
|
+
* Repairs tool call/result pairing before each provider request.
|
|
254
245
|
*
|
|
255
246
|
* Providers reject a `tool_result` without its `tool_call`, a `tool_call`
|
|
256
247
|
* without its result, AND (Chat-Completions style APIs) results that are not
|
|
@@ -315,7 +306,7 @@ export function ensureToolPairing(context: AgentContext): void {
|
|
|
315
306
|
}
|
|
316
307
|
|
|
317
308
|
// ---------------------------------------------------------------------------
|
|
318
|
-
//
|
|
309
|
+
// Abort + limits helpers.
|
|
319
310
|
// ---------------------------------------------------------------------------
|
|
320
311
|
|
|
321
312
|
export interface LoopLimits {
|
|
@@ -328,7 +319,7 @@ export interface LoopLimits {
|
|
|
328
319
|
streamIdleTimeoutMs: number;
|
|
329
320
|
}
|
|
330
321
|
|
|
331
|
-
/** Resolves `(config as any).limits` with
|
|
322
|
+
/** Resolves `(config as any).limits` with defaults (timeouts tolerate reasoning). */
|
|
332
323
|
function resolveLoopLimits(config: AgentLoopConfig, fallbackMaxTurns: number): LoopLimits & { maxTurns: number; deadlineMs: number; maxToolCallsPerRun: number; maxTotalTokens: number } {
|
|
333
324
|
const raw = ((config as unknown as { limits?: Record<string, unknown> }).limits ?? {}) as Record<string, unknown>;
|
|
334
325
|
const num = (v: unknown): number | undefined =>
|
|
@@ -350,6 +341,7 @@ function resolveLoopLimits(config: AgentLoopConfig, fallbackMaxTurns: number): L
|
|
|
350
341
|
return { maxTurns, deadlineMs, maxToolCallsPerRun, maxTotalTokens, requestTimeoutMs, firstByteTimeoutMs, streamIdleTimeoutMs };
|
|
351
342
|
}
|
|
352
343
|
|
|
344
|
+
// The session id a run's provider calls use (run override, else agent default); in: AgentLoopConfig, out: rollback and routing.
|
|
353
345
|
function loopSessionId(config: AgentLoopConfig): string | undefined {
|
|
354
346
|
try {
|
|
355
347
|
return (
|
|
@@ -359,6 +351,7 @@ function loopSessionId(config: AgentLoopConfig): string | undefined {
|
|
|
359
351
|
} catch { return undefined; }
|
|
360
352
|
}
|
|
361
353
|
|
|
354
|
+
// Builds an AbortError that carries the run's partial progress; in: an abort reason + partial usage, out: the error the run rejects with.
|
|
362
355
|
function makeAbortError(
|
|
363
356
|
partial: { usage: TokenUsage; turns: number; text?: string },
|
|
364
357
|
reason?: unknown
|
|
@@ -373,6 +366,7 @@ function makeAbortError(
|
|
|
373
366
|
return base;
|
|
374
367
|
}
|
|
375
368
|
|
|
369
|
+
// Throws the run's AbortError as soon as its signal is aborted; in: loop checkpoints, out: nothing or a thrown abort.
|
|
376
370
|
function throwIfAborted(
|
|
377
371
|
signal: AbortSignal | undefined,
|
|
378
372
|
partial: { usage: TokenUsage; turns: number; text?: string }
|
|
@@ -382,6 +376,7 @@ function throwIfAborted(
|
|
|
382
376
|
}
|
|
383
377
|
}
|
|
384
378
|
|
|
379
|
+
// Throws BudgetExceededError when the run's tokens pass limits.maxTotalTokens; in: both loops after usage changes, out: nothing or a thrown error.
|
|
385
380
|
function checkTokenLimit(usage: TokenUsage, maxTotalTokens: number): void {
|
|
386
381
|
if (Number.isFinite(maxTotalTokens) && usage.totalTokens > maxTotalTokens) {
|
|
387
382
|
throw new BudgetExceededError(
|
|
@@ -391,12 +386,14 @@ function checkTokenLimit(usage: TokenUsage, maxTotalTokens: number): void {
|
|
|
391
386
|
}
|
|
392
387
|
}
|
|
393
388
|
|
|
389
|
+
// Throws when the run has been going longer than limits.deadlineMs; in: both loops each turn, out: nothing or a thrown error.
|
|
394
390
|
function checkDeadline(startTime: number, deadlineMs: number): void {
|
|
395
391
|
if (Number.isFinite(deadlineMs) && deadlineMs !== Infinity && Date.now() - startTime > deadlineMs) {
|
|
396
392
|
throw new Error(`[Agent Accelerator] Run deadline exceeded after ${deadlineMs}ms.`);
|
|
397
393
|
}
|
|
398
394
|
}
|
|
399
395
|
|
|
396
|
+
// Races one provider call against the request timeout (a TimeoutError), clearing its timer either way; in: runAgentLoop, out: the call's result.
|
|
400
397
|
async function generateWithTimeout<T>(task: () => Promise<T>, timeoutMs: number): Promise<T> {
|
|
401
398
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs === Infinity) return task();
|
|
402
399
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -439,7 +436,7 @@ export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec, serv
|
|
|
439
436
|
}
|
|
440
437
|
|
|
441
438
|
// ---------------------------------------------------------------------------
|
|
442
|
-
//
|
|
439
|
+
// Canonical usage accumulation (pure w.r.t. source).
|
|
443
440
|
// Invariants (see assertUsageInvariants in session/store.ts):
|
|
444
441
|
// - token counts are finite integers >= 0;
|
|
445
442
|
// - cachedTokens/cacheReadTokens are a SUBSET of inputTokens (clamped below
|
|
@@ -447,7 +444,7 @@ export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec, serv
|
|
|
447
444
|
// - thinkingTokens is a SUBSET of outputTokens when both are reported
|
|
448
445
|
// (reasoning is generated output; enforced in the test helper, documented
|
|
449
446
|
// here because providers vary on which side they omit).
|
|
450
|
-
// This function never mutates `source
|
|
447
|
+
// This function never mutates `source`: the priced cost is cloned
|
|
451
448
|
// into a local before folding into the target.
|
|
452
449
|
// ---------------------------------------------------------------------------
|
|
453
450
|
export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: ModelSpec, serviceTier?: string): void {
|
|
@@ -468,7 +465,7 @@ export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: M
|
|
|
468
465
|
target.cachedTokens = Math.min(preCached, target.inputTokens);
|
|
469
466
|
target.cacheReadTokens = Math.min(preRead, target.inputTokens);
|
|
470
467
|
if ((target.cachedTokens ?? 0) < preCached || (target.cacheReadTokens ?? 0) < preRead) {
|
|
471
|
-
//
|
|
468
|
+
// No console — report via the anomaly hook + telemetry bus.
|
|
472
469
|
reportUsageAnomaly({
|
|
473
470
|
type: "cache_clamp",
|
|
474
471
|
inputTokens: target.inputTokens,
|
|
@@ -478,7 +475,7 @@ export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: M
|
|
|
478
475
|
}
|
|
479
476
|
|
|
480
477
|
const priced = computeCostFromPricing(source, spec, serviceTier);
|
|
481
|
-
//
|
|
478
|
+
// Pure — read `source` only; never assign back to `source.cost`.
|
|
482
479
|
// Clone the priced cost so later target folds cannot alias source state.
|
|
483
480
|
const sourceCost = priced ? { ...priced } : undefined;
|
|
484
481
|
|
|
@@ -493,6 +490,7 @@ export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: M
|
|
|
493
490
|
}
|
|
494
491
|
}
|
|
495
492
|
|
|
493
|
+
// Turns a tool result into what the model sees: a branded delegation envelope becomes its text and yields validated worker metadata, anything else passes through; in: executed results, out: history + usage roll-up.
|
|
496
494
|
function sanitizeToolResult(r: ToolResultRecord): { sanitized: ToolResultRecord; metas: SubAgentExecutionMetadata[] } {
|
|
497
495
|
// Only SDK-branded delegation envelopes are trusted. A plain object that
|
|
498
496
|
// merely carries a `_subagentMetadata` key (e.g. scraped JSON) is ordinary
|
|
@@ -675,11 +673,11 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
675
673
|
const maxTurns = limits.maxTurns;
|
|
676
674
|
// Structured outputs: normalize once (throws ValidationError for bad shapes).
|
|
677
675
|
const loopOutput = resolveLoopOutput(config);
|
|
678
|
-
//
|
|
676
|
+
// Pre-run checkpoint for onFailure rollback.
|
|
679
677
|
const preRunCp = context.checkpoint();
|
|
680
678
|
const sessionIdForRollback = loopSessionId(config);
|
|
681
679
|
|
|
682
|
-
//
|
|
680
|
+
// Partial-run state hoisted outside try so the catch block can attach
|
|
683
681
|
// {usage,cost,turns,toolCalls,toolResults,text} to ANY thrown error.
|
|
684
682
|
const startTime = Date.now();
|
|
685
683
|
const accumulatedUsage: TokenUsage = {
|
|
@@ -719,11 +717,11 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
719
717
|
}
|
|
720
718
|
|
|
721
719
|
while (!stoppedByTool && turns < maxTurns) {
|
|
722
|
-
//
|
|
720
|
+
// Fail fast on abort (throws AbortError with partial).
|
|
723
721
|
throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: finalResult?.text });
|
|
724
722
|
checkDeadline(startTime, limits.deadlineMs);
|
|
725
723
|
config.enforceBudget?.();
|
|
726
|
-
//
|
|
724
|
+
// Repair orphan tool pairs before every request.
|
|
727
725
|
ensureToolPairing(context);
|
|
728
726
|
if (config.fileAccess?.roots) await enforceFileAccess(context.messages, config.fileAccess.roots);
|
|
729
727
|
turns++;
|
|
@@ -739,13 +737,13 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
739
737
|
toolChoice: toolChoiceForTurn(options?.toolChoice, turns),
|
|
740
738
|
...(loopOutput.spec ? { output: loopOutput.spec } : {}),
|
|
741
739
|
};
|
|
742
|
-
//
|
|
740
|
+
// Per-request timeout pass-through (providers honor timeoutMs).
|
|
743
741
|
try {
|
|
744
742
|
if (Number.isFinite(limits.requestTimeoutMs) && limits.requestTimeoutMs !== Infinity) {
|
|
745
743
|
(providerOptions as unknown as { timeoutMs?: number }).timeoutMs = limits.requestTimeoutMs;
|
|
746
744
|
}
|
|
747
745
|
} catch {}
|
|
748
|
-
//
|
|
746
|
+
// google:{store:false} opt-out pass-through (provider
|
|
749
747
|
// already respects body.store===false for chaining).
|
|
750
748
|
try {
|
|
751
749
|
const gStore =
|
|
@@ -786,8 +784,8 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
786
784
|
provider.id
|
|
787
785
|
);
|
|
788
786
|
|
|
789
|
-
// Thinking is exposed via events/traces and onTurn. Promoting it to answer text is gated (
|
|
790
|
-
//
|
|
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.
|
|
791
789
|
if ((!genResult.text || genResult.text.trim() === "") && (!genResult.toolCalls || genResult.toolCalls.length === 0) && genResult.thinking) {
|
|
792
790
|
if (shouldPromoteThinkingToAnswer(config, runOptions)) {
|
|
793
791
|
if (genResult.thinking.includes("</think>")) {
|
|
@@ -855,11 +853,11 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
855
853
|
}
|
|
856
854
|
|
|
857
855
|
// Execute tool calls — always parallel (model-driven, bloatfree DX7)
|
|
858
|
-
//
|
|
856
|
+
// Check abort before tools (throws AbortError with partial).
|
|
859
857
|
throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: genResult.text });
|
|
860
858
|
emitProgress(config); // the calls are on record BEFORE they run, so a crash can finish them instead of forgetting them
|
|
861
859
|
allToolCalls.push(...genResult.toolCalls);
|
|
862
|
-
//
|
|
860
|
+
// Per-run tool-call budget.
|
|
863
861
|
if (allToolCalls.length > limits.maxToolCallsPerRun) {
|
|
864
862
|
throw new Error(
|
|
865
863
|
`[Agent Accelerator] maxToolCallsPerRun exceeded (${allToolCalls.length} > ${limits.maxToolCallsPerRun}).`
|
|
@@ -903,7 +901,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
903
901
|
allToolResults.push(...sanitizedResults);
|
|
904
902
|
context.addToolResults(sanitizedResults);
|
|
905
903
|
checkTokenLimit(accumulatedUsage, limits.maxTotalTokens);
|
|
906
|
-
//
|
|
904
|
+
// Results are stored BEFORE the cancellation check so an abort
|
|
907
905
|
// never drops executed tool work from history.
|
|
908
906
|
throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: genResult.text });
|
|
909
907
|
emitTurn(runOptions, {
|
|
@@ -928,7 +926,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
928
926
|
}
|
|
929
927
|
emitProgress(config);
|
|
930
928
|
|
|
931
|
-
//
|
|
929
|
+
// Abort always throws AbortError with partial attached (never
|
|
932
930
|
// silently breaks).
|
|
933
931
|
throwIfAborted(runOptions?.signal, { usage: accumulatedUsage, turns, text: finalResult?.text });
|
|
934
932
|
}
|
|
@@ -965,7 +963,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
965
963
|
responseId: finalResult?.responseId,
|
|
966
964
|
model: modelId,
|
|
967
965
|
provider: provider.id,
|
|
968
|
-
//
|
|
966
|
+
// Canonical finish reason + raw vendor passthrough.
|
|
969
967
|
...(stoppedByTool
|
|
970
968
|
? { finishReason: "stopped" as const, ...(finalResult?.finishReason ? { rawFinishReason: finalResult.finishReason } : {}) }
|
|
971
969
|
: canonicalFinishReason(finalResult?.finishReason, truncatedByMaxTurns)),
|
|
@@ -974,7 +972,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
974
972
|
turns,
|
|
975
973
|
});
|
|
976
974
|
} catch (err) {
|
|
977
|
-
//
|
|
975
|
+
// Run-level onFailure rollback (default restores pre-run
|
|
978
976
|
// checkpoint; opt out with (config as any).onFailure === "keep").
|
|
979
977
|
// Aborts are excluded so partial history survives interruption.
|
|
980
978
|
const isAbort =
|
|
@@ -985,14 +983,14 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
985
983
|
// why: rollback is best-effort — the original error takes precedence.
|
|
986
984
|
}
|
|
987
985
|
}
|
|
988
|
-
//
|
|
986
|
+
// Ensure aborts surface as AbortError with partial attached.
|
|
989
987
|
if (isAbort && (err as { name?: string } | null)?.name !== "AbortError") {
|
|
990
988
|
throw makeAbortError(
|
|
991
989
|
{ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }, turns: 0 },
|
|
992
990
|
err
|
|
993
991
|
);
|
|
994
992
|
}
|
|
995
|
-
//
|
|
993
|
+
// Every failure carries its partial progress for observability.
|
|
996
994
|
try {
|
|
997
995
|
(err as unknown as { partial?: unknown }).partial ??= {
|
|
998
996
|
usage: accumulatedUsage,
|
|
@@ -1044,7 +1042,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1044
1042
|
}
|
|
1045
1043
|
};
|
|
1046
1044
|
|
|
1047
|
-
//
|
|
1045
|
+
// Per-run checkpoint + limits for the streaming loop.
|
|
1048
1046
|
const streamPreCp = config.context.checkpoint();
|
|
1049
1047
|
const streamSessionId = loopSessionId(config);
|
|
1050
1048
|
const streamFallbackMax = normalizeMaxTurns((config as { maxTurns?: number }).maxTurns);
|
|
@@ -1052,7 +1050,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1052
1050
|
const streamStartTime = startTime;
|
|
1053
1051
|
|
|
1054
1052
|
(async () => {
|
|
1055
|
-
// Hoisted for catch-block partial reporting
|
|
1053
|
+
// Hoisted for catch-block partial reporting.
|
|
1056
1054
|
let accumulatedUsage: TokenUsage = {
|
|
1057
1055
|
inputTokens: 0,
|
|
1058
1056
|
outputTokens: 0,
|
|
@@ -1064,7 +1062,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1064
1062
|
};
|
|
1065
1063
|
let lastResponse: AgentResponse | null = null;
|
|
1066
1064
|
let turns = 0;
|
|
1067
|
-
//
|
|
1065
|
+
// Tool progress hoisted so failures carry toolCalls/toolResults.
|
|
1068
1066
|
const allToolCalls: ToolCallRecord[] = [];
|
|
1069
1067
|
const allToolResults: ToolResultRecord[] = [];
|
|
1070
1068
|
const allSubagents: SubAgentExecutionMetadata[] = [];
|
|
@@ -1120,7 +1118,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1120
1118
|
thinkingTokens: 0,
|
|
1121
1119
|
};
|
|
1122
1120
|
|
|
1123
|
-
//
|
|
1121
|
+
// allToolCalls/allToolResults/allSubagents are hoisted above (fresh
|
|
1124
1122
|
// per stream run); just reset turn counters here.
|
|
1125
1123
|
lastResponse = null;
|
|
1126
1124
|
turns = 0;
|
|
@@ -1178,7 +1176,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1178
1176
|
);
|
|
1179
1177
|
currentInner = innerStream as AssistantMessageEventStream;
|
|
1180
1178
|
|
|
1181
|
-
//
|
|
1179
|
+
// First-byte + idle timeouts around SSE (tolerate reasoning).
|
|
1182
1180
|
// Implemented as races on the inner iterator so a hung provider
|
|
1183
1181
|
// surfaces as TimeoutError instead of hanging the run forever.
|
|
1184
1182
|
{
|
|
@@ -1240,7 +1238,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1240
1238
|
);
|
|
1241
1239
|
|
|
1242
1240
|
// Thinking stays on events/traces + onTurn (default exposed). Promoting
|
|
1243
|
-
// it to answer text is gated (
|
|
1241
|
+
// it to answer text is gated (default OFF): without the opt-in
|
|
1244
1242
|
// the turn keeps the provider's empty text and finishReason.
|
|
1245
1243
|
if ((!turnResponse.text || turnResponse.text.trim() === "") && (!turnResponse.toolCalls || turnResponse.toolCalls.length === 0) && turnResponse.thinking) {
|
|
1246
1244
|
if (shouldPromoteThinkingToAnswer(config, runOptions)) {
|
|
@@ -1317,7 +1315,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1317
1315
|
`[Agent Accelerator] maxToolCallsPerRun exceeded (${allToolCalls.length} > ${streamLimits.maxToolCallsPerRun}).`
|
|
1318
1316
|
);
|
|
1319
1317
|
}
|
|
1320
|
-
//
|
|
1318
|
+
// Typed tool lifecycle for streaming consumers.
|
|
1321
1319
|
try {
|
|
1322
1320
|
outerStream.push({ type: "tool_start", toolCalls: turnResponse.toolCalls });
|
|
1323
1321
|
} catch {
|
|
@@ -1381,14 +1379,14 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1381
1379
|
toolResult: res,
|
|
1382
1380
|
});
|
|
1383
1381
|
}
|
|
1384
|
-
//
|
|
1382
|
+
// Close the tool lifecycle opened by `tool_start` above.
|
|
1385
1383
|
try {
|
|
1386
1384
|
outerStream.push({ type: "tool_end", toolResults: sanitizedResults });
|
|
1387
1385
|
} catch {
|
|
1388
1386
|
// why: stream push never throws, but guard anyway to protect the loop.
|
|
1389
1387
|
}
|
|
1390
1388
|
|
|
1391
|
-
//
|
|
1389
|
+
// Store tool results BEFORE the cancellation check.
|
|
1392
1390
|
allToolResults.push(...sanitizedResults);
|
|
1393
1391
|
context.addToolResults(sanitizedResults);
|
|
1394
1392
|
throwIfCancelled({ usage: accumulatedUsage, turns, text: turnResponse.text });
|
|
@@ -1465,7 +1463,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1465
1463
|
responseId: lastResponse?.responseId,
|
|
1466
1464
|
model: modelId,
|
|
1467
1465
|
provider: provider.id,
|
|
1468
|
-
//
|
|
1466
|
+
// Canonical finish reason + raw vendor passthrough.
|
|
1469
1467
|
...truncatedFinish,
|
|
1470
1468
|
durationMs: Date.now() - startTime,
|
|
1471
1469
|
raw: applyCaptureRaw(lastResponse?.raw, config.captureRaw) as ProviderRawData,
|
|
@@ -1488,7 +1486,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1488
1486
|
linked.signal.aborted ||
|
|
1489
1487
|
outerStream.isCancelled() ||
|
|
1490
1488
|
(raw as any)?.name === "AbortError";
|
|
1491
|
-
//
|
|
1489
|
+
// Streaming onFailure rollback (same policy as runAgentLoop;
|
|
1492
1490
|
// aborts excluded so partial history survives).
|
|
1493
1491
|
try {
|
|
1494
1492
|
if (shouldRollbackOnFailure(config.onFailure, isAbort, toolsExecuted)) {
|
|
@@ -1499,7 +1497,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1499
1497
|
} catch {
|
|
1500
1498
|
// why: rollback policy itself must never mask the original error.
|
|
1501
1499
|
}
|
|
1502
|
-
//
|
|
1500
|
+
// Every failure carries its partial progress for observability.
|
|
1503
1501
|
const partial = {
|
|
1504
1502
|
usage: accumulatedUsage,
|
|
1505
1503
|
cost: accumulatedUsage.cost,
|
|
@@ -1508,7 +1506,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1508
1506
|
toolResults: allToolResults,
|
|
1509
1507
|
text: lastResponse?.text,
|
|
1510
1508
|
};
|
|
1511
|
-
//
|
|
1509
|
+
// Aborts always surface as AbortError with partial attached.
|
|
1512
1510
|
if (isAbort) {
|
|
1513
1511
|
const abortErr =
|
|
1514
1512
|
raw.name === "AbortError" ? raw : makeAbortError(partial, raw);
|
|
@@ -1539,12 +1537,12 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1539
1537
|
}
|
|
1540
1538
|
|
|
1541
1539
|
// ---------------------------------------------------------------------------
|
|
1542
|
-
//
|
|
1540
|
+
// Thinking-promotion policy + canonical finish helper.
|
|
1543
1541
|
// (Appended here to avoid conflicts with other loop sections.)
|
|
1544
1542
|
// ---------------------------------------------------------------------------
|
|
1545
1543
|
|
|
1546
1544
|
/**
|
|
1547
|
-
* Whether an empty-text turn may promote `thinking` to answer `text
|
|
1545
|
+
* Whether an empty-text turn may promote `thinking` to answer `text`.
|
|
1548
1546
|
* Default false: the run returns the provider's empty text with its
|
|
1549
1547
|
* finishReason instead of fabricating an answer from reasoning. Opt in via
|
|
1550
1548
|
* `(runOptions as any).thinkingAsAnswerFallback === true` or
|
|
@@ -1569,7 +1567,7 @@ export function shouldPromoteThinkingToAnswer(config?: unknown, runOptions?: unk
|
|
|
1569
1567
|
}
|
|
1570
1568
|
|
|
1571
1569
|
/**
|
|
1572
|
-
* Canonicalizes a provider finish reason for AgentResponse
|
|
1570
|
+
* Canonicalizes a provider finish reason for AgentResponse.
|
|
1573
1571
|
* When `truncated` (maxTurns exhausted with pending tools), forces
|
|
1574
1572
|
* `"max_turns"` and preserves the provider's reason in `rawFinishReason`.
|
|
1575
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/agent/worker.ts
CHANGED
|
@@ -3,12 +3,9 @@ import type { SubAgentStep } from "../types/response.ts";
|
|
|
3
3
|
import type { AgentConfig } from "../types/agent.ts";
|
|
4
4
|
import type { Agent } from "./agent.ts";
|
|
5
5
|
import { normalizeToolName } from "../tools/executor.ts";
|
|
6
|
-
import { escapeXml } from "../utils/serialization.ts";
|
|
7
6
|
import { getModel, getSubModel } from "../utils/env.ts";
|
|
8
7
|
import { resolveModel } from "../providers/registry.ts";
|
|
9
8
|
|
|
10
|
-
export { escapeXml };
|
|
11
|
-
|
|
12
9
|
/** Minimal live-trace shape shared by fixed + dynamic workers (structural subset of `SubAgentTrace`). */
|
|
13
10
|
export interface LiveWorkerTrace {
|
|
14
11
|
trackingId: string;
|
|
@@ -200,6 +197,7 @@ export function grantWorkerTools(
|
|
|
200
197
|
return grantedTools;
|
|
201
198
|
}
|
|
202
199
|
|
|
200
|
+
// The provider id a model string resolves to, or undefined when it cannot be resolved; in: resolveWorkerCredentials, out: the provider match check.
|
|
203
201
|
function providerOf(modelStr: string | unknown | undefined): string | undefined {
|
|
204
202
|
if (!modelStr) return undefined;
|
|
205
203
|
try {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# src/
|
|
2
|
+
|
|
3
|
+
The whole SDK. `index.ts` is the only public entry (`main`/`module`/`types`); everything else is reached through it.
|
|
4
|
+
|
|
5
|
+
| File / folder | What it is |
|
|
6
|
+
| :-- | :-- |
|
|
7
|
+
| `index.ts` | The public barrel. Adding or removing an export is a public API change. |
|
|
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: a persisted session's events go to `<session folder>/telemetry.ndjson`, the rest to `<home>/telemetry/<date>.ndjson`; never outside a home. |
|
|
10
|
+
| `update-models.ts` | CLI behind `bun run update-models` (refreshes the model catalog cache). |
|
|
11
|
+
| `providers.ts` | Provider-agnostic mappers (thinking, cache, tool choice, structured output, generation params). No HTTP. |
|
|
12
|
+
| `agent/` | `Agent`, the model-to-tools loop, workers, delegation. |
|
|
13
|
+
| `providers/` | One adapter per wire (Google, OpenAI, OpenRouter, OpenAI-compatible), the registry, shared helpers. |
|
|
14
|
+
| `models/` | The model catalog (models.dev) and its cache. |
|
|
15
|
+
| `tools/` | `tool()`, schemas, the executor. |
|
|
16
|
+
| `streaming/` | The event stream and the SSE parser. |
|
|
17
|
+
| `session/` | Persistence (snapshot + journal), usage accounting, tool logs. |
|
|
18
|
+
| `scheduler/` | Scheduled actions. |
|
|
19
|
+
| `types/` | Shared contracts (no logic except small helpers). |
|
|
20
|
+
| `utils/` | Small shared helpers (durable writes, home, env, media, retry, safe I/O). |
|
|
21
|
+
|
|
22
|
+
## Rules for every file here
|
|
23
|
+
- Imports use `.ts` extensions; type-only imports use `import type`.
|
|
24
|
+
- No `console.*`: warnings and traces go to `telemetryBus` (exceptions: the `update-models.ts` CLI and one compat `console.warn` in `providers.ts`).
|
|
25
|
+
- All state is written under the one home folder (`utils/home.ts`). Files are replaced through `utils/durable.ts`; the append-only ones are the session journal and tool logs (fsynced per line) and the opt-in telemetry file (not fsynced).
|
|
26
|
+
- Provider differences never leave `providers/` and `providers.ts`.
|
|
27
|
+
- Every function has one comment line above it (how it works, where input comes from, where output goes).
|
|
28
|
+
- Change a public option, export, env var or telemetry code: update `README.md` and `docs/` in the same change.
|