atom-agent 1.1.0 → 1.3.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/README.md +18 -8
  3. package/atom.example.json +11 -0
  4. package/dist/App.js +1637 -255
  5. package/dist/adapters.js +112 -21
  6. package/dist/agent/gates.js +14 -1
  7. package/dist/agent/goal-evaluator.js +69 -0
  8. package/dist/agent/loop-guard.js +11 -13
  9. package/dist/agent/loop.js +716 -132
  10. package/dist/agent/normalize.js +9 -2
  11. package/dist/cli.js +25 -3
  12. package/dist/compact.js +169 -17
  13. package/dist/config.js +43 -7
  14. package/dist/context-manager.js +16 -198
  15. package/dist/context-windows.js +4 -2
  16. package/dist/env-block.js +46 -8
  17. package/dist/extension-commands.js +196 -0
  18. package/dist/extension-ui.js +153 -0
  19. package/dist/extensions.js +1571 -0
  20. package/dist/goal.js +583 -0
  21. package/dist/project-trust.js +96 -0
  22. package/dist/providers.js +6 -6
  23. package/dist/scheduler.js +159 -41
  24. package/dist/session.js +23 -5
  25. package/dist/sessions.js +543 -0
  26. package/dist/system.js +89 -13
  27. package/dist/telemetry-dashboard.js +28 -0
  28. package/dist/telemetry.js +39 -0
  29. package/dist/tools/compaction-hooks.js +165 -0
  30. package/dist/tools/custom.js +189 -0
  31. package/dist/tools/dir-cache.js +7 -0
  32. package/dist/tools/filesystem.js +3 -2
  33. package/dist/tools/intercept.js +145 -0
  34. package/dist/tools/overrides.js +105 -0
  35. package/dist/tools/provider-hooks.js +224 -0
  36. package/dist/tools/registry.js +247 -17
  37. package/dist/tools/ripgrep.js +256 -0
  38. package/dist/tools/search.js +119 -58
  39. package/dist/tools/shared.js +39 -0
  40. package/dist/tools/shell.js +7 -5
  41. package/dist/tools/web.js +6 -6
  42. package/dist/tools.js +45 -0
  43. package/dist/ui/diff-view.js +7 -2
  44. package/dist/ui/live-host.js +18 -0
  45. package/dist/ui/live-tail.js +9 -3
  46. package/dist/ui/markdown.js +26 -2
  47. package/dist/ui/palette.js +3 -1
  48. package/dist/ui/side-by-side.js +2 -2
  49. package/dist/ui/status-bar.js +80 -5
  50. package/dist/ui/status-host.js +22 -0
  51. package/dist/ui/stream-store.js +48 -0
  52. package/dist/ui/tool-inspector.js +7 -1
  53. package/dist/ui/transcript.js +92 -38
  54. package/dist/zen.js +370 -87
  55. package/documentation/architecture.md +114 -0
  56. package/documentation/cli.md +82 -0
  57. package/documentation/compaction.md +50 -0
  58. package/documentation/configuration.md +111 -0
  59. package/documentation/development.md +62 -0
  60. package/documentation/extensions.md +160 -0
  61. package/documentation/getting-started.md +63 -0
  62. package/documentation/goals.md +41 -0
  63. package/documentation/index.md +41 -0
  64. package/documentation/observability.md +70 -0
  65. package/documentation/permissions.md +66 -0
  66. package/documentation/providers.md +78 -0
  67. package/documentation/sessions.md +92 -0
  68. package/documentation/skills.md +57 -0
  69. package/documentation/tools.md +94 -0
  70. package/documentation/troubleshooting.md +54 -0
  71. package/examples/extensions/01-audit-gate.js +24 -0
  72. package/examples/extensions/02-notes-tool.js +32 -0
  73. package/examples/extensions/03-custom-command.js +32 -0
  74. 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 only here.",
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 never sent.",
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 never sent.",
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 never sent.",
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,10 +1,15 @@
1
- // Effect-aware tool scheduler: data-driven parallelism without Promise.all-
2
- // ing every tool.
1
+ // Effect-aware tool scheduler: data-driven parallelism, parallel by default.
3
2
  //
4
- // The conservative contract (unchanged): batchable reads run concurrently,
5
- // mutations stay serialized, results commit in original call order, cancel
6
- // stops between batches, approval happens per call inside runOneTool. This
7
- // module only PLANS batches; execution (zen.ts runLoopWithChat) is untouched.
3
+ // The contract: batchable calls run concurrently, conflicting calls stay
4
+ // serialized, results commit in original call order, cancel stops between
5
+ // batches, approval happens per call in order before execution. This module
6
+ // only PLANS batches; execution (agent/loop.ts runLoopWithChat) is untouched.
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).
8
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):
@@ -16,24 +21,35 @@
16
21
  // become inline-error singletons downstream, exactly as before).
