agent-sh 0.7.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 +28 -33
- package/dist/agent/agent-loop.d.ts +31 -8
- package/dist/agent/agent-loop.js +277 -66
- package/dist/agent/conversation-state.d.ts +41 -9
- package/dist/agent/conversation-state.js +340 -17
- package/dist/agent/history-file.d.ts +36 -0
- package/dist/agent/history-file.js +167 -0
- package/dist/agent/nuclear-form.d.ts +41 -0
- package/dist/agent/nuclear-form.js +176 -0
- package/dist/agent/system-prompt.d.ts +4 -5
- package/dist/agent/system-prompt.js +16 -11
- package/dist/agent/token-budget.d.ts +13 -0
- package/dist/agent/token-budget.js +50 -0
- package/dist/agent/tool-protocol.d.ts +83 -0
- package/dist/agent/tool-protocol.js +386 -0
- package/dist/agent/tools/user-shell.js +4 -1
- package/dist/agent/types.d.ts +21 -1
- package/dist/context-manager.d.ts +0 -1
- package/dist/context-manager.js +5 -110
- package/dist/core.d.ts +7 -7
- package/dist/core.js +76 -180
- package/dist/event-bus.d.ts +40 -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 +44 -1
- package/dist/extensions/terminal-buffer.d.ts +1 -1
- package/dist/extensions/terminal-buffer.js +22 -8
- package/dist/extensions/tui-renderer.js +177 -122
- package/dist/index.js +14 -20
- package/dist/settings.d.ts +25 -2
- package/dist/settings.js +25 -4
- 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} +24 -6
- package/dist/types.d.ts +49 -32
- package/dist/utils/ansi.d.ts +10 -0
- package/dist/utils/ansi.js +27 -0
- 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 +34 -3
- package/dist/utils/floating-panel.js +315 -82
- package/dist/utils/handler-registry.d.ts +26 -10
- package/dist/utils/handler-registry.js +52 -16
- package/dist/utils/line-editor.d.ts +32 -3
- package/dist/utils/line-editor.js +218 -36
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +4 -4
- package/dist/utils/message-utils.d.ts +35 -0
- package/dist/utils/message-utils.js +75 -0
- package/dist/utils/terminal-buffer.d.ts +9 -1
- package/dist/utils/terminal-buffer.js +31 -2
- package/dist/utils/tool-display.d.ts +1 -0
- package/dist/utils/tool-display.js +1 -1
- 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/claude-code-bridge/index.ts +77 -1
- 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/pi-bridge/index.ts +87 -2
- 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 -11
- package/dist/extensions/overlay-agent.js +0 -43
- package/examples/extensions/terminal-buffer.ts +0 -184
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,90 +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
|
-
});
|
|
179
|
-
bus.on("config:switch-provider", ({ provider: name }) => {
|
|
180
|
-
const p = providerRegistry.get(name);
|
|
181
|
-
if (!p) {
|
|
182
|
-
bus.emit("ui:error", { message: `Unknown provider: ${name}` });
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
if (!llmClient) {
|
|
186
|
-
bus.emit("ui:error", { message: `Provider switching requires internal agent mode` });
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
const newApiKey = p.apiKey;
|
|
190
|
-
if (!newApiKey) {
|
|
191
|
-
bus.emit("ui:error", { message: `Provider "${name}" has no API key configured` });
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
const switchModel = p.defaultModel ?? p.models[0];
|
|
195
|
-
if (!switchModel) {
|
|
196
|
-
bus.emit("ui:error", { message: `Provider "${name}" has no models configured` });
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
llmClient.reconfigure({
|
|
200
|
-
apiKey: newApiKey,
|
|
201
|
-
baseURL: p.baseURL,
|
|
202
|
-
model: switchModel,
|
|
203
|
-
});
|
|
204
|
-
const newModes = p.models.map((m) => {
|
|
205
|
-
const mc = p.modelCapabilities?.get(m);
|
|
206
|
-
return {
|
|
207
|
-
model: m,
|
|
208
|
-
provider: name,
|
|
209
|
-
providerConfig: { apiKey: newApiKey, baseURL: p.baseURL },
|
|
210
|
-
contextWindow: mc?.contextWindow ?? p.contextWindow,
|
|
211
|
-
reasoning: mc?.reasoning,
|
|
212
|
-
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
213
|
-
};
|
|
214
|
-
});
|
|
215
|
-
bus.emit("config:set-modes", { modes: newModes });
|
|
216
|
-
activeProvider = p;
|
|
217
|
-
bus.emit("agent:info", { name: "agent-sh", version: "0.4", model: switchModel, provider: name, contextWindow: p.contextWindow });
|
|
218
|
-
bus.emit("ui:info", { message: `Switched to ${name} (${switchModel})` });
|
|
219
|
-
bus.emit("config:changed", {});
|
|
220
|
-
});
|
|
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);
|
|
221
85
|
// ── Lazy singleton terminal buffer ──────────────────────────
|
|
222
86
|
let terminalBufferSingleton; // undefined = not yet created
|
|
223
87
|
const getTerminalBuffer = () => {
|
|
@@ -229,23 +93,17 @@ export function createCore(config) {
|
|
|
229
93
|
return {
|
|
230
94
|
bus,
|
|
231
95
|
contextManager,
|
|
232
|
-
|
|
96
|
+
handlers,
|
|
233
97
|
activateBackend() {
|
|
234
98
|
// Silent — backend info is shown in the startup banner.
|
|
235
99
|
// Runtime switches (config:switch-backend) still emit ui:info.
|
|
100
|
+
if (backends.size === 0)
|
|
101
|
+
return;
|
|
236
102
|
const preferred = settings.defaultBackend;
|
|
237
103
|
if (preferred && backends.has(preferred)) {
|
|
238
104
|
activateByName(preferred, true);
|
|
239
105
|
}
|
|
240
|
-
else
|
|
241
|
-
activateByName(backends.keys().next().value, true);
|
|
242
|
-
}
|
|
243
|
-
else if (agentLoop) {
|
|
244
|
-
agentLoop.wire();
|
|
245
|
-
activeBackendName = "agent-sh";
|
|
246
|
-
bus.emit("agent:info", { name: "agent-sh", version: "0.4", model: llmClient?.model, provider: activeProvider?.id, contextWindow: activeProvider?.contextWindow });
|
|
247
|
-
}
|
|
248
|
-
else if (backends.size > 0) {
|
|
106
|
+
else {
|
|
249
107
|
activateByName(backends.keys().next().value, true);
|
|
250
108
|
}
|
|
251
109
|
},
|
|
@@ -290,30 +148,68 @@ export function createCore(config) {
|
|
|
290
148
|
return {
|
|
291
149
|
bus,
|
|
292
150
|
contextManager,
|
|
293
|
-
|
|
151
|
+
instanceId,
|
|
294
152
|
quit: opts.quit,
|
|
295
153
|
setPalette,
|
|
296
154
|
createBlockTransform: (o) => streamTransform.createBlockTransform(bus, o),
|
|
297
155
|
createFencedBlockTransform: (o) => streamTransform.createFencedBlockTransform(bus, o),
|
|
298
156
|
getExtensionSettings: settingsMod.getExtensionSettings,
|
|
299
157
|
registerCommand: (name, description, handler) => bus.emit("command:register", { name, description, handler }),
|
|
300
|
-
registerTool: (tool) =>
|
|
301
|
-
|
|
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 }),
|
|
302
163
|
define: (name, fn) => handlers.define(name, fn),
|
|
303
164
|
advise: (name, wrapper) => handlers.advise(name, wrapper),
|
|
304
165
|
call: (name, ...args) => handlers.call(name, ...args),
|
|
305
166
|
get terminalBuffer() { return getTerminalBuffer(); },
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
+
};
|
|
309
208
|
},
|
|
310
209
|
};
|
|
311
210
|
},
|
|
312
211
|
kill() {
|
|
313
|
-
if (activeBackendName
|
|
314
|
-
agentLoop?.kill();
|
|
315
|
-
}
|
|
316
|
-
else if (activeBackendName) {
|
|
212
|
+
if (activeBackendName) {
|
|
317
213
|
backends.get(activeBackendName)?.kill();
|
|
318
214
|
}
|
|
319
215
|
},
|
package/dist/event-bus.d.ts
CHANGED
|
@@ -28,6 +28,10 @@ export interface ShellEvents {
|
|
|
28
28
|
"shell:pty-write": {
|
|
29
29
|
data: string;
|
|
30
30
|
};
|
|
31
|
+
"shell:pty-resize": {
|
|
32
|
+
cols: number;
|
|
33
|
+
rows: number;
|
|
34
|
+
};
|
|
31
35
|
"shell:buffer-request": Record<string, never>;
|
|
32
36
|
"shell:buffer-snapshot": {
|
|
33
37
|
text: string;
|
|
@@ -111,10 +115,14 @@ export interface ShellEvents {
|
|
|
111
115
|
"agent:tool-output-chunk": {
|
|
112
116
|
chunk: string;
|
|
113
117
|
};
|
|
118
|
+
"tool:interactive-start": Record<string, never>;
|
|
119
|
+
"tool:interactive-end": Record<string, never>;
|
|
114
120
|
"permission:request": {
|
|
115
121
|
kind: string;
|
|
116
122
|
title: string;
|
|
117
123
|
metadata: Record<string, unknown>;
|
|
124
|
+
/** Interactive UI capability — available when the built-in agent is active. */
|
|
125
|
+
ui?: unknown;
|
|
118
126
|
decision: Record<string, unknown>;
|
|
119
127
|
};
|
|
120
128
|
"command:register": {
|
|
@@ -171,6 +179,13 @@ export interface ShellEvents {
|
|
|
171
179
|
contextWindow?: number;
|
|
172
180
|
};
|
|
173
181
|
"agent:reset-session": Record<string, never>;
|
|
182
|
+
"agent:compact-request": Record<string, never>;
|
|
183
|
+
"context:get-stats": {
|
|
184
|
+
activeTokens: number;
|
|
185
|
+
nuclearEntries: number;
|
|
186
|
+
recallArchiveSize: number;
|
|
187
|
+
budgetTokens: number;
|
|
188
|
+
};
|
|
174
189
|
"agent:register-backend": {
|
|
175
190
|
name: string;
|
|
176
191
|
kill: () => void;
|
|
@@ -207,9 +222,16 @@ export interface ShellEvents {
|
|
|
207
222
|
"config:switch-provider": {
|
|
208
223
|
provider: string;
|
|
209
224
|
};
|
|
225
|
+
"config:get-initial-modes": {
|
|
226
|
+
modes: AgentMode[];
|
|
227
|
+
initialModeIndex: number;
|
|
228
|
+
};
|
|
210
229
|
"config:set-modes": {
|
|
211
230
|
modes: AgentMode[];
|
|
212
231
|
};
|
|
232
|
+
"config:add-modes": {
|
|
233
|
+
modes: AgentMode[];
|
|
234
|
+
};
|
|
213
235
|
"provider:register": {
|
|
214
236
|
id: string;
|
|
215
237
|
apiKey?: string;
|
|
@@ -223,6 +245,22 @@ export interface ShellEvents {
|
|
|
223
245
|
/** Provider supports the reasoning_effort parameter. Default: true. */
|
|
224
246
|
supportsReasoningEffort?: boolean;
|
|
225
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
|
+
};
|
|
226
264
|
"autocomplete:request": {
|
|
227
265
|
buffer: string;
|
|
228
266
|
/** Parsed slash command name (e.g. "/backend"), or null if not a command. */
|
|
@@ -277,6 +315,8 @@ export declare class EventBus {
|
|
|
277
315
|
emitTransform<K extends keyof ShellEvents>(event: K, payload: ShellEvents[K]): void;
|
|
278
316
|
/** Register a transform listener for a pipeline event. */
|
|
279
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;
|
|
280
320
|
/**
|
|
281
321
|
* Emit a pipeline event — each registered pipe listener receives the
|
|
282
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;
|