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/adapters.js CHANGED
@@ -6,12 +6,68 @@
6
6
  // Normalized output matches zen ChatResult:
7
7
  // {content, tool_calls:[{id,function:{name,arguments}}], usage?}
8
8
  // so runAgenticLoop/retry/rollback/status code is untouched.
9
- import { TOOL_DEFINITIONS } from "./tools.js";
9
+ import { allToolDefinitions } from "./tools.js";
10
10
  import { getProvider, modelsUrlForProvider, } from "./providers.js";
11
11
  export const ANTHROPIC_VERSION = "2023-06-01";
12
12
  export const ANTHROPIC_MAX_TOKENS = 4096;
13
+ // ---- Reasoning-effort mappings (one /effort knob, three wire shapes) ----
14
+ //
15
+ // OpenAI-chat kind sends `reasoning_effort` verbatim (no mapping needed).
16
+ // Anthropic takes a thinking budget in tokens: minimum 1024, and it must
17
+ // stay under max_tokens for the turn (budgets count toward max_tokens).
18
+ // Gemini takes a thinkingLevel enum (minimal/low/medium/high): our Max maps
19
+ // to high, the deepest level the API offers.
20
+ // Minimum thinking budget Anthropic accepts (see extended-thinking docs).
21
+ export const ANTHROPIC_MIN_THINKING_BUDGET = 1024;
22
+ export const ANTHROPIC_EFFORT_BUDGETS = {
23
+ low: 1024,
24
+ medium: 2048,
25
+ high: 3072,
26
+ max: 3500,
27
+ };
28
+ // Budget for an effort level under a max_tokens cap, or undefined when the
29
+ // knob must be omitted (Auto/unknown effort, or a cap too small to fit the
30
+ // 1024 minimum — e.g. a tiny compaction cap). Oversized wants shrink to
31
+ // cap - 1 instead of 400ing.
32
+ export function anthropicThinkingFor(effort, maxTokens) {
33
+ if (!effort)
34
+ return undefined;
35
+ const want = ANTHROPIC_EFFORT_BUDGETS[effort];
36
+ if (want === undefined)
37
+ return undefined;
38
+ if (typeof maxTokens !== "number" || !Number.isFinite(maxTokens))
39
+ return want;
40
+ const max = Math.floor(maxTokens);
41
+ if (want < max)
42
+ return want;
43
+ const shrunk = max - 1;
44
+ return shrunk >= ANTHROPIC_MIN_THINKING_BUDGET ? shrunk : undefined;
45
+ }
46
+ const GEMINI_EFFORT_LEVELS = {
47
+ low: "low",
48
+ medium: "medium",
49
+ high: "high",
50
+ // The API's deepest level is high — Max rides it.
51
+ max: "high",
52
+ };
53
+ // thinkingLevel for an effort level, or undefined when the knob must be
54
+ // omitted (Auto/unknown effort).
55
+ export function geminiThinkingLevelFor(effort) {
56
+ if (!effort)
57
+ return undefined;
58
+ return GEMINI_EFFORT_LEVELS[effort];
59
+ }
60
+ // Server-authoritative unsupported detection: a 400 that names the effort
61
+ // knob means this model/deployment has no such control, and the caller
62
+ // retries once without it. Deliberately narrow (knob names only) so
63
+ // unrelated 400s keep failing loudly instead of silently dropping effort.
64
+ export function isEffortRejection(errorText) {
65
+ return /reasoning_effort|reasoning effort|thinking_level|budget_tokens|\bthinking\b/i.test(errorText);
66
+ }
13
67
  function toolDefs() {
14
- return TOOL_DEFINITIONS;
68
+ // Builtins plus extension-registered custom tools, so non-OpenAI kinds
69
+ // see the same model-visible surface as the OpenAI-chat path.
70
+ return allToolDefinitions();
15
71
  }
