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.
- package/README.md +64 -16
- package/examples/prompts/SYSTEM_PROMPT.md +2 -0
- package/examples/prompts/SYSTEM_PROMPT_AGENT.md +4 -0
- package/examples/prompts/SYSTEM_PROMPT_TOOLS.md +1 -0
- package/package.json +4 -1
- package/src/agent/agent.ts +447 -172
- package/src/agent/architecture.md +16 -12
- package/src/agent/config-blocks.ts +39 -12
- package/src/agent/context.ts +9 -3
- package/src/agent/defaults.ts +88 -0
- package/src/agent/delegation.ts +86 -28
- package/src/agent/loop.ts +474 -374
- package/src/agent/subagent.ts +1 -3
- package/src/agent/subagents-tool.ts +50 -19
- package/src/agent/turns.ts +3 -1
- package/src/agent/usage-limit.ts +92 -0
- package/src/agent/worker.ts +73 -30
- package/src/agent/workers.ts +126 -21
- package/src/architecture.md +17 -15
- package/src/index.ts +43 -19
- package/src/models/architecture.md +4 -3
- package/src/models/catalog-cache.ts +6 -5
- package/src/models/catalog.ts +39 -30
- package/src/prompts/additional-context.md +2 -0
- package/src/prompts/architecture.md +19 -0
- package/src/prompts/prompts.ts +81 -0
- package/src/prompts/reasoning-directive.md +5 -0
- package/src/{scheduler/scheduled-actions.md → prompts/scheduled-actions-tool.md} +4 -0
- package/src/prompts/structured-output.md +2 -0
- package/src/{agent/subagents.md → prompts/subagents-tool.md} +3 -0
- package/src/prompts/tool-bridge.md +1 -0
- package/src/prompts/tool-interrupted.md +1 -0
- package/src/prompts/tool-not-found.md +1 -0
- package/src/prompts/tool-repeat-blocked.md +1 -0
- package/src/prompts/tool-schema-error.md +1 -0
- package/src/prompts/tool-truncated-arguments.md +1 -0
- package/src/prompts/worker-failed.md +1 -0
- package/src/prompts/worker-replied.md +2 -0
- package/src/providers/architecture.md +10 -8
- package/src/providers/google.ts +228 -69
- package/src/providers/openai-compat.ts +74 -56
- package/src/providers/openai.ts +64 -41
- package/src/providers/openrouter.ts +270 -48
- package/src/providers/registry.ts +6 -21
- package/src/providers/shared.ts +87 -33
- package/src/providers.ts +115 -124
- package/src/scheduler/architecture.md +8 -7
- package/src/scheduler/scheduler.ts +226 -60
- package/src/scheduler/store.ts +23 -10
- package/src/scheduler/time.ts +17 -3
- package/src/scheduler/tool.ts +38 -16
- package/src/session/architecture.md +7 -6
- package/src/session/journal.ts +39 -12
- package/src/session/store.ts +126 -61
- package/src/session/usage.ts +58 -9
- package/src/streaming/architecture.md +4 -3
- package/src/streaming/event-stream.ts +26 -12
- package/src/telemetry.ts +6 -2
- package/src/tools/architecture.md +9 -7
- package/src/tools/executor.ts +109 -62
- package/src/tools/schema.ts +10 -6
- package/src/tools/subagent-result.ts +6 -1
- package/src/tools/tool-content.ts +32 -0
- package/src/tools/tool.ts +9 -11
- package/src/types/agent.ts +37 -22
- package/src/types/architecture.md +11 -10
- package/src/types/core.ts +5 -9
- package/src/types/errors.ts +26 -4
- package/src/types/message.ts +7 -0
- package/src/types/model.ts +4 -5
- package/src/types/scheduler.ts +4 -4
- package/src/types/tool.ts +14 -2
- package/src/update-models.ts +5 -5
- package/src/utils/architecture.md +16 -15
- package/src/utils/cache.ts +1 -3
- package/src/utils/durable.ts +17 -5
- package/src/utils/env.ts +9 -7
- package/src/utils/errors.ts +9 -17
- package/src/utils/headers.ts +3 -10
- package/src/utils/media.ts +23 -8
- package/src/utils/raw-capture.ts +9 -2
- package/src/utils/retry.ts +15 -3
- package/src/utils/safe-io.ts +29 -7
- package/src/utils/serialization.ts +1 -4
- package/src/utils/session.ts +1 -2
package/src/scheduler/tool.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
|
89
|
-
|
|
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(
|
|
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 = {
|
|
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(
|
|
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(
|
|
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
|
|
6
|
-
|
|
|
7
|
-
| `store.ts`
|
|
8
|
-
| `journal.ts`
|
|
9
|
-
| `usage.ts`
|
|
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`.
|
package/src/session/journal.ts
CHANGED
|
@@ -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({
|
|
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++)
|
|
84
|
-
|
|
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))
|
|
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(
|
|
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 }) +
|
|
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)
|
|
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 {
|
|
184
|
-
|
|
185
|
-
|
|
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 {
|
|
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
|
}
|
package/src/session/store.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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")
|
|
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
|
|
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")
|
|
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(
|
|
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"]))
|
|
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 {
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
|
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 = [
|
|
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(
|
|
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: {
|
|
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 (
|
|
1039
|
-
|
|
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 = [
|
|
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
|
|
1152
|
-
function sessionLoadError(message: string):
|
|
1153
|
-
|
|
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
|
|
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
|
|
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
|
-
): {
|
|
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<{
|
|
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
|
-
|
|
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;
|