agent-sh 0.12.27 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- package/dist/agent/agent-loop.d.ts +3 -5
- package/dist/agent/agent-loop.js +42 -98
- package/dist/agent/conversation-state.d.ts +9 -0
- package/dist/agent/conversation-state.js +16 -0
- package/dist/agent/history-file.d.ts +6 -0
- package/dist/agent/history-file.js +1 -1
- package/dist/agent/host-types.d.ts +125 -0
- package/dist/agent/index.d.ts +12 -4
- package/dist/agent/index.js +358 -6
- package/dist/agent/nuclear-form.d.ts +7 -0
- package/dist/{extensions → agent}/providers/deepseek.d.ts +2 -2
- package/dist/{extensions → agent}/providers/deepseek.js +5 -4
- package/dist/{extensions → agent}/providers/openai-compatible.d.ts +2 -2
- package/dist/{extensions → agent}/providers/openai.d.ts +2 -2
- package/dist/{extensions → agent}/providers/openai.js +3 -2
- package/dist/{extensions → agent}/providers/openrouter.d.ts +2 -2
- package/dist/{extensions → agent}/providers/openrouter.js +4 -3
- package/dist/agent/skills.js +51 -7
- package/dist/agent/subagent.d.ts +1 -1
- package/dist/agent/system-prompt.js +14 -17
- package/dist/agent/tool-protocol.d.ts +1 -1
- package/dist/agent/tool-protocol.js +5 -3
- package/dist/agent/tool-registry.d.ts +9 -4
- package/dist/agent/tool-registry.js +27 -4
- package/dist/agent/tools/bash.d.ts +1 -1
- package/dist/agent/tools/bash.js +3 -2
- package/dist/agent/tools/edit-file.js +0 -1
- package/dist/agent/tools/glob.js +1 -1
- package/dist/agent/tools/grep.js +1 -1
- package/dist/agent/tools/pwsh.d.ts +1 -1
- package/dist/agent/tools/pwsh.js +1 -2
- package/dist/agent/tools/read-file.js +7 -4
- package/dist/agent/tools/write-file.js +0 -1
- package/dist/agent/types.d.ts +17 -2
- package/dist/cli/auth/cli.d.ts +1 -0
- package/dist/cli/auth/cli.js +216 -0
- package/dist/cli/auth/keys.d.ts +31 -0
- package/dist/cli/auth/keys.js +102 -0
- package/dist/{index.js → cli/index.js} +29 -32
- package/dist/{init.js → cli/init.js} +1 -1
- package/dist/{install.js → cli/install.js} +31 -2
- package/dist/cli/subcommands.d.ts +1 -0
- package/dist/cli/subcommands.js +17 -0
- package/dist/{event-bus.d.ts → core/event-bus.d.ts} +7 -13
- package/dist/{extension-loader.d.ts → core/extension-loader.d.ts} +1 -1
- package/dist/{extension-loader.js → core/extension-loader.js} +62 -70
- package/dist/{core.d.ts → core/index.d.ts} +18 -15
- package/dist/{core.js → core/index.js} +18 -92
- package/dist/{settings.d.ts → core/settings.d.ts} +7 -0
- package/dist/{settings.js → core/settings.js} +1 -0
- package/dist/core/types.d.ts +49 -0
- package/dist/core/types.js +1 -0
- package/dist/extensions/file-autocomplete.d.ts +1 -1
- package/dist/extensions/index.d.ts +7 -14
- package/dist/extensions/index.js +2 -19
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +7 -2
- package/dist/shell/host-types.d.ts +114 -0
- package/dist/shell/host-types.js +1 -0
- package/dist/shell/index.d.ts +8 -7
- package/dist/shell/index.js +58 -9
- package/dist/shell/input-handler.d.ts +7 -1
- package/dist/shell/input-handler.js +5 -2
- package/dist/shell/output-parser.d.ts +1 -1
- package/dist/{extensions → shell}/shell-context.d.ts +1 -1
- package/dist/{extensions → shell}/shell-context.js +18 -12
- package/dist/shell/shell.d.ts +6 -4
- package/dist/shell/shell.js +33 -109
- package/dist/shell/strategies/bash.d.ts +2 -0
- package/dist/shell/strategies/bash.js +68 -0
- package/dist/shell/strategies/fish.d.ts +2 -0
- package/dist/shell/strategies/fish.js +65 -0
- package/dist/shell/strategies/index.d.ts +13 -0
- package/dist/shell/strategies/index.js +17 -0
- package/dist/shell/strategies/types.d.ts +50 -0
- package/dist/shell/strategies/types.js +9 -0
- package/dist/shell/strategies/zsh.d.ts +2 -0
- package/dist/shell/strategies/zsh.js +72 -0
- package/dist/shell/tui-input-view.js +14 -3
- package/dist/{extensions → shell}/tui-renderer.d.ts +1 -1
- package/dist/{extensions → shell}/tui-renderer.js +27 -55
- package/dist/utils/box-frame.d.ts +4 -0
- package/dist/utils/box-frame.js +17 -6
- package/dist/utils/compositor.d.ts +1 -1
- package/dist/utils/compositor.js +2 -1
- package/dist/{executor.js → utils/executor.js} +1 -1
- package/dist/utils/floating-panel.d.ts +1 -1
- package/dist/utils/floating-panel.js +9 -4
- package/dist/utils/llm-client.d.ts +16 -26
- package/dist/utils/llm-client.js +15 -26
- package/dist/utils/llm-facade.d.ts +7 -3
- package/dist/utils/stream-transform.d.ts +1 -1
- package/dist/utils/terminal-buffer.d.ts +1 -1
- package/dist/utils/tool-display.js +4 -0
- package/dist/utils/tool-interactive.d.ts +1 -1
- package/dist/utils/tty.d.ts +7 -0
- package/dist/utils/tty.js +15 -0
- package/examples/extensions/ash-acp-bridge/README.md +4 -1
- package/examples/extensions/ash-acp-bridge/src/index.ts +654 -0
- package/examples/extensions/ash-mcp-bridge/index.ts +1 -1
- package/examples/extensions/ashi/README.md +250 -0
- package/examples/extensions/ashi/package.json +60 -0
- package/examples/extensions/ashi/src/autocomplete.ts +91 -0
- package/examples/extensions/ashi/src/capture.ts +34 -0
- package/examples/extensions/ashi/src/cli.ts +176 -0
- package/examples/extensions/ashi/src/commands.ts +82 -0
- package/examples/extensions/ashi/src/compaction.ts +157 -0
- package/examples/extensions/ashi/src/components.ts +327 -0
- package/examples/extensions/ashi/src/default-renderers.ts +153 -0
- package/examples/extensions/ashi/src/display-config.ts +62 -0
- package/examples/extensions/ashi/src/frontend.ts +735 -0
- package/examples/extensions/ashi/src/hooks.ts +136 -0
- package/examples/extensions/ashi/src/multi-session-store.ts +146 -0
- package/examples/extensions/ashi/src/session-commands.ts +76 -0
- package/examples/extensions/ashi/src/session-store.ts +264 -0
- package/examples/extensions/ashi/src/status-footer.ts +66 -0
- package/examples/extensions/ashi/src/theme.ts +151 -0
- package/examples/extensions/ashi/tsconfig.json +14 -0
- package/examples/extensions/emacs-buffer.ts +1 -1
- package/examples/extensions/interactive-prompts.ts +114 -69
- package/examples/extensions/latex-images.ts +3 -3
- package/examples/extensions/opencode-bridge/index.ts +1 -1
- package/examples/extensions/overlay-agent.ts +7 -5
- package/examples/extensions/peer-mesh.ts +1 -1
- package/examples/extensions/pi-bridge/index.ts +0 -1
- package/examples/extensions/questionnaire.ts +2 -1
- package/examples/extensions/rtk-proxy.ts +3 -3
- package/examples/extensions/solarized-theme.ts +3 -3
- package/examples/extensions/subagents.ts +6 -6
- package/examples/extensions/terminal-buffer.ts +1 -1
- package/examples/extensions/tmux-pane.ts +6 -4
- package/examples/extensions/tunnel-vision.ts +5 -5
- package/examples/extensions/user-shell.ts +1 -1
- package/examples/extensions/web-access.ts +5 -5
- package/package.json +38 -22
- package/dist/extensions/agent-backend.d.ts +0 -14
- package/dist/extensions/agent-backend.js +0 -307
- package/dist/types.d.ts +0 -227
- /package/dist/{types.js → agent/host-types.js} +0 -0
- /package/dist/{extensions → agent}/providers/openai-compatible.js +0 -0
- /package/dist/{index.d.ts → cli/index.d.ts} +0 -0
- /package/dist/{init.d.ts → cli/init.d.ts} +0 -0
- /package/dist/{install.d.ts → cli/install.d.ts} +0 -0
- /package/dist/{event-bus.js → core/event-bus.js} +0 -0
- /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+.
|
|
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 "
|
|
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.
|
package/dist/agent/agent-loop.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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, {
|
|
443
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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}"
|
|
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 &&
|
|
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.
|
|
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
|
|
1540
|
-
const
|
|
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;
|
package/dist/agent/index.d.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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;
|