agent-sh 0.8.0 → 0.9.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 +25 -34
- package/dist/agent/agent-loop.d.ts +29 -6
- package/dist/agent/agent-loop.js +177 -59
- package/dist/agent/conversation-state.d.ts +3 -1
- package/dist/agent/conversation-state.js +6 -2
- package/dist/agent/nuclear-form.js +5 -4
- package/dist/agent/system-prompt.d.ts +4 -5
- package/dist/agent/system-prompt.js +12 -28
- package/dist/{token-budget.js → agent/token-budget.js} +1 -1
- package/dist/agent/tool-protocol.d.ts +83 -0
- package/dist/agent/tool-protocol.js +386 -0
- package/dist/agent/types.d.ts +21 -1
- package/dist/core.d.ts +7 -7
- package/dist/core.js +76 -194
- package/dist/event-bus.d.ts +26 -0
- package/dist/event-bus.js +20 -1
- package/dist/extension-loader.d.ts +5 -0
- package/dist/extension-loader.js +104 -17
- package/dist/extensions/agent-backend.d.ts +13 -0
- package/dist/extensions/agent-backend.js +167 -0
- package/dist/extensions/command-suggest.d.ts +3 -3
- package/dist/extensions/command-suggest.js +4 -3
- package/dist/extensions/index.d.ts +19 -0
- package/dist/extensions/index.js +25 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +16 -1
- package/dist/extensions/terminal-buffer.d.ts +1 -1
- package/dist/extensions/terminal-buffer.js +13 -4
- package/dist/extensions/tui-renderer.js +63 -43
- package/dist/index.js +14 -20
- package/dist/settings.d.ts +6 -0
- package/dist/settings.js +4 -1
- package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +1 -1
- package/dist/{input-handler.js → shell/input-handler.js} +60 -43
- package/dist/{output-parser.d.ts → shell/output-parser.d.ts} +1 -1
- package/dist/{output-parser.js → shell/output-parser.js} +1 -1
- package/dist/{shell.d.ts → shell/shell.d.ts} +8 -2
- package/dist/{shell.js → shell/shell.js} +20 -6
- package/dist/types.d.ts +49 -10
- package/dist/utils/compositor.d.ts +62 -0
- package/dist/utils/compositor.js +88 -0
- package/dist/utils/diff-renderer.js +92 -4
- package/dist/utils/floating-panel.d.ts +2 -0
- package/dist/utils/floating-panel.js +30 -14
- package/dist/utils/handler-registry.d.ts +26 -10
- package/dist/utils/handler-registry.js +52 -16
- package/dist/utils/line-editor.d.ts +23 -3
- package/dist/utils/line-editor.js +180 -42
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +1 -1
- package/dist/utils/message-utils.d.ts +35 -0
- package/dist/utils/message-utils.js +75 -0
- package/dist/utils/terminal-buffer.d.ts +5 -1
- package/dist/utils/terminal-buffer.js +18 -2
- package/dist/utils/tool-interactive.d.ts +12 -0
- package/dist/utils/tool-interactive.js +53 -0
- package/examples/extensions/ash-acp-bridge/README.md +39 -0
- package/examples/extensions/ash-acp-bridge/package.json +23 -0
- package/examples/extensions/ash-acp-bridge/src/index.ts +571 -0
- package/examples/extensions/ash-acp-bridge/tsconfig.json +14 -0
- package/examples/extensions/ash-mcp-bridge/README.md +72 -0
- package/examples/extensions/ash-mcp-bridge/index.ts +154 -0
- package/examples/extensions/ash-mcp-bridge/package.json +9 -0
- package/examples/extensions/interactive-prompts.ts +82 -110
- package/examples/extensions/overlay-agent.ts +84 -38
- package/examples/extensions/peer-mesh.ts +450 -0
- package/examples/extensions/questionnaire.ts +249 -0
- package/examples/extensions/tmux-pane.ts +307 -0
- package/examples/extensions/web-access.ts +327 -0
- package/package.json +9 -1
- package/dist/extensions/overlay-agent.d.ts +0 -14
- package/dist/extensions/overlay-agent.js +0 -147
- package/examples/extensions/terminal-buffer.ts +0 -184
- /package/dist/{token-budget.d.ts → agent/token-budget.d.ts} +0 -0
package/dist/core.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Core kernel — the minimum viable agent-sh.
|
|
3
3
|
*
|
|
4
|
-
* Wires up EventBus + ContextManager
|
|
4
|
+
* Wires up EventBus + ContextManager without any frontend or agent backend.
|
|
5
5
|
* Consumers attach their own I/O (Shell, WebSocket, REST, tests) by
|
|
6
6
|
* subscribing to bus events.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Agent backends are loaded as extensions and register themselves via
|
|
9
|
+
* the agent:register-backend bus event. The built-in "ash" backend is
|
|
10
|
+
* loaded from src/extensions/agent-backend.ts.
|
|
11
11
|
*
|
|
12
12
|
* Usage:
|
|
13
13
|
* import { createCore } from "agent-sh";
|
|
@@ -18,15 +18,13 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { EventBus } from "./event-bus.js";
|
|
20
20
|
import { ContextManager } from "./context-manager.js";
|
|
21
|
-
import { AgentLoop } from "./agent/agent-loop.js";
|
|
22
|
-
import { LlmClient } from "./utils/llm-client.js";
|
|
23
21
|
import { setPalette } from "./utils/palette.js";
|
|
24
22
|
import * as streamTransform from "./utils/stream-transform.js";
|
|
25
23
|
import * as settingsMod from "./settings.js";
|
|
26
|
-
import { resolveProvider, getProviderNames } from "./settings.js";
|
|
27
24
|
import { HandlerRegistry } from "./utils/handler-registry.js";
|
|
28
25
|
import { TerminalBuffer } from "./utils/terminal-buffer.js";
|
|
29
|
-
import
|
|
26
|
+
import crypto from "node:crypto";
|
|
27
|
+
import { DefaultCompositor, StdoutSurface } from "./utils/compositor.js";
|
|
30
28
|
// Re-export types that library consumers need
|
|
31
29
|
export { EventBus } from "./event-bus.js";
|
|
32
30
|
export { palette, setPalette, resetPalette } from "./utils/palette.js";
|
|
@@ -36,92 +34,26 @@ export function createCore(config) {
|
|
|
36
34
|
const bus = new EventBus();
|
|
37
35
|
const handlers = new HandlerRegistry();
|
|
38
36
|
const contextManager = new ContextManager(bus, handlers);
|
|
39
|
-
|
|
37
|
+
const instanceId = crypto.randomBytes(2).toString("hex");
|
|
40
38
|
const settings = settingsMod.getSettings();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const p = resolveProvider(name);
|
|
45
|
-
if (p)
|
|
46
|
-
providerRegistry.set(name, p);
|
|
47
|
-
}
|
|
48
|
-
const providerName = config.provider ?? settings.defaultProvider;
|
|
49
|
-
if (providerName) {
|
|
50
|
-
activeProvider = providerRegistry.get(providerName) ?? null;
|
|
51
|
-
}
|
|
52
|
-
// Build flat modes list across all providers
|
|
53
|
-
const buildModes = () => {
|
|
54
|
-
const allModes = [];
|
|
55
|
-
for (const [id, p] of providerRegistry) {
|
|
56
|
-
if (!p.apiKey)
|
|
57
|
-
continue;
|
|
58
|
-
for (const model of p.models) {
|
|
59
|
-
const mc = p.modelCapabilities?.get(model);
|
|
60
|
-
allModes.push({
|
|
61
|
-
model,
|
|
62
|
-
provider: id,
|
|
63
|
-
providerConfig: { apiKey: p.apiKey, baseURL: p.baseURL },
|
|
64
|
-
contextWindow: mc?.contextWindow ?? p.contextWindow,
|
|
65
|
-
reasoning: mc?.reasoning,
|
|
66
|
-
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return allModes;
|
|
71
|
-
};
|
|
72
|
-
const effectiveApiKey = config.apiKey ?? activeProvider?.apiKey;
|
|
73
|
-
const effectiveBaseURL = config.baseURL ?? activeProvider?.baseURL;
|
|
74
|
-
const effectiveModel = config.model ?? activeProvider?.defaultModel;
|
|
75
|
-
let modes = buildModes();
|
|
76
|
-
if (modes.length === 0 && effectiveApiKey && effectiveModel) {
|
|
77
|
-
modes = [{ model: effectiveModel }];
|
|
78
|
-
}
|
|
79
|
-
const initialModeIndex = Math.max(0, modes.findIndex((m) => m.model === effectiveModel && (!activeProvider || m.provider === activeProvider.id)));
|
|
80
|
-
// Shared LLM client — used by agent loop AND fast-path features
|
|
81
|
-
let llmClient = null;
|
|
82
|
-
if (effectiveApiKey) {
|
|
83
|
-
if (!effectiveModel) {
|
|
84
|
-
throw new Error("No model specified. Use --model or configure a provider with defaultModel in ~/.agent-sh/settings.json");
|
|
85
|
-
}
|
|
86
|
-
llmClient = new LlmClient({
|
|
87
|
-
apiKey: effectiveApiKey,
|
|
88
|
-
baseURL: effectiveBaseURL,
|
|
89
|
-
model: effectiveModel,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
// Create AgentLoop (unwired — tools only, no bus subscriptions yet)
|
|
93
|
-
const agentLoop = llmClient
|
|
94
|
-
? new AgentLoop(bus, contextManager, llmClient, handlers, modes, initialModeIndex)
|
|
95
|
-
: null;
|
|
39
|
+
// Expose raw CLI config so the agent backend extension can resolve
|
|
40
|
+
// providers and create the LLM client.
|
|
41
|
+
handlers.define("config:get-shell-config", () => config);
|
|
96
42
|
const backends = new Map();
|
|
97
43
|
let activeBackendName = null;
|
|
98
44
|
const activateByName = async (name, silent = false) => {
|
|
99
|
-
const backend =
|
|
100
|
-
if (
|
|
45
|
+
const backend = backends.get(name);
|
|
46
|
+
if (!backend) {
|
|
101
47
|
bus.emit("ui:error", { message: `Unknown backend: ${name}` });
|
|
102
48
|
return;
|
|
103
49
|
}
|
|
104
50
|
// Deactivate current backend
|
|
105
|
-
if (activeBackendName
|
|
106
|
-
agentLoop?.unwire();
|
|
107
|
-
}
|
|
108
|
-
else if (activeBackendName) {
|
|
51
|
+
if (activeBackendName) {
|
|
109
52
|
backends.get(activeBackendName)?.kill();
|
|
110
53
|
}
|
|
111
54
|
// Activate new backend
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
bus.emit("ui:error", { message: "No LLM provider configured for built-in backend" });
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
agentLoop.wire();
|
|
118
|
-
activeBackendName = "agent-sh";
|
|
119
|
-
bus.emit("agent:info", { name: "agent-sh", version: "0.4", model: llmClient?.model, provider: activeProvider?.id, contextWindow: activeProvider?.contextWindow });
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
await backend.start?.();
|
|
123
|
-
activeBackendName = name;
|
|
124
|
-
}
|
|
55
|
+
await backend.start?.();
|
|
56
|
+
activeBackendName = name;
|
|
125
57
|
if (!silent) {
|
|
126
58
|
bus.emit("ui:info", { message: `Backend: ${name}` });
|
|
127
59
|
}
|
|
@@ -134,104 +66,22 @@ export function createCore(config) {
|
|
|
134
66
|
activateByName(name);
|
|
135
67
|
});
|
|
136
68
|
bus.on("config:list-backends", () => {
|
|
137
|
-
const names = [];
|
|
138
|
-
if (agentLoop)
|
|
139
|
-
names.push("agent-sh");
|
|
140
|
-
for (const name of backends.keys())
|
|
141
|
-
names.push(name);
|
|
69
|
+
const names = [...backends.keys()];
|
|
142
70
|
const list = names
|
|
143
71
|
.map((n) => n === activeBackendName ? `${n} (active)` : n)
|
|
144
72
|
.join(", ");
|
|
145
73
|
bus.emit("ui:info", { message: `Backends: ${list}` });
|
|
146
74
|
});
|
|
147
|
-
bus.onPipe("config:get-backends", (
|
|
148
|
-
const names = [];
|
|
149
|
-
if (agentLoop)
|
|
150
|
-
names.push("agent-sh");
|
|
151
|
-
for (const name of backends.keys())
|
|
152
|
-
names.push(name);
|
|
75
|
+
bus.onPipe("config:get-backends", () => {
|
|
76
|
+
const names = [...backends.keys()];
|
|
153
77
|
return { names, active: activeBackendName };
|
|
154
78
|
});
|
|
155
|
-
// ──
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (typeof m === "string") {
|
|
162
|
-
modelIds.push(m);
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
modelIds.push(m.id);
|
|
166
|
-
caps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow });
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
providerRegistry.set(p.id, {
|
|
170
|
-
id: p.id,
|
|
171
|
-
apiKey: p.apiKey,
|
|
172
|
-
baseURL: p.baseURL,
|
|
173
|
-
defaultModel: p.defaultModel,
|
|
174
|
-
models: modelIds,
|
|
175
|
-
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
176
|
-
modelCapabilities: caps.size > 0 ? caps : undefined,
|
|
177
|
-
});
|
|
178
|
-
// Push registered models into the agent loop so they appear in
|
|
179
|
-
// autocomplete and are selectable via /model.
|
|
180
|
-
const addModes = modelIds.map((m) => {
|
|
181
|
-
const mc = caps.get(m);
|
|
182
|
-
return {
|
|
183
|
-
model: m,
|
|
184
|
-
provider: p.id,
|
|
185
|
-
providerConfig: { apiKey: p.apiKey ?? "", baseURL: p.baseURL },
|
|
186
|
-
contextWindow: mc?.contextWindow,
|
|
187
|
-
reasoning: mc?.reasoning,
|
|
188
|
-
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
189
|
-
};
|
|
190
|
-
});
|
|
191
|
-
bus.emit("config:add-modes", { modes: addModes });
|
|
192
|
-
});
|
|
193
|
-
bus.on("config:switch-provider", ({ provider: name }) => {
|
|
194
|
-
const p = providerRegistry.get(name);
|
|
195
|
-
if (!p) {
|
|
196
|
-
bus.emit("ui:error", { message: `Unknown provider: ${name}` });
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
if (!llmClient) {
|
|
200
|
-
bus.emit("ui:error", { message: `Provider switching requires internal agent mode` });
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
const newApiKey = p.apiKey;
|
|
204
|
-
if (!newApiKey) {
|
|
205
|
-
bus.emit("ui:error", { message: `Provider "${name}" has no API key configured` });
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
const switchModel = p.defaultModel ?? p.models[0];
|
|
209
|
-
if (!switchModel) {
|
|
210
|
-
bus.emit("ui:error", { message: `Provider "${name}" has no models configured` });
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
llmClient.reconfigure({
|
|
214
|
-
apiKey: newApiKey,
|
|
215
|
-
baseURL: p.baseURL,
|
|
216
|
-
model: switchModel,
|
|
217
|
-
});
|
|
218
|
-
const newModes = p.models.map((m) => {
|
|
219
|
-
const mc = p.modelCapabilities?.get(m);
|
|
220
|
-
return {
|
|
221
|
-
model: m,
|
|
222
|
-
provider: name,
|
|
223
|
-
providerConfig: { apiKey: newApiKey, baseURL: p.baseURL },
|
|
224
|
-
contextWindow: mc?.contextWindow ?? p.contextWindow,
|
|
225
|
-
reasoning: mc?.reasoning,
|
|
226
|
-
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
227
|
-
};
|
|
228
|
-
});
|
|
229
|
-
bus.emit("config:set-modes", { modes: newModes });
|
|
230
|
-
activeProvider = p;
|
|
231
|
-
bus.emit("agent:info", { name: "agent-sh", version: "0.4", model: switchModel, provider: name, contextWindow: p.contextWindow });
|
|
232
|
-
bus.emit("ui:info", { message: `Switched to ${name} (${switchModel})` });
|
|
233
|
-
bus.emit("config:changed", {});
|
|
234
|
-
});
|
|
79
|
+
// ── Compositor ──────────────────────────────────────────────
|
|
80
|
+
const compositor = new DefaultCompositor();
|
|
81
|
+
const stdoutSurface = new StdoutSurface();
|
|
82
|
+
compositor.setDefault("agent", stdoutSurface);
|
|
83
|
+
compositor.setDefault("query", stdoutSurface);
|
|
84
|
+
compositor.setDefault("status", stdoutSurface);
|
|
235
85
|
// ── Lazy singleton terminal buffer ──────────────────────────
|
|
236
86
|
let terminalBufferSingleton; // undefined = not yet created
|
|
237
87
|
const getTerminalBuffer = () => {
|
|
@@ -243,23 +93,17 @@ export function createCore(config) {
|
|
|
243
93
|
return {
|
|
244
94
|
bus,
|
|
245
95
|
contextManager,
|
|
246
|
-
|
|
96
|
+
handlers,
|
|
247
97
|
activateBackend() {
|
|
248
98
|
// Silent — backend info is shown in the startup banner.
|
|
249
99
|
// Runtime switches (config:switch-backend) still emit ui:info.
|
|
100
|
+
if (backends.size === 0)
|
|
101
|
+
return;
|
|
250
102
|
const preferred = settings.defaultBackend;
|
|
251
103
|
if (preferred && backends.has(preferred)) {
|
|
252
104
|
activateByName(preferred, true);
|
|
253
105
|
}
|
|
254
|
-
else
|
|
255
|
-
activateByName(backends.keys().next().value, true);
|
|
256
|
-
}
|
|
257
|
-
else if (agentLoop) {
|
|
258
|
-
agentLoop.wire();
|
|
259
|
-
activeBackendName = "agent-sh";
|
|
260
|
-
bus.emit("agent:info", { name: "agent-sh", version: "0.4", model: llmClient?.model, provider: activeProvider?.id, contextWindow: activeProvider?.contextWindow });
|
|
261
|
-
}
|
|
262
|
-
else if (backends.size > 0) {
|
|
106
|
+
else {
|
|
263
107
|
activateByName(backends.keys().next().value, true);
|
|
264
108
|
}
|
|
265
109
|
},
|
|
@@ -304,30 +148,68 @@ export function createCore(config) {
|
|
|
304
148
|
return {
|
|
305
149
|
bus,
|
|
306
150
|
contextManager,
|
|
307
|
-
|
|
151
|
+
instanceId,
|
|
308
152
|
quit: opts.quit,
|
|
309
153
|
setPalette,
|
|
310
154
|
createBlockTransform: (o) => streamTransform.createBlockTransform(bus, o),
|
|
311
155
|
createFencedBlockTransform: (o) => streamTransform.createFencedBlockTransform(bus, o),
|
|
312
156
|
getExtensionSettings: settingsMod.getExtensionSettings,
|
|
313
157
|
registerCommand: (name, description, handler) => bus.emit("command:register", { name, description, handler }),
|
|
314
|
-
registerTool: (tool) =>
|
|
315
|
-
|
|
158
|
+
registerTool: (tool) => bus.emit("agent:register-tool", { tool }),
|
|
159
|
+
unregisterTool: (name) => bus.emit("agent:unregister-tool", { name }),
|
|
160
|
+
getTools: () => bus.emitPipe("agent:get-tools", { tools: [] }).tools,
|
|
161
|
+
registerInstruction: (name, text) => bus.emit("agent:register-instruction", { name, text }),
|
|
162
|
+
removeInstruction: (name) => bus.emit("agent:remove-instruction", { name }),
|
|
316
163
|
define: (name, fn) => handlers.define(name, fn),
|
|
317
164
|
advise: (name, wrapper) => handlers.advise(name, wrapper),
|
|
318
165
|
call: (name, ...args) => handlers.call(name, ...args),
|
|
319
166
|
get terminalBuffer() { return getTerminalBuffer(); },
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
167
|
+
compositor,
|
|
168
|
+
createRemoteSession: (opts) => {
|
|
169
|
+
const { surface } = opts;
|
|
170
|
+
const cleanups = [];
|
|
171
|
+
let active = true;
|
|
172
|
+
// Redirect all render streams
|
|
173
|
+
cleanups.push(compositor.redirect("agent", surface));
|
|
174
|
+
cleanups.push(compositor.redirect("query", surface));
|
|
175
|
+
cleanups.push(compositor.redirect("status", surface));
|
|
176
|
+
// Keep shell interactive
|
|
177
|
+
cleanups.push(handlers.advise("shell:on-processing-start", (next) => active ? undefined : next()));
|
|
178
|
+
cleanups.push(handlers.advise("shell:on-processing-done", (next) => active ? undefined : next()));
|
|
179
|
+
// Suppress chrome
|
|
180
|
+
if (opts.suppressBorders !== false) {
|
|
181
|
+
cleanups.push(handlers.advise("tui:response-border", (next, ...a) => active ? null : next(...a)));
|
|
182
|
+
}
|
|
183
|
+
if (opts.suppressQueryBox) {
|
|
184
|
+
cleanups.push(handlers.advise("tui:render-user-query", (next, ...a) => active ? [] : next(...a)));
|
|
185
|
+
}
|
|
186
|
+
if (opts.suppressUsage !== false) {
|
|
187
|
+
cleanups.push(handlers.advise("tui:render-usage", (next, ...a) => active ? "" : next(...a)));
|
|
188
|
+
}
|
|
189
|
+
if (opts.interactive) {
|
|
190
|
+
cleanups.push(handlers.advise("dynamic-context:build", (next) => {
|
|
191
|
+
const base = next();
|
|
192
|
+
return active ? base + "\ninteractive-session: true\n" : base;
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
submit(query) { bus.emit("agent:submit", { query }); },
|
|
197
|
+
get surface() { return surface; },
|
|
198
|
+
get active() { return active; },
|
|
199
|
+
close() {
|
|
200
|
+
if (!active)
|
|
201
|
+
return;
|
|
202
|
+
active = false;
|
|
203
|
+
for (const fn of cleanups.reverse())
|
|
204
|
+
fn();
|
|
205
|
+
cleanups.length = 0;
|
|
206
|
+
},
|
|
207
|
+
};
|
|
323
208
|
},
|
|
324
209
|
};
|
|
325
210
|
},
|
|
326
211
|
kill() {
|
|
327
|
-
if (activeBackendName
|
|
328
|
-
agentLoop?.kill();
|
|
329
|
-
}
|
|
330
|
-
else if (activeBackendName) {
|
|
212
|
+
if (activeBackendName) {
|
|
331
213
|
backends.get(activeBackendName)?.kill();
|
|
332
214
|
}
|
|
333
215
|
},
|
package/dist/event-bus.d.ts
CHANGED
|
@@ -115,10 +115,14 @@ export interface ShellEvents {
|
|
|
115
115
|
"agent:tool-output-chunk": {
|
|
116
116
|
chunk: string;
|
|
117
117
|
};
|
|
118
|
+
"tool:interactive-start": Record<string, never>;
|
|
119
|
+
"tool:interactive-end": Record<string, never>;
|
|
118
120
|
"permission:request": {
|
|
119
121
|
kind: string;
|
|
120
122
|
title: string;
|
|
121
123
|
metadata: Record<string, unknown>;
|
|
124
|
+
/** Interactive UI capability — available when the built-in agent is active. */
|
|
125
|
+
ui?: unknown;
|
|
122
126
|
decision: Record<string, unknown>;
|
|
123
127
|
};
|
|
124
128
|
"command:register": {
|
|
@@ -218,6 +222,10 @@ export interface ShellEvents {
|
|
|
218
222
|
"config:switch-provider": {
|
|
219
223
|
provider: string;
|
|
220
224
|
};
|
|
225
|
+
"config:get-initial-modes": {
|
|
226
|
+
modes: AgentMode[];
|
|
227
|
+
initialModeIndex: number;
|
|
228
|
+
};
|
|
221
229
|
"config:set-modes": {
|
|
222
230
|
modes: AgentMode[];
|
|
223
231
|
};
|
|
@@ -237,6 +245,22 @@ export interface ShellEvents {
|
|
|
237
245
|
/** Provider supports the reasoning_effort parameter. Default: true. */
|
|
238
246
|
supportsReasoningEffort?: boolean;
|
|
239
247
|
};
|
|
248
|
+
"agent:register-tool": {
|
|
249
|
+
tool: import("./agent/types.js").ToolDefinition;
|
|
250
|
+
};
|
|
251
|
+
"agent:unregister-tool": {
|
|
252
|
+
name: string;
|
|
253
|
+
};
|
|
254
|
+
"agent:get-tools": {
|
|
255
|
+
tools: import("./agent/types.js").ToolDefinition[];
|
|
256
|
+
};
|
|
257
|
+
"agent:register-instruction": {
|
|
258
|
+
name: string;
|
|
259
|
+
text: string;
|
|
260
|
+
};
|
|
261
|
+
"agent:remove-instruction": {
|
|
262
|
+
name: string;
|
|
263
|
+
};
|
|
240
264
|
"autocomplete:request": {
|
|
241
265
|
buffer: string;
|
|
242
266
|
/** Parsed slash command name (e.g. "/backend"), or null if not a command. */
|
|
@@ -291,6 +315,8 @@ export declare class EventBus {
|
|
|
291
315
|
emitTransform<K extends keyof ShellEvents>(event: K, payload: ShellEvents[K]): void;
|
|
292
316
|
/** Register a transform listener for a pipeline event. */
|
|
293
317
|
onPipe<K extends keyof ShellEvents>(event: K, fn: PipeListener<ShellEvents[K]>): void;
|
|
318
|
+
/** Remove a transform listener from a pipeline event. */
|
|
319
|
+
offPipe<K extends keyof ShellEvents>(event: K, fn: PipeListener<ShellEvents[K]>): void;
|
|
294
320
|
/**
|
|
295
321
|
* Emit a pipeline event — each registered pipe listener receives the
|
|
296
322
|
* output of the previous one. Returns the final transformed payload.
|
package/dist/event-bus.js
CHANGED
|
@@ -49,6 +49,15 @@ export class EventBus {
|
|
|
49
49
|
}
|
|
50
50
|
listeners.push(fn);
|
|
51
51
|
}
|
|
52
|
+
/** Remove a transform listener from a pipeline event. */
|
|
53
|
+
offPipe(event, fn) {
|
|
54
|
+
const listeners = this.pipeListeners.get(event);
|
|
55
|
+
if (!listeners)
|
|
56
|
+
return;
|
|
57
|
+
const idx = listeners.indexOf(fn);
|
|
58
|
+
if (idx !== -1)
|
|
59
|
+
listeners.splice(idx, 1);
|
|
60
|
+
}
|
|
52
61
|
/**
|
|
53
62
|
* Emit a pipeline event — each registered pipe listener receives the
|
|
54
63
|
* output of the previous one. Returns the final transformed payload.
|
|
@@ -60,7 +69,17 @@ export class EventBus {
|
|
|
60
69
|
return payload;
|
|
61
70
|
let result = payload;
|
|
62
71
|
for (const fn of listeners) {
|
|
63
|
-
|
|
72
|
+
try {
|
|
73
|
+
const out = fn(result);
|
|
74
|
+
if (out && typeof out.then === "function") {
|
|
75
|
+
console.error(`[event-bus] Warning: async handler in sync pipe "${String(event)}" — use onPipeAsync instead`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
result = out;
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
console.error(`[event-bus] Pipe handler error in "${String(event)}":`, err instanceof Error ? err.message : err);
|
|
82
|
+
}
|
|
64
83
|
}
|
|
65
84
|
return result;
|
|
66
85
|
}
|
|
@@ -14,3 +14,8 @@ import type { ExtensionContext } from "./types.js";
|
|
|
14
14
|
* Errors are non-fatal — logged via ui:error and skipped.
|
|
15
15
|
*/
|
|
16
16
|
export declare function loadExtensions(ctx: ExtensionContext, cliExtensions?: string[]): Promise<string[]>;
|
|
17
|
+
/**
|
|
18
|
+
* Reload user extensions (from ~/.agent-sh/extensions/).
|
|
19
|
+
* Tears down old registrations, busts the module cache, and re-activates.
|
|
20
|
+
*/
|
|
21
|
+
export declare function reloadExtensions(ctx: ExtensionContext): Promise<string[]>;
|
package/dist/extension-loader.js
CHANGED
|
@@ -17,6 +17,63 @@ async function ensureTsSupport() {
|
|
|
17
17
|
// tsx not available — TS extensions will fail with a clear error
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Wrap an ExtensionContext to track all registrations (bus.on, bus.onPipe,
|
|
22
|
+
* advise, command:register). Returns the wrapped context and a dispose()
|
|
23
|
+
* function that tears down everything registered through it.
|
|
24
|
+
*/
|
|
25
|
+
function createScopedContext(ctx) {
|
|
26
|
+
const cleanups = [];
|
|
27
|
+
const bus = ctx.bus;
|
|
28
|
+
const scopedBus = Object.create(bus);
|
|
29
|
+
// Track bus.on registrations
|
|
30
|
+
scopedBus.on = ((event, fn) => {
|
|
31
|
+
bus.on(event, fn);
|
|
32
|
+
cleanups.push(() => bus.off(event, fn));
|
|
33
|
+
});
|
|
34
|
+
// Track bus.onPipe registrations
|
|
35
|
+
scopedBus.onPipe = ((event, fn) => {
|
|
36
|
+
bus.onPipe(event, fn);
|
|
37
|
+
cleanups.push(() => bus.offPipe(event, fn));
|
|
38
|
+
});
|
|
39
|
+
// Track advise registrations
|
|
40
|
+
const scopedAdvise = (name, wrapper) => {
|
|
41
|
+
const unadvise = ctx.advise(name, wrapper);
|
|
42
|
+
cleanups.push(unadvise);
|
|
43
|
+
return unadvise;
|
|
44
|
+
};
|
|
45
|
+
// Track instruction registrations
|
|
46
|
+
const scopedRegisterInstruction = (name, text) => {
|
|
47
|
+
ctx.registerInstruction(name, text);
|
|
48
|
+
cleanups.push(() => ctx.removeInstruction(name));
|
|
49
|
+
};
|
|
50
|
+
// Track tool registrations
|
|
51
|
+
const scopedRegisterTool = (tool) => {
|
|
52
|
+
ctx.registerTool(tool);
|
|
53
|
+
cleanups.push(() => ctx.unregisterTool(tool.name));
|
|
54
|
+
};
|
|
55
|
+
const scoped = {
|
|
56
|
+
...ctx,
|
|
57
|
+
bus: scopedBus,
|
|
58
|
+
advise: scopedAdvise,
|
|
59
|
+
registerInstruction: scopedRegisterInstruction,
|
|
60
|
+
removeInstruction: ctx.removeInstruction,
|
|
61
|
+
registerTool: scopedRegisterTool,
|
|
62
|
+
unregisterTool: ctx.unregisterTool,
|
|
63
|
+
};
|
|
64
|
+
const dispose = () => {
|
|
65
|
+
for (const fn of cleanups) {
|
|
66
|
+
try {
|
|
67
|
+
fn();
|
|
68
|
+
}
|
|
69
|
+
catch { /* ignore */ }
|
|
70
|
+
}
|
|
71
|
+
cleanups.length = 0;
|
|
72
|
+
};
|
|
73
|
+
return { scoped, dispose };
|
|
74
|
+
}
|
|
75
|
+
// Track disposers for user extensions so reload can tear them down
|
|
76
|
+
const extensionDisposers = new Map();
|
|
20
77
|
/**
|
|
21
78
|
* Load extensions from three sources (merged, deduplicated):
|
|
22
79
|
*
|
|
@@ -43,19 +100,32 @@ export async function loadExtensions(ctx, cliExtensions) {
|
|
|
43
100
|
specifiers.push(...settings.extensions);
|
|
44
101
|
}
|
|
45
102
|
// 3. ~/.agent-sh/extensions/ directory
|
|
103
|
+
const userSpecifiers = await discoverUserExtensions();
|
|
104
|
+
specifiers.push(...userSpecifiers);
|
|
105
|
+
// Deduplicate
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
const unique = specifiers.filter((s) => {
|
|
108
|
+
if (seen.has(s))
|
|
109
|
+
return false;
|
|
110
|
+
seen.add(s);
|
|
111
|
+
return true;
|
|
112
|
+
});
|
|
113
|
+
// Load each extension (user extensions get scoped contexts for reloadability)
|
|
114
|
+
const loaded = await loadSpecifiers(unique, ctx, false, userSpecifiers);
|
|
115
|
+
return loaded;
|
|
116
|
+
}
|
|
117
|
+
async function discoverUserExtensions() {
|
|
118
|
+
const specifiers = [];
|
|
46
119
|
try {
|
|
47
120
|
const entries = await fs.readdir(EXT_DIR, { withFileTypes: true });
|
|
48
121
|
for (const entry of entries) {
|
|
49
122
|
const fullPath = path.join(EXT_DIR, entry.name);
|
|
50
|
-
// Resolve symlinks to check if they point to directories
|
|
51
123
|
const isDir = entry.isDirectory() ||
|
|
52
124
|
(entry.isSymbolicLink() && (await fs.stat(fullPath)).isDirectory());
|
|
53
125
|
if (isDir) {
|
|
54
|
-
// Directory extension: look for index.{ts,js,mjs,...}
|
|
55
126
|
const indexFile = await findIndex(fullPath);
|
|
56
|
-
if (indexFile)
|
|
127
|
+
if (indexFile)
|
|
57
128
|
specifiers.push(indexFile);
|
|
58
|
-
}
|
|
59
129
|
}
|
|
60
130
|
else if (SCRIPT_EXTS.some((ext) => entry.name.endsWith(ext))) {
|
|
61
131
|
specifiers.push(fullPath);
|
|
@@ -65,22 +135,22 @@ export async function loadExtensions(ctx, cliExtensions) {
|
|
|
65
135
|
catch {
|
|
66
136
|
// Directory doesn't exist — no user extensions
|
|
67
137
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return false;
|
|
73
|
-
seen.add(s);
|
|
74
|
-
return true;
|
|
75
|
-
});
|
|
76
|
-
// Load each extension
|
|
138
|
+
return specifiers;
|
|
139
|
+
}
|
|
140
|
+
async function loadSpecifiers(specifiers, ctx, bustCache, userSpecifiers) {
|
|
141
|
+
const userSet = new Set(userSpecifiers ?? []);
|
|
77
142
|
const loaded = [];
|
|
78
|
-
for (const specifier of
|
|
143
|
+
for (const specifier of specifiers) {
|
|
79
144
|
try {
|
|
80
|
-
|
|
145
|
+
let importPath = await resolveSpecifier(specifier);
|
|
81
146
|
if (TS_EXTS.some((ext) => importPath.endsWith(ext))) {
|
|
82
147
|
await ensureTsSupport();
|
|
83
148
|
}
|
|
149
|
+
// Append timestamp query to bust Node's module cache on reload
|
|
150
|
+
if (bustCache) {
|
|
151
|
+
const sep = importPath.includes("?") ? "&" : "?";
|
|
152
|
+
importPath += `${sep}t=${Date.now()}`;
|
|
153
|
+
}
|
|
84
154
|
const mod = await import(importPath);
|
|
85
155
|
// tsx may double-wrap default exports: mod.default.default
|
|
86
156
|
const activate = typeof mod.default === "function"
|
|
@@ -89,10 +159,19 @@ export async function loadExtensions(ctx, cliExtensions) {
|
|
|
89
159
|
? mod.default.default
|
|
90
160
|
: mod.activate;
|
|
91
161
|
if (typeof activate === "function") {
|
|
92
|
-
activate(ctx);
|
|
93
|
-
// Extract a short name from the specifier
|
|
94
162
|
const base = path.basename(specifier).replace(/\.(ts|js|mjs|mts|tsx)$/, "");
|
|
95
163
|
const name = base === "index" ? path.basename(path.dirname(specifier)) : base;
|
|
164
|
+
// User extensions get a scoped context so /reload can tear them down
|
|
165
|
+
if (userSet.has(specifier)) {
|
|
166
|
+
// Dispose previous load if reloading
|
|
167
|
+
extensionDisposers.get(name)?.();
|
|
168
|
+
const { scoped, dispose } = createScopedContext(ctx);
|
|
169
|
+
activate(scoped);
|
|
170
|
+
extensionDisposers.set(name, dispose);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
activate(ctx);
|
|
174
|
+
}
|
|
96
175
|
loaded.push(name);
|
|
97
176
|
}
|
|
98
177
|
}
|
|
@@ -104,6 +183,14 @@ export async function loadExtensions(ctx, cliExtensions) {
|
|
|
104
183
|
}
|
|
105
184
|
return loaded;
|
|
106
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Reload user extensions (from ~/.agent-sh/extensions/).
|
|
188
|
+
* Tears down old registrations, busts the module cache, and re-activates.
|
|
189
|
+
*/
|
|
190
|
+
export async function reloadExtensions(ctx) {
|
|
191
|
+
const specifiers = await discoverUserExtensions();
|
|
192
|
+
return loadSpecifiers(specifiers, ctx, true, specifiers);
|
|
193
|
+
}
|
|
107
194
|
/**
|
|
108
195
|
* Find an index file in a directory extension.
|
|
109
196
|
*/
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in agent backend extension.
|
|
3
|
+
*
|
|
4
|
+
* Owns the full LLM lifecycle:
|
|
5
|
+
* 1. Resolves providers from settings + CLI config
|
|
6
|
+
* 2. Creates and manages the LlmClient
|
|
7
|
+
* 3. Builds mode list for model cycling
|
|
8
|
+
* 4. Creates AgentLoop and registers it as the "ash" backend
|
|
9
|
+
* 5. Handles runtime provider switching and provider registration
|
|
10
|
+
* 6. Exposes llm:get-client handler for other extensions (e.g. command-suggest)
|
|
11
|
+
*/
|
|
12
|
+
import type { ExtensionContext } from "../types.js";
|
|
13
|
+
export default function agentBackend(ctx: ExtensionContext): void;
|