agent-sh 0.12.27 → 0.13.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 (144) hide show
  1. package/README.md +13 -2
  2. package/dist/agent/agent-loop.d.ts +3 -5
  3. package/dist/agent/agent-loop.js +42 -98
  4. package/dist/agent/conversation-state.d.ts +9 -0
  5. package/dist/agent/conversation-state.js +16 -0
  6. package/dist/agent/history-file.d.ts +6 -0
  7. package/dist/agent/history-file.js +1 -1
  8. package/dist/agent/host-types.d.ts +125 -0
  9. package/dist/agent/index.d.ts +12 -4
  10. package/dist/agent/index.js +357 -6
  11. package/dist/agent/nuclear-form.d.ts +7 -0
  12. package/dist/{extensions → agent}/providers/deepseek.d.ts +2 -2
  13. package/dist/{extensions → agent}/providers/deepseek.js +5 -4
  14. package/dist/{extensions → agent}/providers/openai-compatible.d.ts +2 -2
  15. package/dist/{extensions → agent}/providers/openai.d.ts +2 -2
  16. package/dist/{extensions → agent}/providers/openai.js +3 -2
  17. package/dist/{extensions → agent}/providers/openrouter.d.ts +2 -2
  18. package/dist/{extensions → agent}/providers/openrouter.js +4 -3
  19. package/dist/agent/skills.js +51 -7
  20. package/dist/agent/subagent.d.ts +1 -1
  21. package/dist/agent/system-prompt.js +14 -17
  22. package/dist/agent/tool-protocol.d.ts +1 -1
  23. package/dist/agent/tool-protocol.js +5 -3
  24. package/dist/agent/tool-registry.d.ts +9 -4
  25. package/dist/agent/tool-registry.js +27 -4
  26. package/dist/agent/tools/bash.d.ts +1 -1
  27. package/dist/agent/tools/bash.js +3 -2
  28. package/dist/agent/tools/edit-file.js +0 -1
  29. package/dist/agent/tools/glob.js +1 -1
  30. package/dist/agent/tools/grep.js +1 -1
  31. package/dist/agent/tools/pwsh.d.ts +1 -1
  32. package/dist/agent/tools/pwsh.js +1 -2
  33. package/dist/agent/tools/read-file.js +7 -4
  34. package/dist/agent/tools/write-file.js +0 -1
  35. package/dist/agent/types.d.ts +17 -2
  36. package/dist/cli/auth/cli.d.ts +1 -0
  37. package/dist/cli/auth/cli.js +216 -0
  38. package/dist/cli/auth/keys.d.ts +31 -0
  39. package/dist/cli/auth/keys.js +102 -0
  40. package/dist/{index.js → cli/index.js} +29 -32
  41. package/dist/{init.js → cli/init.js} +1 -1
  42. package/dist/{install.js → cli/install.js} +31 -2
  43. package/dist/cli/subcommands.d.ts +1 -0
  44. package/dist/cli/subcommands.js +17 -0
  45. package/dist/{event-bus.d.ts → core/event-bus.d.ts} +7 -13
  46. package/dist/{extension-loader.d.ts → core/extension-loader.d.ts} +1 -1
  47. package/dist/{extension-loader.js → core/extension-loader.js} +62 -70
  48. package/dist/{core.d.ts → core/index.d.ts} +18 -15
  49. package/dist/{core.js → core/index.js} +18 -92
  50. package/dist/{settings.d.ts → core/settings.d.ts} +7 -0
  51. package/dist/{settings.js → core/settings.js} +1 -0
  52. package/dist/core/types.d.ts +49 -0
  53. package/dist/core/types.js +1 -0
  54. package/dist/extensions/file-autocomplete.d.ts +1 -1
  55. package/dist/extensions/index.d.ts +7 -14
  56. package/dist/extensions/index.js +2 -19
  57. package/dist/extensions/slash-commands.d.ts +1 -1
  58. package/dist/extensions/slash-commands.js +7 -2
  59. package/dist/shell/host-types.d.ts +114 -0
  60. package/dist/shell/host-types.js +1 -0
  61. package/dist/shell/index.d.ts +8 -7
  62. package/dist/shell/index.js +58 -9
  63. package/dist/shell/input-handler.d.ts +7 -1
  64. package/dist/shell/input-handler.js +5 -2
  65. package/dist/shell/output-parser.d.ts +1 -1
  66. package/dist/{extensions → shell}/shell-context.d.ts +1 -1
  67. package/dist/{extensions → shell}/shell-context.js +18 -12
  68. package/dist/shell/shell.d.ts +6 -4
  69. package/dist/shell/shell.js +33 -109
  70. package/dist/shell/strategies/bash.d.ts +2 -0
  71. package/dist/shell/strategies/bash.js +68 -0
  72. package/dist/shell/strategies/fish.d.ts +2 -0
  73. package/dist/shell/strategies/fish.js +65 -0
  74. package/dist/shell/strategies/index.d.ts +13 -0
  75. package/dist/shell/strategies/index.js +17 -0
  76. package/dist/shell/strategies/types.d.ts +50 -0
  77. package/dist/shell/strategies/types.js +9 -0
  78. package/dist/shell/strategies/zsh.d.ts +2 -0
  79. package/dist/shell/strategies/zsh.js +72 -0
  80. package/dist/shell/tui-input-view.js +14 -3
  81. package/dist/{extensions → shell}/tui-renderer.d.ts +1 -1
  82. package/dist/{extensions → shell}/tui-renderer.js +27 -55
  83. package/dist/utils/box-frame.d.ts +4 -0
  84. package/dist/utils/box-frame.js +17 -6
  85. package/dist/utils/compositor.d.ts +1 -1
  86. package/dist/utils/compositor.js +2 -1
  87. package/dist/{executor.js → utils/executor.js} +1 -1
  88. package/dist/utils/floating-panel.d.ts +1 -1
  89. package/dist/utils/floating-panel.js +9 -4
  90. package/dist/utils/llm-facade.d.ts +7 -3
  91. package/dist/utils/stream-transform.d.ts +1 -1
  92. package/dist/utils/terminal-buffer.d.ts +1 -1
  93. package/dist/utils/tool-display.js +4 -0
  94. package/dist/utils/tool-interactive.d.ts +1 -1
  95. package/dist/utils/tty.d.ts +7 -0
  96. package/dist/utils/tty.js +15 -0
  97. package/examples/extensions/ash-acp-bridge/README.md +4 -1
  98. package/examples/extensions/ash-acp-bridge/src/index.ts +654 -0
  99. package/examples/extensions/ash-mcp-bridge/index.ts +1 -1
  100. package/examples/extensions/ashi/README.md +250 -0
  101. package/examples/extensions/ashi/package.json +60 -0
  102. package/examples/extensions/ashi/src/autocomplete.ts +91 -0
  103. package/examples/extensions/ashi/src/capture.ts +34 -0
  104. package/examples/extensions/ashi/src/cli.ts +126 -0
  105. package/examples/extensions/ashi/src/commands.ts +82 -0
  106. package/examples/extensions/ashi/src/compaction.ts +157 -0
  107. package/examples/extensions/ashi/src/components.ts +332 -0
  108. package/examples/extensions/ashi/src/default-renderers.ts +153 -0
  109. package/examples/extensions/ashi/src/display-config.ts +62 -0
  110. package/examples/extensions/ashi/src/frontend.ts +735 -0
  111. package/examples/extensions/ashi/src/hooks.ts +136 -0
  112. package/examples/extensions/ashi/src/multi-session-store.ts +146 -0
  113. package/examples/extensions/ashi/src/session-commands.ts +76 -0
  114. package/examples/extensions/ashi/src/session-store.ts +264 -0
  115. package/examples/extensions/ashi/src/status-footer.ts +66 -0
  116. package/examples/extensions/ashi/src/theme.ts +151 -0
  117. package/examples/extensions/ashi/tsconfig.json +14 -0
  118. package/examples/extensions/emacs-buffer.ts +1 -1
  119. package/examples/extensions/interactive-prompts.ts +114 -69
  120. package/examples/extensions/latex-images.ts +3 -3
  121. package/examples/extensions/opencode-bridge/index.ts +1 -1
  122. package/examples/extensions/overlay-agent.ts +7 -5
  123. package/examples/extensions/peer-mesh.ts +1 -1
  124. package/examples/extensions/pi-bridge/index.ts +0 -1
  125. package/examples/extensions/questionnaire.ts +2 -1
  126. package/examples/extensions/rtk-proxy.ts +3 -3
  127. package/examples/extensions/solarized-theme.ts +3 -3
  128. package/examples/extensions/subagents.ts +6 -6
  129. package/examples/extensions/terminal-buffer.ts +1 -1
  130. package/examples/extensions/tmux-pane.ts +6 -4
  131. package/examples/extensions/tunnel-vision.ts +5 -5
  132. package/examples/extensions/user-shell.ts +1 -1
  133. package/examples/extensions/web-access.ts +5 -5
  134. package/package.json +26 -22
  135. package/dist/extensions/agent-backend.d.ts +0 -14
  136. package/dist/extensions/agent-backend.js +0 -307
  137. package/dist/types.d.ts +0 -227
  138. /package/dist/{types.js → agent/host-types.js} +0 -0
  139. /package/dist/{extensions → agent}/providers/openai-compatible.js +0 -0
  140. /package/dist/{index.d.ts → cli/index.d.ts} +0 -0
  141. /package/dist/{init.d.ts → cli/init.d.ts} +0 -0
  142. /package/dist/{install.d.ts → cli/install.d.ts} +0 -0
  143. /package/dist/{event-bus.js → core/event-bus.js} +0 -0
  144. /package/dist/{executor.d.ts → utils/executor.d.ts} +0 -0
