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
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
// ── API mode (current behavior) ──────────────────────────────────
|
|
2
|
+
export class ApiToolProtocol {
|
|
3
|
+
mode = "api";
|
|
4
|
+
getApiTools(tools) {
|
|
5
|
+
if (tools.length === 0)
|
|
6
|
+
return undefined;
|
|
7
|
+
return tools.map((t) => ({
|
|
8
|
+
type: "function",
|
|
9
|
+
function: {
|
|
10
|
+
name: t.name,
|
|
11
|
+
description: t.description,
|
|
12
|
+
parameters: t.input_schema,
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
getToolPrompt() {
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
extractToolCalls(_text, streamedCalls) {
|
|
20
|
+
return streamedCalls;
|
|
21
|
+
}
|
|
22
|
+
rewriteToolCall(tc) {
|
|
23
|
+
return tc;
|
|
24
|
+
}
|
|
25
|
+
recordAssistant(conv, text, toolCalls) {
|
|
26
|
+
const calls = toolCalls.length
|
|
27
|
+
? toolCalls.map((tc) => ({
|
|
28
|
+
id: tc.id,
|
|
29
|
+
function: { name: tc.name, arguments: tc.argumentsJson },
|
|
30
|
+
}))
|
|
31
|
+
: undefined;
|
|
32
|
+
conv.addAssistantMessage(text || null, calls);
|
|
33
|
+
}
|
|
34
|
+
recordResults(conv, results) {
|
|
35
|
+
for (const r of results) {
|
|
36
|
+
const content = r.isError ? `Error: ${r.content}` : r.content;
|
|
37
|
+
conv.addToolResult(r.callId, content);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
createStreamFilter() {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// ── Inline mode (JSON code block tool calls) ─────────────────────
|
|
45
|
+
export class InlineToolProtocol {
|
|
46
|
+
mode = "inline";
|
|
47
|
+
callCounter = 0;
|
|
48
|
+
getApiTools() {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
getToolPrompt(tools) {
|
|
52
|
+
if (tools.length === 0)
|
|
53
|
+
return "";
|
|
54
|
+
const lines = [
|
|
55
|
+
"",
|
|
56
|
+
"# Tools",
|
|
57
|
+
"",
|
|
58
|
+
"To call a tool, write a ```tool fenced block with JSON:",
|
|
59
|
+
"",
|
|
60
|
+
"```tool",
|
|
61
|
+
'{"tool": "grep", "pattern": "TODO", "path": "src/"}',
|
|
62
|
+
"```",
|
|
63
|
+
"",
|
|
64
|
+
"The `tool` field selects which tool. All other fields are arguments.",
|
|
65
|
+
"Multiple tool blocks allowed per response.",
|
|
66
|
+
"",
|
|
67
|
+
"Available: " + tools.map((t) => `${t.name}${formatParams(t.input_schema)}`).join(", "),
|
|
68
|
+
];
|
|
69
|
+
return lines.join("\n");
|
|
70
|
+
}
|
|
71
|
+
rewriteToolCall(tc) {
|
|
72
|
+
return tc;
|
|
73
|
+
}
|
|
74
|
+
extractToolCalls(text, _streamedCalls) {
|
|
75
|
+
const calls = [];
|
|
76
|
+
// Match ```tool ... ``` blocks
|
|
77
|
+
const regex = /```tool\s*\n([\s\S]*?)```/g;
|
|
78
|
+
let match;
|
|
79
|
+
while ((match = regex.exec(text)) !== null) {
|
|
80
|
+
const body = match[1].trim();
|
|
81
|
+
try {
|
|
82
|
+
const obj = JSON.parse(body);
|
|
83
|
+
const name = obj.tool;
|
|
84
|
+
if (typeof name !== "string")
|
|
85
|
+
continue;
|
|
86
|
+
// Separate tool name from args
|
|
87
|
+
const { tool: _, ...args } = obj;
|
|
88
|
+
calls.push({
|
|
89
|
+
id: `inline_${++this.callCounter}`,
|
|
90
|
+
name,
|
|
91
|
+
argumentsJson: JSON.stringify(args),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Not valid JSON — skip
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return calls;
|
|
99
|
+
}
|
|
100
|
+
recordAssistant(conv, text, _toolCalls) {
|
|
101
|
+
conv.addAssistantMessage(text || null);
|
|
102
|
+
}
|
|
103
|
+
recordResults(conv, results) {
|
|
104
|
+
if (results.length === 0)
|
|
105
|
+
return;
|
|
106
|
+
const parts = results.map((r) => {
|
|
107
|
+
const status = r.isError ? "error" : "ok";
|
|
108
|
+
return `[${r.toolName} ${r.callId} ${status}]\n${r.content}`;
|
|
109
|
+
});
|
|
110
|
+
conv.addToolResultInline(parts.join("\n\n"));
|
|
111
|
+
}
|
|
112
|
+
createStreamFilter(_toolNames) {
|
|
113
|
+
return new CodeBlockFilter();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// ── Code block stream filter ────────────────────────────────────
|
|
117
|
+
/**
|
|
118
|
+
* Strips ```tool ... ``` blocks from streamed text.
|
|
119
|
+
* Simple state machine: normal → in_fence → normal.
|
|
120
|
+
*/
|
|
121
|
+
class CodeBlockFilter {
|
|
122
|
+
buf = "";
|
|
123
|
+
inFence = false;
|
|
124
|
+
lastEmittedNewlines = 0; // track trailing newlines to collapse blanks
|
|
125
|
+
feed(chunk) {
|
|
126
|
+
this.buf += chunk;
|
|
127
|
+
let raw = "";
|
|
128
|
+
while (this.buf.length > 0) {
|
|
129
|
+
if (this.inFence) {
|
|
130
|
+
// Look for closing ```
|
|
131
|
+
const closeIdx = this.buf.indexOf("```");
|
|
132
|
+
if (closeIdx !== -1) {
|
|
133
|
+
// Skip past closing ``` and any trailing whitespace on that line
|
|
134
|
+
let end = closeIdx + 3;
|
|
135
|
+
while (end < this.buf.length && this.buf[end] === "\n")
|
|
136
|
+
end++;
|
|
137
|
+
this.buf = this.buf.slice(end);
|
|
138
|
+
this.inFence = false;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// No closing yet — keep buffering
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
// Look for opening ```tool
|
|
145
|
+
const openIdx = this.buf.indexOf("```tool");
|
|
146
|
+
if (openIdx !== -1) {
|
|
147
|
+
// Emit everything before the fence, trimming trailing newline
|
|
148
|
+
let before = this.buf.slice(0, openIdx);
|
|
149
|
+
if (before.endsWith("\n"))
|
|
150
|
+
before = before.slice(0, -1);
|
|
151
|
+
raw += before;
|
|
152
|
+
this.buf = this.buf.slice(openIdx + 7); // skip ```tool
|
|
153
|
+
this.inFence = true;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
// Stray ``` on its own line (residual closing fence)
|
|
157
|
+
const strayIdx = this.buf.indexOf("```");
|
|
158
|
+
if (strayIdx !== -1) {
|
|
159
|
+
// Check if it's just backticks on a line (possibly with whitespace)
|
|
160
|
+
const lineStart = this.buf.lastIndexOf("\n", strayIdx - 1) + 1;
|
|
161
|
+
const lineEnd = this.buf.indexOf("\n", strayIdx);
|
|
162
|
+
const line = this.buf.slice(lineStart, lineEnd === -1 ? undefined : lineEnd).trim();
|
|
163
|
+
if (line === "```") {
|
|
164
|
+
raw += this.buf.slice(0, lineStart);
|
|
165
|
+
this.buf = this.buf.slice(lineEnd === -1 ? this.buf.length : lineEnd + 1);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Could be a partial match at the end
|
|
170
|
+
const marker = "```tool";
|
|
171
|
+
let partial = false;
|
|
172
|
+
for (let i = Math.min(marker.length - 1, this.buf.length); i >= 1; i--) {
|
|
173
|
+
if (this.buf.endsWith(marker.slice(0, i))) {
|
|
174
|
+
raw += this.buf.slice(0, this.buf.length - i);
|
|
175
|
+
this.buf = this.buf.slice(this.buf.length - i);
|
|
176
|
+
partial = true;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (partial)
|
|
181
|
+
break;
|
|
182
|
+
// No fence anywhere — emit all
|
|
183
|
+
raw += this.buf;
|
|
184
|
+
this.buf = "";
|
|
185
|
+
}
|
|
186
|
+
// Collapse runs of 3+ newlines into 2 (one blank line max)
|
|
187
|
+
return this.collapseNewlines(raw);
|
|
188
|
+
}
|
|
189
|
+
flush() {
|
|
190
|
+
const out = this.collapseNewlines(this.buf);
|
|
191
|
+
this.buf = "";
|
|
192
|
+
this.inFence = false;
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
collapseNewlines(text) {
|
|
196
|
+
if (!text)
|
|
197
|
+
return text;
|
|
198
|
+
// Count leading newlines and merge with trailing from last emit
|
|
199
|
+
let i = 0;
|
|
200
|
+
while (i < text.length && text[i] === "\n")
|
|
201
|
+
i++;
|
|
202
|
+
const leading = i;
|
|
203
|
+
const totalNewlines = this.lastEmittedNewlines + leading;
|
|
204
|
+
// Allow at most 2 consecutive newlines
|
|
205
|
+
let prefix = "";
|
|
206
|
+
if (leading > 0) {
|
|
207
|
+
const allowed = Math.max(0, 2 - this.lastEmittedNewlines);
|
|
208
|
+
prefix = "\n".repeat(Math.min(leading, allowed));
|
|
209
|
+
text = text.slice(leading);
|
|
210
|
+
}
|
|
211
|
+
// Collapse internal runs
|
|
212
|
+
text = text.replace(/\n{3,}/g, "\n\n");
|
|
213
|
+
// Track trailing newlines for next call
|
|
214
|
+
let trailing = 0;
|
|
215
|
+
let j = text.length;
|
|
216
|
+
while (j > 0 && text[j - 1] === "\n") {
|
|
217
|
+
j--;
|
|
218
|
+
trailing++;
|
|
219
|
+
}
|
|
220
|
+
this.lastEmittedNewlines = trailing > 0 ? trailing : (prefix ? totalNewlines - leading + prefix.length : 0);
|
|
221
|
+
return prefix + text;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// ── Helpers ──────────────────────────────────────────────────────
|
|
225
|
+
function formatParams(schema) {
|
|
226
|
+
const props = schema.properties;
|
|
227
|
+
if (!props || Object.keys(props).length === 0)
|
|
228
|
+
return "()";
|
|
229
|
+
const required = new Set(schema.required ?? []);
|
|
230
|
+
const params = Object.entries(props).map(([name, prop]) => {
|
|
231
|
+
const opt = required.has(name) ? "" : "?";
|
|
232
|
+
const enumVals = prop.enum;
|
|
233
|
+
if (enumVals)
|
|
234
|
+
return `${name}${opt}: ${enumVals.join("|")}`;
|
|
235
|
+
return `${name}${opt}`;
|
|
236
|
+
});
|
|
237
|
+
return `(${params.join(", ")})`;
|
|
238
|
+
}
|
|
239
|
+
// ── Deferred mode (core tools full schema, extensions via meta-tool) ──
|
|
240
|
+
const META_TOOL_NAME = "use_extension";
|
|
241
|
+
export class DeferredToolProtocol {
|
|
242
|
+
mode = "deferred";
|
|
243
|
+
coreNames;
|
|
244
|
+
/** Cached extension tool schemas for arg validation. */
|
|
245
|
+
extSchemas = new Map();
|
|
246
|
+
constructor(coreNames) {
|
|
247
|
+
this.coreNames = new Set(coreNames);
|
|
248
|
+
}
|
|
249
|
+
getApiTools(tools) {
|
|
250
|
+
const core = tools.filter((t) => this.coreNames.has(t.name));
|
|
251
|
+
const ext = tools.filter((t) => !this.coreNames.has(t.name));
|
|
252
|
+
// Cache extension schemas for validation in rewriteToolCall
|
|
253
|
+
this.extSchemas.clear();
|
|
254
|
+
for (const t of ext) {
|
|
255
|
+
this.extSchemas.set(t.name, t.input_schema);
|
|
256
|
+
}
|
|
257
|
+
const apiTools = core.map((t) => ({
|
|
258
|
+
type: "function",
|
|
259
|
+
function: {
|
|
260
|
+
name: t.name,
|
|
261
|
+
description: t.description,
|
|
262
|
+
parameters: t.input_schema,
|
|
263
|
+
},
|
|
264
|
+
}));
|
|
265
|
+
if (ext.length > 0) {
|
|
266
|
+
const catalog = ext
|
|
267
|
+
.map((t) => `${t.name}${formatParams(t.input_schema)}`)
|
|
268
|
+
.join(", ");
|
|
269
|
+
apiTools.push({
|
|
270
|
+
type: "function",
|
|
271
|
+
function: {
|
|
272
|
+
name: META_TOOL_NAME,
|
|
273
|
+
description: `Call an extension tool. Available: ${catalog}`,
|
|
274
|
+
parameters: {
|
|
275
|
+
type: "object",
|
|
276
|
+
properties: {
|
|
277
|
+
name: { type: "string", description: "Tool name to call" },
|
|
278
|
+
args: {
|
|
279
|
+
type: "object",
|
|
280
|
+
description: "Tool arguments",
|
|
281
|
+
properties: {},
|
|
282
|
+
additionalProperties: true,
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
required: ["name"],
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return apiTools.length > 0 ? apiTools : undefined;
|
|
291
|
+
}
|
|
292
|
+
getToolPrompt() {
|
|
293
|
+
return "";
|
|
294
|
+
}
|
|
295
|
+
extractToolCalls(_text, streamedCalls) {
|
|
296
|
+
return streamedCalls;
|
|
297
|
+
}
|
|
298
|
+
rewriteToolCall(tc) {
|
|
299
|
+
if (tc.name !== META_TOOL_NAME)
|
|
300
|
+
return tc;
|
|
301
|
+
// Unwrap: use_extension(name="foo", args={...}) → foo({...})
|
|
302
|
+
try {
|
|
303
|
+
const parsed = JSON.parse(tc.argumentsJson);
|
|
304
|
+
const targetName = parsed.name;
|
|
305
|
+
const targetArgs = (parsed.args ?? {});
|
|
306
|
+
// Validate: does the extension exist?
|
|
307
|
+
const schema = this.extSchemas.get(targetName);
|
|
308
|
+
if (!schema) {
|
|
309
|
+
const available = [...this.extSchemas.keys()].join(", ");
|
|
310
|
+
return {
|
|
311
|
+
id: tc.id,
|
|
312
|
+
name: META_TOOL_NAME,
|
|
313
|
+
argumentsJson: JSON.stringify({
|
|
314
|
+
_error: `Unknown extension "${targetName}". Available: ${available}`,
|
|
315
|
+
}),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
// Validate: check for unknown/missing params against schema
|
|
319
|
+
const schemaProps = schema.properties;
|
|
320
|
+
const requiredParams = new Set(schema.required ?? []);
|
|
321
|
+
if (schemaProps) {
|
|
322
|
+
const validParams = new Set(Object.keys(schemaProps));
|
|
323
|
+
const providedParams = Object.keys(targetArgs);
|
|
324
|
+
// Check for unknown params (likely wrong names)
|
|
325
|
+
const unknown = providedParams.filter((p) => !validParams.has(p));
|
|
326
|
+
// Check for missing required params
|
|
327
|
+
const missing = [...requiredParams].filter((p) => !targetArgs[p]);
|
|
328
|
+
if (unknown.length > 0 || missing.length > 0) {
|
|
329
|
+
const expected = [...validParams]
|
|
330
|
+
.map((p) => `${p}${requiredParams.has(p) ? " (required)" : ""}`)
|
|
331
|
+
.join(", ");
|
|
332
|
+
let hint = `Wrong arguments for "${targetName}". Expected params: ${expected}.`;
|
|
333
|
+
if (unknown.length > 0)
|
|
334
|
+
hint += ` Unknown: ${unknown.join(", ")}.`;
|
|
335
|
+
if (missing.length > 0)
|
|
336
|
+
hint += ` Missing: ${missing.join(", ")}.`;
|
|
337
|
+
return {
|
|
338
|
+
id: tc.id,
|
|
339
|
+
name: META_TOOL_NAME,
|
|
340
|
+
argumentsJson: JSON.stringify({ _error: hint }),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
id: tc.id,
|
|
346
|
+
name: targetName,
|
|
347
|
+
argumentsJson: JSON.stringify(targetArgs),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
return tc; // Let it fail naturally downstream
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
recordAssistant(conv, text, toolCalls) {
|
|
355
|
+
const calls = toolCalls.length
|
|
356
|
+
? toolCalls.map((tc) => ({
|
|
357
|
+
id: tc.id,
|
|
358
|
+
function: { name: tc.name, arguments: tc.argumentsJson },
|
|
359
|
+
}))
|
|
360
|
+
: undefined;
|
|
361
|
+
conv.addAssistantMessage(text || null, calls);
|
|
362
|
+
}
|
|
363
|
+
recordResults(conv, results) {
|
|
364
|
+
for (const r of results) {
|
|
365
|
+
const content = r.isError ? `Error: ${r.content}` : r.content;
|
|
366
|
+
conv.addToolResult(r.callId, content);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
createStreamFilter() {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// ── Factory ─────────────────────────────────────────────────────
|
|
374
|
+
/** Core tool names — always sent with full schema. */
|
|
375
|
+
const CORE_TOOLS = [
|
|
376
|
+
"bash", "read_file", "write_file", "edit_file",
|
|
377
|
+
"grep", "glob", "ls", "user_shell", "display",
|
|
378
|
+
"list_skills", "conversation_recall",
|
|
379
|
+
];
|
|
380
|
+
export function createToolProtocol(mode) {
|
|
381
|
+
if (mode === "inline")
|
|
382
|
+
return new InlineToolProtocol();
|
|
383
|
+
if (mode === "deferred")
|
|
384
|
+
return new DeferredToolProtocol(CORE_TOOLS);
|
|
385
|
+
return new ApiToolProtocol();
|
|
386
|
+
}
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
export function createUserShellTool(opts) {
|
|
9
9
|
return {
|
|
10
10
|
name: "user_shell",
|
|
11
|
-
description: "Run a
|
|
11
|
+
description: "Run a complete, non-interactive command in the user's live shell (cd, export, install packages, start servers, git commands). " +
|
|
12
|
+
"Use this for commands that have side effects or that the user wants to see. Output is shown directly to the user but NOT returned " +
|
|
13
|
+
"to you by default — set return_output=true if you need to inspect the result. " +
|
|
14
|
+
"Do NOT use this to interact with programs that are already running in the terminal — use terminal_keys/terminal_read instead.",
|
|
12
15
|
input_schema: {
|
|
13
16
|
type: "object",
|
|
14
17
|
properties: {
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -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). */
|
package/dist/context-manager.js
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
import { getSettings } from "./settings.js";
|
|
2
|
-
// Non-configurable thresholds (agent response and tool output follow shell settings)
|
|
3
|
-
const AGENT_RESPONSE_TRUNCATE_THRESHOLD = 20;
|
|
4
|
-
const AGENT_RESPONSE_HEAD_LINES = 15;
|
|
5
|
-
const TOOL_TRUNCATE_THRESHOLD = 20;
|
|
6
|
-
const TOOL_HEAD_LINES = 5;
|
|
7
|
-
const TOOL_TAIL_LINES = 5;
|
|
8
2
|
export class ContextManager {
|
|
9
3
|
exchanges = [];
|
|
10
4
|
nextId = 1;
|
|
11
5
|
currentCwd;
|
|
12
6
|
sessionStart;
|
|
13
|
-
pendingToolCalls = [];
|
|
14
7
|
firstPrompt = true;
|
|
15
8
|
agentShellActive = false; // true while user_shell command is executing
|
|
16
9
|
handlers = null;
|
|
@@ -43,46 +36,11 @@ export class ContextManager {
|
|
|
43
36
|
bus.on("shell:agent-exec-start", () => { this.agentShellActive = true; });
|
|
44
37
|
bus.on("shell:agent-exec-done", () => { this.agentShellActive = false; });
|
|
45
38
|
// ── Subscribe to agent events ──
|
|
39
|
+
// Only track queries (as markers). Agent responses and tool outputs
|
|
40
|
+
// live exclusively in ConversationState to avoid duplication.
|
|
46
41
|
bus.on("agent:query", (e) => {
|
|
47
|
-
this.pendingToolCalls = [];
|
|
48
42
|
this.addExchange({ type: "agent_query", query: e.query });
|
|
49
43
|
});
|
|
50
|
-
bus.on("agent:response-done", (e) => {
|
|
51
|
-
this.addExchange({
|
|
52
|
-
type: "agent_response",
|
|
53
|
-
response: e.response,
|
|
54
|
-
toolCalls: this.pendingToolCalls,
|
|
55
|
-
});
|
|
56
|
-
this.pendingToolCalls = [];
|
|
57
|
-
});
|
|
58
|
-
bus.on("agent:tool-call", (e) => {
|
|
59
|
-
// Accumulate tool calls for the agent_response summary
|
|
60
|
-
this.pendingToolCalls.push({
|
|
61
|
-
tool: e.tool,
|
|
62
|
-
args: e.args,
|
|
63
|
-
output: "",
|
|
64
|
-
exitCode: null,
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
bus.on("agent:tool-output", (e) => {
|
|
68
|
-
// Update the last pending tool call with output
|
|
69
|
-
const last = this.pendingToolCalls[this.pendingToolCalls.length - 1];
|
|
70
|
-
if (last) {
|
|
71
|
-
last.output = e.output;
|
|
72
|
-
last.exitCode = e.exitCode;
|
|
73
|
-
}
|
|
74
|
-
// Also store as a separate exchange for chronological log
|
|
75
|
-
const lines = e.output.split("\n");
|
|
76
|
-
this.addExchange({
|
|
77
|
-
type: "tool_execution",
|
|
78
|
-
tool: e.tool,
|
|
79
|
-
args: {},
|
|
80
|
-
output: e.output,
|
|
81
|
-
exitCode: e.exitCode,
|
|
82
|
-
outputLines: lines.length,
|
|
83
|
-
outputBytes: e.output.length,
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
44
|
}
|
|
87
45
|
// ── Public query API ──────────────────────────────────────────
|
|
88
46
|
getCwd() {
|
|
@@ -231,7 +189,6 @@ export class ContextManager {
|
|
|
231
189
|
*/
|
|
232
190
|
clear() {
|
|
233
191
|
this.exchanges = [];
|
|
234
|
-
this.pendingToolCalls = [];
|
|
235
192
|
this.firstPrompt = true;
|
|
236
193
|
// Don't reset nextId — IDs should be globally unique within a session
|
|
237
194
|
}
|
|
@@ -249,12 +206,7 @@ export class ContextManager {
|
|
|
249
206
|
const s = getSettings();
|
|
250
207
|
ex.output = truncateOutput(ex.output, s.shellTruncateThreshold, s.shellHeadLines, s.shellTailLines, ex.id);
|
|
251
208
|
}
|
|
252
|
-
|
|
253
|
-
ex.response = truncateHead(ex.response, AGENT_RESPONSE_TRUNCATE_THRESHOLD, AGENT_RESPONSE_HEAD_LINES, ex.id);
|
|
254
|
-
}
|
|
255
|
-
else if (ex.type === "tool_execution") {
|
|
256
|
-
ex.output = truncateOutput(ex.output, TOOL_TRUNCATE_THRESHOLD, TOOL_HEAD_LINES, TOOL_TAIL_LINES, ex.id);
|
|
257
|
-
}
|
|
209
|
+
// agent_query has no output to truncate
|
|
258
210
|
}
|
|
259
211
|
// Pass 2: budget enforcement — strip output from oldest if over budget
|
|
260
212
|
let totalSize = result.reduce((sum, ex) => sum + this.exchangeSize(ex), 0);
|
|
@@ -264,12 +216,6 @@ export class ContextManager {
|
|
|
264
216
|
if (ex.type === "shell_command") {
|
|
265
217
|
ex.output = `[output omitted, use shell_recall tool to expand id ${ex.id}]`;
|
|
266
218
|
}
|
|
267
|
-
else if (ex.type === "tool_execution") {
|
|
268
|
-
ex.output = `[output omitted, use shell_recall tool to expand id ${ex.id}]`;
|
|
269
|
-
}
|
|
270
|
-
else if (ex.type === "agent_response") {
|
|
271
|
-
ex.response = `[response omitted, use shell_recall tool to expand id ${ex.id}]`;
|
|
272
|
-
}
|
|
273
219
|
totalSize -= before - this.exchangeSize(ex);
|
|
274
220
|
}
|
|
275
221
|
return result;
|
|
@@ -288,7 +234,8 @@ export class ContextManager {
|
|
|
288
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`;
|
|
289
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`;
|
|
290
236
|
out += `- After a user_shell command, the user already saw the output. Do NOT repeat or summarize it.\n`;
|
|
291
|
-
out += `- You can browse or search
|
|
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`;
|
|
292
239
|
out += `\n`;
|
|
293
240
|
this.firstPrompt = false;
|
|
294
241
|
}
|
|
@@ -326,25 +273,6 @@ export class ContextManager {
|
|
|
326
273
|
}
|
|
327
274
|
case "agent_query":
|
|
328
275
|
return `#${ex.id} [you] > ${ex.query}\n`;
|
|
329
|
-
case "agent_response": {
|
|
330
|
-
let s = `#${ex.id} [agent] `;
|
|
331
|
-
if (ex.response)
|
|
332
|
-
s += ex.response.split("\n")[0] + "\n";
|
|
333
|
-
if (ex.response.includes("\n")) {
|
|
334
|
-
const rest = ex.response.slice(ex.response.indexOf("\n") + 1);
|
|
335
|
-
if (rest.trim())
|
|
336
|
-
s += indent(rest, " ") + "\n";
|
|
337
|
-
}
|
|
338
|
-
return s;
|
|
339
|
-
}
|
|
340
|
-
case "tool_execution": {
|
|
341
|
-
let s = `#${ex.id} [tool] ${ex.tool}\n`;
|
|
342
|
-
if (ex.output)
|
|
343
|
-
s += indent(ex.output, " ") + "\n";
|
|
344
|
-
if (ex.exitCode !== null)
|
|
345
|
-
s += ` exit ${ex.exitCode}\n`;
|
|
346
|
-
return s;
|
|
347
|
-
}
|
|
348
276
|
}
|
|
349
277
|
}
|
|
350
278
|
formatExchangeFull(ex) {
|
|
@@ -361,16 +289,6 @@ export class ContextManager {
|
|
|
361
289
|
}
|
|
362
290
|
case "agent_query":
|
|
363
291
|
return `#${ex.id} [you] > ${ex.query}`;
|
|
364
|
-
case "agent_response":
|
|
365
|
-
return `#${ex.id} [agent]\n${ex.response}`;
|
|
366
|
-
case "tool_execution": {
|
|
367
|
-
let s = `#${ex.id} [tool] ${ex.tool} (${ex.outputLines} lines, ${ex.outputBytes} bytes)\n`;
|
|
368
|
-
if (ex.output)
|
|
369
|
-
s += ex.output + "\n";
|
|
370
|
-
if (ex.exitCode !== null)
|
|
371
|
-
s += `exit ${ex.exitCode}\n`;
|
|
372
|
-
return s;
|
|
373
|
-
}
|
|
374
292
|
}
|
|
375
293
|
}
|
|
376
294
|
exchangeOneLiner(ex) {
|
|
@@ -381,12 +299,6 @@ export class ContextManager {
|
|
|
381
299
|
}
|
|
382
300
|
case "agent_query":
|
|
383
301
|
return `#${ex.id} query: ${ex.query}`;
|
|
384
|
-
case "agent_response": {
|
|
385
|
-
const preview = ex.response.split("\n")[0]?.slice(0, 80) ?? "";
|
|
386
|
-
return `#${ex.id} agent: ${preview}${ex.response.length > 80 ? "..." : ""}`;
|
|
387
|
-
}
|
|
388
|
-
case "tool_execution":
|
|
389
|
-
return `#${ex.id} tool: ${ex.tool} (${ex.outputLines} lines, exit ${ex.exitCode ?? "?"})`;
|
|
390
302
|
}
|
|
391
303
|
}
|
|
392
304
|
exchangeSearchText(ex) {
|
|
@@ -395,10 +307,6 @@ export class ContextManager {
|
|
|
395
307
|
return `${ex.command}\n${ex.output}`;
|
|
396
308
|
case "agent_query":
|
|
397
309
|
return ex.query;
|
|
398
|
-
case "agent_response":
|
|
399
|
-
return ex.response;
|
|
400
|
-
case "tool_execution":
|
|
401
|
-
return `${ex.tool}\n${ex.output}`;
|
|
402
310
|
}
|
|
403
311
|
}
|
|
404
312
|
exchangeSize(ex) {
|
|
@@ -407,10 +315,6 @@ export class ContextManager {
|
|
|
407
315
|
return ex.command.length + ex.output.length;
|
|
408
316
|
case "agent_query":
|
|
409
317
|
return ex.query.length;
|
|
410
|
-
case "agent_response":
|
|
411
|
-
return ex.response.length;
|
|
412
|
-
case "tool_execution":
|
|
413
|
-
return ex.tool.length + ex.output.length;
|
|
414
318
|
}
|
|
415
319
|
}
|
|
416
320
|
}
|
|
@@ -426,15 +330,6 @@ function truncateOutput(text, threshold, headLines, tailLines, id) {
|
|
|
426
330
|
...lines.slice(-tailLines),
|
|
427
331
|
].join("\n");
|
|
428
332
|
}
|
|
429
|
-
function truncateHead(text, threshold, headLines, id) {
|
|
430
|
-
const lines = text.split("\n");
|
|
431
|
-
if (lines.length <= threshold)
|
|
432
|
-
return text;
|
|
433
|
-
return [
|
|
434
|
-
...lines.slice(0, headLines),
|
|
435
|
-
`[... truncated, use shell_recall tool with expand and id ${id} for full response ...]`,
|
|
436
|
-
].join("\n");
|
|
437
|
-
}
|
|
438
333
|
function indent(text, prefix) {
|
|
439
334
|
return text
|
|
440
335
|
.split("\n")
|
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. */
|