16
72
  function parseArgsObject(raw) {
17
73
  try {
@@ -318,6 +374,24 @@ async function collectSSEText(res) {
318
374
  const body = res.body;
319
375
  const decoder = new TextDecoder();
320
376
  let rawText = "";
377
+ // Data-silence tracking (mirrors zen.readSSEMessage): queue comments and
378
+ // keep-alives carry bytes but no model output, so only chunks containing a
379
+ // `data:` line start refresh the clock. The tail window catches a marker
380
+ // split across chunk boundaries.
381
+ let lastDataAt = Date.now();
382
+ let tail = "";
383
+ const noteChunk = (chunkText) => {
384
+ const joined = tail + chunkText;
385
+ if (/(?:^|\n)data:/.test(joined))
386
+ lastDataAt = Date.now();
387
+ tail = joined.slice(-8);
388
+ };
389
+ const throwIfDataStalled = () => {
390
+ const budget = sseStallTimeoutMs();
391
+ if (Date.now() - lastDataAt > budget) {
392
+ throw new Error(`Truncated stream from model (stall: no output for ${budget}ms — queued or stalled upstream; resend to retry).`);
393
+ }
394
+ };
321
395
  if (body == null)
322
396
  return { rawText, events: [] };
323
397
  try {
@@ -346,10 +420,12 @@ async function collectSSEText(res) {
346
420
  if (chunk.done)
347
421
  break;
348
422
  const v = chunk.value;
349
- rawText +=
350
- typeof v === "string"
351
- ? v
352
- : decoder.decode(v, { stream: true });
423
+ const textPart = typeof v === "string"
424
+ ? v
425
+ : decoder.decode(v, { stream: true });
426
+ rawText += textPart;
427
+ noteChunk(textPart);
428
+ throwIfDataStalled();
353
429
  }
354
430
  }
355
431
  finally {
@@ -369,10 +445,12 @@ async function collectSSEText(res) {
369
445
  if (step.done)
370
446
  break;
371
447
  const v = step.value;
372
- rawText +=
373
- typeof v === "string"
374
- ? v
375
- : decoder.decode(v, { stream: true });
448
+ const textPart = typeof v === "string"
449
+ ? v
450
+ : decoder.decode(v, { stream: true });
451
+ rawText += textPart;
452
+ noteChunk(textPart);
453
+ throwIfDataStalled();
376
454
  }
377
455
  }
378
456
  finally {
@@ -428,16 +506,9 @@ function finiteCount(value) {
428
506
  ? Math.floor(value)
429
507
  : undefined;
430
508
  }
431
- function openAIUsage(prompt, completion, cache) {
509
+ function openAIUsage(prompt, completion, cache, opts) {
432
510
  const out = {};
433
511
  const p = finiteCount(prompt);
434
- if (p !== undefined)
435
- out.prompt_tokens = p;
436
- const c = finiteCount(completion);
437
- if (c !== undefined)
438
- out.completion_tokens = c;
439
- if (p !== undefined && c !== undefined)
440
- out.total_tokens = p + c;
441
512
  // Provider-reported cache counters ride alongside (Anthropic
442
513
  // cache_read/_creation, Gemini cachedContentTokenCount) — present-only.
443
514
  const read = finiteCount(cache?.read);
@@ -446,6 +517,22 @@ function openAIUsage(prompt, completion, cache) {
446
517
  const write = finiteCount(cache?.write);
447
518
  if (write !== undefined)
448
519
  out.cacheWriteTokens = write;
520
+ // Anthropic's input_tokens EXCLUDES cache_read/_creation (they are separate
521
+ // counters for the same context), while OpenAI/Gemini prompt counts already
522
+ // include cached tokens. With foldCacheIntoPrompt, prompt_tokens is
523
+ // normalized to total input-side tokens so load (P%), spend (NK), and the
524
+ // auto-compact trigger all see the real context. Detail fields above stay
525
+ // provider-faithful regardless.
526
+ const folded = opts?.foldCacheIntoPrompt === true
527
+ ? [p, read, write].reduce((acc, v) => (v === undefined ? acc : (acc ?? 0) + v), undefined)
528
+ : p;
529
+ if (folded !== undefined)
530
+ out.prompt_tokens = folded;
531
+ const c = finiteCount(completion);
532
+ if (c !== undefined)
533
+ out.completion_tokens = c;
534
+ if (folded !== undefined && c !== undefined)
535
+ out.total_tokens = folded + c;
449
536
  return out.prompt_tokens !== undefined ||
450
537
  out.completion_tokens !== undefined ||
451
538
  out.total_tokens !== undefined
@@ -524,10 +611,12 @@ export async function readAnthropicSSEMessage(res, opts) {
524
611
  const msg = o["message"];
525
612
  const u = msg?.["usage"];
526
613
  if (u) {
614
+ // Anthropic input_tokens excludes cache_read/_creation — fold them
615
+ // into prompt_tokens so the value is total input-side tokens.
527
616
  const hit = openAIUsage(u["input_tokens"], u["output_tokens"], {
528
617
  read: u["cache_read_input_tokens"],
529
618
  write: u["cache_creation_input_tokens"],
530
- });
619
+ }, { foldCacheIntoPrompt: true });
531
620
  const merged = mergeUsage(usage, hit, { recomputeTotal: true });
532
621
  if (merged !== undefined)
533
622
  usage = merged;
@@ -609,10 +698,11 @@ export async function readAnthropicSSEMessage(res, opts) {
609
698
  const inputSrc = u?.["input_tokens"] !== undefined ? u["input_tokens"] : o["input_tokens"];
610
699
  const outputSrc = u?.["output_tokens"] !== undefined ? u["output_tokens"] : o["output_tokens"];
611
700
  if (u !== undefined || o["input_tokens"] !== undefined || o["output_tokens"] !== undefined) {
701
+ // Same Anthropic-exclusive-cache fold as message_start above.
612
702
  const hit = openAIUsage(inputSrc, outputSrc, {
613
703
  read: u?.["cache_read_input_tokens"] ?? o["cache_read_input_tokens"],
614
704
  write: u?.["cache_creation_input_tokens"] ?? o["cache_creation_input_tokens"],
615
- });
705
+ }, { foldCacheIntoPrompt: true });
616
706
  const merged = mergeUsage(usage, hit, { recomputeTotal: true });
617
707
  if (merged !== undefined)
618
708
  usage = merged;
@@ -731,10 +821,11 @@ export function parseAnthropicJson(data) {
731
821
  };
732
822
  const u = o["usage"];
733
823
  if (u) {
824
+ // Same Anthropic-exclusive-cache fold as the SSE path above.
734
825
  const hit = openAIUsage(u["input_tokens"], u["output_tokens"], {
735
826
  read: u["cache_read_input_tokens"],
736
827
  write: u["cache_creation_input_tokens"],
737
- });
828
+ }, { foldCacheIntoPrompt: true });
738
829
  if (hit)
739
830
  result.usage = hit;
740
831
  }
@@ -19,6 +19,15 @@ export function todoCompletionGate(finalText, ctx) {
19
19
  finalText: `${finalText}${finalText ? "\n" : ""}(blocked: ${open.length} open todo(s) — resolve with todo_update/todowrite before ending the turn:\n${items})`,
20
20
  };
21
21
  }
22
+ // Guard cycles are bounded (like the verification gate): a model that
23
+ // keeps answering without resolving todos ends with a blocked statement
24
+ // instead of looping forever. Normal flows resolve within a round or two.
25
+ if ((ctx.todoRounds ?? 0) >= MAX_TODO_ROUNDS) {
26
+ return {
27
+ action: "end",
28
+ finalText: `${finalText}${finalText ? "\n" : ""}(blocked: ${open.length} open todo(s) remain after ${MAX_TODO_ROUNDS} guard rounds — resolve with todo_update/todowrite before ending the turn:\n${items})`,
29
+ };
30
+ }
22
31
  return {
23
32
  action: "continue",
24
33
  assistantText: finalText,
@@ -30,11 +39,15 @@ export function todoCompletionGate(finalText, ctx) {
30
39
  // report — the system prompt forbids unverified finishes, so the runtime
31
40
  // must not terminate while just labeling): the attempt is recorded and a
32
41
  // verification follow-up re-enters the loop, exactly like the todo guard.
33
- // Two bounded exits: spent step budget, or MAX_VERIFY_ROUNDS nag cycles
42
+ // Two bounded exits: an explicit step budget spent, or MAX_VERIFY_ROUNDS nag
34
43
  // without a passing run — both end with an explicit labeled statement naming
35
44
  // what is unverified and why the loop stopped. Turns with no code writes
36
45
  // (questions, docs, explanations, read-only work) are unaffected.
37
46
  export const MAX_VERIFY_ROUNDS = 3;
47
+ // Todo-guard continues before the turn ends blocked: a model that keeps
48
+ // answering final text without resolving open todos is sent back at most
49
+ // this many times. Mirrors MAX_VERIFY_ROUNDS so no gate can spin forever.
50
+ export const MAX_TODO_ROUNDS = 3;
38
51
  // Source-code extensions whose writes require a passing verification run.
39
52
  // Curated heuristic boundary (not a parser): docs, configs, data, and
40
53
  // extensionless files never arm the gate, so a README edit finishes clean.
@@ -0,0 +1,69 @@
1
+ // Goal evaluator fallback (ticket 04): one bounded, read-only judge call
2
+ // for a goal turn that ends with no disposition report. Follows the
3
+ // compaction summary-POST precedent (requestCompactSummary in src/compact.ts):
4
+ // same provider/model, tools disabled, capped output, single attempt, throws
5
+ // on failure (the loop turns that into a pause, never a crash).
6
+ //
7
+ // Split of responsibilities: the loop owns WHEN (report-less turn end) and
8
+ // WHAT NEXT (consumed verdicts flow through the model-report path); this
9
+ // module owns the request shape, the judge prompt, and the transport. App
10
+ // builds the full request from its live provider/key/model refs; unit tests
11
+ // inject a fake GoalJudgeRunner through AgenticOpts without fetch mocks.
12
+ import { chatCompletionForProvider } from "../zen.js";
13
+ import { parseGoalJudgeVerdict } from "../goal.js";
14
+ // Output cap for the judge POST: a verdict is a few dozen tokens of JSON —
15
+ // 256 leaves headroom for a short reason without letting the judge write prose.
16
+ export const GOAL_JUDGE_MAX_TOKENS = 256;
17
+ // The judge instruction: goal text plus the demand for one strict
18
+ // machine-readable verdict. No tools are offered, so there is nothing to
19
+ // call — the transcript above is the only evidence.
20
+ export function buildGoalJudgeInstruction(goal) {
21
+ return (`You are judging whether a coding-goal turn advanced. Goal: "${goal}".\n` +
22
+ `The recent transcript turns are above. Decide the turn outcome and answer ` +
23
+ `with a single JSON object only (no prose, no code fences):\n` +
24
+ `{"status": "continue", "next": "<concrete next action>"} — work remains; name the next action.\n` +
25
+ `{"status": "complete", "reason": "<why the goal is done>"} — the goal is fully achieved.\n` +
26
+ `{"status": "blocked", "reason": "<what blocks progress>"} — a genuine blocker, not mere remaining work.\n` +
27
+ `Rules: no tools are available for this request — judge from the transcript only, ` +
28
+ `answer with the JSON object only.`);
29
+ }
30
+ export function buildGoalJudgeMessages(systemContent, goal, turns) {
31
+ const system = typeof systemContent === "string" && systemContent.length > 0
32
+ ? systemContent
33
+ : "You are a concise engineering judge.";
34
+ return [
35
+ { role: "system", content: system },
36
+ ...turns.map((m) => ({ ...m })),
37
+ { role: "user", content: buildGoalJudgeInstruction(goal) },
38
+ ];
39
+ }
40
+ // Judge POST: SAME provider/model via the existing chat path but TOOLS
41
+ // DISABLED (no `tools` key in the POST body) and output capped
42
+ // (max_tokens/maxOutputTokens per kind — see zen.ts/adapters.ts). Single
43
+ // attempt, no retry — bounded by construction. Returns the parsed verdict,
44
+ // or null when the judge output is anything but a clear verdict (tolerant
45
+ // parsing lives in parseGoalJudgeVerdict). Transport failures and empty
46
+ // replies throw with history untouched (the loop pauses on them).
47
+ export async function requestGoalVerdict(req) {
48
+ const messages = buildGoalJudgeMessages(req.systemContent, req.goal, req.turns);
49
+ const res = await chatCompletionForProvider(req.provider, req.apiKey, req.model, messages, {
50
+ baseURL: req.baseURL,
51
+ endpointOverride: req.endpointOverride,
52
+ disableTools: true,
53
+ maxOutputTokens: req.maxOutputTokens ?? GOAL_JUDGE_MAX_TOKENS,
54
+ ...(req.signal ? { signal: req.signal } : {}),
55
+ });
56
+ // Totals keep accumulating: forward real judge usage when present.
57
+ if (res.usage !== undefined) {
58
+ try {
59
+ req.onUsage?.(res.usage);
60
+ }
61
+ catch {
62
+ // observer errors never break the judge
63
+ }
64
+ }
65
+ const text = (res.content ?? "").trim();
66
+ if (!text)
67
+ throw new Error("Empty reply from model (unexpected payload).");
68
+ return parseGoalJudgeVerdict(text);
69
+ }
@@ -1,20 +1,18 @@
1
1
  // Loop-guard: repetition/runaway detection + error-streak recovery for the
2
2
  // agentic loop. Pure state machines, no I/O, never throw.
3
3
  //
4
- // Why this exists: maxSteps (30 tool rounds) is the ultimate backstop, but
5
- // a model stuck calling `read <same path>` 30 times burns 30 POSTs before it
6
- // trips. The guard spots the pattern early (consecutive identical signatures)
7
- // and the loop nudges the model toward a different approach with a bounded
8
- // follow-up — then stops hard if the pattern survives the nudges. Error
9
- // streaks get the same treatment: ending on 3+ unaddressed `Error:` results
10
- // is almost always premature, so the loop asks for a fix-forward attempt
11
- // before accepting final text.
4
+ // Why this exists: turns are uncapped by default, so a model stuck calling
5
+ // `read <same path>` forever burns POSTs without end. The guard spots the
6
+ // pattern early (consecutive identical signatures) and the loop nudges the
7
+ // model toward a different approach with a bounded follow-up — then stops
8
+ // hard if the pattern survives the nudges. Error streaks get the same
9
+ // treatment: ending on 3+ unaddressed `Error:` results is almost always
10
+ // premature, so the loop asks for a fix-forward attempt before accepting
11
+ // final text.
12
12
  //
13
- // Defaults preserve the pinned maxSteps contract: repetition intervention is
14
- // OPT-IN (maxRepeatedCalls set by the caller; unset = track-only for stats),
15
- // because the existing suites pin "always same call 31 POSTs stopped
16
- // notice". Error-streak recovery defaults to 3 (single errors still end
17
- // normally — the model may be reporting a blocker).
13
+ // Repetition intervention is OPT-IN (maxRepeatedCalls set by the caller;
14
+ // unset = track-only for stats). Error-streak recovery defaults to 3
15
+ // (single errors still end normally the model may be reporting a blocker).
18
16
  //
19
17
  // All thresholds clamp to sane minima; every method is safe to call with any
20
18
  // input.