17
22
  // - interactive (ask_question) or exclusive (shared ambient state the effect
18
23
  // model cannot see: todowrite/todo_update/todo_get) → serial singleton.
19
- // - any filesystem/network WRITE, or any process SPAWN → serial singleton.
20
- // Writes conflict GLOBALLY (not just same-target): a write splits the
21
- // block and read-after-write stays ordered. Target-scoped write batching
22
- // is a deliberate non-goal correctness over theoretical parallelism.
23
- // - reads (filesystem or network) batch with pairwise-disjoint keys, where
24
- // the key is tool + target. Same tool + same target serializes (the old
25
- // overlap rule, kept verbatim: e.g. two reads of one path). Reads never
26
- // conflict across keys an all-read batch cannot race a writer, because
27
- // writers never join batches.
28
- // - deterministic is declared per tool; its current enforcement is the
29
- // same-key rule (a re-poll of the same background task, whose output can
30
- // grow, never runs concurrently with itself).
24
+ // - process SPAWN or network WRITE (bash: unbounded footprint) → serial
25
+ // singleton, splitting the block globally.
26
+ // - filesystem writes (write/edit) batch on disjoint canonical file keys
27
+ // (see canonicalFileKey): same-file mutations never share a batch, so they
28
+ // stay strictly ordered in program order; different files run concurrently.
29
+ // An unresolvable target stays serial (never batch what you cannot see).
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.
31
42
  //
32
43
  // Pure module except the shared arg validators (same imports zen.ts already
33
- // carries — no new coupling class). Covered by tests/scheduler.test.ts; the
34
- // end-to-end ordering/cancel behavior stays pinned by
35
- // tests/parallel-calls.test.ts.
36
- import { toolNames, validateToolArgs } from "./tools.js";
44
+ // carries — no new coupling class) plus best-effort path canonicalization
45
+ // (node:fs/node:path only, so the architecture boundary is unchanged:
46
+ // scheduler still reasons from metadata, never from tool branches).
47
+ // Covered by tests/scheduler.test.ts; the end-to-end ordering/cancel
48
+ // behavior stays pinned by tests/parallel-calls.test.ts and
49
+ // tests/parallel-writes.test.ts.
50
+ import * as fs from "node:fs";
51
+ import * as path from "node:path";
52
+ import { isCustomTool, toolExecutionMode, toolNames, validateToolArgs } from "./tools.js";
37
53
  const str = (v) => (typeof v === "string" ? v : "");
38
54
  const targetOf = (key) => (args) => {
39
55
  const t = str(args[key]);
@@ -83,8 +99,8 @@ export const TOOL_EFFECTS = {
83
99
  process: "none",
84
100
  interactive: false,
85
101
  exclusive: false,
86
- // Live search results vary call to call; same-query conflict is still
87
- // serialized by the same-key rule.
102
+ // Live search results vary call to call; concurrent same-query searches
103
+ // are merely redundant, never incorrect — reads always batch.
88
104
  deterministic: false,
89
105
  target: targetOf("query"),
90
106
  },
@@ -95,8 +111,8 @@ export const TOOL_EFFECTS = {
95
111
  process: "none",
96
112
  interactive: false,
97
113
  exclusive: false,
98
- // A running task's output grows between polls — same-task polls
99
- // serialize via the same-key rule.
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.
100
116
  deterministic: false,
101
117
  target: targetOf("taskId"),
102
118
  },
@@ -166,22 +182,93 @@ export const TOOL_EFFECTS = {
166
182
  target: () => null,
167
183
  },
168
184
  };