package/README.md CHANGED
@@ -43,9 +43,9 @@ npm run build # produces dist/
43
43
  npm link # exposes `agent-sh` globally
44
44
  ```
45
45
 
46
- Requires Node.js 18+. Currently supports **bash** and **zsh**; other shells (fish, nushell, etc.) are not yet wired up.
46
+ Requires Node.js 18+. Supports **bash**, **zsh**, and **fish**; other shells (nushell, etc.) are not yet wired up.
47
47
 
48
- **Windows:** the interactive shell layer is bash/zsh-only. Run agent-sh inside **WSL** for the full experience. Native Windows (cmd.exe / PowerShell) is not supported as the host shell, though headless / library / ACP-bridge usage may work — file an issue if you hit a gap.
48
+ **Windows:** the interactive shell layer is bash/zsh/fish-only. Run agent-sh inside **WSL** for the full experience. Native Windows (cmd.exe / PowerShell) is not supported as the host shell, though headless / library / ACP-bridge usage may work — file an issue if you hit a gap.
49
49
 
50
50
  Tip — add a shell alias:
51
51
 
@@ -74,6 +74,17 @@ See [Bring your own agent](#bring-your-own-agent) below for full details and the
74
74
 
75
75
  `ash` is agent-sh's own lightweight agent. It works with any OpenAI-compatible API — pick one of the zero-config paths below, no settings file needed. agent-sh auto-activates a built-in provider when it sees a known key.
76
76
 
77
+ **Quickest path** — store a key once via the auth subcommand:
78
+
79
+ ```bash
80
+ agent-sh auth login # picks a provider interactively
81
+ agent-sh # launches with the saved key
82
+ ```
83
+
84
+ Keys are written to `~/.agent-sh/keys.json` (chmod 0600). Resolution order is `settings.json` → `keys.json` → env var, so an env var or settings entry will still win when present. `auth login` also accepts any provider you declare under `providers` in `settings.json` — useful for custom OpenAI-compatible endpoints where the URL is committable but the key shouldn't be.
85
+
86
+ Or export the env var directly:
87
+
77
88
  **Hosted models via OpenRouter** (300+ models, one key):
78
89
 
79
90
  ```bash
