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
|
@@ -0,0 +1,551 @@
|
|
|
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, r.isError);
|
|
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, r.isError);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
createStreamFilter() {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// ── Deferred-lookup mode (load-on-demand with full schema) ──────
|
|
374
|
+
//
|
|
375
|
+
// Like deferred, but instead of wrapping extension calls through a meta-
|
|
376
|
+
// tool dispatcher, we expose a `load_tool` meta-tool that returns the
|
|
377
|
+
// full schema as a tool result AND mutates the protocol's loaded set.
|
|
378
|
+
// Loaded tools become first-class on the NEXT LLM call — the model calls
|
|
379
|
+
// them natively with complete schema fidelity. One round-trip per group
|
|
380
|
+
// of tools loaded, not per call. Prevents the whole class of bugs where
|
|
381
|
+
// models guess arg names from a schema they can only see partially.
|
|
382
|
+
export class DeferredLookupProtocol {
|
|
383
|
+
mode = "deferred-lookup";
|
|
384
|
+
coreNames;
|
|
385
|
+
loadedExt = new Set();
|
|
386
|
+
/** Cache of the current tools list so load_tool's execute can find schemas. */
|
|
387
|
+
toolsRef = [];
|
|
388
|
+
constructor(coreNames) {
|
|
389
|
+
this.coreNames = new Set(coreNames);
|
|
390
|
+
}
|
|
391
|
+
getApiTools(tools) {
|
|
392
|
+
this.toolsRef = tools;
|
|
393
|
+
const visible = [];
|
|
394
|
+
const unloadedExt = [];
|
|
395
|
+
for (const t of tools) {
|
|
396
|
+
if (t.name === "load_tool")
|
|
397
|
+
continue; // rebuilt below with fresh catalog
|
|
398
|
+
const isCore = this.coreNames.has(t.name);
|
|
399
|
+
const isLoaded = this.loadedExt.has(t.name);
|
|
400
|
+
if (isCore || isLoaded) {
|
|
401
|
+
visible.push({
|
|
402
|
+
type: "function",
|
|
403
|
+
function: {
|
|
404
|
+
name: t.name,
|
|
405
|
+
description: t.description,
|
|
406
|
+
parameters: t.input_schema,
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
unloadedExt.push(t.name);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (unloadedExt.length > 0) {
|
|
415
|
+
visible.push({
|
|
416
|
+
type: "function",
|
|
417
|
+
function: {
|
|
418
|
+
name: "load_tool",
|
|
419
|
+
description: `Load extension tool schemas so you can call them on the next turn. ` +
|
|
420
|
+
`Unloaded: ${unloadedExt.join(", ")}. ` +
|
|
421
|
+
`After load_tool succeeds, call those tools directly — not through load_tool again.`,
|
|
422
|
+
parameters: {
|
|
423
|
+
type: "object",
|
|
424
|
+
properties: {
|
|
425
|
+
names: {
|
|
426
|
+
type: "array",
|
|
427
|
+
items: { type: "string" },
|
|
428
|
+
description: "Names of extension tools to load.",
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
required: ["names"],
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
return visible.length > 0 ? visible : undefined;
|
|
437
|
+
}
|
|
438
|
+
getToolPrompt() {
|
|
439
|
+
return "";
|
|
440
|
+
}
|
|
441
|
+
extractToolCalls(_text, streamedCalls) {
|
|
442
|
+
return streamedCalls;
|
|
443
|
+
}
|
|
444
|
+
rewriteToolCall(tc) {
|
|
445
|
+
return tc; // no dispatching needed — load_tool is a real registered tool
|
|
446
|
+
}
|
|
447
|
+
recordAssistant(conv, text, toolCalls) {
|
|
448
|
+
const calls = toolCalls.length
|
|
449
|
+
? toolCalls.map((tc) => ({
|
|
450
|
+
id: tc.id,
|
|
451
|
+
function: { name: tc.name, arguments: tc.argumentsJson },
|
|
452
|
+
}))
|
|
453
|
+
: undefined;
|
|
454
|
+
conv.addAssistantMessage(text || null, calls);
|
|
455
|
+
}
|
|
456
|
+
recordResults(conv, results) {
|
|
457
|
+
for (const r of results) {
|
|
458
|
+
const content = r.isError ? `Error: ${r.content}` : r.content;
|
|
459
|
+
conv.addToolResult(r.callId, content, r.isError);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
createStreamFilter() {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
getProtocolTools() {
|
|
466
|
+
// load_tool is registered as a real tool so the executor can run it
|
|
467
|
+
// through the normal dispatch path. Its execute closes over the protocol
|
|
468
|
+
// instance to mutate the loadedExt set and return schemas.
|
|
469
|
+
const self = this;
|
|
470
|
+
return [
|
|
471
|
+
{
|
|
472
|
+
name: "load_tool",
|
|
473
|
+
description: "Load extension tool schemas so you can call them natively on the next turn.",
|
|
474
|
+
input_schema: {
|
|
475
|
+
type: "object",
|
|
476
|
+
properties: {
|
|
477
|
+
names: {
|
|
478
|
+
type: "array",
|
|
479
|
+
items: { type: "string" },
|
|
480
|
+
description: "Names of extension tools to load.",
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
required: ["names"],
|
|
484
|
+
},
|
|
485
|
+
showOutput: false,
|
|
486
|
+
async execute(args) {
|
|
487
|
+
const names = Array.isArray(args.names) ? args.names : [];
|
|
488
|
+
if (names.length === 0) {
|
|
489
|
+
return { content: "No tool names provided. Pass { names: [...] }.", exitCode: 1, isError: true };
|
|
490
|
+
}
|
|
491
|
+
const loaded = [];
|
|
492
|
+
const alreadyLoaded = [];
|
|
493
|
+
const errors = [];
|
|
494
|
+
const sections = [];
|
|
495
|
+
for (const name of names) {
|
|
496
|
+
const tool = self.toolsRef.find((t) => t.name === name);
|
|
497
|
+
if (!tool) {
|
|
498
|
+
errors.push(`Unknown tool: ${name}`);
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (self.coreNames.has(name) || name === "load_tool") {
|
|
502
|
+
errors.push(`${name} is already available — no need to load.`);
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
if (self.loadedExt.has(name)) {
|
|
506
|
+
alreadyLoaded.push(name);
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
self.loadedExt.add(name);
|
|
510
|
+
loaded.push(name);
|
|
511
|
+
sections.push(`## ${name}\n${tool.description}\n\nSchema:\n\`\`\`json\n${JSON.stringify(tool.input_schema, null, 2)}\n\`\`\``);
|
|
512
|
+
}
|
|
513
|
+
const lines = [];
|
|
514
|
+
if (loaded.length > 0) {
|
|
515
|
+
lines.push(`Loaded ${loaded.length} tool(s): ${loaded.join(", ")}. ` +
|
|
516
|
+
`They are now available as first-class tools on your next turn — call directly.`);
|
|
517
|
+
lines.push("");
|
|
518
|
+
lines.push(sections.join("\n\n"));
|
|
519
|
+
}
|
|
520
|
+
if (alreadyLoaded.length > 0) {
|
|
521
|
+
lines.push(`Already loaded: ${alreadyLoaded.join(", ")}.`);
|
|
522
|
+
}
|
|
523
|
+
if (errors.length > 0) {
|
|
524
|
+
lines.push(`Errors:\n${errors.map((e) => `- ${e}`).join("\n")}`);
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
content: lines.join("\n") || "Nothing to do.",
|
|
528
|
+
exitCode: 0,
|
|
529
|
+
isError: loaded.length === 0 && alreadyLoaded.length === 0 && errors.length > 0,
|
|
530
|
+
};
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
];
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// ── Factory ─────────────────────────────────────────────────────
|
|
537
|
+
/** Core tool names — always sent with full schema. */
|
|
538
|
+
const CORE_TOOLS = [
|
|
539
|
+
"bash", "read_file", "write_file", "edit_file",
|
|
540
|
+
"grep", "glob", "ls",
|
|
541
|
+
"list_skills",
|
|
542
|
+
];
|
|
543
|
+
export function createToolProtocol(mode) {
|
|
544
|
+
if (mode === "inline")
|
|
545
|
+
return new InlineToolProtocol();
|
|
546
|
+
if (mode === "deferred")
|
|
547
|
+
return new DeferredToolProtocol(CORE_TOOLS);
|
|
548
|
+
if (mode === "deferred-lookup")
|
|
549
|
+
return new DeferredLookupProtocol(CORE_TOOLS);
|
|
550
|
+
return new ApiToolProtocol();
|
|
551
|
+
}
|
package/dist/agent/tools/bash.js
CHANGED
|
@@ -3,10 +3,10 @@ export function createBashTool(opts) {
|
|
|
3
3
|
return {
|
|
4
4
|
name: "bash",
|
|
5
5
|
description: "Execute a bash command in an isolated subprocess. Output is captured and returned. " +
|
|
6
|
-
"Does not affect the user's shell state
|
|
6
|
+
"Does not affect the user's shell state. " +
|
|
7
|
+
"cwd is set to the working directory from the shell context. " +
|
|
7
8
|
"Do NOT use bash for file searching — use grep/glob instead. " +
|
|
8
|
-
"Do NOT use bash for reading files — use read_file instead.
|
|
9
|
-
"Provide a description parameter to explain what the command does.",
|
|
9
|
+
"Do NOT use bash for reading files — use read_file instead.",
|
|
10
10
|
input_schema: {
|
|
11
11
|
type: "object",
|
|
12
12
|
properties: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { computeEditDiff } from "../../utils/diff.js";
|
|
4
4
|
/**
|
|
5
5
|
* Find the closest matching region in the file content to help diagnose
|
|
6
6
|
* why an exact match failed. Returns a hint string.
|
|
@@ -103,9 +103,12 @@ export function createEditFileTool(getCwd) {
|
|
|
103
103
|
};
|
|
104
104
|
}
|
|
105
105
|
const normalizedNew = newText.replace(/\r\n/g, "\n");
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
// Use split/join for literal replacement everywhere. String.replace()
|
|
107
|
+
// treats dollar-sign patterns in the replacement as special substitution
|
|
108
|
+
// variables, which corrupts file content containing regex escape sequences.
|
|
109
|
+
const newContent = normalized.split(normalizedOld).join(normalizedNew);
|
|
110
|
+
// Note: when !replaceAll, we rely on the occurrence check above to ensure
|
|
111
|
+
// normalizedOld appears exactly once, so split/join replaces only that one.
|
|
109
112
|
// Restore original line endings — only convert if the file was
|
|
110
113
|
// predominantly CRLF (>50% of line endings), to avoid corrupting
|
|
111
114
|
// mixed-ending files.
|
|
@@ -116,8 +119,8 @@ export function createEditFileTool(getCwd) {
|
|
|
116
119
|
? newContent.replace(/\n/g, "\r\n")
|
|
117
120
|
: newContent;
|
|
118
121
|
await fs.writeFile(absPath, finalContent);
|
|
119
|
-
// Compute and stream diff for display
|
|
120
|
-
const diff =
|
|
122
|
+
// Compute and stream diff for display (windowed — only diffs the edit region)
|
|
123
|
+
const diff = computeEditDiff(normalized, normalizedOld, normalizedNew, replaceAll);
|
|
121
124
|
if (onChunk && diff.hunks.length > 0) {
|
|
122
125
|
for (const hunk of diff.hunks) {
|
|
123
126
|
for (const line of hunk.lines) {
|
package/dist/agent/tools/glob.js
CHANGED
|
@@ -4,9 +4,11 @@ import { executeCommand } from "../../executor.js";
|
|
|
4
4
|
export function createGlobTool(getCwd) {
|
|
5
5
|
return {
|
|
6
6
|
name: "glob",
|
|
7
|
-
description: "
|
|
7
|
+
description: "Use this when you know a FILENAME or PATH SHAPE (e.g. `**/*.ts`, `src/**/*.md`, `package.json`). " +
|
|
8
|
+
"Returns matching file paths sorted by modification time (newest first). " +
|
|
9
|
+
"This does NOT search file contents — use `grep` for that. " +
|
|
8
10
|
"ALWAYS use this instead of find/ls via bash. " +
|
|
9
|
-
"
|
|
11
|
+
"Typical flow: `glob` to locate files, then `read_file` or `grep` to inspect contents.",
|
|
10
12
|
input_schema: {
|
|
11
13
|
type: "object",
|
|
12
14
|
properties: {
|
package/dist/agent/tools/grep.js
CHANGED
|
@@ -2,7 +2,9 @@ import { executeCommand } from "../../executor.js";
|
|
|
2
2
|
export function createGrepTool(getCwd) {
|
|
3
3
|
return {
|
|
4
4
|
name: "grep",
|
|
5
|
-
description: "
|
|
5
|
+
description: "Use this when you know something INSIDE the file (text, identifier, regex). " +
|
|
6
|
+
"To find files by filename alone, use `glob` instead. " +
|
|
7
|
+
"Search file contents using ripgrep. ALWAYS use this instead of running grep/rg via bash. " +
|
|
6
8
|
"Supports three output modes: " +
|
|
7
9
|
"'files_with_matches' (default, returns file paths only — use this to find which files contain a pattern), " +
|
|
8
10
|
"'content' (matching lines with optional context_before/context_after), and " +
|
|
@@ -13,7 +15,7 @@ export function createGrepTool(getCwd) {
|
|
|
13
15
|
properties: {
|
|
14
16
|
pattern: {
|
|
15
17
|
type: "string",
|
|
16
|
-
description: "Regex pattern to search for",
|
|
18
|
+
description: "Regex pattern to search for (NOT a glob — `*.md` is invalid here; use `.*\\.md` for regex, or use the glob tool to find files by name). For filename filtering while searching content, use the `include` parameter.",
|
|
17
19
|
},
|
|
18
20
|
path: {
|
|
19
21
|
type: "string",
|
|
@@ -124,12 +126,34 @@ export function createGrepTool(getCwd) {
|
|
|
124
126
|
});
|
|
125
127
|
await done;
|
|
126
128
|
if (session.exitCode === 1 && !session.output.trim()) {
|
|
129
|
+
// If the pattern looks like a filename (e.g. "SKILL.md", "package.json"),
|
|
130
|
+
// the agent probably meant to find files by name, not search inside them.
|
|
131
|
+
// Surface a redirect hint instead of a silent zero.
|
|
132
|
+
const looksLikeFilename = /^[A-Za-z0-9_.\-*/]+\.[A-Za-z0-9]{1,6}$/.test(pattern) &&
|
|
133
|
+
!/[\\()\[\]|^$+{}]/.test(pattern);
|
|
134
|
+
const hint = looksLikeFilename
|
|
135
|
+
? ` Hint: "${pattern}" looks like a filename. grep searches file *contents* — to find files by name, use the \`glob\` tool instead.`
|
|
136
|
+
: "";
|
|
127
137
|
return {
|
|
128
|
-
content:
|
|
138
|
+
content: `No matches found.${hint}`,
|
|
129
139
|
exitCode: 0,
|
|
130
140
|
isError: false,
|
|
131
141
|
};
|
|
132
142
|
}
|
|
143
|
+
// exit code >= 2 is a ripgrep error (invalid regex, unreadable path, etc).
|
|
144
|
+
// Surface it as an error so the model retries with a correct pattern
|
|
145
|
+
// rather than treating "no useful output" as a successful no-match.
|
|
146
|
+
if (session.exitCode != null && session.exitCode >= 2) {
|
|
147
|
+
const looksLikeGlob = /^[*?]|\*\./.test(pattern) && !/[\\()\[\]|^$]/.test(pattern);
|
|
148
|
+
const hint = looksLikeGlob
|
|
149
|
+
? " Hint: `*.md` is a glob, not a regex — use the glob tool to find files by name, or pass `include: \"*.md\"` here to filter files while searching content for a regex pattern."
|
|
150
|
+
: "";
|
|
151
|
+
return {
|
|
152
|
+
content: `grep failed (rg exit ${session.exitCode}): ${session.output.trim() || "no output"}${hint}`,
|
|
153
|
+
exitCode: session.exitCode,
|
|
154
|
+
isError: true,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
133
157
|
let output = session.output;
|
|
134
158
|
// Cap individual line lengths to 500 chars to prevent minified/base64 flood
|
|
135
159
|
if (mode === "content") {
|