agent-accelerator 0.5.9 → 0.6.5

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.
Files changed (85) hide show
  1. package/README.md +64 -16
  2. package/examples/prompts/SYSTEM_PROMPT.md +2 -0
  3. package/examples/prompts/SYSTEM_PROMPT_AGENT.md +4 -0
  4. package/examples/prompts/SYSTEM_PROMPT_TOOLS.md +1 -0
  5. package/package.json +4 -1
  6. package/src/agent/agent.ts +447 -172
  7. package/src/agent/architecture.md +16 -12
  8. package/src/agent/config-blocks.ts +39 -12
  9. package/src/agent/context.ts +9 -3
  10. package/src/agent/defaults.ts +88 -0
  11. package/src/agent/delegation.ts +86 -28
  12. package/src/agent/loop.ts +474 -374
  13. package/src/agent/subagent.ts +1 -3
  14. package/src/agent/subagents-tool.ts +50 -19
  15. package/src/agent/turns.ts +3 -1
  16. package/src/agent/usage-limit.ts +92 -0
  17. package/src/agent/worker.ts +73 -30
  18. package/src/agent/workers.ts +126 -21
  19. package/src/architecture.md +17 -15
  20. package/src/index.ts +43 -19
  21. package/src/models/architecture.md +4 -3
  22. package/src/models/catalog-cache.ts +6 -5
  23. package/src/models/catalog.ts +39 -30
  24. package/src/prompts/additional-context.md +2 -0
  25. package/src/prompts/architecture.md +19 -0
  26. package/src/prompts/prompts.ts +81 -0
  27. package/src/prompts/reasoning-directive.md +5 -0
  28. package/src/{scheduler/scheduled-actions.md → prompts/scheduled-actions-tool.md} +4 -0
  29. package/src/prompts/structured-output.md +2 -0
  30. package/src/{agent/subagents.md → prompts/subagents-tool.md} +3 -0
  31. package/src/prompts/tool-bridge.md +1 -0
  32. package/src/prompts/tool-interrupted.md +1 -0
  33. package/src/prompts/tool-not-found.md +1 -0
  34. package/src/prompts/tool-repeat-blocked.md +1 -0
  35. package/src/prompts/tool-schema-error.md +1 -0
  36. package/src/prompts/tool-truncated-arguments.md +1 -0
  37. package/src/prompts/worker-failed.md +1 -0
  38. package/src/prompts/worker-replied.md +2 -0
  39. package/src/providers/architecture.md +10 -8
  40. package/src/providers/google.ts +228 -69
  41. package/src/providers/openai-compat.ts +74 -56
  42. package/src/providers/openai.ts +64 -41
  43. package/src/providers/openrouter.ts +270 -48
  44. package/src/providers/registry.ts +6 -21
  45. package/src/providers/shared.ts +87 -33
  46. package/src/providers.ts +115 -124
  47. package/src/scheduler/architecture.md +8 -7
  48. package/src/scheduler/scheduler.ts +226 -60
  49. package/src/scheduler/store.ts +23 -10
  50. package/src/scheduler/time.ts +17 -3
  51. package/src/scheduler/tool.ts +38 -16
  52. package/src/session/architecture.md +7 -6
  53. package/src/session/journal.ts +39 -12
  54. package/src/session/store.ts +126 -61
  55. package/src/session/usage.ts +58 -9
  56. package/src/streaming/architecture.md +4 -3
  57. package/src/streaming/event-stream.ts +26 -12
  58. package/src/telemetry.ts +6 -2
  59. package/src/tools/architecture.md +9 -7
  60. package/src/tools/executor.ts +109 -62
  61. package/src/tools/schema.ts +10 -6
  62. package/src/tools/subagent-result.ts +6 -1
  63. package/src/tools/tool-content.ts +32 -0
  64. package/src/tools/tool.ts +9 -11
  65. package/src/types/agent.ts +37 -22
  66. package/src/types/architecture.md +11 -10
  67. package/src/types/core.ts +5 -9
  68. package/src/types/errors.ts +26 -4
  69. package/src/types/message.ts +7 -0
  70. package/src/types/model.ts +4 -5
  71. package/src/types/scheduler.ts +4 -4
  72. package/src/types/tool.ts +14 -2
  73. package/src/update-models.ts +5 -5
  74. package/src/utils/architecture.md +16 -15
  75. package/src/utils/cache.ts +1 -3
  76. package/src/utils/durable.ts +17 -5
  77. package/src/utils/env.ts +9 -7
  78. package/src/utils/errors.ts +9 -17
  79. package/src/utils/headers.ts +3 -10
  80. package/src/utils/media.ts +23 -8
  81. package/src/utils/raw-capture.ts +9 -2
  82. package/src/utils/retry.ts +15 -3
  83. package/src/utils/safe-io.ts +29 -7
  84. package/src/utils/serialization.ts +1 -4
  85. package/src/utils/session.ts +1 -2
@@ -2,9 +2,8 @@
2
2
  * The two model-facing tools of scheduled actions: `scheduled_actions` (one
3
3
  * flat tool with a `command` field, a thin wrapper over the developer
4
4
  * functions) and `get_datetime`. The tool manual lives in
5
- * `scheduled-actions.md`, not in code.
5
+ * `src/prompts/scheduled-actions-tool.md`, not in code.
6
6
  */
7
- import { readFileSync } from "node:fs";
8
7
  import { z } from "zod";
9
8
  import type { Agent } from "../agent/agent.ts";
10
9
  import { tool } from "../tools/tool.ts";
@@ -23,15 +22,10 @@ import {
23
22
  } from "./scheduler.ts";
24
23
  import { readRun } from "./store.ts";
25
24
  import { formatDatetime } from "./time.ts";
25
+ import { fillPrompt } from "../prompts/prompts.ts";
26
26
 
27
27
  const COMMANDS = ["create", "list", "get", "update", "delete", "run_now", "skip"] as const;
28
28
 
