agent-sh 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -30
- package/dist/agent/agent-loop.d.ts +43 -6
- package/dist/agent/agent-loop.js +817 -157
- package/dist/agent/conversation-state.d.ts +72 -21
- package/dist/agent/conversation-state.js +364 -151
- 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 +84 -3
- 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 +34 -1
- package/dist/agent/system-prompt.js +96 -47
- package/dist/agent/token-budget.d.ts +10 -13
- package/dist/agent/token-budget.js +6 -46
- package/dist/agent/tool-protocol.d.ts +23 -1
- package/dist/agent/tool-protocol.js +169 -4
- 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 +1 -2
- package/dist/context-manager.d.ts +16 -19
- package/dist/context-manager.js +48 -152
- package/dist/core.js +27 -6
- package/dist/event-bus.d.ts +59 -3
- package/dist/executor.d.ts +4 -3
- package/dist/executor.js +18 -15
- package/dist/extension-loader.js +75 -17
- package/dist/extensions/agent-backend.d.ts +8 -7
- package/dist/extensions/agent-backend.js +72 -50
- package/dist/extensions/index.js +0 -2
- package/dist/extensions/slash-commands.js +14 -9
- package/dist/extensions/tui-renderer.js +67 -80
- package/dist/index.js +25 -6
- package/dist/settings.d.ts +39 -16
- package/dist/settings.js +51 -11
- package/dist/shell/input-handler.d.ts +2 -1
- package/dist/shell/input-handler.js +84 -76
- package/dist/shell/shell.js +19 -2
- package/dist/types.d.ts +15 -0
- package/dist/utils/ansi.d.ts +7 -0
- package/dist/utils/ansi.js +69 -8
- package/dist/utils/box-frame.js +8 -2
- package/dist/utils/compositor.d.ts +5 -0
- package/dist/utils/compositor.js +31 -3
- package/dist/utils/diff-renderer.d.ts +9 -0
- package/dist/utils/diff-renderer.js +221 -143
- package/dist/utils/diff.d.ts +21 -2
- package/dist/utils/diff.js +165 -89
- package/dist/utils/handler-registry.d.ts +5 -0
- package/dist/utils/handler-registry.js +6 -0
- package/dist/utils/line-editor.d.ts +11 -1
- package/dist/utils/line-editor.js +44 -5
- package/dist/utils/markdown.js +23 -8
- package/dist/utils/package-version.d.ts +1 -0
- package/dist/utils/package-version.js +10 -0
- package/dist/utils/shell-output-spill.d.ts +2 -0
- package/dist/utils/shell-output-spill.js +81 -0
- package/dist/utils/tool-display.d.ts +1 -1
- package/dist/utils/tool-display.js +4 -4
- package/examples/extensions/ash-acp-bridge/src/index.ts +4 -1
- package/examples/extensions/ash-mcp-bridge/index.ts +13 -3
- package/examples/extensions/claude-code-bridge/README.md +14 -0
- package/examples/extensions/claude-code-bridge/index.ts +204 -145
- package/examples/extensions/claude-code-bridge/package.json +1 -0
- package/examples/extensions/interactive-prompts.ts +39 -25
- package/examples/extensions/overlay-agent.ts +3 -3
- package/examples/extensions/peer-mesh.ts +115 -0
- package/examples/extensions/pi-bridge/README.md +16 -0
- package/examples/extensions/pi-bridge/index.ts +9 -155
- package/examples/extensions/questionnaire.ts +16 -5
- package/examples/extensions/subagents.ts +19 -4
- package/examples/extensions/terminal-buffer.ts +163 -0
- package/examples/extensions/user-shell.ts +136 -0
- package/examples/extensions/web-access.ts +8 -0
- package/package.json +36 -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/shell-recall.d.ts +0 -9
- package/dist/extensions/shell-recall.js +0 -8
- package/dist/extensions/terminal-buffer.d.ts +0 -14
- package/dist/extensions/terminal-buffer.js +0 -134
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
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* Backends self-wire to bus events in their constructor:
|
|
5
5
|
* - agent:submit → handle queries
|
|
6
6
|
* - agent:cancel-request → handle cancellation
|
|
7
|
-
* - config:cycle → handle mode switching
|
|
8
7
|
*
|
|
9
8
|
* They emit bus events for results:
|
|
10
9
|
* - agent:response-chunk, agent:tool-started, agent:tool-completed, etc.
|
|
@@ -38,7 +37,7 @@ export type ToolResultBody = {
|
|
|
38
37
|
maxLines?: number;
|
|
39
38
|
};
|
|
40
39
|
export interface ToolDisplayInfo {
|
|
41
|
-
kind: "read" | "write" | "execute" | "search"
|
|
40
|
+
kind: "read" | "write" | "execute" | "search";
|
|
42
41
|
locations?: {
|
|
43
42
|
path: string;
|
|
44
43
|
line?: number | null;
|
|
@@ -4,41 +4,38 @@ export declare class ContextManager {
|
|
|
4
4
|
private exchanges;
|
|
5
5
|
private nextId;
|
|
6
6
|
private currentCwd;
|
|
7
|
-
private sessionStart;
|
|
8
|
-
private firstPrompt;
|
|
9
7
|
private agentShellActive;
|
|
10
|
-
|
|
11
|
-
constructor(bus: EventBus, handlers?: HandlerRegistry);
|
|
8
|
+
constructor(bus: EventBus, _handlers?: HandlerRegistry);
|
|
12
9
|
getCwd(): string;
|
|
13
|
-
/**
|
|
14
|
-
* Build the <shell_context> block for the agent prompt.
|
|
15
|
-
* Pipeline: window → truncate → format
|
|
16
|
-
*/
|
|
17
|
-
getContext(budget?: number): string;
|
|
18
10
|
/**
|
|
19
11
|
* Regex/keyword search across all exchanges. Returns formatted results.
|
|
20
12
|
*/
|
|
21
13
|
search(query: string): string;
|
|
22
14
|
/**
|
|
23
|
-
* Return
|
|
24
|
-
*
|
|
15
|
+
* Return shell events with id > afterId, formatted as an incremental
|
|
16
|
+
* delta suitable for injection into conversation history. Skips
|
|
17
|
+
* agent-source commands (already visible in tool results). Returns
|
|
18
|
+
* null when nothing new exists.
|
|
19
|
+
*
|
|
20
|
+
* The motivation: resending the full <shell_context> every turn wastes
|
|
21
|
+
* tokens — N turns × full history = O(N²) cost for O(N) information.
|
|
22
|
+
* Instead we inject only new events as regular conversation messages,
|
|
23
|
+
* so the provider's prefix cache amortizes them to O(N).
|
|
25
24
|
*/
|
|
26
|
-
|
|
25
|
+
getEventsSince(afterId: number): {
|
|
26
|
+
text: string;
|
|
27
|
+
lastSeq: number;
|
|
28
|
+
} | null;
|
|
29
|
+
/** Highest exchange id seen so far (0 if none). */
|
|
30
|
+
lastSeq(): number;
|
|
27
31
|
/**
|
|
28
32
|
* One-line summaries of last N exchanges.
|
|
29
33
|
*/
|
|
30
34
|
getRecentSummary(n?: number): string;
|
|
31
|
-
/**
|
|
32
|
-
* Parse and handle shell_recall commands.
|
|
33
|
-
*/
|
|
34
|
-
handleRecallCommand(command: string): string;
|
|
35
35
|
/**
|
|
36
36
|
* Clear exchange history (used by /clear command).
|
|
37
37
|
*/
|
|
38
38
|
clear(): void;
|
|
39
|
-
private applyWindow;
|
|
40
|
-
private applyTruncation;
|
|
41
|
-
private formatContext;
|
|
42
39
|
private addExchange;
|
|
43
40
|
private formatExchangeTruncated;
|
|
44
41
|
private formatExchangeFull;
|
package/dist/context-manager.js
CHANGED
|
@@ -1,32 +1,43 @@
|
|
|
1
1
|
import { getSettings } from "./settings.js";
|
|
2
|
+
import { spillOutput } from "./utils/shell-output-spill.js";
|
|
2
3
|
export class ContextManager {
|
|
3
4
|
exchanges = [];
|
|
4
5
|
nextId = 1;
|
|
5
6
|
currentCwd;
|
|
6
|
-
sessionStart;
|
|
7
|
-
firstPrompt = true;
|
|
8
7
|
agentShellActive = false; // true while user_shell command is executing
|
|
9
|
-
|
|
10
|
-
constructor(bus, handlers) {
|
|
11
|
-
if (handlers) {
|
|
12
|
-
this.handlers = handlers;
|
|
13
|
-
// Extensions can advise this to inject extra context (e.g. terminal buffer)
|
|
14
|
-
handlers.define("context:build-extra", () => "");
|
|
15
|
-
}
|
|
8
|
+
constructor(bus, _handlers) {
|
|
16
9
|
this.currentCwd = process.cwd();
|
|
17
|
-
this.sessionStart = Date.now();
|
|
18
10
|
// ── Subscribe to shell events ──
|
|
19
11
|
bus.on("shell:command-done", (e) => {
|
|
20
12
|
const lines = e.output.split("\n");
|
|
13
|
+
const s = getSettings();
|
|
14
|
+
// Spill long outputs to a tempfile so the agent can `read_file` them
|
|
15
|
+
// on demand instead of carrying the full text in LLM context.
|
|
16
|
+
let output = e.output;
|
|
17
|
+
let spillPath;
|
|
18
|
+
if (lines.length > s.shellTruncateThreshold) {
|
|
19
|
+
// Reserve the id we're about to assign so the tempfile name matches.
|
|
20
|
+
const id = this.nextId;
|
|
21
|
+
try {
|
|
22
|
+
spillPath = spillOutput(id, e.output);
|
|
23
|
+
output = buildSpillStub(lines, s.shellHeadLines, s.shellTailLines, spillPath);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// If spill fails (e.g. disk full), fall back to keeping output in memory.
|
|
27
|
+
output = e.output;
|
|
28
|
+
spillPath = undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
21
31
|
this.addExchange({
|
|
22
32
|
type: "shell_command",
|
|
23
33
|
command: e.command,
|
|
24
|
-
output
|
|
34
|
+
output,
|
|
25
35
|
cwd: e.cwd,
|
|
26
36
|
exitCode: e.exitCode,
|
|
27
37
|
outputLines: lines.length,
|
|
28
38
|
outputBytes: e.output.length,
|
|
29
39
|
source: this.agentShellActive ? "agent" : "user",
|
|
40
|
+
spillPath,
|
|
30
41
|
});
|
|
31
42
|
});
|
|
32
43
|
bus.on("shell:cwd-change", (e) => {
|
|
@@ -46,16 +57,6 @@ export class ContextManager {
|
|
|
46
57
|
getCwd() {
|
|
47
58
|
return this.currentCwd;
|
|
48
59
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Build the <shell_context> block for the agent prompt.
|
|
51
|
-
* Pipeline: window → truncate → format
|
|
52
|
-
*/
|
|
53
|
-
getContext(budget) {
|
|
54
|
-
budget ??= getSettings().contextBudget;
|
|
55
|
-
let exchanges = this.applyWindow(this.exchanges);
|
|
56
|
-
exchanges = this.applyTruncation(exchanges, budget);
|
|
57
|
-
return this.formatContext(exchanges);
|
|
58
|
-
}
|
|
59
60
|
/**
|
|
60
61
|
* Regex/keyword search across all exchanges. Returns formatted results.
|
|
61
62
|
*/
|
|
@@ -107,38 +108,31 @@ export class ContextManager {
|
|
|
107
108
|
return parts.join("\n");
|
|
108
109
|
}
|
|
109
110
|
/**
|
|
110
|
-
* Return
|
|
111
|
-
*
|
|
111
|
+
* Return shell events with id > afterId, formatted as an incremental
|
|
112
|
+
* delta suitable for injection into conversation history. Skips
|
|
113
|
+
* agent-source commands (already visible in tool results). Returns
|
|
114
|
+
* null when nothing new exists.
|
|
115
|
+
*
|
|
116
|
+
* The motivation: resending the full <shell_context> every turn wastes
|
|
117
|
+
* tokens — N turns × full history = O(N²) cost for O(N) information.
|
|
118
|
+
* Instead we inject only new events as regular conversation messages,
|
|
119
|
+
* so the provider's prefix cache amortizes them to O(N).
|
|
112
120
|
*/
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
results.push(lines.slice(s, e).join("\n") +
|
|
129
|
-
`\n[showing lines ${s + 1}-${Math.min(e, total)} of ${total}]`);
|
|
130
|
-
}
|
|
131
|
-
else if (total > getSettings().recallExpandMaxLines) {
|
|
132
|
-
// Too large — tell the agent to narrow down
|
|
133
|
-
results.push(`#${ex.id}: output is ${total} lines, too large to expand fully. ` +
|
|
134
|
-
`Use start/end params to select a line range (e.g. start=1, end=50), ` +
|
|
135
|
-
`or use search with a regex to find specific content.`);
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
results.push(text);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return results.join("\n\n");
|
|
121
|
+
getEventsSince(afterId) {
|
|
122
|
+
const fresh = this.exchanges.filter((e) => e.id > afterId && !(e.type === "shell_command" && e.source === "agent"));
|
|
123
|
+
if (fresh.length === 0)
|
|
124
|
+
return null;
|
|
125
|
+
const lastSeq = this.exchanges[this.exchanges.length - 1].id;
|
|
126
|
+
// Outputs already carry head+tail+spillPath stubs from capture time.
|
|
127
|
+
const body = fresh.map((ex) => this.formatExchangeTruncated(ex)).join("\n");
|
|
128
|
+
return {
|
|
129
|
+
text: `<shell-events>\n${body}</shell-events>`,
|
|
130
|
+
lastSeq,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** Highest exchange id seen so far (0 if none). */
|
|
134
|
+
lastSeq() {
|
|
135
|
+
return this.exchanges.length === 0 ? 0 : this.exchanges[this.exchanges.length - 1].id;
|
|
142
136
|
}
|
|
143
137
|
/**
|
|
144
138
|
* One-line summaries of last N exchanges.
|
|
@@ -149,108 +143,13 @@ export class ContextManager {
|
|
|
149
143
|
return "No exchanges yet.";
|
|
150
144
|
return recent.map((ex) => this.exchangeOneLiner(ex)).join("\n");
|
|
151
145
|
}
|
|
152
|
-
/**
|
|
153
|
-
* Parse and handle shell_recall commands.
|
|
154
|
-
*/
|
|
155
|
-
handleRecallCommand(command) {
|
|
156
|
-
const args = command.replace(/^_*shell_recall\s*/, "").trim();
|
|
157
|
-
if (!args || args === "--help") {
|
|
158
|
-
return [
|
|
159
|
-
"Usage:",
|
|
160
|
-
" shell_recall Browse recent exchanges",
|
|
161
|
-
" shell_recall --search <query> Search all exchanges",
|
|
162
|
-
" shell_recall --expand <id,...> Show full content of exchanges",
|
|
163
|
-
"",
|
|
164
|
-
"Examples:",
|
|
165
|
-
' shell_recall --search "test fail"',
|
|
166
|
-
" shell_recall --expand 41",
|
|
167
|
-
" shell_recall --expand 41,42,43",
|
|
168
|
-
].join("\n");
|
|
169
|
-
}
|
|
170
|
-
const searchMatch = args.match(/^--search\s+(?:"([^"]+)"|(\S+))/);
|
|
171
|
-
if (searchMatch) {
|
|
172
|
-
return this.search(searchMatch[1] ?? searchMatch[2] ?? "");
|
|
173
|
-
}
|
|
174
|
-
const expandMatch = args.match(/^--expand\s+([\d,\s]+)/);
|
|
175
|
-
if (expandMatch) {
|
|
176
|
-
const ids = expandMatch[1]
|
|
177
|
-
.split(/[,\s]+/)
|
|
178
|
-
.map(Number)
|
|
179
|
-
.filter((n) => !isNaN(n));
|
|
180
|
-
if (ids.length === 0)
|
|
181
|
-
return "No valid IDs provided.";
|
|
182
|
-
return this.expand(ids);
|
|
183
|
-
}
|
|
184
|
-
// Default: browse
|
|
185
|
-
return this.getRecentSummary();
|
|
186
|
-
}
|
|
187
146
|
/**
|
|
188
147
|
* Clear exchange history (used by /clear command).
|
|
189
148
|
*/
|
|
190
149
|
clear() {
|
|
191
150
|
this.exchanges = [];
|
|
192
|
-
this.firstPrompt = true;
|
|
193
151
|
// Don't reset nextId — IDs should be globally unique within a session
|
|
194
152
|
}
|
|
195
|
-
// ── Pipeline stages ───────────────────────────────────────────
|
|
196
|
-
applyWindow(exchanges, windowSize) {
|
|
197
|
-
windowSize ??= getSettings().contextWindowSize;
|
|
198
|
-
return exchanges.slice(-windowSize);
|
|
199
|
-
}
|
|
200
|
-
applyTruncation(exchanges, budget) {
|
|
201
|
-
// Deep clone so we don't mutate the source
|
|
202
|
-
const result = exchanges.map((e) => ({ ...e }));
|
|
203
|
-
// Pass 1: per-type truncation
|
|
204
|
-
for (const ex of result) {
|
|
205
|
-
if (ex.type === "shell_command") {
|
|
206
|
-
const s = getSettings();
|
|
207
|
-
ex.output = truncateOutput(ex.output, s.shellTruncateThreshold, s.shellHeadLines, s.shellTailLines, ex.id);
|
|
208
|
-
}
|
|
209
|
-
// agent_query has no output to truncate
|
|
210
|
-
}
|
|
211
|
-
// Pass 2: budget enforcement — strip output from oldest if over budget
|
|
212
|
-
let totalSize = result.reduce((sum, ex) => sum + this.exchangeSize(ex), 0);
|
|
213
|
-
for (let i = 0; i < result.length - 1 && totalSize > budget; i++) {
|
|
214
|
-
const ex = result[i];
|
|
215
|
-
const before = this.exchangeSize(ex);
|
|
216
|
-
if (ex.type === "shell_command") {
|
|
217
|
-
ex.output = `[output omitted, use shell_recall tool to expand id ${ex.id}]`;
|
|
218
|
-
}
|
|
219
|
-
totalSize -= before - this.exchangeSize(ex);
|
|
220
|
-
}
|
|
221
|
-
return result;
|
|
222
|
-
}
|
|
223
|
-
formatContext(exchanges) {
|
|
224
|
-
const elapsed = Math.round((Date.now() - this.sessionStart) / 60000);
|
|
225
|
-
const totalCount = this.exchanges.length;
|
|
226
|
-
let out = "<shell_context>\n";
|
|
227
|
-
if (this.firstPrompt) {
|
|
228
|
-
out += `You are an AI assistant living inside agent-sh, a shell-first terminal.\n`;
|
|
229
|
-
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
|
-
out += `\n`;
|
|
231
|
-
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
|
-
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
|
-
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
|
-
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
|
-
out += `\n`;
|
|
240
|
-
this.firstPrompt = false;
|
|
241
|
-
}
|
|
242
|
-
out += `cwd: ${this.currentCwd}\n`;
|
|
243
|
-
out += `session: ${totalCount} exchanges, ${elapsed}m elapsed\n`;
|
|
244
|
-
for (const ex of exchanges) {
|
|
245
|
-
out += "\n" + this.formatExchangeTruncated(ex);
|
|
246
|
-
}
|
|
247
|
-
// Allow extensions to inject extra context (e.g. terminal buffer snapshot)
|
|
248
|
-
const extra = this.handlers?.call("context:build-extra");
|
|
249
|
-
if (extra)
|
|
250
|
-
out += "\n" + extra + "\n";
|
|
251
|
-
out += "\n</shell_context>\n";
|
|
252
|
-
return out;
|
|
253
|
-
}
|
|
254
153
|
// ── Internal helpers ──────────────────────────────────────────
|
|
255
154
|
addExchange(partial) {
|
|
256
155
|
const exchange = {
|
|
@@ -319,14 +218,11 @@ export class ContextManager {
|
|
|
319
218
|
}
|
|
320
219
|
}
|
|
321
220
|
// ── Utility functions ─────────────────────────────────────────
|
|
322
|
-
function
|
|
323
|
-
const lines = text.split("\n");
|
|
324
|
-
if (lines.length <= threshold)
|
|
325
|
-
return text;
|
|
221
|
+
function buildSpillStub(lines, headLines, tailLines, spillPath) {
|
|
326
222
|
const omitted = lines.length - headLines - tailLines;
|
|
327
223
|
return [
|
|
328
224
|
...lines.slice(0, headLines),
|
|
329
|
-
`[... ${omitted} lines truncated
|
|
225
|
+
`[... ${omitted} lines truncated — full output at ${spillPath}; use read_file to expand ...]`,
|
|
330
226
|
...lines.slice(-tailLines),
|
|
331
227
|
].join("\n");
|
|
332
228
|
}
|
package/dist/core.js
CHANGED
|
@@ -24,7 +24,11 @@ import * as settingsMod from "./settings.js";
|
|
|
24
24
|
import { HandlerRegistry } from "./utils/handler-registry.js";
|
|
25
25
|
import { TerminalBuffer } from "./utils/terminal-buffer.js";
|
|
26
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";
|
|
27
30
|
import { DefaultCompositor, StdoutSurface } from "./utils/compositor.js";
|
|
31
|
+
const STORAGE_ROOT = path.join(os.homedir(), ".agent-sh");
|
|
28
32
|
// Re-export types that library consumers need
|
|
29
33
|
export { EventBus } from "./event-bus.js";
|
|
30
34
|
export { palette, setPalette, resetPalette } from "./utils/palette.js";
|
|
@@ -34,7 +38,10 @@ export function createCore(config) {
|
|
|
34
38
|
const bus = new EventBus();
|
|
35
39
|
const handlers = new HandlerRegistry();
|
|
36
40
|
const contextManager = new ContextManager(bus, handlers);
|
|
37
|
-
|
|
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");
|
|
38
45
|
const settings = settingsMod.getSettings();
|
|
39
46
|
// Expose raw CLI config so the agent backend extension can resolve
|
|
40
47
|
// providers and create the LLM client.
|
|
@@ -63,7 +70,12 @@ export function createCore(config) {
|
|
|
63
70
|
backends.set(backend.name, backend);
|
|
64
71
|
});
|
|
65
72
|
bus.on("config:switch-backend", ({ name }) => {
|
|
66
|
-
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
|
+
});
|
|
67
79
|
});
|
|
68
80
|
bus.on("config:list-backends", () => {
|
|
69
81
|
const names = [...backends.keys()];
|
|
@@ -77,7 +89,7 @@ export function createCore(config) {
|
|
|
77
89
|
return { names, active: activeBackendName };
|
|
78
90
|
});
|
|
79
91
|
// ── Compositor ──────────────────────────────────────────────
|
|
80
|
-
const compositor = new DefaultCompositor();
|
|
92
|
+
const compositor = new DefaultCompositor(bus);
|
|
81
93
|
const stdoutSurface = new StdoutSurface();
|
|
82
94
|
compositor.setDefault("agent", stdoutSurface);
|
|
83
95
|
compositor.setDefault("query", stdoutSurface);
|
|
@@ -145,7 +157,7 @@ export function createCore(config) {
|
|
|
145
157
|
bus.emit("agent:cancel-request", {});
|
|
146
158
|
},
|
|
147
159
|
extensionContext(opts) {
|
|
148
|
-
|
|
160
|
+
const ctx = {
|
|
149
161
|
bus,
|
|
150
162
|
contextManager,
|
|
151
163
|
instanceId,
|
|
@@ -154,15 +166,23 @@ export function createCore(config) {
|
|
|
154
166
|
createBlockTransform: (o) => streamTransform.createBlockTransform(bus, o),
|
|
155
167
|
createFencedBlockTransform: (o) => streamTransform.createFencedBlockTransform(bus, o),
|
|
156
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
|
+
},
|
|
157
174
|
registerCommand: (name, description, handler) => bus.emit("command:register", { name, description, handler }),
|
|
158
|
-
registerTool: (tool) => bus.emit("agent:register-tool", { tool }),
|
|
175
|
+
registerTool: (tool) => bus.emit("agent:register-tool", { tool, extensionName: "" }),
|
|
159
176
|
unregisterTool: (name) => bus.emit("agent:unregister-tool", { name }),
|
|
160
177
|
getTools: () => bus.emitPipe("agent:get-tools", { tools: [] }).tools,
|
|
161
|
-
registerInstruction: (name, text) => bus.emit("agent:register-instruction", { name, text }),
|
|
178
|
+
registerInstruction: (name, text) => bus.emit("agent:register-instruction", { name, text, extensionName: "" }),
|
|
162
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 }),
|
|
163
182
|
define: (name, fn) => handlers.define(name, fn),
|
|
164
183
|
advise: (name, wrapper) => handlers.advise(name, wrapper),
|
|
165
184
|
call: (name, ...args) => handlers.call(name, ...args),
|
|
185
|
+
list: () => handlers.list(),
|
|
166
186
|
get terminalBuffer() { return getTerminalBuffer(); },
|
|
167
187
|
compositor,
|
|
168
188
|
createRemoteSession: (opts) => {
|
|
@@ -207,6 +227,7 @@ export function createCore(config) {
|
|
|
207
227
|
};
|
|
208
228
|
},
|
|
209
229
|
};
|
|
230
|
+
return ctx;
|
|
210
231
|
},
|
|
211
232
|
kill() {
|
|
212
233
|
if (activeBackendName) {
|
package/dist/event-bus.d.ts
CHANGED
|
@@ -89,6 +89,28 @@ export interface ShellEvents {
|
|
|
89
89
|
}>;
|
|
90
90
|
}>;
|
|
91
91
|
};
|
|
92
|
+
"agent:tool-batch-complete": {
|
|
93
|
+
results: Array<{
|
|
94
|
+
name: string;
|
|
95
|
+
isError: boolean;
|
|
96
|
+
errorSummary?: string;
|
|
97
|
+
}>;
|
|
98
|
+
};
|
|
99
|
+
"conversation:message-appended": {
|
|
100
|
+
role: "user" | "assistant" | "tool" | "system";
|
|
101
|
+
content: string;
|
|
102
|
+
/** For role="tool": name of the tool whose result this is. */
|
|
103
|
+
toolName?: string;
|
|
104
|
+
/** For role="tool": parsed arguments passed to the tool. */
|
|
105
|
+
toolArgs?: Record<string, unknown>;
|
|
106
|
+
/** For role="tool": whether the tool errored. */
|
|
107
|
+
isError?: boolean;
|
|
108
|
+
};
|
|
109
|
+
"conversation:after-compact": {
|
|
110
|
+
beforeTokens: number;
|
|
111
|
+
afterTokens: number;
|
|
112
|
+
evictedCount: number;
|
|
113
|
+
};
|
|
92
114
|
"agent:tool-started": {
|
|
93
115
|
title: string;
|
|
94
116
|
toolCallId?: string;
|
|
@@ -115,6 +137,16 @@ export interface ShellEvents {
|
|
|
115
137
|
"agent:tool-output-chunk": {
|
|
116
138
|
chunk: string;
|
|
117
139
|
};
|
|
140
|
+
"agent:subagent-started": {
|
|
141
|
+
taskId: string;
|
|
142
|
+
task: string;
|
|
143
|
+
};
|
|
144
|
+
"agent:subagent-completed": {
|
|
145
|
+
taskId: string;
|
|
146
|
+
task: string;
|
|
147
|
+
result: string;
|
|
148
|
+
isError: boolean;
|
|
149
|
+
};
|
|
118
150
|
"tool:interactive-start": Record<string, never>;
|
|
119
151
|
"tool:interactive-end": Record<string, never>;
|
|
120
152
|
"permission:request": {
|
|
@@ -130,6 +162,9 @@ export interface ShellEvents {
|
|
|
130
162
|
description: string;
|
|
131
163
|
handler: (args: string) => Promise<void> | void;
|
|
132
164
|
};
|
|
165
|
+
"command:unregister": {
|
|
166
|
+
name: string;
|
|
167
|
+
};
|
|
133
168
|
"command:execute": {
|
|
134
169
|
name: string;
|
|
135
170
|
args: string;
|
|
@@ -143,6 +178,10 @@ export interface ShellEvents {
|
|
|
143
178
|
"ui:suggestion": {
|
|
144
179
|
text: string;
|
|
145
180
|
};
|
|
181
|
+
"compositor:write": {
|
|
182
|
+
stream: string;
|
|
183
|
+
text: string;
|
|
184
|
+
};
|
|
146
185
|
"input:keypress": {
|
|
147
186
|
key: string;
|
|
148
187
|
};
|
|
@@ -182,8 +221,7 @@ export interface ShellEvents {
|
|
|
182
221
|
"agent:compact-request": Record<string, never>;
|
|
183
222
|
"context:get-stats": {
|
|
184
223
|
activeTokens: number;
|
|
185
|
-
|
|
186
|
-
recallArchiveSize: number;
|
|
224
|
+
totalTokens: number;
|
|
187
225
|
budgetTokens: number;
|
|
188
226
|
};
|
|
189
227
|
"agent:register-backend": {
|
|
@@ -200,7 +238,6 @@ export interface ShellEvents {
|
|
|
200
238
|
active: string | null;
|
|
201
239
|
};
|
|
202
240
|
"config:changed": Record<string, never>;
|
|
203
|
-
"config:cycle": Record<string, never>;
|
|
204
241
|
"config:switch-model": {
|
|
205
242
|
model: string;
|
|
206
243
|
};
|
|
@@ -228,10 +265,12 @@ export interface ShellEvents {
|
|
|
228
265
|
};
|
|
229
266
|
"config:set-modes": {
|
|
230
267
|
modes: AgentMode[];
|
|
268
|
+
activeIndex?: number;
|
|
231
269
|
};
|
|
232
270
|
"config:add-modes": {
|
|
233
271
|
modes: AgentMode[];
|
|
234
272
|
};
|
|
273
|
+
"core:extensions-loaded": Record<string, never>;
|
|
235
274
|
"provider:register": {
|
|
236
275
|
id: string;
|
|
237
276
|
apiKey?: string;
|
|
@@ -247,6 +286,7 @@ export interface ShellEvents {
|
|
|
247
286
|
};
|
|
248
287
|
"agent:register-tool": {
|
|
249
288
|
tool: import("./agent/types.js").ToolDefinition;
|
|
289
|
+
extensionName?: string;
|
|
250
290
|
};
|
|
251
291
|
"agent:unregister-tool": {
|
|
252
292
|
name: string;
|
|
@@ -257,10 +297,26 @@ export interface ShellEvents {
|
|
|
257
297
|
"agent:register-instruction": {
|
|
258
298
|
name: string;
|
|
259
299
|
text: string;
|
|
300
|
+
extensionName: string;
|
|
260
301
|
};
|
|
261
302
|
"agent:remove-instruction": {
|
|
262
303
|
name: string;
|
|
263
304
|
};
|
|
305
|
+
"agent:register-skill": {
|
|
306
|
+
name: string;
|
|
307
|
+
description: string;
|
|
308
|
+
filePath: string;
|
|
309
|
+
extensionName: string;
|
|
310
|
+
};
|
|
311
|
+
"agent:remove-skill": {
|
|
312
|
+
name: string;
|
|
313
|
+
};
|
|
314
|
+
"banner:collect": {
|
|
315
|
+
sections: Array<{
|
|
316
|
+
label: string;
|
|
317
|
+
items: string[];
|
|
318
|
+
}>;
|
|
319
|
+
};
|
|
264
320
|
"autocomplete:request": {
|
|
265
321
|
buffer: string;
|
|
266
322
|
/** Parsed slash command name (e.g. "/backend"), or null if not a command. */
|
package/dist/executor.d.ts
CHANGED
|
@@ -25,7 +25,8 @@ export declare function executeCommand(opts: {
|
|
|
25
25
|
done: Promise<void>;
|
|
26
26
|
};
|
|
27
27
|
/**
|
|
28
|
-
* Kill a running session's process group.
|
|
29
|
-
*
|
|
28
|
+
* Kill a running session's process group: SIGTERM, then SIGKILL after 5s.
|
|
29
|
+
* Returns a cleanup that cancels the pending SIGKILL — callers should invoke
|
|
30
|
+
* it once the process has exited.
|
|
30
31
|
*/
|
|
31
|
-
export declare function killSession(session: ExecutorSession): void;
|
|
32
|
+
export declare function killSession(session: ExecutorSession): () => void;
|
package/dist/executor.js
CHANGED
|
@@ -60,14 +60,15 @@ export function executeCommand(opts) {
|
|
|
60
60
|
};
|
|
61
61
|
child.stdout?.on("data", handleData);
|
|
62
62
|
child.stderr?.on("data", handleData);
|
|
63
|
-
|
|
63
|
+
let cancelKill;
|
|
64
64
|
const timer = setTimeout(() => {
|
|
65
65
|
if (!session.done) {
|
|
66
|
-
killSession(session);
|
|
66
|
+
cancelKill = killSession(session);
|
|
67
67
|
}
|
|
68
68
|
}, timeout);
|
|
69
69
|
child.on("exit", (code, signal) => {
|
|
70
70
|
clearTimeout(timer);
|
|
71
|
+
cancelKill?.();
|
|
71
72
|
session.exitCode = code ?? (signal ? -1 : null);
|
|
72
73
|
session.done = true;
|
|
73
74
|
session.process = null;
|
|
@@ -75,6 +76,7 @@ export function executeCommand(opts) {
|
|
|
75
76
|
});
|
|
76
77
|
child.on("error", (err) => {
|
|
77
78
|
clearTimeout(timer);
|
|
79
|
+
cancelKill?.();
|
|
78
80
|
if (!session.done) {
|
|
79
81
|
session.exitCode = -1;
|
|
80
82
|
session.output += `\nProcess error: ${err.message}`;
|
|
@@ -86,31 +88,32 @@ export function executeCommand(opts) {
|
|
|
86
88
|
return { session, done };
|
|
87
89
|
}
|
|
88
90
|
/**
|
|
89
|
-
* Kill a running session's process group.
|
|
90
|
-
*
|
|
91
|
+
* Kill a running session's process group: SIGTERM, then SIGKILL after 5s.
|
|
92
|
+
* Returns a cleanup that cancels the pending SIGKILL — callers should invoke
|
|
93
|
+
* it once the process has exited.
|
|
91
94
|
*/
|
|
92
95
|
export function killSession(session) {
|
|
93
96
|
const proc = session.process;
|
|
94
97
|
if (!proc || !proc.pid)
|
|
95
|
-
return;
|
|
98
|
+
return () => { };
|
|
96
99
|
try {
|
|
97
|
-
// Kill the entire process group
|
|
98
100
|
process.kill(-proc.pid, "SIGTERM");
|
|
99
101
|
}
|
|
100
|
-
catch {
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
// Fallback: SIGKILL after 5 seconds
|
|
102
|
+
catch { }
|
|
103
|
+
let settled = false;
|
|
104
104
|
const fallback = setTimeout(() => {
|
|
105
|
-
if (!session.done && proc.pid) {
|
|
105
|
+
if (!settled && !session.done && proc.pid) {
|
|
106
106
|
try {
|
|
107
107
|
process.kill(-proc.pid, "SIGKILL");
|
|
108
108
|
}
|
|
109
|
-
catch {
|
|
110
|
-
// Ignore
|
|
111
|
-
}
|
|
109
|
+
catch { }
|
|
112
110
|
}
|
|
113
111
|
}, 5000);
|
|
114
|
-
// Don't let the timer keep the process alive
|
|
115
112
|
fallback.unref();
|
|
113
|
+
return () => {
|
|
114
|
+
if (!settled) {
|
|
115
|
+
settled = true;
|
|
116
|
+
clearTimeout(fallback);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
116
119
|
}
|