agent-sh 0.8.0 → 0.10.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 +27 -43
- package/dist/agent/agent-loop.d.ts +69 -6
- package/dist/agent/agent-loop.js +954 -153
- package/dist/agent/conversation-state.d.ts +74 -21
- package/dist/agent/conversation-state.js +361 -150
- package/dist/agent/history-file.d.ts +13 -4
- package/dist/agent/history-file.js +110 -36
- package/dist/agent/nuclear-form.d.ts +28 -3
- package/dist/agent/nuclear-form.js +88 -6
- package/dist/agent/skills.d.ts +2 -4
- package/dist/agent/skills.js +10 -4
- package/dist/agent/subagent.d.ts +23 -0
- package/dist/agent/subagent.js +53 -11
- package/dist/agent/system-prompt.d.ts +37 -5
- package/dist/agent/system-prompt.js +100 -67
- package/dist/{token-budget.d.ts → agent/token-budget.d.ts} +5 -4
- package/dist/{token-budget.js → agent/token-budget.js} +15 -20
- package/dist/agent/tool-protocol.d.ts +105 -0
- package/dist/agent/tool-protocol.js +551 -0
- package/dist/agent/tools/bash.js +3 -3
- package/dist/agent/tools/edit-file.js +9 -6
- package/dist/agent/tools/glob.js +4 -2
- package/dist/agent/tools/grep.js +27 -3
- package/dist/agent/tools/ls.js +5 -6
- package/dist/agent/types.d.ts +22 -2
- package/dist/context-manager.d.ts +17 -0
- package/dist/context-manager.js +37 -4
- package/dist/core.d.ts +7 -7
- package/dist/core.js +99 -196
- package/dist/event-bus.d.ts +85 -2
- package/dist/event-bus.js +20 -1
- package/dist/executor.d.ts +4 -3
- package/dist/executor.js +18 -15
- package/dist/extension-loader.d.ts +5 -0
- package/dist/extension-loader.js +143 -19
- package/dist/extensions/agent-backend.d.ts +14 -0
- package/dist/extensions/agent-backend.js +188 -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 +24 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +30 -10
- package/dist/extensions/tui-renderer.js +117 -113
- package/dist/index.js +39 -26
- package/dist/settings.d.ts +40 -3
- package/dist/settings.js +57 -10
- package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +3 -2
- package/dist/{input-handler.js → shell/input-handler.js} +111 -85
- 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} +39 -8
- package/dist/types.d.ts +61 -10
- package/dist/utils/ansi.d.ts +5 -0
- package/dist/utils/ansi.js +1 -1
- package/dist/utils/compositor.d.ts +67 -0
- package/dist/utils/compositor.js +116 -0
- package/dist/utils/diff-renderer.d.ts +9 -0
- package/dist/utils/diff-renderer.js +312 -146
- package/dist/utils/diff.d.ts +21 -2
- package/dist/utils/diff.js +165 -89
- package/dist/utils/floating-panel.d.ts +2 -0
- package/dist/utils/floating-panel.js +30 -14
- package/dist/utils/handler-registry.d.ts +31 -10
- package/dist/utils/handler-registry.js +58 -16
- package/dist/utils/line-editor.d.ts +33 -3
- package/dist/utils/line-editor.js +221 -44
- 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-display.d.ts +1 -1
- package/dist/utils/tool-display.js +4 -4
- 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 +574 -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 +164 -0
- package/examples/extensions/ash-mcp-bridge/package.json +9 -0
- package/examples/extensions/claude-code-bridge/index.ts +198 -51
- package/examples/extensions/claude-code-bridge/package.json +1 -0
- package/examples/extensions/interactive-prompts.ts +98 -112
- package/examples/extensions/overlay-agent.ts +84 -38
- package/examples/extensions/peer-mesh.ts +565 -0
- package/examples/extensions/pi-bridge/index.ts +2 -2
- package/examples/extensions/questionnaire.ts +260 -0
- package/examples/extensions/subagents.ts +19 -4
- package/examples/extensions/terminal-buffer.ts +32 -53
- package/examples/extensions/tmux-pane.ts +307 -0
- package/examples/extensions/user-shell.ts +136 -0
- package/examples/extensions/web-access.ts +335 -0
- package/package.json +44 -2
- package/dist/agent/tools/display.d.ts +0 -13
- package/dist/agent/tools/display.js +0 -70
- package/dist/agent/tools/user-shell.d.ts +0 -13
- package/dist/agent/tools/user-shell.js +0 -87
- package/dist/extensions/overlay-agent.d.ts +0 -14
- package/dist/extensions/overlay-agent.js +0 -147
- package/dist/extensions/terminal-buffer.d.ts +0 -14
- package/dist/extensions/terminal-buffer.js +0 -125
package/dist/agent/tools/ls.js
CHANGED
|
@@ -44,21 +44,20 @@ export function createLsTool(getCwd) {
|
|
|
44
44
|
const entries = await fs.readdir(absPath, {
|
|
45
45
|
withFileTypes: true,
|
|
46
46
|
});
|
|
47
|
-
const
|
|
48
|
-
for (const e of entries) {
|
|
47
|
+
const items = await Promise.all(entries.map(async (e) => {
|
|
49
48
|
const fullPath = path.join(absPath, e.name);
|
|
50
49
|
try {
|
|
51
50
|
const stat = await fs.stat(fullPath);
|
|
52
51
|
const size = e.isDirectory() ? "-" : formatSize(stat.size);
|
|
53
52
|
const mtime = stat.mtime.toISOString().slice(0, 16).replace("T", " ");
|
|
54
|
-
|
|
53
|
+
return `${mtime} ${size.padStart(8)} ${e.isDirectory() ? e.name + "/" : e.name}`;
|
|
55
54
|
}
|
|
56
55
|
catch {
|
|
57
|
-
|
|
56
|
+
return `${"?".padStart(16)} ${"?".padStart(8)} ${e.isDirectory() ? e.name + "/" : e.name}`;
|
|
58
57
|
}
|
|
59
|
-
}
|
|
58
|
+
}));
|
|
60
59
|
return {
|
|
61
|
-
content:
|
|
60
|
+
content: items.join("\n") || "(empty directory)",
|
|
62
61
|
exitCode: 0,
|
|
63
62
|
isError: false,
|
|
64
63
|
};
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export type ToolResultBody = {
|
|
|
38
38
|
maxLines?: number;
|
|
39
39
|
};
|
|
40
40
|
export interface ToolDisplayInfo {
|
|
41
|
-
kind: "read" | "write" | "execute" | "search"
|
|
41
|
+
kind: "read" | "write" | "execute" | "search";
|
|
42
42
|
locations?: {
|
|
43
43
|
path: string;
|
|
44
44
|
line?: number | null;
|
|
@@ -47,13 +47,33 @@ export interface ToolDisplayInfo {
|
|
|
47
47
|
* icon + detail only. When absent, the tool name is shown alongside the detail. */
|
|
48
48
|
icon?: string;
|
|
49
49
|
}
|
|
50
|
+
/** Interactive UI session — imperative control over rendering + input. */
|
|
51
|
+
export interface InteractiveSession<T> {
|
|
52
|
+
/** Return lines to render. Called on mount and after each input. */
|
|
53
|
+
render(width: number): string[];
|
|
54
|
+
/** Handle raw input. Call done(result) to finish the session. */
|
|
55
|
+
handleInput(data: string, done: (result: T) => void): void;
|
|
56
|
+
/** Called when session starts. Receives invalidate() for async re-renders. */
|
|
57
|
+
onMount?(invalidate: () => void): void;
|
|
58
|
+
/** Called when session ends (cleanup). */
|
|
59
|
+
onUnmount?(): void;
|
|
60
|
+
}
|
|
61
|
+
/** Interactive UI capability passed to tools during execution. */
|
|
62
|
+
export interface ToolUI {
|
|
63
|
+
/** Present a custom interactive UI and wait for the user's response. */
|
|
64
|
+
custom<T>(session: InteractiveSession<T>): Promise<T>;
|
|
65
|
+
}
|
|
66
|
+
/** Context passed to tool execute() as optional third parameter. */
|
|
67
|
+
export interface ToolExecutionContext {
|
|
68
|
+
ui: ToolUI;
|
|
69
|
+
}
|
|
50
70
|
export interface ToolDefinition {
|
|
51
71
|
name: string;
|
|
52
72
|
/** Short label for TUI display (e.g. "search" instead of "ads_search"). Defaults to name. */
|
|
53
73
|
displayName?: string;
|
|
54
74
|
description: string;
|
|
55
75
|
input_schema: Record<string, unknown>;
|
|
56
|
-
execute(args: Record<string, unknown>, onChunk?: (chunk: string) => void): Promise<ToolResult>;
|
|
76
|
+
execute(args: Record<string, unknown>, onChunk?: (chunk: string) => void, ctx?: ToolExecutionContext): Promise<ToolResult>;
|
|
57
77
|
/** Whether to stream tool output to the TUI (default: true). */
|
|
58
78
|
showOutput?: boolean;
|
|
59
79
|
/** Whether this tool may modify files — triggers file watcher (default: false). */
|
|
@@ -24,6 +24,23 @@ export declare class ContextManager {
|
|
|
24
24
|
* Optional start/end restrict to a line range (1-indexed).
|
|
25
25
|
*/
|
|
26
26
|
expand(ids: number[], start?: number, end?: number): string;
|
|
27
|
+
/**
|
|
28
|
+
* Return shell events with id > afterId, formatted as an incremental
|
|
29
|
+
* delta suitable for injection into conversation history. Skips
|
|
30
|
+
* agent-source commands (already visible in tool results). Returns
|
|
31
|
+
* null when nothing new exists.
|
|
32
|
+
*
|
|
33
|
+
* The motivation: resending the full <shell_context> every turn wastes
|
|
34
|
+
* tokens — N turns × full history = O(N²) cost for O(N) information.
|
|
35
|
+
* Instead we inject only new events as regular conversation messages,
|
|
36
|
+
* so the provider's prefix cache amortizes them to O(N).
|
|
37
|
+
*/
|
|
38
|
+
getEventsSince(afterId: number): {
|
|
39
|
+
text: string;
|
|
40
|
+
lastSeq: number;
|
|
41
|
+
} | null;
|
|
42
|
+
/** Highest exchange id seen so far (0 if none). */
|
|
43
|
+
lastSeq(): number;
|
|
27
44
|
/**
|
|
28
45
|
* One-line summaries of last N exchanges.
|
|
29
46
|
*/
|
package/dist/context-manager.js
CHANGED
|
@@ -140,6 +140,43 @@ export class ContextManager {
|
|
|
140
140
|
}
|
|
141
141
|
return results.join("\n\n");
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Return shell events with id > afterId, formatted as an incremental
|
|
145
|
+
* delta suitable for injection into conversation history. Skips
|
|
146
|
+
* agent-source commands (already visible in tool results). Returns
|
|
147
|
+
* null when nothing new exists.
|
|
148
|
+
*
|
|
149
|
+
* The motivation: resending the full <shell_context> every turn wastes
|
|
150
|
+
* tokens — N turns × full history = O(N²) cost for O(N) information.
|
|
151
|
+
* Instead we inject only new events as regular conversation messages,
|
|
152
|
+
* so the provider's prefix cache amortizes them to O(N).
|
|
153
|
+
*/
|
|
154
|
+
getEventsSince(afterId) {
|
|
155
|
+
const fresh = this.exchanges.filter((e) => e.id > afterId && !(e.type === "shell_command" && e.source === "agent"));
|
|
156
|
+
if (fresh.length === 0)
|
|
157
|
+
return null;
|
|
158
|
+
const lastSeq = this.exchanges[this.exchanges.length - 1].id;
|
|
159
|
+
// Apply per-type truncation so giant outputs don't blow up the turn.
|
|
160
|
+
const truncated = fresh.map((ex) => {
|
|
161
|
+
if (ex.type === "shell_command") {
|
|
162
|
+
const s = getSettings();
|
|
163
|
+
return {
|
|
164
|
+
...ex,
|
|
165
|
+
output: truncateOutput(ex.output, s.shellTruncateThreshold, s.shellHeadLines, s.shellTailLines, ex.id),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return { ...ex };
|
|
169
|
+
});
|
|
170
|
+
const body = truncated.map((ex) => this.formatExchangeTruncated(ex)).join("\n");
|
|
171
|
+
return {
|
|
172
|
+
text: `<shell-events>\n${body}</shell-events>`,
|
|
173
|
+
lastSeq,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/** Highest exchange id seen so far (0 if none). */
|
|
177
|
+
lastSeq() {
|
|
178
|
+
return this.exchanges.length === 0 ? 0 : this.exchanges[this.exchanges.length - 1].id;
|
|
179
|
+
}
|
|
143
180
|
/**
|
|
144
181
|
* One-line summaries of last N exchanges.
|
|
145
182
|
*/
|
|
@@ -229,13 +266,9 @@ export class ContextManager {
|
|
|
229
266
|
out += `The user interacts with a real shell (PTY) and sends you queries inline. You are there to help them with their tasks.\n`;
|
|
230
267
|
out += `\n`;
|
|
231
268
|
out += `IMPORTANT tool usage rules:\n`;
|
|
232
|
-
out += `- user_shell runs commands in the user's live shell (PTY). The user sees output directly — no summary needed.\n`;
|
|
233
269
|
out += `- Your internal tools (bash, read, write, ls, etc.) run in an isolated subprocess. The user CANNOT see their output.\n`;
|
|
234
|
-
out += `- When the user asks to see, list, view, or display anything, ALWAYS use user_shell. NEVER use internal tools like ls/read/bash for display — the user won't see it.\n`;
|
|
235
270
|
out += `- Only use internal tools when YOU need to reason about content silently (e.g. reading a file to answer a question about it).\n`;
|
|
236
|
-
out += `- After a user_shell command, the user already saw the output. Do NOT repeat or summarize it.\n`;
|
|
237
271
|
out += `- You can browse or search shell history with shell_recall.\n`;
|
|
238
|
-
out += `- You can browse or search evicted conversation turns with conversation_recall.\n`;
|
|
239
272
|
out += `\n`;
|
|
240
273
|
this.firstPrompt = false;
|
|
241
274
|
}
|
package/dist/core.d.ts
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,8 +18,8 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { EventBus } from "./event-bus.js";
|
|
20
20
|
import { ContextManager } from "./context-manager.js";
|
|
21
|
-
import { LlmClient } from "./utils/llm-client.js";
|
|
22
21
|
import type { AgentShellConfig, ExtensionContext } from "./types.js";
|
|
22
|
+
import { HandlerRegistry } from "./utils/handler-registry.js";
|
|
23
23
|
export { EventBus } from "./event-bus.js";
|
|
24
24
|
export type { ShellEvents } from "./event-bus.js";
|
|
25
25
|
export type { AgentShellConfig, ExtensionContext } from "./types.js";
|
|
@@ -31,8 +31,8 @@ export { LlmClient } from "./utils/llm-client.js";
|
|
|
31
31
|
export interface AgentShellCore {
|
|
32
32
|
bus: EventBus;
|
|
33
33
|
contextManager: ContextManager;
|
|
34
|
-
/**
|
|
35
|
-
|
|
34
|
+
/** Handler registry for define/advise/call. */
|
|
35
|
+
handlers: HandlerRegistry;
|
|
36
36
|
/** Activate the agent backend (call after extensions load). */
|
|
37
37
|
activateBackend(): void;
|
|
38
38
|
/** Convenience: emit agent:submit and await the response. */
|
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,17 @@
|
|
|
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 * as fs from "node:fs";
|
|
28
|
+
import * as path from "node:path";
|
|
29
|
+
import * as os from "node:os";
|
|
30
|
+
import { DefaultCompositor, StdoutSurface } from "./utils/compositor.js";
|
|
31
|
+
const STORAGE_ROOT = path.join(os.homedir(), ".agent-sh");
|
|
30
32
|
// Re-export types that library consumers need
|
|
31
33
|
export { EventBus } from "./event-bus.js";
|
|
32
34
|
export { palette, setPalette, resetPalette } from "./utils/palette.js";
|
|
@@ -36,92 +38,29 @@ export function createCore(config) {
|
|
|
36
38
|
const bus = new EventBus();
|
|
37
39
|
const handlers = new HandlerRegistry();
|
|
38
40
|
const contextManager = new ContextManager(bus, handlers);
|
|
39
|
-
//
|
|
41
|
+
// 3 bytes = 6 hex chars, ~16M values — ample for per-lineage uniqueness and
|
|
42
|
+
// short enough to read/remember. Legacy content may have 16-char iids; any
|
|
43
|
+
// parsers should accept ≥6 hex chars.
|
|
44
|
+
const instanceId = crypto.randomBytes(3).toString("hex");
|
|
40
45
|
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;
|
|
46
|
+
// Expose raw CLI config so the agent backend extension can resolve
|
|
47
|
+
// providers and create the LLM client.
|
|
48
|
+
handlers.define("config:get-shell-config", () => config);
|
|
96
49
|
const backends = new Map();
|
|
97
50
|
let activeBackendName = null;
|
|
98
51
|
const activateByName = async (name, silent = false) => {
|
|
99
|
-
const backend =
|
|
100
|
-
if (
|
|
52
|
+
const backend = backends.get(name);
|
|
53
|
+
if (!backend) {
|
|
101
54
|
bus.emit("ui:error", { message: `Unknown backend: ${name}` });
|
|
102
55
|
return;
|
|
103
56
|
}
|
|
104
57
|
// Deactivate current backend
|
|
105
|
-
if (activeBackendName
|
|
106
|
-
agentLoop?.unwire();
|
|
107
|
-
}
|
|
108
|
-
else if (activeBackendName) {
|
|
58
|
+
if (activeBackendName) {
|
|
109
59
|
backends.get(activeBackendName)?.kill();
|
|
110
60
|
}
|
|
111
61
|
// 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
|
-
}
|
|
62
|
+
await backend.start?.();
|
|
63
|
+
activeBackendName = name;
|
|
125
64
|
if (!silent) {
|
|
126
65
|
bus.emit("ui:info", { message: `Backend: ${name}` });
|
|
127
66
|
}
|
|
@@ -131,107 +70,30 @@ export function createCore(config) {
|
|
|
131
70
|
backends.set(backend.name, backend);
|
|
132
71
|
});
|
|
133
72
|
bus.on("config:switch-backend", ({ name }) => {
|
|
134
|
-
activateByName(name)
|
|
73
|
+
activateByName(name).then(() => {
|
|
74
|
+
if (activeBackendName === name) {
|
|
75
|
+
settingsMod.updateSettings({ defaultBackend: name });
|
|
76
|
+
bus.emit("ui:info", { message: `Saved '${name}' as default backend.` });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
135
79
|
});
|
|
136
80
|
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);
|
|
81
|
+
const names = [...backends.keys()];
|
|
142
82
|
const list = names
|
|
143
83
|
.map((n) => n === activeBackendName ? `${n} (active)` : n)
|
|
144
84
|
.join(", ");
|
|
145
85
|
bus.emit("ui:info", { message: `Backends: ${list}` });
|
|
146
86
|
});
|
|
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);
|
|
87
|
+
bus.onPipe("config:get-backends", () => {
|
|
88
|
+
const names = [...backends.keys()];
|
|
153
89
|
return { names, active: activeBackendName };
|
|
154
90
|
});
|
|
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
|
-
});
|
|
91
|
+
// ── Compositor ──────────────────────────────────────────────
|
|
92
|
+
const compositor = new DefaultCompositor(bus);
|
|
93
|
+
const stdoutSurface = new StdoutSurface();
|
|
94
|
+
compositor.setDefault("agent", stdoutSurface);
|
|
95
|
+
compositor.setDefault("query", stdoutSurface);
|
|
96
|
+
compositor.setDefault("status", stdoutSurface);
|
|
235
97
|
// ── Lazy singleton terminal buffer ──────────────────────────
|
|
236
98
|
let terminalBufferSingleton; // undefined = not yet created
|
|
237
99
|
const getTerminalBuffer = () => {
|
|
@@ -243,23 +105,17 @@ export function createCore(config) {
|
|
|
243
105
|
return {
|
|
244
106
|
bus,
|
|
245
107
|
contextManager,
|
|
246
|
-
|
|
108
|
+
handlers,
|
|
247
109
|
activateBackend() {
|
|
248
110
|
// Silent — backend info is shown in the startup banner.
|
|
249
111
|
// Runtime switches (config:switch-backend) still emit ui:info.
|
|
112
|
+
if (backends.size === 0)
|
|
113
|
+
return;
|
|
250
114
|
const preferred = settings.defaultBackend;
|
|
251
115
|
if (preferred && backends.has(preferred)) {
|
|
252
116
|
activateByName(preferred, true);
|
|
253
117
|
}
|
|
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) {
|
|
118
|
+
else {
|
|
263
119
|
activateByName(backends.keys().next().value, true);
|
|
264
120
|
}
|
|
265
121
|
},
|
|
@@ -301,33 +157,80 @@ export function createCore(config) {
|
|
|
301
157
|
bus.emit("agent:cancel-request", {});
|
|
302
158
|
},
|
|
303
159
|
extensionContext(opts) {
|
|
304
|
-
|
|
160
|
+
const ctx = {
|
|
305
161
|
bus,
|
|
306
162
|
contextManager,
|
|
307
|
-
|
|
163
|
+
instanceId,
|
|
308
164
|
quit: opts.quit,
|
|
309
165
|
setPalette,
|
|
310
166
|
createBlockTransform: (o) => streamTransform.createBlockTransform(bus, o),
|
|
311
167
|
createFencedBlockTransform: (o) => streamTransform.createFencedBlockTransform(bus, o),
|
|
312
168
|
getExtensionSettings: settingsMod.getExtensionSettings,
|
|
169
|
+
getStoragePath: (namespace) => {
|
|
170
|
+
const dir = path.join(STORAGE_ROOT, namespace);
|
|
171
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
172
|
+
return dir;
|
|
173
|
+
},
|
|
313
174
|
registerCommand: (name, description, handler) => bus.emit("command:register", { name, description, handler }),
|
|
314
|
-
registerTool: (tool) =>
|
|
315
|
-
|
|
175
|
+
registerTool: (tool) => bus.emit("agent:register-tool", { tool, extensionName: "" }),
|
|
176
|
+
unregisterTool: (name) => bus.emit("agent:unregister-tool", { name }),
|
|
177
|
+
getTools: () => bus.emitPipe("agent:get-tools", { tools: [] }).tools,
|
|
178
|
+
registerInstruction: (name, text) => bus.emit("agent:register-instruction", { name, text, extensionName: "" }),
|
|
179
|
+
removeInstruction: (name) => bus.emit("agent:remove-instruction", { name }),
|
|
180
|
+
registerSkill: (name, description, filePath) => bus.emit("agent:register-skill", { name, description, filePath, extensionName: "" }),
|
|
181
|
+
removeSkill: (name) => bus.emit("agent:remove-skill", { name }),
|
|
316
182
|
define: (name, fn) => handlers.define(name, fn),
|
|
317
183
|
advise: (name, wrapper) => handlers.advise(name, wrapper),
|
|
318
184
|
call: (name, ...args) => handlers.call(name, ...args),
|
|
185
|
+
list: () => handlers.list(),
|
|
319
186
|
get terminalBuffer() { return getTerminalBuffer(); },
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
187
|
+
compositor,
|
|
188
|
+
createRemoteSession: (opts) => {
|
|
189
|
+
const { surface } = opts;
|
|
190
|
+
const cleanups = [];
|
|
191
|
+
let active = true;
|
|
192
|
+
// Redirect all render streams
|
|
193
|
+
cleanups.push(compositor.redirect("agent", surface));
|
|
194
|
+
cleanups.push(compositor.redirect("query", surface));
|
|
195
|
+
cleanups.push(compositor.redirect("status", surface));
|
|
196
|
+
// Keep shell interactive
|
|
197
|
+
cleanups.push(handlers.advise("shell:on-processing-start", (next) => active ? undefined : next()));
|
|
198
|
+
cleanups.push(handlers.advise("shell:on-processing-done", (next) => active ? undefined : next()));
|
|
199
|
+
// Suppress chrome
|
|
200
|
+
if (opts.suppressBorders !== false) {
|
|
201
|
+
cleanups.push(handlers.advise("tui:response-border", (next, ...a) => active ? null : next(...a)));
|
|
202
|
+
}
|
|
203
|
+
if (opts.suppressQueryBox) {
|
|
204
|
+
cleanups.push(handlers.advise("tui:render-user-query", (next, ...a) => active ? [] : next(...a)));
|
|
205
|
+
}
|
|
206
|
+
if (opts.suppressUsage !== false) {
|
|
207
|
+
cleanups.push(handlers.advise("tui:render-usage", (next, ...a) => active ? "" : next(...a)));
|
|
208
|
+
}
|
|
209
|
+
if (opts.interactive) {
|
|
210
|
+
cleanups.push(handlers.advise("dynamic-context:build", (next) => {
|
|
211
|
+
const base = next();
|
|
212
|
+
return active ? base + "\ninteractive-session: true\n" : base;
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
submit(query) { bus.emit("agent:submit", { query }); },
|
|
217
|
+
get surface() { return surface; },
|
|
218
|
+
get active() { return active; },
|
|
219
|
+
close() {
|
|
220
|
+
if (!active)
|
|
221
|
+
return;
|
|
222
|
+
active = false;
|
|
223
|
+
for (const fn of cleanups.reverse())
|
|
224
|
+
fn();
|
|
225
|
+
cleanups.length = 0;
|
|
226
|
+
},
|
|
227
|
+
};
|
|
323
228
|
},
|
|
324
229
|
};
|
|
230
|
+
return ctx;
|
|
325
231
|
},
|
|
326
232
|
kill() {
|
|
327
|
-
if (activeBackendName
|
|
328
|
-
agentLoop?.kill();
|
|
329
|
-
}
|
|
330
|
-
else if (activeBackendName) {
|
|
233
|
+
if (activeBackendName) {
|
|
331
234
|
backends.get(activeBackendName)?.kill();
|
|
332
235
|
}
|
|
333
236
|
},
|