@@ -11,8 +11,8 @@
11
11
  * agent:tool-completed, agent:tool-output
12
12
  * - agent:thinking-chunk, agent:cancelled, agent:error
13
13
  */
14
- import type { EventBus } from "../event-bus.js";
15
- import type { AgentMode } from "../types.js";
14
+ import type { EventBus } from "../core/event-bus.js";
15
+ import type { AgentMode } from "./host-types.js";
16
16
  import type { LlmClient } from "../utils/llm-client.js";
17
17
  import type { HandlerFunctions } from "../utils/handler-registry.js";
18
18
  import type { AgentBackend, ToolDefinition } from "./types.js";
@@ -67,7 +67,7 @@ export declare class AgentLoop implements AgentBackend {
67
67
  wire(): void;
68
68
  /** Unsubscribe from bus events — deactivates this backend. */
69
69
  unwire(): void;
70
- /** Register a tool (used by extensions via ctx.registerTool). */
70
+ /** Register a tool (used by extensions via ctx.agent.registerTool). */
71
71
  registerTool(tool: ToolDefinition): void;
72
72
  /** Unregister a tool by name. */
73
73
  unregisterTool(name: string): void;
@@ -81,11 +81,9 @@ export declare class AgentLoop implements AgentBackend {
81
81
  private toolExtensions;
82
82
  /** Register a named instruction block for the system prompt. */
83
83
  registerInstruction(name: string, text: string, extensionName: string): void;
84
- /** Remove a named instruction block. */
85
84
  removeInstruction(name: string): void;
86
85
  /** Register a named skill (on-demand reference material). */
87
86
  registerSkill(name: string, description: string, filePath: string, extensionName: string): void;
88
- /** Remove a registered skill. */
89
87
  removeSkill(name: string): void;
90
88
  /**
91
89
  * Build the system prompt grouped by extension.
@@ -1,8 +1,5 @@
1
1
  import { setMaxListeners } from "node:events";
2
- import * as fs from "node:fs/promises";
3
2
  import * as path from "node:path";
4
- import * as os from "node:os";
5
- import { computeDiff, computeEditDiff, computeInputDiff } from "../utils/diff.js";
6
3
  import { ToolRegistry } from "./tool-registry.js";
7
4
  import { normalizeToolArgs } from "./normalize-args.js";
8
5
  import { ConversationState } from "./conversation-state.js";
@@ -13,12 +10,12 @@ import { createToolUI } from "../utils/tool-interactive.js";
13
10
  import { RESPONSE_RESERVE, DEFAULT_CONTEXT_WINDOW } from "./token-budget.js";
14
11
  import { PACKAGE_VERSION } from "../utils/package-version.js";
15
12
  import { wrapTrailingWithDynamicContext } from "../utils/message-utils.js";
16
- import { getSettings, updateSettings } from "../settings.js";
13
+ import { getSettings, updateSettings } from "../core/settings.js";
17
14
  import { createToolProtocol } from "./tool-protocol.js";
18
15
  // Core tool factories
19
16
  import { createBashTool } from "./tools/bash.js";
20
17
  import { createPwshTool } from "./tools/pwsh.js";
21
- import { findBash } from "../executor.js";
18
+ import { findBash } from "../utils/executor.js";
22
19
  import { createReadFileTool } from "./tools/read-file.js";
23
20
  import { createWriteFileTool } from "./tools/write-file.js";
24
21
  import { createEditFileTool } from "./tools/edit-file.js";
@@ -52,7 +49,7 @@ function summarizeDescription(desc) {
52
49
  }
53
50
  export class AgentLoop {
54
51
  abortController = null;
55
- toolRegistry = new ToolRegistry();
52
+ toolRegistry;
56
53
  history;
57
54
  conversation;
58
55
  fileReadCache = new Map();
@@ -104,6 +101,7 @@ export class AgentLoop {
104
101
  this.handlers = config.handlers;
105
102
  this.compositor = config.compositor ?? null;
106
103
  this.instanceId = config.instanceId ?? "unknown";
104
+ this.toolRegistry = new ToolRegistry(this.handlers);
107
105
  // Shell-history-shaped log. Default writes go through the advisable
108
106
  // `history:append` handler registered below; extensions swap the
109
107
  // backend without touching this wiring.
@@ -117,7 +115,7 @@ export class AgentLoop {
117
115
  : [{ model: config.llmClient.model }];
118
116
  this.currentModeIndex = config.initialModeIndex ?? 0;
119
117
  // Tool protocol — controls how tools are presented to the LLM
120
- this.toolProtocol = createToolProtocol(getSettings().toolMode ?? "api");
118
+ this.toolProtocol = createToolProtocol(getSettings().toolMode ?? "api", getSettings().coreTools ?? []);
121
119
  // Register core tools
122
120
  this.registerCoreTools();
123
121
  // Register any protocol-provided tools (e.g. load_tool for deferred-lookup).
@@ -291,7 +289,6 @@ export class AgentLoop {
291
289
  return;
292
290
  }
293
291
  this.thinkingLevel = level;
294
- this.bus.emit("ui:info", { message: `Thinking: ${level}` });
295
292
  this.bus.emit("config:changed", {});
296
293
  });
297
294
  onPipe("config:get-thinking", () => {
@@ -387,7 +384,7 @@ export class AgentLoop {
387
384
  }
388
385
  this.boundPipeListeners = [];
389
386
  }
390
- /** Register a tool (used by extensions via ctx.registerTool). */
387
+ /** Register a tool (used by extensions via ctx.agent.registerTool). */
391
388
  registerTool(tool) {
392
389
  this.toolRegistry.register(tool);
393
390
  }
@@ -409,18 +406,23 @@ export class AgentLoop {
409
406
  /** Register a named instruction block for the system prompt. */
410
407
  registerInstruction(name, text, extensionName) {
411
408
  this.instructions.set(name, { text, extensionName });
409
+ this.handlers.define(`instruction:${name}`, () => this.instructions.get(name)?.text ?? "");
412
410
  }
413
- /** Remove a named instruction block. */
414
411
  removeInstruction(name) {
415
412
  this.instructions.delete(name);
413
+ // Handler entry retained so external advisors survive a reload of the owner.
416
414
  }
417
415
  /** Register a named skill (on-demand reference material). */
418
416
  registerSkill(name, description, filePath, extensionName) {
419
417
  this.skills.set(name, { description, filePath, extensionName });
418
+ this.handlers.define(`skill:${name}:view`, () => {
419
+ const s = this.skills.get(name);
420
+ return { description: s?.description ?? "", filePath: s?.filePath ?? "" };
421
+ });
420
422
  }
421
- /** Remove a registered skill. */
422
423
  removeSkill(name) {
423
424
  this.skills.delete(name);
425
+ // Handler entry retained so external advisors survive a reload of the owner.
424
426
  }
425
427
  /**
426
428
  * Build the system prompt grouped by extension.
@@ -434,13 +436,15 @@ export class AgentLoop {
434
436
  buildExtensionSections() {
435
437
  const groups = new Map();
436
438
  const ensure = (name) => groups.get(name) ?? (groups.set(name, { tools: [], skills: [], instructions: [] }).get(name));
437
- // Attribute instructions
438
- for (const { text, extensionName } of this.instructions.values()) {
439
+ // Attribute instructions — read text through the advisor chain
440
+ for (const [name, { extensionName }] of this.instructions) {
441
+ const text = this.handlers.call(`instruction:${name}`);
439
442
  ensure(extensionName).instructions.push({ text });
440
443
  }
441
- // Attribute skills
442
- for (const [skillName, { description, filePath, extensionName }] of this.skills) {
443
- ensure(extensionName).skills.push({ name: skillName, description, filePath });
444
+ // Attribute skills — read description/filePath through the advisor chain
445
+ for (const [skillName, { extensionName }] of this.skills) {
446
+ const view = this.handlers.call(`skill:${skillName}:view`);
447
+ ensure(extensionName).skills.push({ name: skillName, description: view.description, filePath: view.filePath });
444
448
  }
445
449
  // Attribute tools (skip built-in scratchpad tools).
446
450
  // In "api" mode the full tool schemas are in the API `tools` param,
@@ -453,7 +457,7 @@ export class AgentLoop {
453
457
  "bash", "read_file", "write_file", "edit_file", "grep", "glob", "ls",
454
458
  "list_skills",
455
459
  ]);
456
- for (const tool of this.toolRegistry.all()) {
460
+ for (const tool of this.toolRegistry.allView()) {
457
461
  if (builtinTools.has(tool.name))
458
462
  continue;
459
463
  const extName = this.toolExtensions.get(tool.name);
@@ -832,6 +836,8 @@ export class AgentLoop {
832
836
  h.define("conversation:nucleate-user", (text, iid, seq) => nucleate("user", text, iid, seq));
833
837
  h.define("conversation:nucleate-agent", (text, iid, seq) => nucleate("agent", text, iid, seq));
834
838
  h.define("conversation:nucleate-tool", (toolName, args, content, isError, iid, seq) => nucleate(isError ? "error" : "tool", toolName, args, content, isError, iid, seq));
839
+ h.define("conversation:allocate-seq", () => this.conversation.allocateSeq());
840
+ h.define("conversation:reset-for-session", (nextSeq) => this.conversation.resetForSession(nextSeq));
835
841
  // Read-only views into the nuclear state, for compact strategies
836
842
  // and introspect that read without replacing.
837
843
  h.define("conversation:get-nuclear-entries", () => this.conversation.getNuclearEntries());
@@ -858,6 +864,11 @@ export class AgentLoop {
858
864
  h.define("history:search", async (query) => this.history.search(query));
859
865
  h.define("history:find-by-seq", async (seq) => this.history.findBySeq(seq));
860
866
  h.define("history:read-recent", async (max) => this.history.readRecent(max));
867
+ h.define("history:get-branch", async (leafSeq) => this.history.getBranch ? this.history.getBranch(leafSeq) : this.history.readRecent());
868
+ h.define("history:get-tree", async () => this.history.getTree ? this.history.getTree() : this.history.readRecent());
869
+ h.define("history:set-leaf", (seq) => {
870
+ this.history.setLeaf?.(seq);
871
+ });
861
872
  // Prior-session preamble renderer. Default: flat chronological list.
862
873
  h.define("conversation:format-prior-history", (entries) => {
863
874
  if (!entries || entries.length === 0)
@@ -910,74 +921,6 @@ export class AgentLoop {
910
921
  return { content: msg, exitCode: 1, isError: true };
911
922
  }
912
923
  const display = tool.getDisplayInfo?.(args) ?? { kind: "execute" };
913
- let diffShown = false;
914
- // Permission gating
915
- if (tool.requiresPermission) {
916
- let permKind = "tool-call";
917
- let permTitle = typeof args.description === "string"
918
- ? `${name}: ${args.description}`
919
- : name;
920
- let metadata = { args };
921
- // For file-modifying tools, pre-compute diff for display
922
- if (tool.modifiesFiles && typeof args.path === "string") {
923
- try {
924
- const absPath = path.resolve(process.cwd(), args.path);
925
- let diff;
926
- if (typeof args.old_text === "string" && typeof args.new_text === "string") {
927
- // edit_file — read the file so line numbers are real (not relative to the edit region)
928
- const normalizedOld = args.old_text.replace(/\r\n/g, "\n");
929
- const normalizedNew = args.new_text.replace(/\r\n/g, "\n");
930
- try {
931
- const oldFileContent = await fs.readFile(absPath, "utf-8");
932
- diff = computeEditDiff(oldFileContent, normalizedOld, normalizedNew, args.replace_all === true);
933
- }
934
- catch {
935
- // File doesn't exist yet — fall back to input-only diff
936
- diff = computeInputDiff(normalizedOld, normalizedNew);
937
- }
938
- }
939
- else if (typeof args.content === "string") {
940
- // write_file — still need to read the old file for comparison
941
- let oldContent = null;
942
- try {
943
- oldContent = await fs.readFile(absPath, "utf-8");
944
- }
945
- catch { /* new file */ }
946
- if (oldContent !== null) {
947
- diff = computeDiff(oldContent, args.content);
948
- }
949
- }
950
- if (diff && !diff.isIdentical) {
951
- permKind = "file-write";
952
- // Shorten path for display
953
- const cwd = process.cwd();
954
- const home = process.env.HOME ?? os.homedir();
955
- let displayPath = absPath;
956
- if (absPath.startsWith(cwd + "/"))
957
- displayPath = absPath.slice(cwd.length + 1);
958
- else if (home && absPath.startsWith(home + "/"))
959
- displayPath = "~/" + absPath.slice(home.length + 1);
960
- permTitle = displayPath;
961
- metadata = { args, diff };
962
- diffShown = true;
963
- }
964
- }
965
- catch { /* fall back to generic permission */ }
966
- }
967
- const ui = this.compositor
968
- ? createToolUI(this.bus, this.compositor.surface("agent"))
969
- : undefined;
970
- const perm = await this.bus.emitPipeAsync("permission:request", {
971
- kind: permKind,
972
- title: permTitle,
973
- metadata,
974
- ui,
975
- decision: { outcome: "approved" },
976
- });
977
- if (perm.decision.outcome !== "approved") {
978
- return { content: "Permission denied by user.", exitCode: 1, isError: true };
979
- }
980
- }
981
924
  // Emit tool-started for TUI
