atom-agent 1.2.0 → 1.4.0
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/CHANGELOG.md +97 -0
- package/README.md +13 -4
- package/atom.example.json +11 -0
- package/dist/App.js +923 -200
- package/dist/adapters.js +82 -13
- package/dist/agent/goal-evaluator.js +69 -0
- package/dist/agent/loop.js +517 -76
- package/dist/cli.js +11 -3
- package/dist/compact.js +41 -15
- package/dist/config.js +43 -7
- package/dist/context-manager.js +16 -198
- package/dist/context-windows.js +4 -2
- package/dist/env-block.js +5 -5
- package/dist/extension-commands.js +196 -0
- package/dist/extension-ui.js +153 -0
- package/dist/extensions.js +1571 -0
- package/dist/goal.js +583 -0
- package/dist/project-trust.js +96 -0
- package/dist/providers.js +6 -6
- package/dist/scheduler.js +74 -36
- package/dist/session.js +23 -5
- package/dist/sessions.js +25 -6
- package/dist/telemetry-dashboard.js +28 -0
- package/dist/telemetry.js +39 -0
- package/dist/tools/compaction-hooks.js +165 -0
- package/dist/tools/custom.js +189 -0
- package/dist/tools/intercept.js +145 -0
- package/dist/tools/overrides.js +105 -0
- package/dist/tools/provider-hooks.js +224 -0
- package/dist/tools/registry.js +246 -17
- package/dist/tools.js +44 -0
- package/dist/ui/diff-panel.js +5 -5
- package/dist/ui/diff-view.js +3 -2
- package/dist/ui/diff.js +7 -52
- package/dist/ui/modals.js +5 -5
- package/dist/ui/palette.js +1 -1
- package/dist/ui/side-by-side.js +7 -5
- package/dist/ui/status-bar.js +80 -5
- package/dist/ui/transcript.js +3 -3
- package/dist/zen.js +305 -75
- package/documentation/architecture.md +114 -0
- package/documentation/cli.md +82 -0
- package/documentation/compaction.md +50 -0
- package/documentation/configuration.md +111 -0
- package/documentation/development.md +62 -0
- package/documentation/extensions.md +160 -0
- package/documentation/getting-started.md +63 -0
- package/documentation/goals.md +41 -0
- package/documentation/index.md +41 -0
- package/documentation/observability.md +70 -0
- package/documentation/permissions.md +66 -0
- package/documentation/providers.md +78 -0
- package/documentation/sessions.md +92 -0
- package/documentation/skills.md +57 -0
- package/documentation/tools.md +94 -0
- package/documentation/troubleshooting.md +54 -0
- package/examples/extensions/01-audit-gate.js +24 -0
- package/examples/extensions/02-notes-tool.js +32 -0
- package/examples/extensions/03-custom-command.js +32 -0
- package/package.json +6 -2
package/dist/providers.js
CHANGED
|
@@ -68,7 +68,7 @@ export const PROVIDERS = [
|
|
|
68
68
|
"minimax-m2.7",
|
|
69
69
|
"big-pickle",
|
|
70
70
|
],
|
|
71
|
-
notes: "OpenAI-compatible chat/completions. reasoning_effort
|
|
71
|
+
notes: "OpenAI-compatible chat/completions. /effort sends reasoning_effort (Auto omits it).",
|
|
72
72
|
cache: {
|
|
73
73
|
explicitBreakpoints: false,
|
|
74
74
|
implicitPrefix: true,
|
|
@@ -90,7 +90,7 @@ export const PROVIDERS = [
|
|
|
90
90
|
"gpt-5.6-terra",
|
|
91
91
|
"gpt-5.6-luna",
|
|
92
92
|
],
|
|
93
|
-
notes: "OpenAI-compatible chat/completions. reasoning_effort
|
|
93
|
+
notes: "OpenAI-compatible chat/completions. /effort sends reasoning_effort (Auto omits it).",
|
|
94
94
|
cache: {
|
|
95
95
|
explicitBreakpoints: false,
|
|
96
96
|
implicitPrefix: true,
|
|
@@ -112,7 +112,7 @@ export const PROVIDERS = [
|
|
|
112
112
|
"claude-3-5-sonnet-20241022",
|
|
113
113
|
"claude-3-5-haiku-20241022",
|
|
114
114
|
],
|
|
115
|
-
notes: "Messages API with tool_use blocks. max_tokens 4096.",
|
|
115
|
+
notes: "Messages API with tool_use blocks. max_tokens 4096. /effort maps to the thinking budget (Auto omits it).",
|
|
116
116
|
cache: {
|
|
117
117
|
explicitBreakpoints: true,
|
|
118
118
|
implicitPrefix: true,
|
|
@@ -134,7 +134,7 @@ export const PROVIDERS = [
|
|
|
134
134
|
"deepseek-v4-flash",
|
|
135
135
|
"deepseek-v4-pro",
|
|
136
136
|
],
|
|
137
|
-
notes: "OpenAI-compatible (no /v1 prefix). reasoning_effort
|
|
137
|
+
notes: "OpenAI-compatible (no /v1 prefix). /effort sends reasoning_effort (Auto omits it).",
|
|
138
138
|
cache: {
|
|
139
139
|
explicitBreakpoints: false,
|
|
140
140
|
implicitPrefix: true,
|
|
@@ -156,7 +156,7 @@ export const PROVIDERS = [
|
|
|
156
156
|
"mistral-small-latest",
|
|
157
157
|
"open-mistral-nemo",
|
|
158
158
|
],
|
|
159
|
-
notes: "OpenAI-compatible chat/completions. reasoning_effort
|
|
159
|
+
notes: "OpenAI-compatible chat/completions. /effort sends reasoning_effort (Auto omits it).",
|
|
160
160
|
cache: {
|
|
161
161
|
explicitBreakpoints: false,
|
|
162
162
|
implicitPrefix: false,
|
|
@@ -179,7 +179,7 @@ export const PROVIDERS = [
|
|
|
179
179
|
"gemini-2.0-flash",
|
|
180
180
|
"gemini-1.5-flash",
|
|
181
181
|
],
|
|
182
|
-
notes: "streamGenerateContent SSE; :generateContent fallback.",
|
|
182
|
+
notes: "streamGenerateContent SSE; :generateContent fallback. /effort maps to thinkingLevel (Auto omits it; Max rides high).",
|
|
183
183
|
cache: {
|
|
184
184
|
explicitBreakpoints: false,
|
|
185
185
|
implicitPrefix: true,
|
package/dist/scheduler.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
// Effect-aware tool scheduler: data-driven parallelism
|
|
2
|
-
// ing every tool.
|
|
1
|
+
// Effect-aware tool scheduler: data-driven parallelism, parallel by default.
|
|
3
2
|
//
|
|
4
3
|
// The contract: batchable calls run concurrently, conflicting calls stay
|
|
5
4
|
// serialized, results commit in original call order, cancel stops between
|
|
6
5
|
// batches, approval happens per call in order before execution. This module
|
|
7
6
|
// only PLANS batches; execution (agent/loop.ts runLoopWithChat) is untouched.
|
|
8
7
|
//
|
|
8
|
+
// Parallel-by-default posture: side-effect-free reads batch unconditionally
|
|
9
|
+
// (same target included — two reads can never race each other); only real
|
|
10
|
+
// conflicts serialize: same-file write order, read/write on the same file,
|
|
11
|
+
// invisible footprints (bash), shared ambient state (todos), interactive
|
|
12
|
+
// prompts, and calls the planner cannot see (unknown/malformed/invalid).
|
|
13
|
+
//
|
|
9
14
|
// How it reasons (per tool, from the TOOL_EFFECTS table — the single
|
|
10
15
|
// coupling point; the algorithm below has no per-tool branches):
|
|
11
16
|
// - missing metadata → serial singleton (new tools fail safe, like the old
|
|
@@ -22,17 +27,18 @@
|
|
|
22
27
|
// (see canonicalFileKey): same-file mutations never share a batch, so they
|
|
23
28
|
// stay strictly ordered in program order; different files run concurrently.
|
|
24
29
|
// An unresolvable target stays serial (never batch what you cannot see).
|
|
25
|
-
// - reads
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// write
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// - deterministic is
|
|
34
|
-
//
|
|
35
|
-
//
|
|
30
|
+
// - reads (filesystem or network) always batch: a pure read has no observable
|
|
31
|
+
// footprint, so even the same tool + same target runs concurrently. A path
|
|
32
|
+
// read against an open write to the same canonical file still stays ordered
|
|
33
|
+
// (read-after-write): it splits the batch — and a write conflicts with any
|
|
34
|
+
// open read on its key (write-after-read stays ordered). Directory-scoped
|
|
35
|
+
// scans (grep/glob) vs concurrent writes to unscanned-listed files are out
|
|
36
|
+
// of scope — same exposure as an editor saving mid-scan; only
|
|
37
|
+
// same-canonical-path pairs are ordered.
|
|
38
|
+
// - deterministic is informational only (same output for same args); it no
|
|
39
|
+
// longer drives batching — not even same-task bash_output polls serialize,
|
|
40
|
+
// since concurrent polls are side-effect-free reads whose results commit in
|
|
41
|
+
// call order anyway.
|
|
36
42
|
//
|
|
37
43
|
// Pure module except the shared arg validators (same imports zen.ts already
|
|
38
44
|
// carries — no new coupling class) plus best-effort path canonicalization
|
|
@@ -43,7 +49,7 @@
|
|
|
43
49
|
// tests/parallel-writes.test.ts.
|
|
44
50
|
import * as fs from "node:fs";
|
|
45
51
|
import * as path from "node:path";
|
|
46
|
-
import { toolNames, validateToolArgs } from "./tools.js";
|
|
52
|
+
import { isCustomTool, toolExecutionMode, toolNames, validateToolArgs } from "./tools.js";
|
|
47
53
|
const str = (v) => (typeof v === "string" ? v : "");
|
|
48
54
|
const targetOf = (key) => (args) => {
|
|
49
55
|
const t = str(args[key]);
|
|
@@ -93,8 +99,8 @@ export const TOOL_EFFECTS = {
|
|
|
93
99
|
process: "none",
|
|
94
100
|
interactive: false,
|
|
95
101
|
exclusive: false,
|
|
96
|
-
// Live search results vary call to call; same-query
|
|
97
|
-
//
|
|
102
|
+
// Live search results vary call to call; concurrent same-query searches
|
|
103
|
+
// are merely redundant, never incorrect — reads always batch.
|
|
98
104
|
deterministic: false,
|
|
99
105
|
target: targetOf("query"),
|
|
100
106
|
},
|
|
@@ -105,8 +111,8 @@ export const TOOL_EFFECTS = {
|
|
|
105
111
|
process: "none",
|
|
106
112
|
interactive: false,
|
|
107
113
|
exclusive: false,
|
|
108
|
-
// A running task's output grows between polls — same-task
|
|
109
|
-
//
|
|
114
|
+
// A running task's output grows between polls — concurrent same-task
|
|
115
|
+
// polls are side-effect-free reads whose results commit in call order.
|
|
110
116
|
deterministic: false,
|
|
111
117
|
target: targetOf("taskId"),
|
|
112
118
|
},
|
|
@@ -213,27 +219,55 @@ export function canonicalFileKey(rawPath, cwd = process.cwd()) {
|
|
|
213
219
|
return abs;
|
|
214
220
|
}
|
|
215
221
|
// Partition one assistant message's tool_calls into commit batches,
|
|
216
|
-
// preserving program order: consecutive batchable calls
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
// also splits, so per-file program order always holds. A later batch never
|
|
222
|
+
// preserving program order: consecutive batchable calls form one batch; any
|
|
223
|
+
// serial-only call closes the batch and runs as a strict serial singleton.
|
|
224
|
+
// Filesystem writes join a batch only on a disjoint canonical file key
|
|
225
|
+
// (same-file mutations split into sequential batches, never concurrent); a
|
|
226
|
+
// path read conflicting with an open write — or a write conflicting with any
|
|
227
|
+
// open member — on the same canonical key also splits, so per-file program
|
|
228
|
+
// order always holds. Reads never split on each other. A later batch never
|
|
224
229
|
// moves ahead of an earlier serial call, and batches never span the block
|
|
225
230
|
// boundary.
|
|
226
231
|
export function planBatches(calls) {
|
|
227
232
|
const batches = [];
|
|
228
233
|
let open = [];
|
|
229
|
-
const keys = new Set();
|
|
230
234
|
// Canonical file keys of the open batch ("read" and/or "write" per key).
|
|
235
|
+
// This is the ONLY cross-call ordering state: same-file read/write pairs
|
|
236
|
+
// stay in program order; everything else batches freely.
|
|
231
237
|
const openFiles = new Map();
|
|
238
|
+
// Lenient parse for classification/fallback only (mirrors the loop: {}
|
|
239
|
+
// when the JSON is malformed — the error result commits downstream).
|
|
240
|
+
const lenientParse = (call) => {
|
|
241
|
+
try {
|
|
242
|
+
const raw = call?.function?.arguments ?? "{}";
|
|
243
|
+
const v = JSON.parse(typeof raw === "string" ? raw : "{}");
|
|
244
|
+
return typeof v === "object" && v !== null ? v : {};
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return {};
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
// Extension sequential hint (ticket 06): a tool declaring sequential
|
|
251
|
+
// execution forces its whole sibling batch one-at-a-time (Pi-style). This
|
|
252
|
+
// only ever ADDS serialization — same-file order, bash/todo/prompt
|
|
253
|
+
// isolation, ordered commits, and the approval pre-pass are untouched
|
|
254
|
+
// (singletons satisfy every one of them; the loop below handles them
|
|
255
|
+
// exactly as before, one call per batch).
|
|
256
|
+
if (calls.some((call) => {
|
|
257
|
+
const name = typeof call?.function?.name === "string" ? call.function.name : "";
|
|
258
|
+
try {
|
|
259
|
+
return toolExecutionMode(name) === "sequential";
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
})) {
|
|
265
|
+
return calls.map((call) => [{ call, parsed: lenientParse(call), parallelKey: null }]);
|
|
266
|
+
}
|
|
232
267
|
const flush = () => {
|
|
233
268
|
if (open.length > 0) {
|
|
234
269
|
batches.push(open);
|
|
235
270
|
open = [];
|
|
236
|
-
keys.clear();
|
|
237
271
|
openFiles.clear();
|
|
238
272
|
}
|
|
239
273
|
};
|
|
@@ -254,6 +288,15 @@ export function planBatches(calls) {
|
|
|
254
288
|
malformed = true;
|
|
255
289
|
}
|
|
256
290
|
const name = typeof call?.function?.name === "string" ? call.function.name : "(unknown)";
|
|
291
|
+
// Extension tools carry no scheduler effect metadata: their footprint is
|
|
292
|
+
// invisible, so they always run as serial singletons (fail safe, exactly
|
|
293
|
+
// like the old allowlist-miss). Batch planning is never corrupted by
|
|
294
|
+
// what it cannot see; validation still runs in the loop, where failures
|
|
295
|
+
// become inline-error results.
|
|
296
|
+
if (isCustomTool(name)) {
|
|
297
|
+
singleton(call, parsed);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
257
300
|
// Missing metadata fails safe to serial (never batch the unknown).
|
|
258
301
|
const meta = TOOL_EFFECTS[name];
|
|
259
302
|
if (malformed || !meta || !toolNames().includes(name)) {
|
|
@@ -287,8 +330,9 @@ export function planBatches(calls) {
|
|
|
287
330
|
open.push({ call, parsed, parallelKey: `${name} ${fileKey}` });
|
|
288
331
|
continue;
|
|
289
332
|
}
|
|
290
|
-
// Reads batch
|
|
291
|
-
// footprint
|
|
333
|
+
// Reads always batch (parallel by default): a pure read has no
|
|
334
|
+
// observable footprint, so even the same tool + same target runs
|
|
335
|
+
// concurrently. Empty target = unknown footprint = serial.
|
|
292
336
|
let target = null;
|
|
293
337
|
try {
|
|
294
338
|
target = meta.target(parsed);
|
|
@@ -301,10 +345,6 @@ export function planBatches(calls) {
|
|
|
301
345
|
continue;
|
|
302
346
|
}
|
|
303
347
|
const key = `${name} ${target}`;
|
|
304
|
-
if (keys.has(key)) {
|
|
305
|
-
singleton(call, parsed);
|
|
306
|
-
continue;
|
|
307
|
-
}
|
|
308
348
|
// A path read against an open write to the same canonical file stays
|
|
309
349
|
// ordered (read-after-write): split the batch.
|
|
310
350
|
if (name === "read") {
|
|
@@ -313,13 +353,11 @@ export function planBatches(calls) {
|
|
|
313
353
|
singleton(call, parsed);
|
|
314
354
|
continue;
|
|
315
355
|
}
|
|
316
|
-
keys.add(key);
|
|
317
356
|
if (fileKey && !openFiles.has(fileKey))
|
|
318
357
|
openFiles.set(fileKey, "read");
|
|
319
358
|
open.push({ call, parsed, parallelKey: key });
|
|
320
359
|
continue;
|
|
321
360
|
}
|
|
322
|
-
keys.add(key);
|
|
323
361
|
open.push({ call, parsed, parallelKey: key });
|
|
324
362
|
}
|
|
325
363
|
flush();
|
package/dist/session.js
CHANGED
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
// cannot corrupt or clobber the last good save.
|
|
7
7
|
//
|
|
8
8
|
// Shape: {version:1, savedAt, provider, model, effort, mode, usageTotals,
|
|
9
|
+
// goal (ticket 07: the live session goal plus cumulative stats, or null),
|
|
9
10
|
// history (full API history incl. system + tool pairs), turns (display
|
|
10
11
|
// transcript)}. Writes are atomic (temp file + rename) to survive kills
|
|
11
12
|
// mid-write. Loads never throw: missing -> "missing", anything malformed ->
|
|
12
|
-
// "corrupt" (caller shows a one-line notice and starts fresh).
|
|
13
|
+
// "corrupt" (caller shows a one-line notice and starts fresh). A missing or
|
|
14
|
+
// corrupt goal degrades to no-goal (null) WITHOUT failing the load — the
|
|
15
|
+
// conversation still restores.
|
|
13
16
|
//
|
|
14
17
|
// Privacy: the file can contain pasted secrets if the user typed them as
|
|
15
18
|
// chat. Never print its contents; never commit it (it lives under ~/.atom,
|
|
@@ -19,6 +22,7 @@ import * as path from "node:path";
|
|
|
19
22
|
import { atomDir, getStoredBaseURL, loadAuth, resolveApiKey } from "./auth.js";
|
|
20
23
|
import { chatEndpointFor, isProviderId, openaiCompatibleChatEndpoint, } from "./providers.js";
|
|
21
24
|
import { EFFORT_OPTIONS, } from "./zen.js";
|
|
25
|
+
import { restoreGoalFromPersist, serializeGoalForPersist, } from "./goal.js";
|
|
22
26
|
export const SESSION_VERSION = 1;
|
|
23
27
|
export const SESSION_FILENAME = "session.json";
|
|
24
28
|
export function sessionFilePath(home) {
|
|
@@ -74,6 +78,9 @@ export function saveSession(snapshot, home) {
|
|
|
74
78
|
effort: snapshot.effort,
|
|
75
79
|
mode: snapshot.mode,
|
|
76
80
|
usageTotals: snapshot.usageTotals,
|
|
81
|
+
// Piggyback: the live goal rides every completed-turn save (no new save
|
|
82
|
+
// cadence — compaction and clean exit flow through here too).
|
|
83
|
+
goal: serializeGoalForPersist(snapshot.goal ?? null),
|
|
77
84
|
history: snapshot.history.map((m) => ({ ...m })),
|
|
78
85
|
turns: snapshot.turns.map((t) => ({ ...t })),
|
|
79
86
|
};
|
|
@@ -210,9 +217,16 @@ function validateSession(data) {
|
|
|
210
217
|
const model = data["model"];
|
|
211
218
|
if (!isNonEmptyString(model))
|
|
212
219
|
return null;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
220
|
+
// "default" is the pre-auto name for the same level: old saves map to
|
|
221
|
+
// "auto" instead of failing the load.
|
|
222
|
+
const rawEffort = data["effort"];
|
|
223
|
+
const effort = rawEffort === "default"
|
|
224
|
+
? "auto"
|
|
225
|
+
: typeof rawEffort === "string" &&
|
|
226
|
+
EFFORT_OPTIONS.includes(rawEffort)
|
|
227
|
+
? rawEffort
|
|
228
|
+
: null;
|
|
229
|
+
if (effort === null) {
|
|
216
230
|
return null;
|
|
217
231
|
}
|
|
218
232
|
const mode = data["mode"];
|
|
@@ -241,9 +255,13 @@ function validateSession(data) {
|
|
|
241
255
|
savedAt,
|
|
242
256
|
provider,
|
|
243
257
|
model,
|
|
244
|
-
effort
|
|
258
|
+
effort,
|
|
245
259
|
mode,
|
|
246
260
|
usageTotals: validateUsageTotals(data["usageTotals"]),
|
|
261
|
+
// Tolerant: a trashed goal degrades to no-goal (null) without failing
|
|
262
|
+
// the load — the conversation still restores. Re-serialized so the
|
|
263
|
+
// loaded record always carries concrete stats.
|
|
264
|
+
goal: serializeGoalForPersist(restoreGoalFromPersist(data["goal"])),
|
|
247
265
|
history: history,
|
|
248
266
|
turns: turns,
|
|
249
267
|
};
|
package/dist/sessions.js
CHANGED
|
@@ -19,18 +19,30 @@
|
|
|
19
19
|
// import LLM clients.
|
|
20
20
|
// - No transient UI state (scroll, cursor, picker, queue) is stored.
|
|
21
21
|
//
|
|
22
|
-
// Import budget: value imports are node:fs, node:path, node:crypto
|
|
23
|
-
// ./auth.js
|
|
24
|
-
//
|
|
22
|
+
// Import budget: value imports are node:fs, node:path, node:crypto,
|
|
23
|
+
// ./auth.js, and ./goal.js (goal persistence-shape helpers only —
|
|
24
|
+
// serialize/restore/validate; goal.js itself is React-free and imports no
|
|
25
|
+
// session module, so the runtime DAG stays acyclic) plus type-only imports
|
|
26
|
+
// from ./zen.js / ./providers.js / ./goal.js (erased at compile).
|
|
25
27
|
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
26
28
|
import { randomUUID } from "node:crypto";
|
|
27
29
|
import * as path from "node:path";
|
|
28
30
|
import { atomDir } from "./auth.js";
|
|
31
|
+
import { restoreGoalFromPersist, serializeGoalForPersist, } from "./goal.js";
|
|
29
32
|
// Literal defaults for new sessions (opaque carried fields — see header).
|
|
30
33
|
export const SESSION_DEFAULT_PROVIDER = "opencode-zen";
|
|
31
34
|
export const SESSION_DEFAULT_MODEL = "";
|
|
32
|
-
export const SESSION_DEFAULT_EFFORT = "
|
|
35
|
+
export const SESSION_DEFAULT_EFFORT = "auto";
|
|
33
36
|
export const SESSION_DEFAULT_MODE = "normal";
|
|
37
|
+
// "default" is the pre-auto name for the same level: old records map to
|
|
38
|
+
// "auto" on load/create instead of carrying a dead value. Local one-liner
|
|
39
|
+
// (not imported from zen.js) to honor this module's import budget above.
|
|
40
|
+
function canonicalSessionEffort(value) {
|
|
41
|
+
if (value === "low" || value === "medium" || value === "high" || value === "max") {
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
return "auto";
|
|
45
|
+
}
|
|
34
46
|
export const SESSIONS_DIRNAME = "sessions";
|
|
35
47
|
export const ACTIVE_FILENAME = "active";
|
|
36
48
|
export function sessionsDir(home) {
|
|
@@ -254,9 +266,13 @@ function validateSessionRecord(data) {
|
|
|
254
266
|
cwd: data["cwd"],
|
|
255
267
|
provider: data["provider"],
|
|
256
268
|
model: data["model"],
|
|
257
|
-
effort: data["effort"],
|
|
269
|
+
effort: canonicalSessionEffort(data["effort"]),
|
|
258
270
|
mode: data["mode"],
|
|
259
271
|
usageTotals: validateUsageTotals(data["usageTotals"]),
|
|
272
|
+
// Tolerant: a missing or trashed goal reads as no-goal (null) — the
|
|
273
|
+
// record still loads, so one corrupt field can never strand a session.
|
|
274
|
+
// Re-serialized so the loaded record always carries concrete stats.
|
|
275
|
+
goal: serializeGoalForPersist(restoreGoalFromPersist(data["goal"])),
|
|
260
276
|
history: history,
|
|
261
277
|
turns: turns,
|
|
262
278
|
metadata: isRecord(metadata) ? { ...metadata } : {},
|
|
@@ -311,11 +327,14 @@ export function createSession(opts = {}, home) {
|
|
|
311
327
|
cwd: typeof opts.cwd === "string" ? opts.cwd : safeCwd(),
|
|
312
328
|
provider: opts.provider ?? SESSION_DEFAULT_PROVIDER,
|
|
313
329
|
model: opts.model ?? SESSION_DEFAULT_MODEL,
|
|
314
|
-
effort: opts.effort ?? SESSION_DEFAULT_EFFORT,
|
|
330
|
+
effort: canonicalSessionEffort(opts.effort ?? SESSION_DEFAULT_EFFORT),
|
|
315
331
|
mode: opts.mode ?? SESSION_DEFAULT_MODE,
|
|
316
332
|
usageTotals: opts.usageTotals === undefined || opts.usageTotals === null
|
|
317
333
|
? null
|
|
318
334
|
: validateUsageTotals(opts.usageTotals),
|
|
335
|
+
// Fresh sessions start with no goal unless the caller restores one
|
|
336
|
+
// (tolerantly validated — corrupt input reads as no-goal, never throws).
|
|
337
|
+
goal: serializeGoalForPersist(restoreGoalFromPersist(opts.goal ?? null)),
|
|
319
338
|
history: (opts.history ?? []).map((m) => ({ ...m })),
|
|
320
339
|
turns: (opts.turns ?? []).map((t) => ({ ...t })),
|
|
321
340
|
metadata: isRecord(opts.metadata) ? { ...opts.metadata } : {},
|
|
@@ -243,6 +243,26 @@ function iterationTimeline(s) {
|
|
|
243
243
|
});
|
|
244
244
|
return `<div class="timeline">${rows.join("")}</div>`;
|
|
245
245
|
}
|
|
246
|
+
// Goal fragment for one turn (ticket 09): rendered only when the trace
|
|
247
|
+
// carries a goal snapshot — absent or malformed reads as no-goal and renders
|
|
248
|
+
// nothing (never a fake claim). Counters show only when reported.
|
|
249
|
+
function goalFragment(t) {
|
|
250
|
+
const g = t.goal;
|
|
251
|
+
if (!g || typeof g.objective !== "string" || g.objective.length === 0)
|
|
252
|
+
return "";
|
|
253
|
+
const state = g.active === true ? "active" : "paused";
|
|
254
|
+
const bits = [];
|
|
255
|
+
if (typeof g.turns === "number")
|
|
256
|
+
bits.push(`${fmtCount(g.turns)} turn(s)`);
|
|
257
|
+
if (typeof g.requests === "number")
|
|
258
|
+
bits.push(`${fmtCount(g.requests)} request(s)`);
|
|
259
|
+
if (typeof g.tokens === "number")
|
|
260
|
+
bits.push(`${fmtCount(g.tokens)} tokens`);
|
|
261
|
+
if (typeof g.workMs === "number")
|
|
262
|
+
bits.push(fmtMs(g.workMs));
|
|
263
|
+
const counters = bits.length > 0 ? ` <span class="mute">(${bits.join(" · ")})</span>` : "";
|
|
264
|
+
return `<p><strong>Goal:</strong> <span class="mono">${escapeHtml(g.objective)}</span> <span class="pill">${state}</span>${counters}</p>`;
|
|
265
|
+
}
|
|
246
266
|
function turnBlock(t) {
|
|
247
267
|
const outcomeCls = t.outcome === "completed" ? "ok" : t.outcome === "failed" ? "err" : "warn";
|
|
248
268
|
const total = turnTokensTotal(t);
|
|
@@ -272,6 +292,7 @@ function turnBlock(t) {
|
|
|
272
292
|
<summary><span class="mono">${escapeHtml(t.id)}</span> · ${fmtTime(t.startedAt)} · ${escapeHtml(t.provider)} · ${escapeHtml(t.model)} · ${head}</summary>
|
|
273
293
|
<div class="turnbody">
|
|
274
294
|
<p><strong>Input:</strong> <span class="mono">${escapeHtml(t.inputPreview)}</span> <span class="mute">(${fmtCount(t.inputChars)} chars)</span></p>
|
|
295
|
+
${goalFragment(t)}
|
|
275
296
|
${t.error ? `<p><strong>Error:</strong> <span class="err">${escapeHtml(t.error)}</span></p>` : ""}
|
|
276
297
|
${t.replyPreview ? `<p><strong>Reply:</strong> <span class="mono">${escapeHtml(t.replyPreview)}</span></p>` : ""}
|
|
277
298
|
<h5>Timeline</h5>
|
|
@@ -361,6 +382,12 @@ export function buildDashboardHtml(sessions, opts = {}) {
|
|
|
361
382
|
const successRate = agg.toolSuccessRate !== null
|
|
362
383
|
? `${(agg.toolSuccessRate * 100).toFixed(1)}%`
|
|
363
384
|
: `<span class="na" title="No tool calls recorded — a rate over zero calls would be fake.">n/a</span>`;
|
|
385
|
+
// Goal overview (ticket 09): present only when at least one stored turn
|
|
386
|
+
// carried a goal snapshot — zero goal turns omit the card entirely (same
|
|
387
|
+
// conditional-render precedent as the corrupt-file note above).
|
|
388
|
+
const goalCard = agg.goalTurns > 0
|
|
389
|
+
? `<div class="card"><div class="k">Goal turns</div><div class="v">${fmtCount(agg.goalTurns)}</div></div>`
|
|
390
|
+
: "";
|
|
364
391
|
const refreshSeconds = typeof opts.refreshSeconds === "number" && Number.isFinite(opts.refreshSeconds)
|
|
365
392
|
? Math.floor(opts.refreshSeconds)
|
|
366
393
|
: 0;
|
|
@@ -504,6 +531,7 @@ ${corruptNote}
|
|
|
504
531
|
<div class="card"><div class="k">Completion tokens (reported)</div><div class="v">${fmtTokens(agg.usage.completion_tokens, agg.usageReported, "No model call reported completion_tokens.")}</div></div>
|
|
505
532
|
<div class="card"><div class="k">Cache read / write (reported)</div><div class="v">${fmtTokens(agg.usage.cacheReadTokens, agg.usageReported, "No provider reported cache-read counters.")} / ${fmtTokens(agg.usage.cacheWriteTokens, agg.usageReported, "No provider reported cache-write counters.")}</div></div>
|
|
506
533
|
<div class="card"><div class="k">Retries</div><div class="v">${fmtCount(agg.retries)}</div></div>
|
|
534
|
+
${goalCard}
|
|
507
535
|
<div class="card"><div class="k">Read-cache hits (local)</div><div class="v">${fmtCount(agg.cacheHits)}</div></div>
|
|
508
536
|
<div class="card"><div class="k">Loop-guard hits</div><div class="v">${fmtCount(agg.repetitionHits)}</div></div>
|
|
509
537
|
<div class="card"><div class="k">Avg model latency</div><div class="v">${fmtMs(agg.avgModelLatencyMs)}</div></div>
|
package/dist/telemetry.js
CHANGED
|
@@ -111,6 +111,38 @@ export function cleanUsage(value) {
|
|
|
111
111
|
? out
|
|
112
112
|
: undefined;
|
|
113
113
|
}
|
|
114
|
+
// Keep only a well-formed goal snapshot (tolerant reader/writer pair with
|
|
115
|
+
// the dashboard: absent or malformed reads as no-goal, never a throw).
|
|
116
|
+
// Returns undefined when no goal was live. The objective is capped so a
|
|
117
|
+
// pasted paragraph cannot bloat the trace; counters copy finite values only.
|
|
118
|
+
export function cleanGoalSnapshot(value) {
|
|
119
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
120
|
+
return undefined;
|
|
121
|
+
const o = value;
|
|
122
|
+
const objective = o["objective"];
|
|
123
|
+
if (typeof objective !== "string" || objective.length === 0)
|
|
124
|
+
return undefined;
|
|
125
|
+
const snap = {
|
|
126
|
+
objective: objective.length > TELEMETRY_INPUT_PREVIEW_CHARS
|
|
127
|
+
? objective.slice(0, TELEMETRY_INPUT_PREVIEW_CHARS)
|
|
128
|
+
: objective,
|
|
129
|
+
active: o["active"] === true,
|
|
130
|
+
};
|
|
131
|
+
const counter = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.floor(v) : undefined;
|
|
132
|
+
const turns = counter(o["turns"]);
|
|
133
|
+
if (turns !== undefined)
|
|
134
|
+
snap.turns = turns;
|
|
135
|
+
const requests = counter(o["requests"]);
|
|
136
|
+
if (requests !== undefined)
|
|
137
|
+
snap.requests = requests;
|
|
138
|
+
const tokens = counter(o["tokens"]);
|
|
139
|
+
if (tokens !== undefined)
|
|
140
|
+
snap.tokens = tokens;
|
|
141
|
+
const workMs = counter(o["workMs"]);
|
|
142
|
+
if (workMs !== undefined)
|
|
143
|
+
snap.workMs = workMs;
|
|
144
|
+
return snap;
|
|
145
|
+
}
|
|
114
146
|
export function addUsageInto(target, extra) {
|
|
115
147
|
if (!extra)
|
|
116
148
|
return false;
|
|
@@ -438,6 +470,7 @@ export function summarizeTelemetry(sessions) {
|
|
|
438
470
|
retries: 0,
|
|
439
471
|
cacheHits: 0,
|
|
440
472
|
repetitionHits: 0,
|
|
473
|
+
goalTurns: 0,
|
|
441
474
|
outcomes: emptyOutcomes(),
|
|
442
475
|
byTool: [],
|
|
443
476
|
avgModelLatencyMs: null,
|
|
@@ -462,6 +495,9 @@ export function summarizeTelemetry(sessions) {
|
|
|
462
495
|
agg.turns += 1;
|
|
463
496
|
if (t.outcome in agg.outcomes)
|
|
464
497
|
agg.outcomes[t.outcome] += 1;
|
|
498
|
+
if (t.goal && typeof t.goal.objective === "string" && t.goal.objective.length > 0) {
|
|
499
|
+
agg.goalTurns += 1;
|
|
500
|
+
}
|
|
465
501
|
if (t.usageReported) {
|
|
466
502
|
if (addUsageInto(agg.usage, t.usage))
|
|
467
503
|
agg.usageReported = true;
|
|
@@ -657,6 +693,9 @@ export class TelemetryRecorder {
|
|
|
657
693
|
usageReported: false,
|
|
658
694
|
retryCount: 0,
|
|
659
695
|
};
|
|
696
|
+
const goalSnap = cleanGoalSnapshot(meta.goal);
|
|
697
|
+
if (goalSnap)
|
|
698
|
+
turn.goal = goalSnap;
|
|
660
699
|
this.session.turns.push(turn);
|
|
661
700
|
this.openTurns.set(id, turn);
|
|
662
701
|
this.pendingRetries = [];
|