atom-agent 1.2.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 (54) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/README.md +13 -4
  3. package/atom.example.json +11 -0
  4. package/dist/App.js +923 -200
  5. package/dist/adapters.js +82 -13
  6. package/dist/agent/goal-evaluator.js +69 -0
  7. package/dist/agent/loop.js +517 -76
  8. package/dist/cli.js +11 -3
  9. package/dist/compact.js +41 -15
  10. package/dist/config.js +43 -7
  11. package/dist/context-manager.js +16 -198
  12. package/dist/context-windows.js +4 -2
  13. package/dist/env-block.js +5 -5
  14. package/dist/extension-commands.js +196 -0
  15. package/dist/extension-ui.js +153 -0
  16. package/dist/extensions.js +1571 -0
  17. package/dist/goal.js +583 -0
  18. package/dist/project-trust.js +96 -0
  19. package/dist/providers.js +6 -6
  20. package/dist/scheduler.js +74 -36
  21. package/dist/session.js +23 -5
  22. package/dist/sessions.js +25 -6
  23. package/dist/telemetry-dashboard.js +28 -0
  24. package/dist/telemetry.js +39 -0
  25. package/dist/tools/compaction-hooks.js +165 -0
  26. package/dist/tools/custom.js +189 -0
  27. package/dist/tools/intercept.js +145 -0
  28. package/dist/tools/overrides.js +105 -0
  29. package/dist/tools/provider-hooks.js +224 -0
  30. package/dist/tools/registry.js +246 -17
  31. package/dist/tools.js +44 -0
  32. package/dist/ui/palette.js +1 -1
  33. package/dist/ui/status-bar.js +80 -5
  34. package/dist/zen.js +305 -75
  35. package/documentation/architecture.md +114 -0
  36. package/documentation/cli.md +82 -0
  37. package/documentation/compaction.md +50 -0
  38. package/documentation/configuration.md +111 -0
  39. package/documentation/development.md +62 -0
  40. package/documentation/extensions.md +160 -0
  41. package/documentation/getting-started.md +63 -0
  42. package/documentation/goals.md +41 -0
  43. package/documentation/index.md +41 -0
  44. package/documentation/observability.md +70 -0
  45. package/documentation/permissions.md +66 -0
  46. package/documentation/providers.md +78 -0
  47. package/documentation/sessions.md +92 -0
  48. package/documentation/skills.md +57 -0
  49. package/documentation/tools.md +94 -0
  50. package/documentation/troubleshooting.md +54 -0
  51. package/examples/extensions/01-audit-gate.js +24 -0
  52. package/examples/extensions/02-notes-tool.js +32 -0
  53. package/examples/extensions/03-custom-command.js +32 -0
  54. 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 {
@@ -450,16 +506,9 @@ function finiteCount(value) {
450
506
  ? Math.floor(value)
451
507
  : undefined;
452
508
  }
453
- function openAIUsage(prompt, completion, cache) {
509
+ function openAIUsage(prompt, completion, cache, opts) {
454
510
  const out = {};
455
511
  const p = finiteCount(prompt);
456
- if (p !== undefined)
457
- out.prompt_tokens = p;
458
- const c = finiteCount(completion);
459
- if (c !== undefined)
460
- out.completion_tokens = c;
461
- if (p !== undefined && c !== undefined)
462
- out.total_tokens = p + c;
463
512
  // Provider-reported cache counters ride alongside (Anthropic
464
513
  // cache_read/_creation, Gemini cachedContentTokenCount) — present-only.
465
514
  const read = finiteCount(cache?.read);
@@ -468,6 +517,22 @@ function openAIUsage(prompt, completion, cache) {
468
517
  const write = finiteCount(cache?.write);
469
518
  if (write !== undefined)
470
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;
471
536
  return out.prompt_tokens !== undefined ||
472
537
  out.completion_tokens !== undefined ||
473
538
  out.total_tokens !== undefined
@@ -546,10 +611,12 @@ export async function readAnthropicSSEMessage(res, opts) {
546
611
  const msg = o["message"];
547
612
  const u = msg?.["usage"];
548
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.
549
616
  const hit = openAIUsage(u["input_tokens"], u["output_tokens"], {
550
617
  read: u["cache_read_input_tokens"],
551
618
  write: u["cache_creation_input_tokens"],
552
- });
619
+ }, { foldCacheIntoPrompt: true });
553
620
  const merged = mergeUsage(usage, hit, { recomputeTotal: true });
554
621
  if (merged !== undefined)
555
622
  usage = merged;
@@ -631,10 +698,11 @@ export async function readAnthropicSSEMessage(res, opts) {
631
698
  const inputSrc = u?.["input_tokens"] !== undefined ? u["input_tokens"] : o["input_tokens"];
632
699
  const outputSrc = u?.["output_tokens"] !== undefined ? u["output_tokens"] : o["output_tokens"];
633
700
  if (u !== undefined || o["input_tokens"] !== undefined || o["output_tokens"] !== undefined) {
701
+ // Same Anthropic-exclusive-cache fold as message_start above.
634
702
  const hit = openAIUsage(inputSrc, outputSrc, {
635
703
  read: u?.["cache_read_input_tokens"] ?? o["cache_read_input_tokens"],
636
704
  write: u?.["cache_creation_input_tokens"] ?? o["cache_creation_input_tokens"],
637
- });
705
+ }, { foldCacheIntoPrompt: true });
638
706
  const merged = mergeUsage(usage, hit, { recomputeTotal: true });
639
707
  if (merged !== undefined)
640
708
  usage = merged;
@@ -753,10 +821,11 @@ export function parseAnthropicJson(data) {
753
821
  };
754
822
  const u = o["usage"];
755
823
  if (u) {
824
+ // Same Anthropic-exclusive-cache fold as the SSE path above.
756
825
  const hit = openAIUsage(u["input_tokens"], u["output_tokens"], {
757
826
  read: u["cache_read_input_tokens"],
758
827
  write: u["cache_creation_input_tokens"],
759
- });
828
+ }, { foldCacheIntoPrompt: true });
760
829
  if (hit)
761
830
  result.usage = hit;
762
831
  }
@@ -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
+ }