982
925
  const label = tool.displayName ?? name;
983
926
  this.bus.emit("agent:tool-started", {
@@ -985,21 +928,19 @@ export class AgentLoop {
985
928
  toolCallId: id,
986
929
  kind: display.kind, icon: display.icon, locations: display.locations, rawInput: args,
987
930
  displayDetail: tool.formatCall?.(args),
931
+ sourceLanguage: display.sourceLanguage,
988
932
  batchIndex: ctx.batchIndex, batchTotal: ctx.batchTotal,
989
933
  });
990
934
  this.bus.emit("agent:tool-call", { tool: name, args });
991
935
  // Execute — use ctx.onChunk so advisors can wrap the streaming callback.
992
- // Suppress streaming output if diff was already shown.
993
- const onChunk = (tool.showOutput !== false && !diffShown)
994
- ? ctx.onChunk
995
- : undefined;
936
+ const onChunk = tool.showOutput !== false ? ctx.onChunk : undefined;
996
937
  const toolCtx = { signal };
997
938
  if (this.compositor) {
998
939
  toolCtx.ui = createToolUI(this.bus, this.compositor.surface("agent"));
999
940
  }
1000
941
  let result;
1001
942
  try {
1002
- result = await raceAbort(tool.execute(args, onChunk, toolCtx), signal);
943
+ result = await raceAbort(this.toolRegistry.call(name, args, onChunk, toolCtx), signal);
1003
944
  }
1004
945
  catch (err) {
1005
946
  if (signal.aborted) {
@@ -1192,9 +1133,11 @@ export class AgentLoop {
1192
1133
  catch { /* not an error payload, continue */ }
1193
1134
  const tool = this.toolRegistry.get(tc.name);
1194
1135
  if (!tool) {
1136
+ const available = this.toolRegistry.all().map((t) => t.name).join(", ");
1195
1137
  collectedResults.push({
1196
1138
  callId: tc.id, toolName: tc.name,
1197
- content: `Unknown tool "${tc.name}"`, isError: true,
1139
+ content: `Unknown tool "${tc.name}". Available tools: ${available}`,
1140
+ isError: true,
1198
1141
  });
1199
1142
  return;
1200
1143
  }
@@ -1214,7 +1157,7 @@ export class AgentLoop {
1214
1157
  // normalize-args.ts for the diagnostic that uncovered this.
1215
1158
  args = normalizeToolArgs(args, tool.input_schema);
1216
1159
  // ── Round-scoped cache for cacheable read-only tools ──
1217
- const cacheable = !tool.modifiesFiles && !tool.requiresPermission && tool.showOutput !== true;
1160
+ const cacheable = !tool.modifiesFiles && tool.showOutput !== true;
1218
1161
  const cacheKey = cacheable ? `${tc.name}:${JSON.stringify(args)}` : null;
1219
1162
  if (cacheKey) {
1220
1163
  const cached = roundCache.get(cacheKey);
@@ -1225,6 +1168,7 @@ export class AgentLoop {
1225
1168
  toolCallId: tc.id,
1226
1169
  kind: display.kind, icon: display.icon, locations: display.locations, rawInput: args,
1227
1170
  displayDetail: tool.formatCall?.(args),
1171
+ sourceLanguage: display.sourceLanguage,
1228
1172
  batchIndex, batchTotal: batchTotal > 1 ? batchTotal : undefined,
1229
1173
  });
1230
1174
  this.bus.emit("agent:tool-call", { tool: tc.name, args });
@@ -1254,9 +1198,9 @@ export class AgentLoop {
1254
1198
  const result = await this.handlers.call("tool:execute", { name: tc.name, id: tc.id, args, tool, onChunk: defaultOnChunk,
1255
1199
  batchIndex, batchTotal: batchTotal > 1 ? batchTotal : undefined,
1256
1200
  signal });
1257
- // Truncate large outputs to avoid blowing context
1201
+ // Truncate large outputs to avoid blowing context.
1258
1202
  let content = result.content;
1259
- const maxBytes = 16_384; // ~4k tokens
1203
+ const maxBytes = tool.maxResultBytes ?? 16_384; // ~4k tokens
1260
1204
  if (content.length > maxBytes) {
1261
1205
  const headBytes = Math.floor(maxBytes * 0.6);
1262
1206
  const tailBytes = maxBytes - headBytes;
@@ -1287,12 +1231,11 @@ export class AgentLoop {
1287
1231
  }
1288
1232
  collectedResults.push(finalResult);
1289
1233
  };
1290
- // Partition into parallel-safe (read-only) and sequential (needs permission)
1291
1234
  const parallel = [];
1292
1235
  const sequential = [];
1293
1236
  for (const tc of toolCalls) {
1294
1237
  const tool = this.toolRegistry.get(tc.name);
1295
- if (tool && !tool.requiresPermission && !tool.modifiesFiles) {
1238
+ if (tool && !tool.modifiesFiles) {
1296
1239
  parallel.push(tc);
1297
1240
  }
1298
1241
  else {
@@ -1536,8 +1479,9 @@ export class AgentLoop {
1536
1479
  const reasoningDetailsByIndex = new Map();
1537
1480
  const pendingToolCalls = [];
1538
1481
  // Tool protocol controls what goes in the API tools param vs dynamic context
1539
- const apiTools = this.toolProtocol.getApiTools(this.toolRegistry.all());
1540
- const toolPrompt = this.toolProtocol.getToolPrompt(this.toolRegistry.all());
1482
+ const toolView = this.toolRegistry.allView();
1483
+ const apiTools = this.toolProtocol.getApiTools(toolView);
1484
+ const toolPrompt = this.toolProtocol.getToolPrompt(toolView);
1541
1485
  // Dynamic context rides on the trailing message — see
1542
1486
  // wrapTrailingWithDynamicContext for the cache-stability rationale.
1543
1487
  const rawMessages = [
@@ -97,6 +97,15 @@ export declare class ConversationState {
97
97
  /** Track an entry in memory (nuclear list + recall archive). */
98
98
  private recordNuclearEntry;
99
99
  private appendToHistory;
100
+ /** Bump and return the global sequence counter. For extensions that
101
+ * synthesize their own NuclearEntries (e.g. compaction summaries that
102
+ * should land in the same sequence space as kernel-produced entries). */
103
+ allocateSeq(): number;
104
+ /** Clear nuclear bookkeeping and reset the seq counter. For extensions
105
+ * that swap sessions (multi-session history adapters) so the in-memory
106
+ * nuclear list, recall archive, and seq counter don't carry over from
107
+ * the previous session's tree. */
108
+ resetForSession(nextSeq: number): void;
100
109
  updateApiTokenCount(promptTokens: number): void;
101
110
  estimatePromptTokens(): number;
102
111
  estimateTokens(): number;
@@ -331,6 +331,22 @@ export class ConversationState {
331
331
  return;
332
332
  this.handlers.call("history:append", entries);
333
333
  }
334
+ /** Bump and return the global sequence counter. For extensions that
335
+ * synthesize their own NuclearEntries (e.g. compaction summaries that
336
+ * should land in the same sequence space as kernel-produced entries). */
337
+ allocateSeq() {
338
+ return this.nextSeq++;
339
+ }
340
+ /** Clear nuclear bookkeeping and reset the seq counter. For extensions
341
+ * that swap sessions (multi-session history adapters) so the in-memory
342
+ * nuclear list, recall archive, and seq counter don't carry over from
343
+ * the previous session's tree. */
344
+ resetForSession(nextSeq) {
345
+ this.nuclearEntries = [];
346
+ this.nuclearBySeq.clear();
347
+ this.recallArchive.clear();
348
+ this.nextSeq = nextSeq;
349
+ }
334
350
  // ── Token estimation ──────────────────────────────────────────
335
351
  updateApiTokenCount(promptTokens) {
336
352
  this.lastApiTokenCount = promptTokens;
@@ -7,6 +7,12 @@ export interface HistoryAdapter {
7
7
  line: string;
8
8
  }[]>;
9
9
  findBySeq(seq: number): Promise<NuclearEntry | null>;
10
+ /** Walk parent pointers from a leaf back to the root. Tree-aware adapters only. */
11
+ getBranch?(leafSeq: number): Promise<NuclearEntry[]>;
12
+ /** Return every entry, including sibling branches. Tree-aware adapters only. */
13
+ getTree?(): Promise<NuclearEntry[]>;
14
+ /** Move the active leaf for the next append. Tree-aware adapters only. */
15
+ setLeaf?(seq: number): void;
10
16
  }
11
17
  export declare class InMemoryHistory implements HistoryAdapter {
12
18
  private entries;
@@ -9,7 +9,7 @@ import * as fs from "node:fs/promises";
9
9
  import * as fss from "node:fs";
10
10
  import * as path from "node:path";
11
11
  import * as crypto from "node:crypto";
12
- import { CONFIG_DIR, getSettings } from "../settings.js";
12
+ import { CONFIG_DIR, getSettings } from "../core/settings.js";
13
13
  import { serializeEntry, deserializeEntry, isReadOnly, compileSearchRegex, matchEntry, } from "./nuclear-form.js";
14
14
  const HISTORY_PATH = path.join(CONFIG_DIR, "history");
15
15
  const LOCK_STALE_MS = 10_000; // consider lock stale after 10s
@@ -0,0 +1,125 @@
1
+ import type { CoreConfig, CoreContext } from "../core/types.js";
2
+ import type { HistoryAdapter } from "./history-file.js";
3
+ import type { SkillView, ToolDefinition, ToolExecutionContext, ToolSchemaView } from "./types.js";
4
+ /**
5
+ * Backend-agnostic LLM interface exposed via `ctx.agent.llm`. Fulfilled
6
+ * by defining an `llm:invoke` handler; backends without an LLM leave
7
+ * `available` false and calls reject.
8
+ */
9
+ export interface LlmMessage {
10
+ role: "system" | "user" | "assistant";
11
+ content: string;
12
+ }
13
+ export interface LlmSession {
14
+ send(message: string): Promise<string>;
15
+ history(): ReadonlyArray<LlmMessage>;
16
+ }
17
+ export interface LlmInterface {
18
+ readonly available: boolean;
19
+ /** `model` overrides the globally-configured model for this call only.
20
+ * Provider-specific identifier (e.g. "claude-haiku-4-5"). When omitted,
21
+ * the active provider's configured default is used.
22
+ *
23
+ * `reasoningEffort` controls thinking-model token allocation between
24
+ * reasoning and final content (e.g. "low", "medium", "high", or
25
+ * provider-specific). For non-reasoning models it is ignored. Set to
26
+ * "low" for cheap structured-output calls so reasoning doesn't exhaust
27
+ * the max-tokens budget and leave content empty. */
28
+ ask(opts: {
29
+ query: string;
30
+ system?: string;
31
+ maxTokens?: number;
32
+ model?: string;
33
+ reasoningEffort?: string;
34
+ }): Promise<string>;
35
+ session(opts?: {
36
+ system?: string;
37
+ maxTokens?: number;
38
+ model?: string;
39
+ reasoningEffort?: string;
40
+ }): LlmSession;
41
+ }
42
+ /** A model entry in the cycling list, optionally tied to a provider. */
43
+ export interface AgentMode {
44
+ model: string;
45
+ /** Provider id — when cycling changes provider, LlmClient is reconfigured. */
46
+ provider?: string;
47
+ /** Provider-specific config for reconfiguring LlmClient on switch. */
48
+ providerConfig?: {
49
+ apiKey: string;
50
+ baseURL?: string;
51
+ };
52
+ /** Context window size in tokens (for usage display). */
53
+ contextWindow?: number;
54
+ /** Max output tokens for this mode. */
55
+ maxTokens?: number;
56
+ /** Model supports reasoning/thinking tokens. */
57
+ reasoning?: boolean;
58
+ /** Provider supports the reasoning_effort parameter. */
59
+ supportsReasoningEffort?: boolean;
60
+ /** Echo reasoning_content back on assistant turns. Required by DeepSeek;
61
+ * default off (leaky shims may forward it to the model as OOD input). */
62
+ echoReasoning?: boolean;
63
+ buildReasoningParams?: (level: string) => Record<string, unknown>;
64
+ }
65
+ /**
66
+ * Capabilities the agent host adds on top of CoreContext. Only available
67
+ * when the built-in agent backend is loaded; bridge backends pass a bare
68
+ * CoreContext, so extensions that need these should type as AgentContext.
69
+ */
70
+ export interface AgentSurface {
71
+ llm: LlmInterface;
72
+ providers: {
73
+ configure: (id: string, opts: {
74
+ reasoningParams?: (level: string, model?: string) => Record<string, unknown>;
75
+ }) => void;
76
+ };
77
+ registerTool: (tool: ToolDefinition) => void;
78
+ unregisterTool: (name: string) => void;
79
+ adviseTool: (name: string, advisor: (next: ToolDefinition["execute"], args: Record<string, unknown>, onChunk?: (chunk: string) => void, ctx?: ToolExecutionContext) => ReturnType<ToolDefinition["execute"]>) => () => void;
80
+ adviseToolSchema: (name: string, advisor: (next: () => ToolSchemaView) => ToolSchemaView) => () => void;
81
+ getTools: () => ToolDefinition[];
82
+ registerInstruction: (name: string, text: string) => void;
83
+ removeInstruction: (name: string) => void;
84
+ adviseInstruction: (name: string, advisor: (next: () => string) => string) => () => void;
85
+ registerSkill: (name: string, description: string, filePath: string) => void;
86
+ removeSkill: (name: string) => void;
87
+ adviseSkill: (name: string, advisor: (next: () => SkillView) => SkillView) => () => void;
88
+ /**
89
+ * Register a context producer — a function that contributes a string
90
+ * (or `null` to skip) into one of two lifecycles:
91
+ *
92
+ * - `mode: "per-request"` (default) — fires on every LLM request,
93
+ * including each tool-loop iteration. Output is ephemerally wrapped
94
+ * in `<dynamic_context>` onto the trailing message at request time;
95
+ * never persisted. Use for "current state" signals.
96
+ *
97
+ * - `mode: "per-query"` — fires once at user-query start. Output is
98
+ * wrapped in `<query_context>` and frozen into the user message;
99
+ * persists in conversation history.
100
+ *
101
+ * Returns a dispose fn that unregisters the producer.
102
+ */
103
+ registerContextProducer: (name: string, producer: () => string | null, opts?: {
104
+ mode?: "per-request" | "per-query";
105
+ }) => () => void;
106
+ }
107
+ /** Substrate + agent surface. Use this when an extension only touches
108
+ * agent-side features (tools, instructions, LLM) and doesn't need
109
+ * shell rendering. */
110
+ export type AgentContext = CoreContext & {
111
+ agent: AgentSurface;
112
+ };
113
+ export interface AgentConfigSurface {
114
+ /** API key for OpenAI-compatible provider. */
115
+ apiKey?: string;
116
+ /** Base URL for OpenAI-compatible API. */
117
+ baseURL?: string;
118
+ /** Named provider to use from settings.json. */
119
+ provider?: string;
120
+ /** Default model id. */
121
+ model?: string;
122
+ /** Conversation history backend. Defaults to the on-disk HistoryFile. */
123
+ history?: HistoryAdapter;
124
+ }
125
+ export type AgentConfig = CoreConfig & AgentConfigSurface;
@@ -1,11 +1,19 @@
1
1
  /**
2
- * Agent backend exports.
3
- *
4
- * The default backend is AgentLoop (in-process, OpenAI-compatible API).
5
- * Extensions can register alternative backends via agent:register-backend.
2
+ * Constructs the AgentLoop synchronously with a placeholder LlmClient,
3
+ * so core handlers (history:append, system-prompt:build, conversation:*)
4
+ * are defined before user extensions activate. Mode resolution is
5
+ * deferred to `core:extensions-loaded`, giving runtime-registered
6
+ * providers (e.g. openrouter) a chance to register before we look up
7
+ * settings.defaultProvider. Without this deferral, a persisted
8
+ * `defaultProvider: "openrouter"` loses to a cold-start race and the
9
+ * backend bails silently.
6
10
  */
11
+ import type { ExtensionContext } from "../shell/host-types.js";
12
+ export default function agentBackend(ctx: ExtensionContext): void;
7
13
  export type { AgentBackend } from "./types.js";
8
14
  export type { ToolDefinition, ToolResult, ToolDisplayInfo } from "./types.js";
9
15
  export { AgentLoop } from "./agent-loop.js";
10
16
  export { ToolRegistry } from "./tool-registry.js";
11
17
  export { runSubagent, type SubagentOptions } from "./subagent.js";
18
+ /** Activate the ash backend and any provider whose key is configured. */
19
+ export declare function activateAgent(ctx: ExtensionContext): void;