agent-sh 0.13.7 → 0.14.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.
- package/README.md +1 -1
- package/dist/agent/agent-loop.d.ts +13 -17
- package/dist/agent/agent-loop.js +118 -224
- package/dist/agent/conversation-state.d.ts +1 -1
- package/dist/agent/events.d.ts +218 -0
- package/dist/agent/events.js +1 -0
- package/dist/agent/host-types.d.ts +20 -0
- package/dist/agent/index.d.ts +5 -9
- package/dist/agent/index.js +269 -167
- package/dist/agent/llm-facade.d.ts +13 -0
- package/dist/{utils → agent}/llm-facade.js +1 -1
- package/dist/agent/nuclear-form.d.ts +1 -1
- package/dist/agent/providers/deepseek.js +2 -5
- package/dist/agent/providers/openai-compatible.js +2 -2
- package/dist/agent/providers/openai.js +2 -5
- package/dist/agent/providers/openrouter.js +5 -5
- package/dist/agent/subagent.d.ts +1 -1
- package/dist/agent/tool-protocol.d.ts +1 -1
- package/dist/agent/tool-registry.d.ts +1 -1
- package/dist/cli/auth/cli.js +11 -6
- package/dist/cli/auth/discover.d.ts +5 -0
- package/dist/cli/auth/discover.js +25 -0
- package/dist/cli/auth/keys.d.ts +5 -2
- package/dist/cli/auth/keys.js +22 -2
- package/dist/cli/index.d.ts +16 -0
- package/dist/cli/index.js +12 -2
- package/dist/core/event-bus.d.ts +28 -371
- package/dist/core/extension-loader.js +6 -6
- package/dist/core/index.d.ts +10 -29
- package/dist/core/index.js +32 -84
- package/dist/extensions/index.d.ts +2 -1
- package/dist/extensions/index.js +1 -1
- package/dist/extensions/slash-commands/events.d.ts +18 -0
- package/dist/extensions/slash-commands/events.js +1 -0
- package/dist/extensions/slash-commands/index.d.ts +15 -0
- package/dist/extensions/{slash-commands.js → slash-commands/index.js} +4 -3
- package/dist/shell/events.d.ts +85 -0
- package/dist/shell/events.js +1 -0
- package/dist/shell/index.d.ts +1 -0
- package/dist/shell/index.js +6 -0
- package/dist/shell/tui-renderer.js +0 -1
- package/examples/extensions/ash-acp-bridge/src/index.ts +2 -2
- package/examples/extensions/ashi/package.json +1 -1
- package/examples/extensions/ollama.ts +47 -42
- package/examples/extensions/opencode-bridge/README.md +4 -0
- package/examples/extensions/opencode-bridge/index.ts +3 -1
- package/examples/extensions/pi-bridge/index.ts +3 -4
- package/examples/extensions/zai-coding-plan.ts +2 -6
- package/package.json +1 -1
- package/dist/extensions/slash-commands.d.ts +0 -2
- package/dist/utils/llm-facade.d.ts +0 -11
- /package/dist/{utils → agent}/llm-client.d.ts +0 -0
- /package/dist/{utils → agent}/llm-client.js +0 -0
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ See [Bring your own agent](#bring-your-own-agent) below for full details and the
|
|
|
72
72
|
|
|
73
73
|
### Option B: Use the built-in agent (ash)
|
|
74
74
|
|
|
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.
|
|
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. The built-in providers (openrouter, openai, openai-compatible, deepseek) register on startup; ash activates the first one with a usable key.
|
|
76
76
|
|
|
77
77
|
**Quickest path** — store a key once via the auth subcommand:
|
|
78
78
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { EventBus } from "../core/event-bus.js";
|
|
15
15
|
import type { AgentMode } from "./host-types.js";
|
|
16
|
-
import type { LlmClient } from "
|
|
16
|
+
import type { LlmClient } from "./llm-client.js";
|
|
17
17
|
import type { HandlerFunctions } from "../utils/handler-registry.js";
|
|
18
18
|
import type { AgentBackend, ToolDefinition } from "./types.js";
|
|
19
19
|
import { type HistoryAdapter } from "./history-file.js";
|
|
@@ -22,8 +22,7 @@ export interface AgentLoopConfig {
|
|
|
22
22
|
bus: EventBus;
|
|
23
23
|
llmClient: LlmClient;
|
|
24
24
|
handlers: HandlerFunctions;
|
|
25
|
-
|
|
26
|
-
initialModeIndex?: number;
|
|
25
|
+
initialMode?: AgentMode;
|
|
27
26
|
compositor?: Compositor;
|
|
28
27
|
/** Instance ID from core — ensures history entries match the ID in prompts. */
|
|
29
28
|
instanceId?: string;
|
|
@@ -35,14 +34,10 @@ export declare class AgentLoop implements AgentBackend {
|
|
|
35
34
|
private history;
|
|
36
35
|
private conversation;
|
|
37
36
|
private fileReadCache;
|
|
38
|
-
private
|
|
39
|
-
private currentModeIndex;
|
|
37
|
+
private activeMode;
|
|
40
38
|
private boundListeners;
|
|
41
39
|
private boundPipeListeners;
|
|
42
|
-
private ctorListeners;
|
|
43
|
-
private ctorPipeListeners;
|
|
44
40
|
private lastProjectSkillNames;
|
|
45
|
-
private lastAgentInfo;
|
|
46
41
|
private sessionStartTime;
|
|
47
42
|
private toolCallCounts;
|
|
48
43
|
private totalToolCalls;
|
|
@@ -72,8 +67,10 @@ export declare class AgentLoop implements AgentBackend {
|
|
|
72
67
|
registerTool(tool: ToolDefinition): void;
|
|
73
68
|
/** Unregister a tool by name. */
|
|
74
69
|
unregisterTool(name: string): void;
|
|
75
|
-
/** Get all registered tools. */
|
|
70
|
+
/** Get all registered tools (union of builtins + extension contributions). */
|
|
76
71
|
getTools(): ToolDefinition[];
|
|
72
|
+
/** Find a tool by name across the full pipe union. */
|
|
73
|
+
private findTool;
|
|
77
74
|
/** Instructions keyed by name, with extension attribution. */
|
|
78
75
|
private instructions;
|
|
79
76
|
/** Skills keyed by name, with extension attribution. */
|
|
@@ -87,20 +84,19 @@ export declare class AgentLoop implements AgentBackend {
|
|
|
87
84
|
registerSkill(name: string, description: string, filePath: string, extensionName: string): void;
|
|
88
85
|
removeSkill(name: string): void;
|
|
89
86
|
/**
|
|
90
|
-
* Build the system prompt
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* ### Skills
|
|
96
|
-
* ### Instructions
|
|
87
|
+
* Build the "Extensions" section of the system prompt. Includes tools,
|
|
88
|
+
* skills, and instructions contributed by extensions (i.e. anything
|
|
89
|
+
* registered via ctx.agent.registerTool/Skill/Instruction). AgentLoop's
|
|
90
|
+
* own builtins are excluded by name — they're documented elsewhere in
|
|
91
|
+
* the prompt or in the tool API params.
|
|
97
92
|
*/
|
|
98
93
|
buildExtensionSections(): string[];
|
|
99
94
|
kill(): void;
|
|
100
95
|
private cancel;
|
|
101
96
|
private reasoningParams;
|
|
102
97
|
private get currentMode();
|
|
103
|
-
private
|
|
98
|
+
private pullModes;
|
|
99
|
+
private emitIdentity;
|
|
104
100
|
private get currentModel();
|
|
105
101
|
/**
|
|
106
102
|
* Run compaction via the `conversation:compact` handler. After any
|
package/dist/agent/agent-loop.js
CHANGED
|
@@ -12,17 +12,6 @@ import { PACKAGE_VERSION } from "../utils/package-version.js";
|
|
|
12
12
|
import { wrapTrailingWithDynamicContext } from "../utils/message-utils.js";
|
|
13
13
|
import { getSettings, updateSettings } from "../core/settings.js";
|
|
14
14
|
import { createToolProtocol } from "./tool-protocol.js";
|
|
15
|
-
// Core tool factories
|
|
16
|
-
import { createBashTool } from "./tools/bash.js";
|
|
17
|
-
import { createPwshTool } from "./tools/pwsh.js";
|
|
18
|
-
import { findBash } from "../utils/executor.js";
|
|
19
|
-
import { createReadFileTool } from "./tools/read-file.js";
|
|
20
|
-
import { createWriteFileTool } from "./tools/write-file.js";
|
|
21
|
-
import { createEditFileTool } from "./tools/edit-file.js";
|
|
22
|
-
import { createGrepTool } from "./tools/grep.js";
|
|
23
|
-
import { createGlobTool } from "./tools/glob.js";
|
|
24
|
-
import { createLsTool } from "./tools/ls.js";
|
|
25
|
-
import { createListSkillsTool } from "./tools/list-skills.js";
|
|
26
15
|
import { discoverGlobalSkills, discoverProjectSkills } from "./skills.js";
|
|
27
16
|
/**
|
|
28
17
|
* Compact one-line summary of a tool description for the extension
|
|
@@ -52,15 +41,11 @@ export class AgentLoop {
|
|
|
52
41
|
toolRegistry;
|
|
53
42
|
history;
|
|
54
43
|
conversation;
|
|
55
|
-
fileReadCache
|
|
56
|
-
|
|
57
|
-
currentModeIndex = 0;
|
|
44
|
+
fileReadCache;
|
|
45
|
+
activeMode;
|
|
58
46
|
boundListeners = [];
|
|
59
47
|
boundPipeListeners = [];
|
|
60
|
-
ctorListeners = [];
|
|
61
|
-
ctorPipeListeners = [];
|
|
62
48
|
lastProjectSkillNames = new Set();
|
|
63
|
-
lastAgentInfo = null;
|
|
64
49
|
// ── Session telemetry — behavioral self-awareness ──────────────
|
|
65
50
|
// Every ash deserves to know what it's been doing. This tracks the
|
|
66
51
|
// agent's own behavioral patterns across the session: which tools
|
|
@@ -103,18 +88,14 @@ export class AgentLoop {
|
|
|
103
88
|
this.compositor = config.compositor ?? null;
|
|
104
89
|
this.instanceId = config.instanceId ?? "unknown";
|
|
105
90
|
this.toolRegistry = new ToolRegistry(this.handlers);
|
|
91
|
+
this.fileReadCache = this.handlers.call("agent:file-read-cache");
|
|
106
92
|
// Shell-history-shaped log. Default writes go through the advisable
|
|
107
93
|
// `history:append` handler registered below; extensions swap the
|
|
108
94
|
// backend without touching this wiring.
|
|
109
95
|
const filePath = process.env.AGENT_SH_HISTORY_FILE || getSettings().historyFilePath;
|
|
110
96
|
this.history = config.history ?? new HistoryFile({ instanceId: this.instanceId, filePath });
|
|
111
97
|
this.conversation = new ConversationState(this.handlers, this.instanceId);
|
|
112
|
-
|
|
113
|
-
// empty array (agent-backend does this pre-resolution).
|
|
114
|
-
this.modes = config.modes?.length
|
|
115
|
-
? config.modes
|
|
116
|
-
: [{ model: config.llmClient.model }];
|
|
117
|
-
this.currentModeIndex = config.initialModeIndex ?? 0;
|
|
98
|
+
this.activeMode = config.initialMode ?? { model: config.llmClient.model };
|
|
118
99
|
// Tool protocol — controls how tools are presented to the LLM
|
|
119
100
|
this.toolProtocol = createToolProtocol(getSettings().toolMode ?? "api", getSettings().coreTools ?? []);
|
|
120
101
|
// Register core tools
|
|
@@ -125,87 +106,6 @@ export class AgentLoop {
|
|
|
125
106
|
this.registerTool(t);
|
|
126
107
|
// Register handlers — extensions can advise these
|
|
127
108
|
this.registerHandlers();
|
|
128
|
-
// Subscribe to bus-based tool/instruction registration from extensions.
|
|
129
|
-
// These must be in the constructor (not wire()) because extensions call
|
|
130
|
-
// registerTool() during activate(), before activateBackend() calls wire().
|
|
131
|
-
const onCtor = (event, fn) => {
|
|
132
|
-
this.bus.on(event, fn);
|
|
133
|
-
this.ctorListeners.push({ event, fn });
|
|
134
|
-
};
|
|
135
|
-
onCtor("agent:register-tool", ({ tool, extensionName }) => {
|
|
136
|
-
this.registerTool(tool);
|
|
137
|
-
if (extensionName)
|
|
138
|
-
this.toolExtensions.set(tool.name, extensionName);
|
|
139
|
-
});
|
|
140
|
-
onCtor("agent:unregister-tool", ({ name }) => {
|
|
141
|
-
this.unregisterTool(name);
|
|
142
|
-
this.toolExtensions.delete(name);
|
|
143
|
-
});
|
|
144
|
-
onCtor("agent:register-instruction", ({ name, text, extensionName }) => this.registerInstruction(name, text, extensionName));
|
|
145
|
-
onCtor("agent:remove-instruction", ({ name }) => this.removeInstruction(name));
|
|
146
|
-
onCtor("agent:register-skill", ({ name, description, filePath, extensionName }) => this.registerSkill(name, description, filePath, extensionName));
|
|
147
|
-
onCtor("agent:remove-skill", ({ name }) => this.removeSkill(name));
|
|
148
|
-
// Provider registration from user extensions (e.g. openrouter.ts) fires
|
|
149
|
-
// during extension activation, which happens before wire(). Subscribe
|
|
150
|
-
// here in the ctor so late-registered modes aren't dropped.
|
|
151
|
-
onCtor("config:add-modes", ({ modes: extra }) => {
|
|
152
|
-
const providers = new Set(extra.map((m) => m.provider).filter(Boolean));
|
|
153
|
-
const prev = this.modes[this.currentModeIndex];
|
|
154
|
-
// Keep the active mode even if the re-registration drops it (persisted
|
|
155
|
-
// model missing from a refreshed catalog) — otherwise currentModeIndex
|
|
156
|
-
// slips to modes[0] and the next stream() call uses a different model
|
|
157
|
-
// mid-turn.
|
|
158
|
-
const activePreserved = prev &&
|
|
159
|
-
prev.provider &&
|
|
160
|
-
providers.has(prev.provider) &&
|
|
161
|
-
!extra.some((m) => m.model === prev.model && m.provider === prev.provider);
|
|
162
|
-
this.modes = [
|
|
163
|
-
...this.modes.filter((m) => {
|
|
164
|
-
if (activePreserved && m === prev)
|
|
165
|
-
return true;
|
|
166
|
-
return !m.provider || !providers.has(m.provider);
|
|
167
|
-
}),
|
|
168
|
-
...extra,
|
|
169
|
-
];
|
|
170
|
-
if (prev) {
|
|
171
|
-
const newIdx = this.modes.findIndex((m) => m.model === prev.model && m.provider === prev.provider);
|
|
172
|
-
if (newIdx !== -1) {
|
|
173
|
-
this.currentModeIndex = newIdx;
|
|
174
|
-
const next = this.modes[newIdx];
|
|
175
|
-
if (next.providerConfig && next.providerConfig !== prev.providerConfig) {
|
|
176
|
-
this.llmClient.reconfigure({ ...next.providerConfig, model: next.model });
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
if (activePreserved && prev) {
|
|
181
|
-
this.bus.emit("ui:info", {
|
|
182
|
-
message: `${prev.provider}:${prev.model} is not in the refreshed catalog — keeping it active until you /model to another.`,
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
this.emitAgentInfoIfChanged();
|
|
186
|
-
this.bus.emit("config:changed", {});
|
|
187
|
-
});
|
|
188
|
-
// Fires before wire() too — agent-backend emits this from
|
|
189
|
-
// `core:extensions-loaded` to replace the placeholder mode list.
|
|
190
|
-
onCtor("config:set-modes", ({ modes: newModes, activeIndex }) => {
|
|
191
|
-
this.modes = newModes;
|
|
192
|
-
const inRange = activeIndex != null && activeIndex >= 0 && activeIndex < newModes.length;
|
|
193
|
-
this.currentModeIndex = inRange ? activeIndex : 0;
|
|
194
|
-
const m = newModes[this.currentModeIndex];
|
|
195
|
-
if (!m)
|
|
196
|
-
return;
|
|
197
|
-
if (m.providerConfig) {
|
|
198
|
-
this.llmClient.reconfigure({ ...m.providerConfig, model: m.model });
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
this.llmClient.model = m.model;
|
|
202
|
-
}
|
|
203
|
-
this.emitAgentInfoIfChanged();
|
|
204
|
-
this.bus.emit("config:changed", {});
|
|
205
|
-
});
|
|
206
|
-
const getToolsPipe = () => ({ tools: this.getTools() });
|
|
207
|
-
this.bus.onPipe("agent:get-tools", getToolsPipe);
|
|
208
|
-
this.ctorPipeListeners.push({ event: "agent:get-tools", fn: getToolsPipe });
|
|
209
109
|
}
|
|
210
110
|
/** Subscribe to bus events — activates this backend. */
|
|
211
111
|
wire() {
|
|
@@ -221,6 +121,27 @@ export class AgentLoop {
|
|
|
221
121
|
this.bus.onPipeAsync(event, fn);
|
|
222
122
|
this.boundPipeListeners.push({ event, fn, async: true });
|
|
223
123
|
};
|
|
124
|
+
onPipe("agent:tools", (acc) => {
|
|
125
|
+
// Read internal storage, NOT this.getTools() — that queries the
|
|
126
|
+
// pipe and would recurse.
|
|
127
|
+
for (const tool of this.toolRegistry.allView())
|
|
128
|
+
acc.tools.push(tool);
|
|
129
|
+
return acc;
|
|
130
|
+
});
|
|
131
|
+
onPipe("agent:instructions", (acc) => {
|
|
132
|
+
for (const [name] of this.instructions) {
|
|
133
|
+
const text = this.handlers.call(`instruction:${name}`);
|
|
134
|
+
acc.instructions.push({ name, text });
|
|
135
|
+
}
|
|
136
|
+
return acc;
|
|
137
|
+
});
|
|
138
|
+
onPipe("agent:skills", (acc) => {
|
|
139
|
+
for (const [name] of this.skills) {
|
|
140
|
+
const view = this.handlers.call(`skill:${name}:view`);
|
|
141
|
+
acc.skills.push({ name, description: view.description, filePath: view.filePath });
|
|
142
|
+
}
|
|
143
|
+
return acc;
|
|
144
|
+
});
|
|
224
145
|
on("agent:submit", ({ query }) => {
|
|
225
146
|
this.handleQuery(query).catch(() => { });
|
|
226
147
|
});
|
|
@@ -235,29 +156,29 @@ export class AgentLoop {
|
|
|
235
156
|
const atIdx = target.lastIndexOf("@");
|
|
236
157
|
const modelId = atIdx > 0 ? target.slice(0, atIdx) : target;
|
|
237
158
|
const providerHint = atIdx > 0 ? target.slice(atIdx + 1) : undefined;
|
|
238
|
-
const
|
|
239
|
-
|
|
159
|
+
const modes = this.pullModes();
|
|
160
|
+
const found = modes.find((m) => m.model === modelId && (!providerHint || m.provider === providerHint));
|
|
161
|
+
if (!found) {
|
|
240
162
|
this.bus.emit("ui:error", { message: `Unknown model: ${target}` });
|
|
241
163
|
return;
|
|
242
164
|
}
|
|
243
|
-
this.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
this.llmClient.reconfigure({ ...m.providerConfig, model: m.model });
|
|
165
|
+
this.activeMode = found;
|
|
166
|
+
if (found.providerConfig) {
|
|
167
|
+
this.llmClient.reconfigure({ ...found.providerConfig, model: found.model });
|
|
247
168
|
}
|
|
248
169
|
else {
|
|
249
|
-
this.llmClient.model =
|
|
170
|
+
this.llmClient.model = found.model;
|
|
250
171
|
}
|
|
251
|
-
const label =
|
|
252
|
-
this.
|
|
172
|
+
const label = found.provider ? `${found.provider}: ${found.model}` : found.model;
|
|
173
|
+
this.emitIdentity();
|
|
253
174
|
// Persist as the new default — selection survives restart.
|
|
254
175
|
// Safe even for dynamic providers: agent-backend defers mode
|
|
255
176
|
// resolution to `core:extensions-loaded`, so the extension gets
|
|
256
177
|
// to re-register before the persisted default is looked up.
|
|
257
|
-
if (
|
|
178
|
+
if (found.provider) {
|
|
258
179
|
updateSettings({
|
|
259
|
-
defaultProvider:
|
|
260
|
-
providers: { [
|
|
180
|
+
defaultProvider: found.provider,
|
|
181
|
+
providers: { [found.provider]: { defaultModel: found.model } },
|
|
261
182
|
});
|
|
262
183
|
this.bus.emit("ui:info", { message: `Model: ${label} (saved as default)` });
|
|
263
184
|
}
|
|
@@ -266,10 +187,33 @@ export class AgentLoop {
|
|
|
266
187
|
}
|
|
267
188
|
this.bus.emit("config:changed", {});
|
|
268
189
|
});
|
|
190
|
+
on("agent:modes-changed", () => {
|
|
191
|
+
const modes = this.pullModes();
|
|
192
|
+
const prev = this.activeMode;
|
|
193
|
+
const fresh = modes.find((m) => m.model === prev.model && m.provider === prev.provider);
|
|
194
|
+
if (fresh) {
|
|
195
|
+
this.activeMode = fresh;
|
|
196
|
+
if (fresh.providerConfig && fresh.providerConfig !== prev.providerConfig) {
|
|
197
|
+
this.llmClient.reconfigure({ ...fresh.providerConfig, model: fresh.model });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else if (prev.provider) {
|
|
201
|
+
// Ghost: keep prev active so mid-turn stream() doesn't switch models.
|
|
202
|
+
this.bus.emit("ui:info", {
|
|
203
|
+
message: `${prev.provider}:${prev.model} is not in the refreshed catalog — keeping it active until you /model to another.`,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
this.emitIdentity();
|
|
207
|
+
this.bus.emit("config:changed", {});
|
|
208
|
+
});
|
|
269
209
|
onPipe("config:get-models", () => {
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
|
|
210
|
+
const modes = this.pullModes();
|
|
211
|
+
const models = modes.map((m) => ({ model: m.model, provider: m.provider ?? "" }));
|
|
212
|
+
// Surface a ghost active mode so /model still shows it.
|
|
213
|
+
if (!modes.some((m) => m.model === this.activeMode.model && m.provider === this.activeMode.provider)) {
|
|
214
|
+
models.push({ model: this.activeMode.model, provider: this.activeMode.provider ?? "" });
|
|
215
|
+
}
|
|
216
|
+
const active = { model: this.activeMode.model, provider: this.activeMode.provider ?? "" };
|
|
273
217
|
return { models, active };
|
|
274
218
|
});
|
|
275
219
|
on("config:set-thinking", ({ level }) => {
|
|
@@ -368,8 +312,7 @@ export class AgentLoop {
|
|
|
368
312
|
this.bus.emit("conversation:message-appended", { role: "system", content: note });
|
|
369
313
|
}
|
|
370
314
|
});
|
|
371
|
-
this.
|
|
372
|
-
this.emitAgentInfoIfChanged();
|
|
315
|
+
this.emitIdentity();
|
|
373
316
|
}
|
|
374
317
|
/** Unsubscribe from bus events — deactivates this backend. */
|
|
375
318
|
unwire() {
|
|
@@ -393,9 +336,13 @@ export class AgentLoop {
|
|
|
393
336
|
unregisterTool(name) {
|
|
394
337
|
this.toolRegistry.unregister(name);
|
|
395
338
|
}
|
|
396
|
-
/** Get all registered tools. */
|
|
339
|
+
/** Get all registered tools (union of builtins + extension contributions). */
|
|
397
340
|
getTools() {
|
|
398
|
-
return this.
|
|
341
|
+
return this.bus.emitPipe("agent:tools", { tools: [] }).tools;
|
|
342
|
+
}
|
|
343
|
+
/** Find a tool by name across the full pipe union. */
|
|
344
|
+
findTool(name) {
|
|
345
|
+
return this.getTools().find((t) => t.name === name);
|
|
399
346
|
}
|
|
400
347
|
// ── Extension instructions, skills & tool tracking ──────────────────
|
|
401
348
|
/** Instructions keyed by name, with extension attribution. */
|
|
@@ -426,73 +373,41 @@ export class AgentLoop {
|
|
|
426
373
|
// Handler entry retained so external advisors survive a reload of the owner.
|
|
427
374
|
}
|
|
428
375
|
/**
|
|
429
|
-
* Build the system prompt
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
* ### Skills
|
|
435
|
-
* ### Instructions
|
|
376
|
+
* Build the "Extensions" section of the system prompt. Includes tools,
|
|
377
|
+
* skills, and instructions contributed by extensions (i.e. anything
|
|
378
|
+
* registered via ctx.agent.registerTool/Skill/Instruction). AgentLoop's
|
|
379
|
+
* own builtins are excluded by name — they're documented elsewhere in
|
|
380
|
+
* the prompt or in the tool API params.
|
|
436
381
|
*/
|
|
437
382
|
buildExtensionSections() {
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if (builtinTools.has(tool.name))
|
|
463
|
-
continue;
|
|
464
|
-
const extName = this.toolExtensions.get(tool.name);
|
|
465
|
-
if (!extName)
|
|
466
|
-
continue;
|
|
467
|
-
ensure(extName).tools.push({ name: tool.name, description: summarizeDescription(tool.description) });
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
// Render
|
|
471
|
-
return [...groups.entries()]
|
|
472
|
-
.filter(([, g]) => g.tools.length + g.skills.length + g.instructions.length > 0)
|
|
473
|
-
.map(([name, g]) => {
|
|
474
|
-
const parts = [];
|
|
475
|
-
if (g.tools.length > 0)
|
|
476
|
-
parts.push("### Tools\n" + g.tools.map(t => `${t.name} — ${t.description}`).join("\n"));
|
|
477
|
-
if (g.skills.length > 0)
|
|
478
|
-
parts.push("### Skills\n" + g.skills.map(s => `${s.name}: ${s.description}\n → ${s.filePath}`).join("\n\n"));
|
|
479
|
-
if (g.instructions.length > 0)
|
|
480
|
-
parts.push("### Instructions\n" + g.instructions.map(i => i.text).join("\n\n"));
|
|
481
|
-
return `## ${name}\n${parts.join("\n\n")}`;
|
|
482
|
-
});
|
|
383
|
+
const BUILTIN_TOOLS = new Set([
|
|
384
|
+
"bash", "read_file", "write_file", "edit_file", "grep", "glob", "ls",
|
|
385
|
+
"list_skills",
|
|
386
|
+
]);
|
|
387
|
+
const BUILTIN_INSTRUCTIONS = new Set(["recall-guidance"]);
|
|
388
|
+
const BUILTIN_SKILLS = new Set();
|
|
389
|
+
const allTools = this.bus.emitPipe("agent:tools", { tools: [] }).tools;
|
|
390
|
+
const allInstructions = this.bus.emitPipe("agent:instructions", { instructions: [] }).instructions;
|
|
391
|
+
const allSkills = this.bus.emitPipe("agent:skills", { skills: [] }).skills;
|
|
392
|
+
const extTools = this.toolProtocol.mode === "api"
|
|
393
|
+
? []
|
|
394
|
+
: allTools.filter((t) => !BUILTIN_TOOLS.has(t.name));
|
|
395
|
+
const extInstructions = allInstructions.filter((i) => !BUILTIN_INSTRUCTIONS.has(i.name));
|
|
396
|
+
const extSkills = allSkills.filter((s) => !BUILTIN_SKILLS.has(s.name));
|
|
397
|
+
if (extTools.length + extInstructions.length + extSkills.length === 0)
|
|
398
|
+
return [];
|
|
399
|
+
const parts = [];
|
|
400
|
+
if (extTools.length > 0)
|
|
401
|
+
parts.push("### Tools\n" + extTools.map(t => `${t.name} — ${summarizeDescription(t.description)}`).join("\n"));
|
|
402
|
+
if (extSkills.length > 0)
|
|
403
|
+
parts.push("### Skills\n" + extSkills.map(s => `${s.name}: ${s.description}\n → ${s.filePath}`).join("\n\n"));
|
|
404
|
+
if (extInstructions.length > 0)
|
|
405
|
+
parts.push("### Instructions\n" + extInstructions.map(i => i.text).join("\n\n"));
|
|
406
|
+
return [`## Extensions\n${parts.join("\n\n")}`];
|
|
483
407
|
}
|
|
484
408
|
kill() {
|
|
485
409
|
this.cancel();
|
|
486
410
|
this.unwire();
|
|
487
|
-
// Clean up constructor-level bus subscriptions
|
|
488
|
-
for (const { event, fn } of this.ctorListeners) {
|
|
489
|
-
this.bus.off(event, fn);
|
|
490
|
-
}
|
|
491
|
-
this.ctorListeners = [];
|
|
492
|
-
for (const { event, fn } of this.ctorPipeListeners) {
|
|
493
|
-
this.bus.offPipe(event, fn);
|
|
494
|
-
}
|
|
495
|
-
this.ctorPipeListeners = [];
|
|
496
411
|
}
|
|
497
412
|
cancel() {
|
|
498
413
|
this.abortController?.abort();
|
|
@@ -511,17 +426,18 @@ export class AgentLoop {
|
|
|
511
426
|
return { reasoning_effort: effort };
|
|
512
427
|
}
|
|
513
428
|
get currentMode() {
|
|
514
|
-
return this.
|
|
429
|
+
return this.activeMode;
|
|
515
430
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
return;
|
|
520
|
-
const prev = this.lastAgentInfo;
|
|
521
|
-
if (prev && prev.model === m.model && prev.provider === m.provider && prev.contextWindow === m.contextWindow) {
|
|
522
|
-
return;
|
|
431
|
+
pullModes() {
|
|
432
|
+
try {
|
|
433
|
+
return this.handlers.call("agent:get-modes") ?? [];
|
|
523
434
|
}
|
|
524
|
-
|
|
435
|
+
catch {
|
|
436
|
+
return [];
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
emitIdentity() {
|
|
440
|
+
const m = this.activeMode;
|
|
525
441
|
this.bus.emit("agent:info", {
|
|
526
442
|
name: "ash",
|
|
527
443
|
version: PACKAGE_VERSION,
|
|
@@ -531,7 +447,7 @@ export class AgentLoop {
|
|
|
531
447
|
});
|
|
532
448
|
}
|
|
533
449
|
get currentModel() {
|
|
534
|
-
return this.
|
|
450
|
+
return this.activeMode.model;
|
|
535
451
|
}
|
|
536
452
|
/**
|
|
537
453
|
* Run compaction via the `conversation:compact` handler. After any
|
|
@@ -637,30 +553,8 @@ export class AgentLoop {
|
|
|
637
553
|
return `${raw}${context}`;
|
|
638
554
|
}
|
|
639
555
|
registerCoreTools() {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
const env = {};
|
|
643
|
-
for (const [k, v] of Object.entries(process.env)) {
|
|
644
|
-
if (v !== undefined)
|
|
645
|
-
env[k] = v;
|
|
646
|
-
}
|
|
647
|
-
return env;
|
|
648
|
-
};
|
|
649
|
-
if (findBash() !== null) {
|
|
650
|
-
this.toolRegistry.register(createBashTool({ getCwd, getEnv, bus: this.bus }));
|
|
651
|
-
}
|
|
652
|
-
if (process.platform === "win32") {
|
|
653
|
-
this.toolRegistry.register(createPwshTool({ getCwd, getEnv, bus: this.bus }));
|
|
654
|
-
}
|
|
655
|
-
this.toolRegistry.register(createReadFileTool(getCwd, this.fileReadCache));
|
|
656
|
-
this.toolRegistry.register(createWriteFileTool(getCwd));
|
|
657
|
-
this.toolRegistry.register(createEditFileTool(getCwd));
|
|
658
|
-
this.toolRegistry.register(createGrepTool(getCwd));
|
|
659
|
-
this.toolRegistry.register(createGlobTool(getCwd));
|
|
660
|
-
this.toolRegistry.register(createLsTool(getCwd));
|
|
661
|
-
this.toolRegistry.register(createListSkillsTool(getCwd));
|
|
662
|
-
// conversation_recall — browse/search/expand evicted turns from
|
|
663
|
-
// the in-session archive and the persistent history file.
|
|
556
|
+
// Stateless core tools register in agentBackend; conversation_recall
|
|
557
|
+
// stays here because it needs this.conversation.
|
|
664
558
|
this.toolRegistry.register({
|
|
665
559
|
name: "conversation_recall",
|
|
666
560
|
displayName: "recall",
|
|
@@ -1112,7 +1006,7 @@ export class AgentLoop {
|
|
|
1112
1006
|
{
|
|
1113
1007
|
const groupMap = new Map();
|
|
1114
1008
|
for (const tc of toolCalls) {
|
|
1115
|
-
const tool = this.
|
|
1009
|
+
const tool = this.findTool(tc.name);
|
|
1116
1010
|
const kind = tool?.getDisplayInfo?.((() => { try {
|
|
1117
1011
|
return JSON.parse(tc.argumentsJson);
|
|
1118
1012
|
}
|
|
@@ -1153,9 +1047,9 @@ export class AgentLoop {
|
|
|
1153
1047
|
}
|
|
1154
1048
|
}
|
|
1155
1049
|
catch { /* not an error payload, continue */ }
|
|
1156
|
-
const tool = this.
|
|
1050
|
+
const tool = this.findTool(tc.name);
|
|
1157
1051
|
if (!tool) {
|
|
1158
|
-
const available = this.
|
|
1052
|
+
const available = this.getTools().map((t) => t.name).join(", ");
|
|
1159
1053
|
collectedResults.push({
|
|
1160
1054
|
callId: tc.id, toolName: tc.name,
|
|
1161
1055
|
content: `Unknown tool "${tc.name}". Available tools: ${available}`,
|
|
@@ -1256,7 +1150,7 @@ export class AgentLoop {
|
|
|
1256
1150
|
const parallel = [];
|
|
1257
1151
|
const sequential = [];
|
|
1258
1152
|
for (const tc of toolCalls) {
|
|
1259
|
-
const tool = this.
|
|
1153
|
+
const tool = this.findTool(tc.name);
|
|
1260
1154
|
if (tool && !tool.modifiesFiles) {
|
|
1261
1155
|
parallel.push(tc);
|
|
1262
1156
|
}
|
|
@@ -1501,7 +1395,7 @@ export class AgentLoop {
|
|
|
1501
1395
|
const reasoningDetailsByIndex = new Map();
|
|
1502
1396
|
const pendingToolCalls = [];
|
|
1503
1397
|
// Tool protocol controls what goes in the API tools param vs dynamic context
|
|
1504
|
-
const toolView = this.
|
|
1398
|
+
const toolView = this.getTools();
|
|
1505
1399
|
const apiTools = this.toolProtocol.getApiTools(toolView);
|
|
1506
1400
|
const toolPrompt = this.toolProtocol.getToolPrompt(toolView);
|
|
1507
1401
|
// Dynamic context rides on the trailing message — see
|
|
@@ -1513,7 +1407,7 @@ export class AgentLoop {
|
|
|
1513
1407
|
// Let extensions transform the message array (compact, summarize, filter, etc.)
|
|
1514
1408
|
const messages = this.handlers.call("conversation:prepare", rawMessages);
|
|
1515
1409
|
// Stream filter strips tool tags from display (inline mode only)
|
|
1516
|
-
const streamFilter = this.toolProtocol.createStreamFilter(this.
|
|
1410
|
+
const streamFilter = this.toolProtocol.createStreamFilter(this.getTools().map((t) => t.name));
|
|
1517
1411
|
const requestParams = {
|
|
1518
1412
|
messages,
|
|
1519
1413
|
tools: apiTools,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChatCompletionMessageParam } from "
|
|
1
|
+
import type { ChatCompletionMessageParam } from "./llm-client.js";
|
|
2
2
|
import { type NuclearEntry } from "./nuclear-form.js";
|
|
3
3
|
import type { HandlerFunctions } from "../utils/handler-registry.js";
|
|
4
4
|
/** Search hit shape returned by the `history:search` handler. */
|