185
+ // Canonical per-file mutation key: the identity two mutation calls compare
186
+ // before sharing a batch. Lexical resolve against the process cwd (the same
187
+ // base the executors default to), then best-effort symlink resolution, then
188
+ // case-folding on case-insensitive filesystems. Null when the target is
189
+ // missing, empty, or unresolvable — unknown footprints stay serial. Sync and
190
+ // best-effort by design: a handful of calls per tool block, and a miss only
191
+ // costs parallelism, never correctness.
192
+ export function canonicalFileKey(rawPath, cwd = process.cwd()) {
193
+ if (typeof rawPath !== "string" || rawPath.length === 0)
194
+ return null;
195
+ if (rawPath.includes("\0"))
196
+ return null;
197
+ let abs;
198
+ try {
199
+ abs = path.resolve(cwd, rawPath);
200
+ }
201
+ catch {
202
+ return null;
203
+ }
204
+ try {
205
+ abs = fs.realpathSync(abs);
206
+ }
207
+ catch {
208
+ // Fresh write target or unreadable link: the lexical path stands.
209
+ }
210
+ try {
211
+ abs = path.normalize(abs);
212
+ }
213
+ catch {
214
+ return null;
215
+ }
216
+ if (process.platform === "win32" || process.platform === "darwin") {
217
+ abs = abs.toLowerCase();
218
+ }
219
+ return abs;
220
+ }
169
221
  // Partition one assistant message's tool_calls into commit batches,
170
- // preserving program order: consecutive batchable calls with pairwise
171
- // disjoint keys form one batch; any serial-only call and any call whose
172
- // key already appears in the open batch closes the batch and runs as a
173
- // strict serial singleton. A later batch never moves ahead of an earlier
174
- // serial call (read-after-write stays ordered), and batches never span the
175
- // block boundary.
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
229
+ // moves ahead of an earlier serial call, and batches never span the block
230
+ // boundary.
176
231
  export function planBatches(calls) {
177
232
  const batches = [];
178
233
  let open = [];
179
- const keys = new Set();
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.
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
+ }
180
267
  const flush = () => {
181
268
  if (open.length > 0) {
182
269
  batches.push(open);
183
270
  open = [];
184
- keys.clear();
271
+ openFiles.clear();
185
272
  }
186
273
  };
187
274
  const singleton = (call, parsed) => {
@@ -201,6 +288,15 @@ export function planBatches(calls) {
201
288
  malformed = true;
202
289
  }
203
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
+ }
204
300
  // Missing metadata fails safe to serial (never batch the unknown).
205
301
  const meta = TOOL_EFFECTS[name];
206
302
  if (malformed || !meta || !toolNames().includes(name)) {
@@ -212,17 +308,31 @@ export function planBatches(calls) {
212
308
  singleton(call, parsed);
213
309
  continue;
214
310
  }
215
- // Interactive, ambient-state, mutating, or spawning calls serialize.
311
+ // Interactive, ambient-state, spawning, or network-writing calls
312
+ // serialize (bash has no statically visible footprint: global conflict).
216
313
  if (meta.interactive ||
217
314
  meta.exclusive ||
218
- meta.filesystem === "write" ||
219
315
  meta.network === "write" ||
220
316
  meta.process !== "none") {
221
317
  singleton(call, parsed);
222
318
  continue;
223
319
  }
224
- // Reads batch on disjoint tool+target keys; empty target = unknown
225
- // footprint = serial.
320
+ // Filesystem writes batch on disjoint canonical file keys: the
321
+ // per-file mutation queue. Same-file mutations split into sequential
322
+ // batches (never interleave); disjoint files run concurrently.
323
+ if (meta.filesystem === "write") {
324
+ const fileKey = canonicalFileKey(parsed["path"]);
325
+ if (!fileKey || openFiles.has(fileKey)) {
326
+ singleton(call, parsed);
327
+ continue;
328
+ }
329
+ openFiles.set(fileKey, "write");
330
+ open.push({ call, parsed, parallelKey: `${name} ${fileKey}` });
331
+ continue;
332
+ }
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.
226
336
  let target = null;
227
337
  try {
228
338
  target = meta.target(parsed);
@@ -235,11 +345,19 @@ export function planBatches(calls) {
235
345
  continue;
236
346
  }
237
347
  const key = `${name} ${target}`;
238
- if (keys.has(key)) {
239
- singleton(call, parsed);
348
+ // A path read against an open write to the same canonical file stays
349
+ // ordered (read-after-write): split the batch.
350
+ if (name === "read") {
351
+ const fileKey = canonicalFileKey(target);
352
+ if (fileKey && openFiles.get(fileKey) === "write") {
353
+ singleton(call, parsed);
354
+ continue;
355
+ }
356
+ if (fileKey && !openFiles.has(fileKey))
357
+ openFiles.set(fileKey, "read");
358
+ open.push({ call, parsed, parallelKey: key });
240
359
  continue;
241
360
  }
242
- keys.add(key);
243
361
  open.push({ call, parsed, parallelKey: key });
244
362
  }
245
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
- const effort = data["effort"];
214
- if (typeof effort !== "string" ||
215
- !EFFORT_OPTIONS.includes(effort)) {
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: 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
  };