29
- let manual: string | undefined;
30
- /** Read on first use (not at import), so a program that never enables scheduling never touches the file. */
31
- function loadManual(): string {
32
- return (manual ??= readFileSync(new URL("./scheduled-actions.md", import.meta.url), "utf8"));
33
- }
34
-
35
29
  /** Returns the machine's local date and time. Exported for any agent; auto-registered with the scheduling tools. */
36
30
  export const getDatetimeTool: ToolDefinition = tool({
37
31
  name: "get_datetime",
@@ -50,7 +44,12 @@ const scheduledActionSchema = z.object({
50
44
  startCookingInAdvance: z.string().optional().describe('Duration "DD:HH:MM" before delivery when the run starts.'),
51
45
  repeatEvery: z.string().optional().describe('Duration "DD:HH:MM" between deliveries. Omit to run once.'),
52
46
  retries: z.number().optional().describe("Extra attempts if a run fails."),
53
- onMissed: z.enum(["skip", "run"]).optional().describe('If the host program was not running at delivery time: "skip" = move on to the next time, "run" = run when it is back. Omit to decide later.'),
47
+ onMissed: z
48
+ .enum(["skip", "run"])
49
+ .optional()
50
+ .describe(
51
+ 'If the host program was not running at delivery time: "skip" = move on to the next time, "run" = run when it is back. Omit to decide later.'
52
+ ),
54
53
  });
55
54
 
56
55
  const describedId = "Full action id including its 6-character suffix (get, update, delete, run_now, skip).";
@@ -78,15 +77,25 @@ export function createScheduledActionsTool(agent: Agent): ToolDefinition {
78
77
 
79
78
  return tool({
80
79
  name: "scheduled_actions",
81
- description: loadManual().replace("{{TOOLS}}", poolNames.length > 0 ? poolNames.join(", ") : "none (omit tools)"),
80
+ description: fillPrompt(agent.prompts.scheduledActionsTool, {
81
+ TOOLS: poolNames.length > 0 ? poolNames.join(", ") : "none (omit tools)",
82
+ }),
82
83
  input: z.object({
83
84
  command: z.enum(COMMANDS),
84
85
  id: z.string().optional().describe(describedId),
85
86
  role: z.string().optional().describe("Short persona for the worker."),
86
87
  instructions: z.string().optional().describe("The worker's system prompt (create, update)."),
87
88
  task: z.string().optional().describe("The concrete assignment for every run (create, update)."),
88
- tools: z.array(z.string()).optional().describe(`Worker tools to grant, from: ${poolNames.join(", ") || "none"}.`),
89
- timeoutMs: z.number().optional().describe("Stop a run that takes longer than this many ms (> 0). Omit it for no limit of your own; the host program may cap it."),
89
+ tools: z
90
+ .array(z.string())
91
+ .optional()
92
+ .describe(`Worker tools to grant, from: ${poolNames.join(", ") || "none"}.`),
93
+ timeoutMs: z
94
+ .number()
95
+ .optional()
96
+ .describe(
97
+ "Stop a run that takes longer than this many ms (> 0). Omit it for no limit of your own; the host program may cap it."
98
+ ),
90
99
  scheduled_action: scheduledActionSchema.optional(),
91
100
  force: z.boolean().optional().describe("delete: abort a run that is cooking. Ask the user first."),
92
101
  purge: z.boolean().optional().describe("delete: also delete past run files."),
@@ -96,14 +105,23 @@ export function createScheduledActionsTool(agent: Agent): ToolDefinition {
96
105
  const needId = (): string => {
97
106
  const id = a.id ?? sa.id;
98
107
  if (!id) {
99
- throw new ToolError(`${a.command} needs "id" (the full id including its 6-character suffix; see list). Example: ${JSON.stringify({ command: a.command, id: "daily-ai-brief-d64aj2" })}`);
108
+ throw new ToolError(
109
+ `${a.command} needs "id" (the full id including its 6-character suffix; see list). Example: ${JSON.stringify({ command: a.command, id: "daily-ai-brief-d64aj2" })}`
110
+ );
100
111
  }
101
112
  return id;
102
113
  };
103
114
 
104
115
  switch (a.command) {
105
116
  case "create": {
106
- const input = { role: a.role, instructions: a.instructions, task: a.task, tools: a.tools, timeoutMs: a.timeoutMs, scheduled_action: sa } as ScheduledActionInput;
117
+ const input = {
118
+ role: a.role,
119
+ instructions: a.instructions,
120
+ task: a.task,
121
+ tools: a.tools,
122
+ timeoutMs: a.timeoutMs,
123
+ scheduled_action: sa,
124
+ } as ScheduledActionInput;
107
125
  const problems = validateInput(input, poolTools, getScheduler(home).now(), { requireLead: true });
108
126
  if (problems.length > 0) {
109
127
  throw new ToolError(
@@ -134,7 +152,9 @@ export function createScheduledActionsTool(agent: Agent): ToolDefinition {
134
152
  };
135
153
  const changes = [a.role, a.instructions, a.task, a.tools, a.timeoutMs, ...Object.values(schedule)];
136
154
  if (changes.every((v) => v === undefined)) {
137
- throw new ToolError(`update needs at least one field to change. Example: ${JSON.stringify({ command: "update", id, task: "The new assignment." })}`);
155
+ throw new ToolError(
156
+ `update needs at least one field to change. Example: ${JSON.stringify({ command: "update", id, task: "The new assignment." })}`
157
+ );
138
158
  }
139
159
  return updateScheduledAction(id, patch, { home });
140
160
  }
@@ -145,7 +165,9 @@ export function createScheduledActionsTool(agent: Agent): ToolDefinition {
145
165
  case "skip":
146
166
  return skipScheduledAction(needId(), { home });
147
167
  default:
148
- throw new ToolError(`Unknown command ${JSON.stringify((a as { command?: unknown }).command)}. Use one of: ${COMMANDS.join(", ")}.`);
168
+ throw new ToolError(
169
+ `Unknown command ${JSON.stringify((a as { command?: unknown }).command)}. Use one of: ${COMMANDS.join(", ")}.`
170
+ );
149
171
  }
150
172
  },
151
173
  });
@@ -2,14 +2,15 @@
2
2
 
3
3
  Saving a conversation, its spending and its tool logs.
4
4
 
5
- | File | What it is |
6
- | :-- | :-- |
7
- | `store.ts` | Snapshot format (`session.json`, one line per field and per turn), load validation, `SessionTelemetry`, media extraction, journal replay (`replayJournal`), and `priceUsage` (the one cost model, integer micro-USD). |
8
- | `journal.ts` | `SessionJournal`: after each step appends one fsynced line to `journal.ndjson`; writes a fresh snapshot (new `epoch`) when history changed any other way or the journal passed 500 lines / 4 MB. |
9
- | `usage.ts` | Pure spending accounting: `UsageTotals`, `usageLine`, `buildUsageReport`. No Agent import. |
10
- | `tool-log.ts` | `ToolLogs`: one fsynced NDJSON file per tool call under `<session>/tool-logs/`. |
5
+ | File | What it is |
6
+ | :------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7
+ | `store.ts` | Snapshot format (`session.json`, one line per field and per turn), load validation, `SessionTelemetry`, media extraction, journal replay (`replayJournal`), and `priceUsage` (the one cost model, integer micro-USD). |
8
+ | `journal.ts` | `SessionJournal`: after each step appends one fsynced line to `journal.ndjson`; writes a fresh snapshot (new `epoch`) when history changed any other way or the journal passed 500 lines / 4 MB. |
9
+ | `usage.ts` | Pure spending accounting: `UsageTotals`, `usageLine`, `buildUsageReport`. No Agent import. |
10
+ | `tool-log.ts` | `ToolLogs`: one fsynced NDJSON file per tool call under `<session>/tool-logs/`. |
11
11
 
12
12
  ## Invariants
13
+
13
14
  - Layout: `<home>/sessions/<id>/{session.json, journal.ndjson, telemetry.ndjson, media/, tool-logs/, workers/}`. `telemetry.ndjson` is written by `telemetry-file.ts` (opt-in), not by the session code; a folder holding only that file is not a session.
14
15
  - A journal whose header epoch differs from the snapshot's is ignored on load.
15
16
  - Journal record types: `base`, `msg`, `meta`, `run`, `end`, `tool`, `pending`, `usage`. Replay ignores unknown types, drops a torn last line, and stops at earlier damage with `session.journal_corrupt`.
@@ -32,7 +32,15 @@ const EMPTY_PENDING = JSON.stringify({ steer: [], queue: [] });
32
32
 
33
33
  // One `tool` journal line; the result is saved as given (tool results are JSON-safe by the time the executor returns them); in: a finished tool call, out: the journal.
34
34
  function toolLine(t: SessionToolRecord): string {
35
- return JSON.stringify({ t: "tool", call: t.id, name: t.name, result: t.result, ...(t.isError ? { isError: true } : {}), ...(t.stopped ? { stopped: true } : {}) });
35
+ return JSON.stringify({
36
+ t: "tool",
37
+ call: t.id,
38
+ name: t.name,
39
+ result: t.result,
40
+ ...(t.isError ? { isError: true } : {}),
41
+ ...(t.stopped ? { stopped: true } : {}),
42
+ ...(t.parts && t.parts.length > 0 ? { parts: t.parts } : {}),
43
+ });
36
44
  }
37
45
 
38
46
  /** Persists one session folder (`sessions/<id>/`) step by step. */
@@ -80,10 +88,13 @@ export class SessionJournal {
80
88
  return;
81
89
  }
82
90
  const records: string[] = [];
83
- for (let i = this.count; i < messages.length; i++) records.push(JSON.stringify({ t: "msg", i, m: this.toDisk(messages[i]!) }));
84
- for (let r = this.runCount; r < runs.length; r++) records.push(JSON.stringify({ t: "run", id: runs[r]!.id, i: runs[r]!.i, at: runs[r]!.at }));
91
+ for (let i = this.count; i < messages.length; i++)
92
+ records.push(JSON.stringify({ t: "msg", i, m: this.toDisk(messages[i]!) }));
93
+ for (let r = this.runCount; r < runs.length; r++)
94
+ records.push(JSON.stringify({ t: "run", id: runs[r]!.id, i: runs[r]!.i, at: runs[r]!.at }));
85
95
  for (const mark of runs) {
86
- if (mark.end && !this.endsOnDisk.has(mark.id)) records.push(JSON.stringify({ t: "end", id: mark.id, status: mark.end }));
96
+ if (mark.end && !this.endsOnDisk.has(mark.id))
97
+ records.push(JSON.stringify({ t: "end", id: mark.id, status: mark.end }));
87
98
  }
88
99
  const tools = agent.toolRecords ?? [];
89
100
  const firstNewTool = this.toolCount > 0 && tools[this.toolCount - 1] === this.lastTool ? this.toolCount : 0; // a new run starts the list over
@@ -129,13 +140,19 @@ export class SessionJournal {
129
140
  const epoch = randomUUID();
130
141
  const target = path.join(this.dir, "session.json");
131
142
  if (this.epoch === undefined) quarantineIfCorrupt(target);
132
- const data = buildSessionData({ ...agent, context: { ...agent.context, messages: messages.map((m) => this.toDisk(m)) } }, null);
143
+ const data = buildSessionData(
144
+ { ...agent, context: { ...agent.context, messages: messages.map((m) => this.toDisk(m)) } },
145
+ null
146
+ );
133
147
  writeFileDurable(target, serializeSession({ ...data, epoch }) + "\n");
134
148
  const pending = this.pendingOf(agent);
135
149
  const tools = agent.toolRecords ?? [];
136
150
  // Waiting prompts and the current run's finished tool results live only in the journal, so a new journal starts with them.
137
151
  const header =
138
- JSON.stringify({ t: "base", e: epoch }) + "\n" + (pending === EMPTY_PENDING ? "" : `{"t":"pending","p":${pending}}\n`) + tools.map((t) => toolLine(t) + "\n").join("");
152
+ JSON.stringify({ t: "base", e: epoch }) +
153
+ "\n" +
154
+ (pending === EMPTY_PENDING ? "" : `{"t":"pending","p":${pending}}\n`) +
155
+ tools.map((t) => toolLine(t) + "\n").join("");
139
156
  writeFileDurable(path.join(this.dir, JOURNAL_FILE), header, { backup: false });
140
157
  this.epoch = epoch;
141
158
  this.pending = pending;
@@ -165,7 +182,8 @@ export class SessionJournal {
165
182
  if (!waiting || waiting.steer.length + waiting.queue.length === 0) return EMPTY_PENDING;
166
183
  const parts = (prompt: ContentPart[]): ContentPart[] => {
167
184
  let disk = this.promptOnDisk.get(prompt);
168
- if (!disk) this.promptOnDisk.set(prompt, (disk = this.toDisk({ role: "user", content: prompt }).content as ContentPart[]));
185
+ if (!disk)
186
+ this.promptOnDisk.set(prompt, (disk = this.toDisk({ role: "user", content: prompt }).content as ContentPart[]));
169
187
  return disk;
170
188
  };
171
189
  const slim = (list: SessionPendingPrompt[]): SessionPendingPrompt[] =>
@@ -180,10 +198,15 @@ export class SessionJournal {
180
198
 
181
199
  // JSON of everything in the snapshot that is not a message, identity or totals; in: the live agent, out: change detection + the `meta` record.
182
200
  private metaOf(agent: SessionAgentLike): string {
183
- const { version: _v, sessionId: _s, messages: _m, totals: _t, runs: _r, usage: _u, ...rest } = buildSessionData(
184
- { ...agent, context: { ...agent.context, messages: [] } },
185
- null
186
- );
201
+ const {
202
+ version: _v,
203
+ sessionId: _s,
204
+ messages: _m,
205
+ totals: _t,
206
+ runs: _r,
207
+ usage: _u,
208
+ ...rest
209
+ } = buildSessionData({ ...agent, context: { ...agent.context, messages: [] } }, null);
187
210
  return JSON.stringify(rest);
188
211
  }
189
212
 
@@ -202,7 +225,11 @@ export class SessionJournal {
202
225
  const dest = path.join(this.dir, "media", file);
203
226
  if (!fs.existsSync(dest)) writeFileDurable(dest, found.bytes, { backup: false });
204
227
  changed = true;
205
- return { ...(part as unknown as Record<string, unknown>), [found.key]: `media/${file}`, ...(found.mimeType ? { mimeType: found.mimeType } : {}) } as unknown as ContentPart;
228
+ return {
229
+ ...(part as unknown as Record<string, unknown>),
230
+ [found.key]: `media/${file}`,
231
+ ...(found.mimeType ? { mimeType: found.mimeType } : {}),
232
+ } as unknown as ContentPart;
206
233
  });
207
234
  if (changed) out = { ...slim, content };
208
235
  }
@@ -30,7 +30,7 @@ import type { AgentResponse } from "../types/response.ts";
30
30
  import { base64ToBytes, bytesToBase64 } from "../utils/base64.ts";
31
31
  import { getModelFromCatalog } from "../models/catalog.ts";
32
32
  import type { SubAgentStep } from "../types/response.ts";
33
- import { SessionLoadError } from "../types/errors.ts";
33
+ import { SecurityPolicyError, SessionLoadError, ValidationError } from "../types/errors.ts";
34
34
  import { createSessionId } from "../utils/session.ts";
35
35
  import { writeFileDurable, readWithBackup } from "../utils/durable.ts";
36
36
  import type { UsageTotals } from "./usage.ts";
@@ -83,6 +83,8 @@ export interface SessionToolRecord {
83
83
  isError?: boolean;
84
84
  /** The tool ended the run with `ctx.stop`. */
85
85
  stopped?: boolean;
86
+ /** Media the tool returned (inline data URLs). */
87
+ parts?: ContentPart[];
86
88
  }
87
89
 
88
90
  /** A prompt that was waiting when the process stopped (its options other than `additionalContext` are not saved). */
@@ -106,7 +108,7 @@ export interface PersistedAgentSession {
106
108
  thinkingLevel?: string;
107
109
  cache?: CacheOption;
108
110
  /** Legacy read-only: pre-compact files duplicated base instructions here;
109
- * no longer written (`systemPrompt` already embeds them). */
111
+ * no longer written (`systemPrompt` already embeds them). */
110
112
  instructions?: string;
111
113
  systemPrompt?: string;
112
114
  messages: Message[];
@@ -230,7 +232,9 @@ export class SessionTelemetry {
230
232
  }
231
233
 
232
234
  /** Restores totals from a loaded session (tolerates legacy `metrics` shape). */
233
- static fromSaved(saved?: PersistedAgentSession | { totals?: SessionTotals; metrics?: Record<string, number> } | null): SessionTelemetry {
235
+ static fromSaved(
236
+ saved?: PersistedAgentSession | { totals?: SessionTotals; metrics?: Record<string, number> } | null
237
+ ): SessionTelemetry {
234
238
  const raw: any = (saved as any)?.totals ?? (saved as any)?.metrics ?? null;
235
239
  if (!raw) return new SessionTelemetry();
236
240
  // Legacy metrics files used {input,output,cacheRead,cacheWrite,reasoning,cost}
@@ -279,15 +283,12 @@ export class SessionTelemetry {
279
283
  * Shows per-turn tokens + `turn-CH%`, session totals + `total-CH%`, cost, context %.
280
284
  * `thinkingLevel` defaults to the agent's level when omitted.
281
285
  */
282
- formatBar(
283
- res: AgentResponse,
284
- opts?: { model?: string; thinkingLevel?: string }
285
- ): string {
286
+ formatBar(res: AgentResponse, opts?: { model?: string; thinkingLevel?: string }): string {
286
287
  const inTok = res.usage.inputTokens ?? 0;
287
288
  const outTok = res.usage.outputTokens ?? 0;
288
289
  const crTok = res.usage.cachedTokens ?? res.usage.cacheReadTokens ?? 0;
289
290
  const cwTurnTok = res.usage.cacheWriteTokens ?? 0;
290
- const turnRate = (this.turnHitRate(res.usage)).toFixed(1);
291
+ const turnRate = this.turnHitRate(res.usage).toFixed(1);
291
292
  const totalRate = this.totalHitRate().toFixed(1);
292
293
  const cwTurnLabel = cwTurnTok > 0 ? ` CW${formatSessionTokens(cwTurnTok)}` : "";
293
294
  const totCW = this.totals.cacheWrite;
@@ -350,7 +351,9 @@ export interface SessionAgentLike {
350
351
  }
351
352
 
352
353
  // Slims worker traces for the snapshot (one key per tracking id, repeated final text dropped, reasoning kept); in: buildSessionData, out: the `subagents` field.
353
- function snapshotSubagents(traces: Record<string, PersistedSubAgentTrace> | undefined): Record<string, PersistedSubAgentTrace> | undefined {
354
+ function snapshotSubagents(
355
+ traces: Record<string, PersistedSubAgentTrace> | undefined
356
+ ): Record<string, PersistedSubAgentTrace> | undefined {
354
357
  if (!traces) return undefined;
355
358
  const keys = Object.keys(traces);
356
359
  if (keys.length === 0) return undefined;
@@ -376,12 +379,7 @@ function snapshotSubagents(traces: Record<string, PersistedSubAgentTrace> | unde
376
379
  // Reasoning is never dropped: `thinking`/`thoughtSignature` survive the
377
380
  // dedup so persisted worker steps keep their reasoning traces.
378
381
  const steps = (t.steps ?? []).map((s) => {
379
- if (
380
- s.type === "assistant" &&
381
- typeof s.text === "string" &&
382
- typeof t.text === "string" &&
383
- s.text === t.text
384
- ) {
382
+ if (s.type === "assistant" && typeof s.text === "string" && typeof t.text === "string" && s.text === t.text) {
385
383
  const marker: SubAgentStep = { turn: s.turn, type: s.type, timestamp: s.timestamp };
386
384
  if (typeof s.thinking === "string" && s.thinking) marker.thinking = s.thinking;
387
385
  const sig = (s as { thoughtSignature?: unknown }).thoughtSignature;
@@ -463,18 +461,20 @@ function inlineBinaryMedia(messages: Message[]): Message[] {
463
461
  }
464
462
 
465
463
  /** Builds a storable snapshot from a live agent + optional telemetry. */
466
- export function buildSessionData(agent: SessionAgentLike, telemetry?: SessionTelemetry | SessionTotals | null): PersistedAgentSession {
464
+ export function buildSessionData(
465
+ agent: SessionAgentLike,
466
+ telemetry?: SessionTelemetry | SessionTotals | null
467
+ ): PersistedAgentSession {
467
468
  const rawModel = agent.modelStringOrSpec as any;
468
469
  const model = typeof rawModel === "string" ? rawModel : (rawModel?.id ?? String(rawModel ?? ""));
469
470
  const rawSub = agent.subagentModel as any;
470
- const subagentModel =
471
- typeof rawSub === "string" ? rawSub : rawSub?.model ?? rawSub?.id ?? undefined;
472
- const totals = telemetry instanceof SessionTelemetry
473
- ? telemetry.toJSON()
474
- : telemetry ?? undefined;
471
+ const subagentModel = typeof rawSub === "string" ? rawSub : (rawSub?.model ?? rawSub?.id ?? undefined);
472
+ const totals = telemetry instanceof SessionTelemetry ? telemetry.toJSON() : (telemetry ?? undefined);
475
473
  const parentSessionId =
476
- typeof (agent as any)?.parentSessionId === "string" ? (agent as any).parentSessionId as string : undefined;
477
- const subagents = snapshotSubagents((agent as any)?.subagentTraces as Record<string, PersistedSubAgentTrace> | undefined);
474
+ typeof (agent as any)?.parentSessionId === "string" ? ((agent as any).parentSessionId as string) : undefined;
475
+ const subagents = snapshotSubagents(
476
+ (agent as any)?.subagentTraces as Record<string, PersistedSubAgentTrace> | undefined
477
+ );
478
478
  const messages = inlineBinaryMedia(stripRedundantRawArguments(agent.context.messages));
479
479
  // Field order is the on-disk layout contract: identity → config →
480
480
  // systemPrompt → totals → messages → lineage/traces. `instructions` is
@@ -494,7 +494,9 @@ export function buildSessionData(agent: SessionAgentLike, telemetry?: SessionTel
494
494
  ...(parentSessionId ? { parentSessionId } : {}),
495
495
  ...(subagents ? { subagents } : {}),
496
496
  ...(agent.runMarks && agent.runMarks.length > 0 ? { runs: agent.runMarks.map((r) => ({ ...r })) } : {}),
497
- ...(agent.usageTotals && agent.usageTotals.turns > 0 ? { usage: JSON.parse(JSON.stringify(agent.usageTotals)) as UsageTotals } : {}),
497
+ ...(agent.usageTotals && agent.usageTotals.turns > 0
498
+ ? { usage: JSON.parse(JSON.stringify(agent.usageTotals)) as UsageTotals }
499
+ : {}),
498
500
  };
499
501
  }
500
502
 
@@ -563,7 +565,8 @@ const OVER_LIMIT = Symbol("session-over-limit");
563
565
 
564
566
  // Reads a stored `cache` field: a current string value, or a pre-0.5.1 `{ retention }` object (short=5m, medium=1h, long=12h); in: a parsed session file, out: `PersistedAgentSession.cache`.
565
567
  function toCacheOption(raw: unknown): CacheOption | undefined {
566
- if (typeof raw === "string") return (CACHE_OPTIONS as readonly string[]).includes(raw) ? (raw as CacheOption) : undefined;
568
+ if (typeof raw === "string")
569
+ return (CACHE_OPTIONS as readonly string[]).includes(raw) ? (raw as CacheOption) : undefined;
567
570
  const legacy: Record<string, CacheOption> = { implicit: "implicit", short: "5m", medium: "1h", long: "12h" };
568
571
  const retention = (raw as { retention?: unknown } | null)?.retention;
569
572
  return typeof retention === "string" ? legacy[retention] : undefined;
@@ -753,14 +756,17 @@ export const writeFileAtomic = writeFileDurable;
753
756
  /** Session id grammar: no traversal, no slashes, no hidden files. */
754
757
  export function assertValidSessionId(sessionId: string): void {
755
758
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(sessionId)) {
756
- throw new Error(`[Agent Accelerator] Invalid sessionId "${sessionId}": must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/`);
759
+ throw new ValidationError(
760
+ `[Agent Accelerator] Invalid sessionId "${sessionId}": must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/`
761
+ );
757
762
  }
758
763
  }
759
764
 
760
765
  /** Renames a genuinely corrupt snapshot to `*.corrupt-<ts>` before it is overwritten (a healthy one over the load limits is our own prior save and is just rewritten). */
761
766
  export function quarantineIfCorrupt(file: string): void {
762
767
  try {
763
- if (inspectSessionPayload(fs.readFileSync(file, "utf8")).status === "corrupt") fs.renameSync(file, `${file}.corrupt-${Date.now()}`);
768
+ if (inspectSessionPayload(fs.readFileSync(file, "utf8")).status === "corrupt")
769
+ fs.renameSync(file, `${file}.corrupt-${Date.now()}`);
764
770
  } catch {
765
771
  // why: no file yet, or it cannot be read: nothing to quarantine.
766
772
  }
@@ -840,9 +846,12 @@ function extForMedia(mimeType?: string, filename?: string): string {
840
846
  }
841
847
 
842
848
  // Finds embedded media bytes in a part (binary, `data:` URL or raw base64); remote URLs and local paths are references and give undefined; in: any content part, out: extractMediaToDir and the session journal.
843
- export function embeddedMedia(part: ContentPart): { key: (typeof MEDIA_PART_KEYS)[number]; bytes: Uint8Array; mimeType?: string; ext: string } | undefined {
849
+ export function embeddedMedia(
850
+ part: ContentPart
851
+ ): { key: (typeof MEDIA_PART_KEYS)[number]; bytes: Uint8Array; mimeType?: string; ext: string } | undefined {
844
852
  const p = part as unknown as Record<string, unknown> | null;
845
- if (!p || typeof p["type"] !== "string" || !(MEDIA_PART_KEYS as readonly string[]).includes(p["type"])) return undefined;
853
+ if (!p || typeof p["type"] !== "string" || !(MEDIA_PART_KEYS as readonly string[]).includes(p["type"]))
854
+ return undefined;
846
855
  const key = p["type"] as (typeof MEDIA_PART_KEYS)[number];
847
856
  const raw = p[key];
848
857
  let bytes: Uint8Array;
@@ -858,7 +867,12 @@ export function embeddedMedia(part: ContentPart): { key: (typeof MEDIA_PART_KEYS
858
867
  } else if (looksLikeEmbeddedBase64(raw)) bytes = base64ToBytes(raw.replace(/\s/g, ""));
859
868
  else return undefined; // local path or short label: keep as reference
860
869
  } else return undefined;
861
- return { key, bytes, ...(mimeType ? { mimeType } : {}), ext: extForMedia(mimeType, typeof p["filename"] === "string" ? (p["filename"] as string) : undefined) };
870
+ return {
871
+ key,
872
+ bytes,
873
+ ...(mimeType ? { mimeType } : {}),
874
+ ext: extForMedia(mimeType, typeof p["filename"] === "string" ? (p["filename"] as string) : undefined),
875
+ };
862
876
  }
863
877
 
864
878
  // Copy of the messages that keeps media bytes (structuredClone, else JSON); in: extractMediaToDir/resolveMediaPaths, out: a new array.
@@ -879,15 +893,12 @@ function cloneMessages(messages: Message[]): Message[] {
879
893
  * `<sessionDir>/media/` and rewrites those parts to relative `media/…`
880
894
  * paths. Remote URLs and filesystem paths are left as references.
881
895
  */
882
- export function extractMediaToDir(
883
- messages: Message[],
884
- sessionDir: string
885
- ): { messages: Message[]; wrote: string[] } {
896
+ export function extractMediaToDir(messages: Message[], sessionDir: string): { messages: Message[]; wrote: string[] } {
886
897
  const cloned = cloneMessages(messages);
887
898
  const root = path.resolve(sessionDir);
888
899
  const mediaDir = path.resolve(root, "media");
889
900
  if (!mediaDir.startsWith(root + path.sep) && mediaDir !== root) {
890
- throw new Error("[Agent Accelerator] Media dir escapes session dir");
901
+ throw new SecurityPolicyError("[Agent Accelerator] Media dir escapes session dir");
891
902
  }
892
903
  try {
893
904
  fs.mkdirSync(mediaDir, { recursive: true, mode: 0o700 });
@@ -896,7 +907,9 @@ export function extractMediaToDir(
896
907
  try {
897
908
  for (const f of fs.readdirSync(mediaDir)) {
898
909
  if (/^(image|audio|video|file)-\d+\.[a-z0-9]{1,5}$/.test(f)) {
899
- try { fs.unlinkSync(path.join(mediaDir, f)); } catch {}
910
+ try {
911
+ fs.unlinkSync(path.join(mediaDir, f));
912
+ } catch {}
900
913
  }
901
914
  }
902
915
  } catch {}
@@ -916,7 +929,9 @@ export function extractMediaToDir(
916
929
  const dest = path.resolve(mediaDir, file);
917
930
  if (!dest.startsWith(mediaDir + path.sep)) continue;
918
931
  fs.writeFileSync(dest, bytes, { mode: 0o600 });
919
- try { fs.chmodSync(dest, 0o600); } catch {}
932
+ try {
933
+ fs.chmodSync(dest, 0o600);
934
+ } catch {}
920
935
  } catch {
921
936
  continue;
922
937
  }
@@ -963,7 +978,7 @@ export function sessionDirFor(rootDir: string, sessionId: string): string {
963
978
  const dir = path.resolve(root, sessionId);
964
979
  const rel = path.relative(root, dir);
965
980
  if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) {
966
- throw new Error(`[Agent Accelerator] Session dir escapes root: ${sessionId}`);
981
+ throw new SecurityPolicyError(`[Agent Accelerator] Session dir escapes root: ${sessionId}`);
967
982
  }
968
983
  return dir;
969
984
  }
@@ -984,10 +999,7 @@ export function saveSessionDir(
984
999
  fs.mkdirSync(sessionDir, { recursive: true });
985
1000
  } catch {}
986
1001
  const { messages } = extractMediaToDir(agent.context.messages, sessionDir);
987
- const data = buildSessionData(
988
- { ...agent, context: { ...agent.context, messages } },
989
- telemetry
990
- );
1002
+ const data = buildSessionData({ ...agent, context: { ...agent.context, messages } }, telemetry);
991
1003
  try {
992
1004
  const target = path.join(sessionDir, "session.json");
993
1005
  quarantineIfCorrupt(target);
@@ -1002,7 +1014,15 @@ export function saveSessionDir(
1002
1014
  export const JOURNAL_FILE = "journal.ndjson";
1003
1015
 
1004
1016
  // Snapshot fields a `meta` journal record replaces (everything except messages, identity and totals).
1005
- const JOURNAL_META_KEYS = ["model", "subagentModel", "thinkingLevel", "cache", "systemPrompt", "parentSessionId", "subagents"] as const;
1017
+ const JOURNAL_META_KEYS = [
1018
+ "model",
1019
+ "subagentModel",
1020
+ "thinkingLevel",
1021
+ "cache",
1022
+ "systemPrompt",
1023
+ "parentSessionId",
1024
+ "subagents",
1025
+ ] as const;
1006
1026
 
1007
1027
  // Applies a journal to its snapshot: records of the snapshot's own epoch only, `msg` by index (already-present ones skipped), `run` adds a run boundary, `end` closes one, `usage` replaces the agent's spending totals, `tool` keeps a finished tool result, `pending` replaces the waiting prompts, `meta` replaces config fields, unknown record types ignored; a torn last line is dropped, damage earlier stops the replay with a bus warning; in: the parsed snapshot + journal text, out: loadSessionDir.
1008
1028
  function replayJournal(snapshot: PersistedAgentSession, text: string, file: string): PersistedAgentSession {
@@ -1011,12 +1031,32 @@ function replayJournal(snapshot: PersistedAgentSession, text: string, file: stri
1011
1031
  const session: PersistedAgentSession = { ...snapshot, messages: [...snapshot.messages] };
1012
1032
  let sawHeader = false;
1013
1033
  const stop = (reason: string, line: number): void => {
1014
- telemetryBus.emitWarning("session.journal_corrupt", `[Agent Accelerator] ${path.basename(file)} line ${line + 1}: ${reason}; the rest of the journal is ignored.`, { file, line: line + 1 });
1034
+ telemetryBus.emitWarning(
1035
+ "session.journal_corrupt",
1036
+ `[Agent Accelerator] ${path.basename(file)} line ${line + 1}: ${reason}; the rest of the journal is ignored.`,
1037
+ { file, line: line + 1 }
1038
+ );
1015
1039
  };
1016
1040
  for (let k = 0; k < lines.length; k++) {
1017
1041
  const line = lines[k]!.trim();
1018
1042
  if (!line) continue;
1019
- let rec: { t?: unknown; e?: unknown; i?: unknown; m?: unknown; p?: unknown; id?: unknown; at?: unknown; u?: unknown; status?: unknown; call?: unknown; name?: unknown; result?: unknown; isError?: unknown; stopped?: unknown };
1043
+ let rec: {
1044
+ t?: unknown;
1045
+ e?: unknown;
1046
+ i?: unknown;
1047
+ m?: unknown;
1048
+ p?: unknown;
1049
+ id?: unknown;
1050
+ at?: unknown;
1051
+ u?: unknown;
1052
+ status?: unknown;
1053
+ call?: unknown;
1054
+ name?: unknown;
1055
+ result?: unknown;
1056
+ isError?: unknown;
1057
+ stopped?: unknown;
1058
+ parts?: unknown;
1059
+ };
1020
1060
  try {
1021
1061
  rec = JSON.parse(line);
1022
1062
  } catch {
@@ -1035,13 +1075,29 @@ function replayJournal(snapshot: PersistedAgentSession, text: string, file: stri
1035
1075
  break;
1036
1076
  }
1037
1077
  session.messages.push(rec.m as Message);
1038
- } else if (rec.t === "run" && typeof rec.id === "string" && typeof rec.i === "number" && typeof rec.at === "number") {
1039
- if (!session.runs?.some((r) => r.id === rec.id)) session.runs = [...(session.runs ?? []), { id: rec.id, i: rec.i, at: rec.at }];
1078
+ } else if (
1079
+ rec.t === "run" &&
1080
+ typeof rec.id === "string" &&
1081
+ typeof rec.i === "number" &&
1082
+ typeof rec.at === "number"
1083
+ ) {
1084
+ if (!session.runs?.some((r) => r.id === rec.id))
1085
+ session.runs = [...(session.runs ?? []), { id: rec.id, i: rec.i, at: rec.at }];
1040
1086
  } else if (rec.t === "end" && typeof rec.id === "string" && typeof rec.status === "string") {
1041
1087
  const mark = session.runs?.find((r) => r.id === rec.id);
1042
1088
  if (mark) session.runs = session.runs!.map((r) => (r === mark ? { ...r, end: rec.status as SessionRunEnd } : r));
1043
1089
  } else if (rec.t === "tool" && typeof rec.call === "string" && typeof rec.name === "string") {
1044
- session.tools = [...(session.tools ?? []).filter((x) => x.id !== rec.call), { id: rec.call, name: rec.name, result: rec.result, ...(rec.isError ? { isError: true } : {}), ...(rec.stopped ? { stopped: true } : {}) }];
1090
+ session.tools = [
1091
+ ...(session.tools ?? []).filter((x) => x.id !== rec.call),
1092
+ {
1093
+ id: rec.call,
1094
+ name: rec.name,
1095
+ result: rec.result,
1096
+ ...(rec.isError ? { isError: true } : {}),
1097
+ ...(rec.stopped ? { stopped: true } : {}),
1098
+ ...(Array.isArray(rec.parts) ? { parts: rec.parts as ContentPart[] } : {}),
1099
+ },
1100
+ ];
1045
1101
  } else if (rec.t === "usage" && rec.u && typeof rec.u === "object") {
1046
1102
  session.usage = rec.u as UsageTotals; // latest record wins
1047
1103
  } else if (rec.t === "pending" && rec.p && typeof rec.p === "object") {
@@ -1148,11 +1204,9 @@ export const MAX_SESSION_MESSAGES = 50_000;
1148
1204
  /** Default max raw JSON size accepted when loading a session snapshot (override via `SessionLimits`). */
1149
1205
  export const MAX_SESSION_JSON_BYTES = 100 * 1024 * 1024;
1150
1206
 
1151
- // A SessionLoadError-named Error with the SDK prefix; in: validateSessionData, out: the error.
1152
- function sessionLoadError(message: string): Error {
1153
- const e = new Error(`[Agent Accelerator] Invalid session: ${message}`);
1154
- e.name = "SessionLoadError";
1155
- return e;
1207
+ // A SessionLoadError with the SDK prefix; in: validateSessionData, out: the error.
1208
+ function sessionLoadError(message: string): SessionLoadError {
1209
+ return new SessionLoadError(`[Agent Accelerator] Invalid session: ${message}`);
1156
1210
  }
1157
1211
 
1158
1212
  /**
@@ -1245,12 +1299,12 @@ export function reportUsageAnomaly(detail: { type: string; [k: string]: unknown
1245
1299
  * (reasoning is generated output); providers that omit one side are
1246
1300
  * accepted, only impossible `thinking > output` with both present fails.
1247
1301
  *
1248
- * Test helper: throws an Error describing the first violation, returns void
1302
+ * Test helper: throws a ValidationError describing the first violation, returns void
1249
1303
  * when invariants hold. Not called in hot paths (validation only).
1250
1304
  */
1251
1305
  export function assertUsageInvariants(usage: TokenUsage): void {
1252
1306
  const fail = (msg: string): never => {
1253
- throw new Error(`[Agent Accelerator] Invalid usage: ${msg}`);
1307
+ throw new ValidationError(`[Agent Accelerator] Invalid usage: ${msg}`);
1254
1308
  };
1255
1309
  const ints: Array<[string, unknown]> = [
1256
1310
  ["inputTokens", usage.inputTokens],
@@ -1356,7 +1410,14 @@ function toDollars(microUSD: number): number {
1356
1410
  function resolveEffectivePricing(
1357
1411
  spec: ModelSpec | undefined,
1358
1412
  inputTokens: number
1359
- ): { input: number; output: number; cacheRead: number | undefined; cacheWrite: number | undefined; audio: number; tier: string } {
1413
+ ): {
1414
+ input: number;
1415
+ output: number;
1416
+ cacheRead: number | undefined;
1417
+ cacheWrite: number | undefined;
1418
+ audio: number;
1419
+ tier: string;
1420
+ } {
1360
1421
  const base = {
1361
1422
  input: spec?.pricing?.inputPerMillion ?? (spec?.cost?.input as number | undefined) ?? 0,
1362
1423
  output: spec?.pricing?.outputPerMillion ?? (spec?.cost?.output as number | undefined) ?? 0,
@@ -1380,7 +1441,12 @@ function resolveEffectivePricing(
1380
1441
  const tiers = spec.cost?.tiers;
1381
1442
  if (Array.isArray(tiers) && tiers.length > 0) {
1382
1443
  let best: { input: number; output: number; cache_read?: number; size: number } | undefined;
1383
- for (const t of tiers as Array<{ tier?: { type?: string; size?: number }; input?: number; output?: number; cache_read?: number }>) {
1444
+ for (const t of tiers as Array<{
1445
+ tier?: { type?: string; size?: number };
1446
+ input?: number;
1447
+ output?: number;
1448
+ cache_read?: number;
1449
+ }>) {
1384
1450
  const size = typeof t?.tier?.size === "number" ? t.tier.size : undefined;
1385
1451
  if (size === undefined || size > inputTokens) continue;
1386
1452
  const type = String(t?.tier?.type ?? "").toLowerCase();
@@ -1455,10 +1521,9 @@ export function priceUsage(
1455
1521
  const cachedTokens = usage.cachedTokens ?? usage.cacheReadTokens ?? 0;
1456
1522
  const cacheWriteTokens = usage.cacheWriteTokens ?? 0;
1457
1523
  const audioTokens =
1458
- (usage as { audioTokens?: unknown }).audioTokens ??
1459
- (usage as { inputAudioTokens?: unknown }).inputAudioTokens ??
1460
- 0;
1461
- const audioCount = typeof audioTokens === "number" && Number.isFinite(audioTokens) && audioTokens > 0 ? Math.round(audioTokens) : 0;
1524
+ (usage as { audioTokens?: unknown }).audioTokens ?? (usage as { inputAudioTokens?: unknown }).inputAudioTokens ?? 0;
1525
+ const audioCount =
1526
+ typeof audioTokens === "number" && Number.isFinite(audioTokens) && audioTokens > 0 ? Math.round(audioTokens) : 0;
1462
1527
  const pricing = resolveEffectivePricing(spec, inputTokens);
1463
1528
  if (!pricing.input && !pricing.output && !pricing.cacheRead && !pricing.cacheWrite && !pricing.audio) {
1464
1529
  return undefined;