agent-accelerator 0.5.2 → 0.5.4
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 +117 -32
- package/examples/prompts/SYSTEM_PROMPT_AGENT.md +14 -20
- package/package.json +1 -1
- package/src/agent/agent.ts +355 -38
- package/src/agent/context.ts +3 -1
- package/src/agent/delegation.ts +208 -524
- package/src/agent/loop.ts +139 -18
- package/src/agent/subagents-tool.ts +87 -0
- package/src/agent/subagents.md +31 -0
- package/src/agent/turns.ts +60 -0
- package/src/agent/worker.ts +38 -30
- package/src/agent/workers.ts +405 -0
- package/src/index.ts +5 -3
- package/src/models/catalog-cache.ts +3 -10
- package/src/scheduler/scheduler.ts +2 -0
- package/src/scheduler/store.ts +34 -21
- package/src/session/journal.ts +212 -0
- package/src/session/store.ts +203 -90
- package/src/session/tool-log.ts +62 -0
- package/src/session/usage.ts +201 -0
- package/src/telemetry-file.ts +4 -1
- package/src/tools/executor.ts +72 -15
- package/src/tools/subagent-result.ts +1 -1
- package/src/tools/tool.ts +0 -6
- package/src/types/agent.ts +45 -6
- package/src/types/message.ts +4 -0
- package/src/types/response.ts +2 -0
- package/src/types/tool.ts +19 -7
- package/src/utils/durable.ts +218 -0
- package/src/utils/headers.ts +1 -1
- package/src/utils/retry.ts +15 -1
package/src/agent/agent.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import type { AgentConfig, AgentRunOptions, MidSessionConfig, ToolChoiceOption, RetryPolicyConfig, Budget, BudgetState } from "../types/agent.ts";
|
|
1
|
+
import type { AgentConfig, AgentRunOptions, MidSessionConfig, ToolChoiceOption, RetryPolicyConfig, Budget, BudgetState, InterruptedRun } from "../types/agent.ts";
|
|
2
2
|
import type { TokenUsage } from "../types/core.ts";
|
|
3
3
|
import type { ProviderRequestOptions } from "../types/model.ts";
|
|
4
4
|
import { normalizeThinkingV2, resolveWhenBusy, normalizeWorkerTimeout } from "../tools/tool.ts";
|
|
5
|
-
import type { ToolDefinition } from "../types/tool.ts";
|
|
5
|
+
import type { ToolDefinition, ToolResultRecord } from "../types/tool.ts";
|
|
6
|
+
import { isSubagentResult } from "../tools/subagent-result.ts";
|
|
6
7
|
import { CACHE_OPTIONS, type ThinkingLevel, type ThinkingConfig, type CacheOption, type ServiceTier } from "../types/core.ts";
|
|
7
8
|
import type { ContentPart } from "../types/message.ts";
|
|
8
9
|
import { AgentResponse, type StreamEvent } from "../types/response.ts";
|
|
9
10
|
import { AssistantMessageEventStream } from "../streaming/event-stream.ts";
|
|
10
11
|
import { AgentContext } from "./context.ts";
|
|
11
12
|
import { resolveModel } from "../providers/registry.ts";
|
|
12
|
-
import { buildAgentTools
|
|
13
|
+
import { buildAgentTools } from "./delegation.ts";
|
|
14
|
+
import { createSubagentsTool } from "./subagents-tool.ts";
|
|
15
|
+
import { WorkerRegistry } from "./workers.ts";
|
|
13
16
|
import {
|
|
17
|
+
DEFAULT_MAX_DEPTH,
|
|
14
18
|
normalizeToolMap,
|
|
15
19
|
resolveDynamicModelInput,
|
|
16
20
|
resolveMainModel,
|
|
@@ -19,10 +23,12 @@ import * as path from "node:path";
|
|
|
19
23
|
import { registerAgent } from "../scheduler/scheduler.ts";
|
|
20
24
|
import { resolveHome } from "../scheduler/store.ts";
|
|
21
25
|
import { persistTelemetry } from "../telemetry-file.ts";
|
|
26
|
+
import { lockFolder } from "../utils/durable.ts";
|
|
22
27
|
import { createScheduledActionsTool, getDatetimeTool } from "../scheduler/tool.ts";
|
|
23
28
|
import { assertNoToolCollisions } from "../tools/executor.ts";
|
|
24
|
-
import { runAgentLoop, streamAgentLoop, type SteerEntry } from "./loop.ts";
|
|
25
|
-
import {
|
|
29
|
+
import { runAgentLoop, streamAgentLoop, unfinishedToolCalls, endsWithStop, accumulateUsage, type SteerEntry } from "./loop.ts";
|
|
30
|
+
import { isRetryableRun, readRetryAfterMs, sleep } from "../utils/retry.ts";
|
|
31
|
+
import { createSessionId, nowMs, newRunId } from "../utils/session.ts";
|
|
26
32
|
import {
|
|
27
33
|
chargeBudgetUsage,
|
|
28
34
|
checkBudget,
|
|
@@ -31,7 +37,10 @@ import {
|
|
|
31
37
|
listSubAgentTraceIdsForSessions,
|
|
32
38
|
type SubAgentTrace,
|
|
33
39
|
} from "./delegation.ts";
|
|
34
|
-
import {
|
|
40
|
+
import { addTurn, buildUsageReport, emptyUsage, mergeUsage, modelName, readUsage, type UsageNode, type UsageReport, type UsageTotals } from "../session/usage.ts";
|
|
41
|
+
import { saveSessionFile, sessionDirFor, loadSessionDir, findInvalidMessage, priceUsage, telemetryBus, type SessionRunMark, type SessionRunEnd, type SessionPending, type SessionToolRecord } from "../session/store.ts";
|
|
42
|
+
import { SessionJournal } from "../session/journal.ts";
|
|
43
|
+
import { ToolLogs, type ToolLogWriter } from "../session/tool-log.ts";
|
|
35
44
|
import { isLocalPathInput } from "../utils/safe-io.ts";
|
|
36
45
|
import { clearSessionRouting, normalizeStructuredOutput, normalizeStop } from "../providers.ts";
|
|
37
46
|
import type { StructuredOutputSpec } from "../types/model.ts";
|
|
@@ -53,7 +62,7 @@ export function resolveEffectiveThinking(
|
|
|
53
62
|
return { enabled: true, level: overrideLevel as ThinkingConfig["level"] };
|
|
54
63
|
}
|
|
55
64
|
|
|
56
|
-
import { ConfigError, SessionLoadError } from "../types/errors.ts";
|
|
65
|
+
import { ConfigError, SessionLoadError, isAbortError } from "../types/errors.ts";
|
|
57
66
|
|
|
58
67
|
/**
|
|
59
68
|
* Disposers for the manual user-signal forward listeners (Q-21 fallback when
|
|
@@ -62,6 +71,9 @@ import { ConfigError, SessionLoadError } from "../types/errors.ts";
|
|
|
62
71
|
*/
|
|
63
72
|
const mergeDisposers = new WeakMap<AbortController, () => void>();
|
|
64
73
|
|
|
74
|
+
/** Waits before run retry 1, 2, 3 ... (the last one repeats). Module-level so tests can shrink it; not public API. */
|
|
75
|
+
export const RUN_RETRY_DELAYS_MS: number[] = [5_000, 15_000, 45_000, 60_000];
|
|
76
|
+
|
|
65
77
|
/** Max named sessions kept per Agent (Q-19a LRU). */
|
|
66
78
|
const SESSION_LRU_MAX = 100;
|
|
67
79
|
/** Idle TTL for named sessions (Q-19a). */
|
|
@@ -159,6 +171,10 @@ export class Agent {
|
|
|
159
171
|
timeout: number;
|
|
160
172
|
/** Scheduled actions (`scheduled_actions` + `get_datetime` tools) are on. */
|
|
161
173
|
allowScheduledActions: boolean;
|
|
174
|
+
/** Levels of workers allowed below this agent. */
|
|
175
|
+
maxDepth: number;
|
|
176
|
+
/** Per-run spending cap of each worker (unset = unlimited). */
|
|
177
|
+
workerBudget?: { maxCostUsd?: number; maxTotalTokens?: number };
|
|
162
178
|
};
|
|
163
179
|
/** Home folder for everything written to disk (`sessions/`, `scheduled_actions/`, `telemetry/`). */
|
|
164
180
|
readonly home: string;
|
|
@@ -246,6 +262,26 @@ export class Agent {
|
|
|
246
262
|
private sessions = new Map<string, { context: AgentContext; lastAccess: number }>();
|
|
247
263
|
/** Coalesced persistence single-flight flag (Q-28). */
|
|
248
264
|
private persistScheduled = false;
|
|
265
|
+
/** Step journal of the persisted session folder (dir mode only). */
|
|
266
|
+
private journal?: SessionJournal;
|
|
267
|
+
/** Log files of tool calls in the persisted session folder (dir mode only). */
|
|
268
|
+
private toolLogs?: ToolLogs;
|
|
269
|
+
private toolLogsDir?: string;
|
|
270
|
+
/** Where each run of the default session began (saved with the session). @internal */
|
|
271
|
+
readonly runMarks: SessionRunMark[] = [];
|
|
272
|
+
/** Tool calls finished in the current run; saved the moment each finishes so a crash never repeats them. @internal */
|
|
273
|
+
readonly toolRecords: SessionToolRecord[] = [];
|
|
274
|
+
/** What this agent spent on its OWN model calls (workers keep their own); saved with the session. @internal */
|
|
275
|
+
readonly usageTotals: UsageTotals = emptyUsage();
|
|
276
|
+
/** The run in flight (its mark has no ending yet, but it is not "interrupted"). */
|
|
277
|
+
private activeMark?: SessionRunMark;
|
|
278
|
+
/** What to do with a cut-off run at construction (see `AgentConfig.resume`). */
|
|
279
|
+
private readonly resumeMode: "automatic" | "explicit";
|
|
280
|
+
/** How often a failed run is tried again (see `AgentConfig.runRetries`). */
|
|
281
|
+
private readonly runRetries: number;
|
|
282
|
+
/** The resume in flight, so a second `resume()` returns the same promise. */
|
|
283
|
+
private resuming?: Promise<AgentResponse | undefined>;
|
|
284
|
+
private workerRegistry?: WorkerRegistry;
|
|
249
285
|
|
|
250
286
|
/**
|
|
251
287
|
* Creates an agent and registers its model, tools, cache, and delegation settings.
|
|
@@ -282,6 +318,17 @@ export class Agent {
|
|
|
282
318
|
const dynEnabled = dynRaw ? (dynRaw.enabled ?? true) : false;
|
|
283
319
|
if (dynRaw) {
|
|
284
320
|
const dynTools = normalizeToolMap((dynRaw as { tools?: Record<string, ToolDefinition> | ToolDefinition[] }).tools);
|
|
321
|
+
const rawDepth = (dynRaw as { maxDepth?: unknown }).maxDepth;
|
|
322
|
+
if (rawDepth !== undefined && !(Number.isInteger(rawDepth) && (rawDepth as number) >= 1)) {
|
|
323
|
+
throw new ConfigError(`[Agent Accelerator] Invalid dynamicSubagents.maxDepth ${JSON.stringify(rawDepth)}: expected a whole number >= 1 (default ${DEFAULT_MAX_DEPTH}); set dynamicSubagents.enabled: false to turn workers off.`);
|
|
324
|
+
}
|
|
325
|
+
const rawBudget = (dynRaw as { workerBudget?: { maxCostUsd?: unknown; maxTotalTokens?: unknown } }).workerBudget;
|
|
326
|
+
for (const key of ["maxCostUsd", "maxTotalTokens"] as const) {
|
|
327
|
+
const v = rawBudget?.[key];
|
|
328
|
+
if (v !== undefined && !(typeof v === "number" && Number.isFinite(v) && v > 0)) {
|
|
329
|
+
throw new ConfigError(`[Agent Accelerator] Invalid dynamicSubagents.workerBudget.${key} ${JSON.stringify(v)}: expected a number greater than 0, or omit it for no limit.`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
285
332
|
const rawMax = (dynRaw as any)?.maxSpawn;
|
|
286
333
|
const maxSpawn = Number.isFinite(rawMax) ? Math.max(1, Math.floor(rawMax as number)) : 4;
|
|
287
334
|
// `workerTimeoutMs ?? timeout`, or -1 when `modelMaySetTimeout` (see normalizeWorkerTimeout).
|
|
@@ -294,6 +341,8 @@ export class Agent {
|
|
|
294
341
|
tools: dynTools,
|
|
295
342
|
timeout,
|
|
296
343
|
allowScheduledActions: dynRaw.allowScheduledActions === true,
|
|
344
|
+
maxDepth: (rawDepth as number | undefined) ?? DEFAULT_MAX_DEPTH,
|
|
345
|
+
...(rawBudget ? { workerBudget: { ...(rawBudget as { maxCostUsd?: number; maxTotalTokens?: number }) } } : {}),
|
|
297
346
|
};
|
|
298
347
|
}
|
|
299
348
|
this.apiKey = (rawModel as any)?.apiKey || config.apiKey;
|
|
@@ -313,6 +362,11 @@ export class Agent {
|
|
|
313
362
|
config.persist && !config.persist.dir && !config.persist.file
|
|
314
363
|
? { ...config.persist, dir: path.join(this.home, "sessions") }
|
|
315
364
|
: config.persist;
|
|
365
|
+
// One process per folder: fail fast here, before anything is written (the home for `persist: {}`, else the folder named).
|
|
366
|
+
if (config.persist) {
|
|
367
|
+
const { dir, file } = config.persist;
|
|
368
|
+
lockFolder(dir ?? (file ? path.dirname(path.resolve(file)) : this.home));
|
|
369
|
+
}
|
|
316
370
|
// `whenBusy` (canonical) wins over legacy `mode`; anything else is "auto".
|
|
317
371
|
const normalizedMode = resolveWhenBusy(config.midSession);
|
|
318
372
|
const rawMaxQueued = (config.midSession as any)?.maxQueued;
|
|
@@ -347,6 +401,14 @@ export class Agent {
|
|
|
347
401
|
);
|
|
348
402
|
}
|
|
349
403
|
this.cache = config.cache;
|
|
404
|
+
if (config.resume !== undefined && config.resume !== "automatic" && config.resume !== "explicit") {
|
|
405
|
+
throw new ConfigError(`[Agent Accelerator] Invalid resume ${JSON.stringify(config.resume)}: expected "automatic" (default) or "explicit".`);
|
|
406
|
+
}
|
|
407
|
+
this.resumeMode = config.resume ?? "automatic";
|
|
408
|
+
if (config.runRetries !== undefined && !(Number.isInteger(config.runRetries) && config.runRetries >= 0)) {
|
|
409
|
+
throw new ConfigError(`[Agent Accelerator] Invalid runRetries ${JSON.stringify(config.runRetries)}: expected a whole number >= 0 (default 3, 0 turns it off).`);
|
|
410
|
+
}
|
|
411
|
+
this.runRetries = config.runRetries ?? 3;
|
|
350
412
|
|
|
351
413
|
this.serviceTier = config.serviceTier;
|
|
352
414
|
this.onFailure =
|
|
@@ -400,8 +462,8 @@ export class Agent {
|
|
|
400
462
|
);
|
|
401
463
|
}
|
|
402
464
|
if (dyn?.enabled === true) {
|
|
403
|
-
const
|
|
404
|
-
this.tools[
|
|
465
|
+
const subagentsTool = createSubagentsTool(this);
|
|
466
|
+
this.tools[subagentsTool.name || "subagents"] = subagentsTool;
|
|
405
467
|
}
|
|
406
468
|
if (dyn?.allowScheduledActions) {
|
|
407
469
|
// Fixed by config, so the tool set (and the prompt cache) stays stable.
|
|
@@ -415,6 +477,157 @@ export class Agent {
|
|
|
415
477
|
|
|
416
478
|
this.context = new AgentContext(this.getFullInstructions());
|
|
417
479
|
if (dyn?.allowScheduledActions) registerAgent(this);
|
|
480
|
+
if (this.recoverSession() && this.resumeMode === "automatic") {
|
|
481
|
+
// Synchronously, so a run() issued right after construction queues BEHIND the resumed run.
|
|
482
|
+
this.resume().catch((err) => this.reportResumeFailure(err));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** The persistent sub-agent workers of this agent: `spawn`, `message`, `close`, `list`, `turns` (see `WorkerRegistry`). */
|
|
487
|
+
get workers(): WorkerRegistry {
|
|
488
|
+
return (this.workerRegistry ??= new WorkerRegistry(this));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Loads a persisted session that a crash cut off (a run without an ending) or that still had waiting prompts, so it can be resumed; true when there is something to resume; in: the session folder of `persist.dir`, out: context, runMarks, toolRecords, pendingQueue.
|
|
492
|
+
private recoverSession(): boolean {
|
|
493
|
+
const root = this.persistConfig?.dir;
|
|
494
|
+
if (!root || this.stateless) return false;
|
|
495
|
+
let dir: string;
|
|
496
|
+
try {
|
|
497
|
+
dir = sessionDirFor(root, this.sessionId);
|
|
498
|
+
} catch {
|
|
499
|
+
return false; // not a usable session id for a folder name: nothing was ever saved under it
|
|
500
|
+
}
|
|
501
|
+
const session = loadSessionDir(dir)?.session; // a session over the load limits throws SessionLoadError: better than overwriting it
|
|
502
|
+
if (!session) return false;
|
|
503
|
+
const waiting = [...(session.pending?.steer ?? []), ...(session.pending?.queue ?? [])];
|
|
504
|
+
if (!(session.runs ?? []).some((r) => !r.end) && waiting.length === 0) return false;
|
|
505
|
+
this.context.messages = session.messages;
|
|
506
|
+
this.runMarks.splice(0, this.runMarks.length, ...(session.runs ?? []));
|
|
507
|
+
this.restoreUsage(session.usage);
|
|
508
|
+
const open = new Set(unfinishedToolCalls(this.context.messages).map((c) => c.id));
|
|
509
|
+
this.toolRecords.splice(0, this.toolRecords.length, ...(session.tools ?? []).filter((t) => open.has(t.id)));
|
|
510
|
+
for (const w of waiting) {
|
|
511
|
+
this.pendingQueue.push({
|
|
512
|
+
prompt: w.prompt,
|
|
513
|
+
options: w.additionalContext ? { additionalContext: w.additionalContext } : undefined,
|
|
514
|
+
isStream: false,
|
|
515
|
+
resolve: () => {},
|
|
516
|
+
reject: (err) => this.reportResumeFailure(err),
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Puts saved spending totals back after a restart or import (zeros when the file has none); in: a loaded session's `usage`, out: this.usageTotals.
|
|
523
|
+
private restoreUsage(saved: unknown): void {
|
|
524
|
+
Object.assign(this.usageTotals, readUsage(saved));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Records one model call of THIS agent (priced per turn, with the service tier); in: the loop's onTurnUsage, out: this.usageTotals (saved by the next persist).
|
|
528
|
+
private recordTurnUsage(usage: TokenUsage, spec: Parameters<typeof priceUsage>[1]): void {
|
|
529
|
+
addTurn(this.usageTotals, usage, priceUsage(usage, spec, this.serviceTier ? { serviceTier: this.serviceTier } : undefined));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Adds another agent's spending to this one (a fixed worker's per-call copy into its template). @internal */
|
|
533
|
+
absorbUsage(other: UsageTotals): void {
|
|
534
|
+
mergeUsage(this.usageTotals, other);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* What this agent and every worker below it spent: per agent and per worker (tokens, cache hits, thinking, exact
|
|
539
|
+
* cost per turn) plus a whole-session total, with nothing counted twice. `agent` is this agent's own model calls;
|
|
540
|
+
* workers (open, closed, nested, and rebuilt from disk after a restart) and fixed `subagents` workers are listed
|
|
541
|
+
* below it. Spending of one-shot scheduled-action workers is in their run files, not here.
|
|
542
|
+
*/
|
|
543
|
+
usageReport(): UsageReport {
|
|
544
|
+
return buildUsageReport({ id: this.sessionId, name: this.name, model: modelName(this.modelStringOrSpec), totals: this.usageTotals }, [...this.workers.usageNodes(), ...this.fixedWorkerNodes()]);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// The `subagents: [...]` workers as usage nodes (their per-call copies' spending is merged into them); in: the constructor config, out: usageReport.
|
|
548
|
+
private fixedWorkerNodes(): UsageNode[] {
|
|
549
|
+
const nodes: UsageNode[] = [];
|
|
550
|
+
for (const entry of this.forkConfig.subagents ?? []) {
|
|
551
|
+
const worker = "agent" in entry ? entry.agent : entry;
|
|
552
|
+
const name = "agent" in entry ? entry.name : worker.name;
|
|
553
|
+
nodes.push({ id: name, name, model: modelName(worker.modelStringOrSpec), status: "fixed", totals: worker.usageTotals, workers: [] });
|
|
554
|
+
}
|
|
555
|
+
return nodes;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Says on the telemetry bus that resuming (or a prompt that was waiting) failed; in: a rejected resume, out: telemetryBus.
|
|
559
|
+
private reportResumeFailure(err: unknown): void {
|
|
560
|
+
telemetryBus.emitWarning("agent.resume_failed", `[Agent Accelerator] Resuming session ${this.sessionId} failed: ${err instanceof Error ? err.message : String(err)}`, { sessionId: this.sessionId });
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** Runs that a crash cut off (they began, never ended and are not running now), oldest first. Needs `persist: { dir }` and the same `sessionId` as before the restart. */
|
|
564
|
+
interrupted(): InterruptedRun[] {
|
|
565
|
+
const calls = unfinishedToolCalls(this.context.messages).map((c) => c.name);
|
|
566
|
+
return this.runMarks
|
|
567
|
+
.filter((m) => !m.end && m !== this.activeMark)
|
|
568
|
+
.map((m) => ({
|
|
569
|
+
runId: m.id,
|
|
570
|
+
startedAt: m.at,
|
|
571
|
+
prompt: this.previewPrompt((this.context.messages[m.i]?.content ?? "") as string | ContentPart[]),
|
|
572
|
+
pendingToolCalls: calls,
|
|
573
|
+
}));
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Continues the run a crash cut off, without a new prompt: tool calls that already have a saved result are NOT run
|
|
578
|
+
* again (only calls with no result are), then the model carries on from the saved conversation, and prompts that
|
|
579
|
+
* were waiting run afterwards. Resolves to that run's response, or `undefined` when nothing was cut off. Returns the
|
|
580
|
+
* same promise while a resume is in flight. With `resume: "automatic"` (default) the constructor already did this.
|
|
581
|
+
*/
|
|
582
|
+
resume(): Promise<AgentResponse | undefined> {
|
|
583
|
+
if (this.resuming) return this.resuming;
|
|
584
|
+
if (this.currentRun) throw new Error("[Agent Accelerator] resume() needs an idle agent: a run is already active.");
|
|
585
|
+
const mark = this.runMarks.filter((m) => !m.end && m !== this.activeMark).pop();
|
|
586
|
+
const last = this.context.messages[this.context.messages.length - 1];
|
|
587
|
+
const nothingOpen = unfinishedToolCalls(this.context.messages).length === 0;
|
|
588
|
+
if (!mark || (nothingOpen && (last?.role === "assistant" || endsWithStop(this.context.messages)))) {
|
|
589
|
+
// The answer was complete (or a tool had ended the run); only the note that it ended was lost.
|
|
590
|
+
if (mark) this.endRun(mark, endsWithStop(this.context.messages) ? "stopped" : "done");
|
|
591
|
+
this.pumpQueue();
|
|
592
|
+
return Promise.resolve(undefined);
|
|
593
|
+
}
|
|
594
|
+
telemetryBus.emit({
|
|
595
|
+
code: "agent.resumed",
|
|
596
|
+
message: `[Agent Accelerator] resuming run ${mark.id} of session ${this.sessionId}`,
|
|
597
|
+
data: { sessionId: this.sessionId, runId: mark.id, pendingToolCalls: unfinishedToolCalls(this.context.messages).map((c) => c.name) },
|
|
598
|
+
});
|
|
599
|
+
const run = this.startNonStreamingRun("", undefined, mark);
|
|
600
|
+
const clear = (): void => {
|
|
601
|
+
if (this.resuming === run) this.resuming = undefined;
|
|
602
|
+
};
|
|
603
|
+
this.resuming = run;
|
|
604
|
+
run.then(clear, clear);
|
|
605
|
+
return run;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// The finished tool calls of the current run as results the loop can reuse; in: toolRecords, out: loopConfig.resume.stored.
|
|
609
|
+
private storedToolResults(): Map<string, ToolResultRecord> {
|
|
610
|
+
return new Map(this.toolRecords.map((t) => [t.id, { id: t.id, name: t.name, result: t.result, ...(t.isError ? { isError: true } : {}), ...(t.stopped ? { stopped: true } : {}) } as ToolResultRecord]));
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Closes a run's mark with how it ended and saves that at once (not on the next tick), so the disk is up to date when `await run()` returns; in: the run's promise handlers, out: runMarks + journal.
|
|
614
|
+
private endRun(mark: SessionRunMark | undefined, status: SessionRunEnd): void {
|
|
615
|
+
if (!mark) return;
|
|
616
|
+
if (!mark.end) mark.end = status;
|
|
617
|
+
if (this.activeMark === mark) this.activeMark = undefined;
|
|
618
|
+
this.doPersist();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Remembers a finished tool call and saves it at once (synchronously): the work is on disk before anything else can happen; in: the executor's per-call callback, out: toolRecords + journal.
|
|
622
|
+
private recordTool(result: ToolResultRecord): void {
|
|
623
|
+
this.toolRecords.push({
|
|
624
|
+
id: result.id,
|
|
625
|
+
name: result.name,
|
|
626
|
+
result: isSubagentResult(result.result) ? result.result.xml : result.result,
|
|
627
|
+
...(result.isError ? { isError: true } : {}),
|
|
628
|
+
...(result.stopped ? { stopped: true } : {}),
|
|
629
|
+
});
|
|
630
|
+
this.doPersist();
|
|
418
631
|
}
|
|
419
632
|
|
|
420
633
|
private getFullInstructions(): string | undefined {
|
|
@@ -432,26 +645,6 @@ export class Agent {
|
|
|
432
645
|
"3. Once your reasoning is complete, output your final response or function calls directly in the standard response output.";
|
|
433
646
|
out = out ? `${out}\n\n${guidance}` : guidance;
|
|
434
647
|
}
|
|
435
|
-
if (this.dynamicSubagents?.enabled === true && !out.includes("[Dynamic Sub-Agents]")) {
|
|
436
|
-
const dyn = this.dynamicSubagents;
|
|
437
|
-
const toolNames = Object.keys(dyn.tools);
|
|
438
|
-
const timeoutNote =
|
|
439
|
-
dyn.timeout === -1
|
|
440
|
-
? "Timeout policy: set a per-sub-agent timeoutMs (ms) for each task; omit it for no limit."
|
|
441
|
-
: dyn.timeout === 0
|
|
442
|
-
? "Timeout policy: workers run with no time limit."
|
|
443
|
-
: `Timeout policy: every worker is limited to ${dyn.timeout}ms; per-task timeouts are ignored.`;
|
|
444
|
-
const policy =
|
|
445
|
-
"[Dynamic Sub-Agents]\n" +
|
|
446
|
-
`1. You may spawn at most ${dyn.maxSpawn} sub-agent(s) per spawn_subagents call. Extra tasks beyond ${dyn.maxSpawn} are ignored.\n` +
|
|
447
|
-
"2. Workers are stateless: each receives one task, returns its result, then shuts down. No conversation history is kept.\n" +
|
|
448
|
-
"3. You cannot choose worker models or reasoning levels — they are fixed by the developer.\n" +
|
|
449
|
-
(toolNames.length > 0
|
|
450
|
-
? `4. Worker-available tools: ${toolNames.join(", ")}. Grant each worker ONLY the tools its task needs via the per-task tools list; omit it for no tools.\n`
|
|
451
|
-
: "4. No worker tools are available; omit the per-task tools list.\n") +
|
|
452
|
-
`5. ${timeoutNote}`;
|
|
453
|
-
out = out ? `${out}\n\n${policy}` : policy;
|
|
454
|
-
}
|
|
455
648
|
return out || undefined;
|
|
456
649
|
}
|
|
457
650
|
|
|
@@ -461,6 +654,7 @@ export class Agent {
|
|
|
461
654
|
this.context.messages = [];
|
|
462
655
|
this.context.thoughtSignatures = [];
|
|
463
656
|
this.context.systemPrompt = this.getFullInstructions();
|
|
657
|
+
this.runMarks.length = 0;
|
|
464
658
|
// Q-25: reset also drops session-scoped provider chaining/routing.
|
|
465
659
|
try { clearInteractionChains(sid); } catch {}
|
|
466
660
|
try { clearSessionRouting(sid); } catch {}
|
|
@@ -546,6 +740,7 @@ export class Agent {
|
|
|
546
740
|
try { entry.reject(err); } catch {}
|
|
547
741
|
try { entry.deferredStream?.fail(err instanceof Error ? err : new Error(String(err))); } catch {}
|
|
548
742
|
}
|
|
743
|
+
this.persistNow();
|
|
549
744
|
return n;
|
|
550
745
|
}
|
|
551
746
|
|
|
@@ -716,6 +911,7 @@ export class Agent {
|
|
|
716
911
|
}
|
|
717
912
|
return new Promise<AgentResponse>((resolve, reject) => {
|
|
718
913
|
this.pendingQueue.push({ prompt, options, isStream: false, resolve, reject });
|
|
914
|
+
this.persistNow();
|
|
719
915
|
try {
|
|
720
916
|
this.currentRun?.outerStream?.push({
|
|
721
917
|
type: "queued",
|
|
@@ -730,6 +926,7 @@ export class Agent {
|
|
|
730
926
|
if (this.currentRun !== null) return;
|
|
731
927
|
const next = this.pendingQueue.shift();
|
|
732
928
|
if (!next) return;
|
|
929
|
+
this.persistNow();
|
|
733
930
|
// Q-20: reject queued items whose signal aborted before start.
|
|
734
931
|
try {
|
|
735
932
|
if ((next.options as { signal?: AbortSignal } | undefined)?.signal?.aborted) {
|
|
@@ -788,6 +985,8 @@ export class Agent {
|
|
|
788
985
|
if (leftover.length > 0) {
|
|
789
986
|
try {
|
|
790
987
|
const max = this.midSessionConfig.maxQueued ?? 20;
|
|
988
|
+
// Late steers were typed BEFORE anything already queued: they go to the front, in the order they arrived.
|
|
989
|
+
const lateSteers: Agent["pendingQueue"] = [];
|
|
791
990
|
for (const entry of leftover) {
|
|
792
991
|
try {
|
|
793
992
|
if ((entry.options as { signal?: AbortSignal } | undefined)?.signal?.aborted) {
|
|
@@ -795,14 +994,14 @@ export class Agent {
|
|
|
795
994
|
continue;
|
|
796
995
|
}
|
|
797
996
|
} catch { continue; }
|
|
798
|
-
if (this.pendingQueue.length >= max) {
|
|
997
|
+
if (this.pendingQueue.length + lateSteers.length >= max) {
|
|
799
998
|
entry.settle?.reject(new Error("[Agent Accelerator] Steer dropped: the follow-up queue is full."));
|
|
800
999
|
continue;
|
|
801
1000
|
}
|
|
802
1001
|
// The steer was never injected: it now runs as a follow-up, and the
|
|
803
1002
|
// caller's promise resolves with that run's response.
|
|
804
1003
|
if (entry.settle) entry.settle.deferredToQueue = true;
|
|
805
|
-
|
|
1004
|
+
lateSteers.push({
|
|
806
1005
|
prompt: entry.prompt,
|
|
807
1006
|
options: entry.options,
|
|
808
1007
|
isStream: false,
|
|
@@ -810,8 +1009,10 @@ export class Agent {
|
|
|
810
1009
|
reject: entry.settle?.reject ?? (() => {}),
|
|
811
1010
|
});
|
|
812
1011
|
}
|
|
1012
|
+
this.pendingQueue.unshift(...lateSteers);
|
|
813
1013
|
} catch {}
|
|
814
1014
|
}
|
|
1015
|
+
this.persistNow();
|
|
815
1016
|
// Defer pump so the completing run's .then handlers settle first.
|
|
816
1017
|
queueMicrotask(() => this.pumpQueue());
|
|
817
1018
|
}
|
|
@@ -962,9 +1163,11 @@ export class Agent {
|
|
|
962
1163
|
|
|
963
1164
|
private startNonStreamingRun(
|
|
964
1165
|
prompt: string | ContentPart[],
|
|
965
|
-
options?: AgentRunOptions
|
|
1166
|
+
options?: AgentRunOptions,
|
|
1167
|
+
resumeMark?: SessionRunMark
|
|
966
1168
|
): Promise<AgentResponse> {
|
|
967
1169
|
this.resetBudgetState();
|
|
1170
|
+
let mark: SessionRunMark | undefined;
|
|
968
1171
|
const runController = new AbortController();
|
|
969
1172
|
const steerInbox: SteerEntry[] = [];
|
|
970
1173
|
const mergedSignal = this.mergeUserSignal(options?.signal, runController);
|
|
@@ -978,7 +1181,14 @@ export class Agent {
|
|
|
978
1181
|
if (effectiveLevel) {
|
|
979
1182
|
validateModelThinking(resolved.provider.id, resolved.modelId, effectiveLevel);
|
|
980
1183
|
}
|
|
981
|
-
|
|
1184
|
+
if (resumeMark) {
|
|
1185
|
+
// Continuing a cut-off run: no new prompt, the saved conversation is the input.
|
|
1186
|
+
mark = resumeMark;
|
|
1187
|
+
this.activeMark = mark;
|
|
1188
|
+
targetContext.systemPrompt = this.getFullInstructions();
|
|
1189
|
+
} else {
|
|
1190
|
+
mark = this.prepareTurn(prompt, runOptions, targetContext);
|
|
1191
|
+
}
|
|
982
1192
|
this.persistNow();
|
|
983
1193
|
|
|
984
1194
|
const providerOptions = this.buildProviderOptions(runOptions);
|
|
@@ -997,6 +1207,10 @@ export class Agent {
|
|
|
997
1207
|
...(this.captureRaw !== undefined ? { captureRaw: this.captureRaw } : {}),
|
|
998
1208
|
...(this.budget ? { enforceBudget: (u?: TokenUsage) => this.enforceBudget(u) } : {}),
|
|
999
1209
|
onProgress: () => this.persistNow(),
|
|
1210
|
+
onTurnUsage: (u: TokenUsage, spec: Parameters<typeof priceUsage>[1]) => this.recordTurnUsage(u, spec),
|
|
1211
|
+
onToolResult: (r: ToolResultRecord) => this.recordTool(r),
|
|
1212
|
+
openToolLog: (call: { name: string }, sid: string | undefined) => this.openToolLog(call, sid),
|
|
1213
|
+
...(resumeMark ? { resume: { stored: this.storedToolResults() } } : {}),
|
|
1000
1214
|
steerInbox,
|
|
1001
1215
|
onSteerInjected: () => this.persistNow(),
|
|
1002
1216
|
// Q-22: loop enforces these (plus local timeout races).
|
|
@@ -1004,7 +1218,28 @@ export class Agent {
|
|
|
1004
1218
|
...(this.output !== undefined ? { output: this.output } : {}),
|
|
1005
1219
|
};
|
|
1006
1220
|
|
|
1007
|
-
|
|
1221
|
+
// A run that failed for a transient reason is tried again from where it stopped (the prompt is already in history,
|
|
1222
|
+
// finished tool calls are kept), holding the agent busy meanwhile; anything else fails at once.
|
|
1223
|
+
let res: AgentResponse;
|
|
1224
|
+
const failedAttempts: FailedAttempt[] = [];
|
|
1225
|
+
for (let attempt = 0; ; attempt++) {
|
|
1226
|
+
try {
|
|
1227
|
+
res = await runAgentLoop(attempt === 0 ? loopConfig : { ...loopConfig, resume: { stored: this.storedToolResults() } });
|
|
1228
|
+
break;
|
|
1229
|
+
} catch (err) {
|
|
1230
|
+
if (attempt >= this.runRetries || runOptions?.signal?.aborted || !isRetryableRun(err)) throw err;
|
|
1231
|
+
const partial = (err as { partial?: FailedAttempt }).partial;
|
|
1232
|
+
if (partial?.usage) failedAttempts.push(partial);
|
|
1233
|
+
const delayMs = Math.max(RUN_RETRY_DELAYS_MS[Math.min(attempt, RUN_RETRY_DELAYS_MS.length - 1)]!, readRetryAfterMs(err) ?? 0);
|
|
1234
|
+
telemetryBus.emitWarning(
|
|
1235
|
+
"agent.run_retry",
|
|
1236
|
+
`[Agent Accelerator] Run failed (${err instanceof Error ? err.message : String(err)}); retry ${attempt + 1} of ${this.runRetries} in ${delayMs} ms.`,
|
|
1237
|
+
{ sessionId: this.sessionId, attempt: attempt + 1, of: this.runRetries, delayMs }
|
|
1238
|
+
);
|
|
1239
|
+
await sleep(delayMs, runOptions?.signal);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
if (failedAttempts.length > 0) res = withFailedAttempts(res, failedAttempts);
|
|
1008
1243
|
this.mergeSubagentTraces(res.subagents as any);
|
|
1009
1244
|
this.persistNow();
|
|
1010
1245
|
if (this.stateless) {
|
|
@@ -1015,6 +1250,10 @@ export class Agent {
|
|
|
1015
1250
|
})();
|
|
1016
1251
|
|
|
1017
1252
|
this.currentRun = { steerInbox, abortController: runController, runPromise: task };
|
|
1253
|
+
task.then(
|
|
1254
|
+
(res) => this.endRun(mark, res.finishReason === "stopped" ? "stopped" : "done"),
|
|
1255
|
+
(err) => this.endRun(mark, isAbortError(err) ? "aborted" : "failed")
|
|
1256
|
+
);
|
|
1018
1257
|
const cleanup = () => {
|
|
1019
1258
|
try { this.cleanupMerge(runController); } catch {}
|
|
1020
1259
|
if (this.currentRun?.runPromise === task) this.finishCurrentRun();
|
|
@@ -1034,7 +1273,7 @@ export class Agent {
|
|
|
1034
1273
|
}
|
|
1035
1274
|
// Q-19a: named session routing (default session for compat).
|
|
1036
1275
|
const targetContext = this.getSessionContext(options?.sessionId || this.sessionId);
|
|
1037
|
-
this.prepareTurn(prompt, options, targetContext);
|
|
1276
|
+
const mark = this.prepareTurn(prompt, options, targetContext);
|
|
1038
1277
|
this.persistNow();
|
|
1039
1278
|
|
|
1040
1279
|
this.resetBudgetState();
|
|
@@ -1059,6 +1298,9 @@ export class Agent {
|
|
|
1059
1298
|
...(this.captureRaw !== undefined ? { captureRaw: this.captureRaw } : {}),
|
|
1060
1299
|
...(this.budget ? { enforceBudget: (u?: TokenUsage) => this.enforceBudget(u) } : {}),
|
|
1061
1300
|
onProgress: () => this.persistNow(),
|
|
1301
|
+
onTurnUsage: (u: TokenUsage, spec: Parameters<typeof priceUsage>[1]) => this.recordTurnUsage(u, spec),
|
|
1302
|
+
onToolResult: (r: ToolResultRecord) => this.recordTool(r),
|
|
1303
|
+
openToolLog: (call: { name: string }, sid: string | undefined) => this.openToolLog(call, sid),
|
|
1062
1304
|
steerInbox,
|
|
1063
1305
|
onSteerInjected: () => this.persistNow(),
|
|
1064
1306
|
// Q-22: loop enforces these (plus local timeout races).
|
|
@@ -1070,6 +1312,7 @@ export class Agent {
|
|
|
1070
1312
|
this.currentRun = { steerInbox, outerStream: s, abortController: runController };
|
|
1071
1313
|
s.result().then(
|
|
1072
1314
|
(res) => {
|
|
1315
|
+
this.endRun(mark, res.finishReason === "stopped" ? "stopped" : "done");
|
|
1073
1316
|
try {
|
|
1074
1317
|
this.mergeSubagentTraces(res.subagents as any);
|
|
1075
1318
|
this.persistNow();
|
|
@@ -1081,7 +1324,8 @@ export class Agent {
|
|
|
1081
1324
|
}
|
|
1082
1325
|
if (this.currentRun?.outerStream === s) this.finishCurrentRun();
|
|
1083
1326
|
},
|
|
1084
|
-
() => {
|
|
1327
|
+
(err) => {
|
|
1328
|
+
this.endRun(mark, isAbortError(err) ? "aborted" : "failed");
|
|
1085
1329
|
try { this.cleanupMerge(runController); } catch {}
|
|
1086
1330
|
if (this.currentRun?.outerStream === s) this.finishCurrentRun();
|
|
1087
1331
|
}
|
|
@@ -1168,6 +1412,25 @@ export class Agent {
|
|
|
1168
1412
|
});
|
|
1169
1413
|
}
|
|
1170
1414
|
|
|
1415
|
+
// Opens the log file of one tool call when this Agent persists its default session to a folder, else nothing; in: the executor, once per call, out: a writer for `tool-logs/<n>-<tool>.ndjson`.
|
|
1416
|
+
private openToolLog(call: { name: string }, sessionId: string | undefined): ToolLogWriter | undefined {
|
|
1417
|
+
const root = this.persistConfig?.dir;
|
|
1418
|
+
if (!root || this.stateless || sessionId !== this.sessionId) return undefined;
|
|
1419
|
+
const dir = sessionDirFor(root, this.sessionId);
|
|
1420
|
+
if (this.toolLogsDir !== dir) {
|
|
1421
|
+
this.toolLogs = new ToolLogs(dir);
|
|
1422
|
+
this.toolLogsDir = dir;
|
|
1423
|
+
}
|
|
1424
|
+
return this.toolLogs!.open(call);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/** The step journal of this session's folder (recreated when `sessionId` changes). */
|
|
1428
|
+
private journalFor(root: string): SessionJournal {
|
|
1429
|
+
const dir = sessionDirFor(root, this.sessionId);
|
|
1430
|
+
if (this.journal?.dir !== dir) this.journal = new SessionJournal(dir);
|
|
1431
|
+
return this.journal;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1171
1434
|
/** Synchronous persistence write behind the coalesced `persistNow()` gate. */
|
|
1172
1435
|
private doPersist(): void {
|
|
1173
1436
|
const cfg = this.persistConfig;
|
|
@@ -1176,7 +1439,7 @@ export class Agent {
|
|
|
1176
1439
|
if (this.stateless && !(cfg as unknown as { allowStateless?: unknown }).allowStateless) return;
|
|
1177
1440
|
} catch {}
|
|
1178
1441
|
try {
|
|
1179
|
-
if (cfg.dir)
|
|
1442
|
+
if (cfg.dir) this.journalFor(cfg.dir).sync(this as any);
|
|
1180
1443
|
else if (cfg.file) saveSessionFile(cfg.file, this as any, null);
|
|
1181
1444
|
} catch (err) {
|
|
1182
1445
|
// Q-28: surface via hook instead of swallowing silently.
|
|
@@ -1285,6 +1548,8 @@ export class Agent {
|
|
|
1285
1548
|
this.context.messages = Array.isArray(data.messages) ? [...data.messages] : [];
|
|
1286
1549
|
}
|
|
1287
1550
|
this.context.thoughtSignatures = [];
|
|
1551
|
+
this.runMarks.splice(0, this.runMarks.length, ...(data.runs ?? []).filter((r) => r.i < this.context.messages.length));
|
|
1552
|
+
this.restoreUsage(data.usage);
|
|
1288
1553
|
if (data.systemPrompt) {
|
|
1289
1554
|
this.context.systemPrompt = data.systemPrompt;
|
|
1290
1555
|
} else {
|
|
@@ -1324,7 +1589,7 @@ export class Agent {
|
|
|
1324
1589
|
} catch {}
|
|
1325
1590
|
}
|
|
1326
1591
|
|
|
1327
|
-
private prepareTurn(prompt: string | ContentPart[], options?: AgentRunOptions, target?: AgentContext):
|
|
1592
|
+
private prepareTurn(prompt: string | ContentPart[], options?: AgentRunOptions, target?: AgentContext): SessionRunMark | undefined {
|
|
1328
1593
|
// Q-20: prompt validation throws synchronously in both run() and
|
|
1329
1594
|
// stream() (same timing; documented here).
|
|
1330
1595
|
if (Array.isArray(prompt) && prompt.length === 0) {
|
|
@@ -1352,7 +1617,28 @@ export class Agent {
|
|
|
1352
1617
|
} else if (ctx.systemPrompt !== fullInstructions) {
|
|
1353
1618
|
ctx.systemPrompt = fullInstructions;
|
|
1354
1619
|
}
|
|
1620
|
+
const mark = ctx === this.context ? this.markRunStart(ctx.messages.length) : undefined;
|
|
1355
1621
|
ctx.addUserMessage(prompt);
|
|
1622
|
+
return mark;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// Records where a run of the default session begins; in: prepareTurn, out: runMarks, saved by the session journal.
|
|
1626
|
+
private markRunStart(index: number): SessionRunMark {
|
|
1627
|
+
for (const old of this.runMarks) if (!old.end) old.end = "abandoned"; // a newer run replaces a cut-off one
|
|
1628
|
+
this.toolRecords.length = 0;
|
|
1629
|
+
const mark: SessionRunMark = { id: newRunId(), i: index, at: Date.now() };
|
|
1630
|
+
this.runMarks.push(mark);
|
|
1631
|
+
this.activeMark = mark;
|
|
1632
|
+
return mark;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
/** Prompts still waiting (steers not yet injected, queued follow-ups), as the session journal saves them. @internal */
|
|
1636
|
+
pendingPrompts(): SessionPending {
|
|
1637
|
+
const pack = (e: { prompt: string | ContentPart[]; options?: AgentRunOptions }) => ({
|
|
1638
|
+
prompt: e.prompt,
|
|
1639
|
+
...(e.options?.additionalContext ? { additionalContext: e.options.additionalContext } : {}),
|
|
1640
|
+
});
|
|
1641
|
+
return { steer: (this.currentRun?.steerInbox ?? []).map(pack), queue: this.pendingQueue.map(pack) };
|
|
1356
1642
|
}
|
|
1357
1643
|
|
|
1358
1644
|
/**
|
|
@@ -1432,6 +1718,7 @@ export class Agent {
|
|
|
1432
1718
|
}
|
|
1433
1719
|
return new Promise<AgentResponse>((resolve, reject) => {
|
|
1434
1720
|
this.pendingQueue.push({ prompt, options, isStream: false, resolve, reject });
|
|
1721
|
+
this.persistNow();
|
|
1435
1722
|
try {
|
|
1436
1723
|
this.currentRun?.outerStream?.push({
|
|
1437
1724
|
type: "queued",
|
|
@@ -1615,6 +1902,7 @@ export class Agent {
|
|
|
1615
1902
|
} catch {}
|
|
1616
1903
|
});
|
|
1617
1904
|
this.pendingQueue.push(queuedEntry as any);
|
|
1905
|
+
this.persistNow();
|
|
1618
1906
|
// Attach deferred completion to queued promise without exposing it.
|
|
1619
1907
|
// Real events are piped in pumpQueue() when the turn starts.
|
|
1620
1908
|
void done.then(
|
|
@@ -1852,6 +2140,35 @@ export class Agent {
|
|
|
1852
2140
|
}
|
|
1853
2141
|
}
|
|
1854
2142
|
|
|
2143
|
+
/** What a failed run attempt got done before it failed (the `partial` the loop attaches to its error). */
|
|
2144
|
+
type FailedAttempt = Partial<AgentResponse> & { usage: TokenUsage };
|
|
2145
|
+
|
|
2146
|
+
// Adds what failed attempts already did (tool calls, results, turns, token usage) to the response of the attempt that finally worked, so a retry never hides side effects or cost; in: the final response + each failed attempt's `err.partial`, out: the run's AgentResponse.
|
|
2147
|
+
function withFailedAttempts(res: AgentResponse, failed: FailedAttempt[]): AgentResponse {
|
|
2148
|
+
const usage: TokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
2149
|
+
for (const f of failed) accumulateUsage(usage, f.usage);
|
|
2150
|
+
accumulateUsage(usage, res.usage);
|
|
2151
|
+
return new AgentResponse({
|
|
2152
|
+
text: res.text,
|
|
2153
|
+
...(res.parsed !== undefined ? { parsed: res.parsed } : {}),
|
|
2154
|
+
thinking: res.thinking,
|
|
2155
|
+
thoughtSignature: res.thoughtSignature,
|
|
2156
|
+
toolCalls: [...failed.flatMap((f) => f.toolCalls ?? []), ...res.toolCalls],
|
|
2157
|
+
toolResults: [...failed.flatMap((f) => f.toolResults ?? []), ...res.toolResults],
|
|
2158
|
+
subagents: res.subagents,
|
|
2159
|
+
usage,
|
|
2160
|
+
responseId: res.responseId,
|
|
2161
|
+
model: res.model,
|
|
2162
|
+
provider: res.provider,
|
|
2163
|
+
finishReason: res.finishReason,
|
|
2164
|
+
rawFinishReason: res.rawFinishReason,
|
|
2165
|
+
durationMs: res.durationMs,
|
|
2166
|
+
raw: res.raw,
|
|
2167
|
+
turns: res.turns + failed.reduce((n, f) => n + (typeof f.turns === "number" ? f.turns : 0), 0),
|
|
2168
|
+
providerSteps: res.providerSteps,
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
|
|
1855
2172
|
/** Normalizes the public `thinkingLevel` flag into provider-neutral thinking settings. */
|
|
1856
2173
|
export function normalizeThinking(config: AgentConfig, fallbackLevel?: string): ThinkingConfig | undefined {
|
|
1857
2174
|
// DX4: only thinkingLevel, values: none, dynamic, minimal, low, medium, high, xhigh, max
|