zidane 4.0.2 → 4.1.4
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 +196 -614
- package/dist/agent-BoV5Twdl.d.ts +2347 -0
- package/dist/agent-BoV5Twdl.d.ts.map +1 -0
- package/dist/contexts-3Arvn7yR.js +321 -0
- package/dist/contexts-3Arvn7yR.js.map +1 -0
- package/dist/contexts.d.ts +2 -25
- package/dist/contexts.js +2 -10
- package/dist/errors-D1lhd6mX.js +118 -0
- package/dist/errors-D1lhd6mX.js.map +1 -0
- package/dist/index-28otmfLX.d.ts +400 -0
- package/dist/index-28otmfLX.d.ts.map +1 -0
- package/dist/index-BfSdALzk.d.ts +113 -0
- package/dist/index-BfSdALzk.d.ts.map +1 -0
- package/dist/index-DPsd0qwm.d.ts +254 -0
- package/dist/index-DPsd0qwm.d.ts.map +1 -0
- package/dist/index.d.ts +5 -95
- package/dist/index.js +141 -271
- package/dist/index.js.map +1 -0
- package/dist/interpolate-CukJwP2G.js +887 -0
- package/dist/interpolate-CukJwP2G.js.map +1 -0
- package/dist/mcp-8wClKY-3.js +771 -0
- package/dist/mcp-8wClKY-3.js.map +1 -0
- package/dist/mcp.d.ts +2 -4
- package/dist/mcp.js +2 -13
- package/dist/messages-z5Pq20p7.js +1020 -0
- package/dist/messages-z5Pq20p7.js.map +1 -0
- package/dist/presets-Cs7_CsMk.js +39 -0
- package/dist/presets-Cs7_CsMk.js.map +1 -0
- package/dist/presets.d.ts +2 -43
- package/dist/presets.js +2 -17
- package/dist/providers-CX-R-Oy-.js +969 -0
- package/dist/providers-CX-R-Oy-.js.map +1 -0
- package/dist/providers.d.ts +2 -4
- package/dist/providers.js +3 -23
- package/dist/session/sqlite.d.ts +7 -12
- package/dist/session/sqlite.d.ts.map +1 -0
- package/dist/session/sqlite.js +67 -79
- package/dist/session/sqlite.js.map +1 -0
- package/dist/session-Cn68UASv.js +440 -0
- package/dist/session-Cn68UASv.js.map +1 -0
- package/dist/session.d.ts +2 -4
- package/dist/session.js +3 -27
- package/dist/skills.d.ts +3 -322
- package/dist/skills.js +24 -47
- package/dist/skills.js.map +1 -0
- package/dist/stats-DoKUtF5T.js +58 -0
- package/dist/stats-DoKUtF5T.js.map +1 -0
- package/dist/tools-DpeWKzP1.js +3941 -0
- package/dist/tools-DpeWKzP1.js.map +1 -0
- package/dist/tools.d.ts +3 -95
- package/dist/tools.js +2 -40
- package/dist/tui.d.ts +533 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +2004 -0
- package/dist/tui.js.map +1 -0
- package/dist/types-Bx_F8jet.js +39 -0
- package/dist/types-Bx_F8jet.js.map +1 -0
- package/dist/types.d.ts +4 -55
- package/dist/types.js +4 -28
- package/package.json +38 -4
- package/dist/agent-BAHrGtqu.d.ts +0 -2425
- package/dist/chunk-4ILGBQ23.js +0 -803
- package/dist/chunk-4LPBN547.js +0 -3540
- package/dist/chunk-64LLNY7F.js +0 -28
- package/dist/chunk-6STZTA4N.js +0 -830
- package/dist/chunk-7GQ7P6DM.js +0 -566
- package/dist/chunk-IC7FT4OD.js +0 -37
- package/dist/chunk-JCOB6IYO.js +0 -22
- package/dist/chunk-JH6IAAFA.js +0 -28
- package/dist/chunk-LNN5UTS2.js +0 -97
- package/dist/chunk-PMCQOMV4.js +0 -490
- package/dist/chunk-UD25QF3H.js +0 -304
- package/dist/chunk-W57VY6DJ.js +0 -834
- package/dist/sandbox-D7v6Wy62.d.ts +0 -28
- package/dist/skills-use-DwZrNmcw.d.ts +0 -80
- package/dist/types-Bai5rKpa.d.ts +0 -89
- package/dist/validation-Pm--dQEU.d.ts +0 -185
package/dist/chunk-4LPBN547.js
DELETED
|
@@ -1,3540 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
buildCatalog,
|
|
3
|
-
createSkillActivationState,
|
|
4
|
-
escapeXml,
|
|
5
|
-
installAllowedToolsGate,
|
|
6
|
-
interpolateShellCommands,
|
|
7
|
-
resolveSkills,
|
|
8
|
-
validateResourcePath
|
|
9
|
-
} from "./chunk-6STZTA4N.js";
|
|
10
|
-
import {
|
|
11
|
-
flattenTurns
|
|
12
|
-
} from "./chunk-IC7FT4OD.js";
|
|
13
|
-
import {
|
|
14
|
-
createProcessContext
|
|
15
|
-
} from "./chunk-UD25QF3H.js";
|
|
16
|
-
import {
|
|
17
|
-
connectMcpServers
|
|
18
|
-
} from "./chunk-7GQ7P6DM.js";
|
|
19
|
-
import {
|
|
20
|
-
toolOutputByteLength
|
|
21
|
-
} from "./chunk-JH6IAAFA.js";
|
|
22
|
-
import {
|
|
23
|
-
AgentAbortedError,
|
|
24
|
-
AgentProviderError,
|
|
25
|
-
toTypedError
|
|
26
|
-
} from "./chunk-LNN5UTS2.js";
|
|
27
|
-
|
|
28
|
-
// src/tools/edit-utils.ts
|
|
29
|
-
function countExactMatches(haystack, needle) {
|
|
30
|
-
if (needle.length === 0)
|
|
31
|
-
return 0;
|
|
32
|
-
let count = 0;
|
|
33
|
-
let idx = 0;
|
|
34
|
-
while (true) {
|
|
35
|
-
const next = haystack.indexOf(needle, idx);
|
|
36
|
-
if (next === -1)
|
|
37
|
-
break;
|
|
38
|
-
count++;
|
|
39
|
-
idx = next + needle.length;
|
|
40
|
-
}
|
|
41
|
-
return count;
|
|
42
|
-
}
|
|
43
|
-
var LEFT_SINGLE_CURLY_QUOTE = "\u2018";
|
|
44
|
-
var RIGHT_SINGLE_CURLY_QUOTE = "\u2019";
|
|
45
|
-
var LEFT_DOUBLE_CURLY_QUOTE = "\u201C";
|
|
46
|
-
var RIGHT_DOUBLE_CURLY_QUOTE = "\u201D";
|
|
47
|
-
function normalizeQuotes(str) {
|
|
48
|
-
return str.replaceAll(LEFT_SINGLE_CURLY_QUOTE, "'").replaceAll(RIGHT_SINGLE_CURLY_QUOTE, "'").replaceAll(LEFT_DOUBLE_CURLY_QUOTE, '"').replaceAll(RIGHT_DOUBLE_CURLY_QUOTE, '"');
|
|
49
|
-
}
|
|
50
|
-
var DESANITIZATIONS = [
|
|
51
|
-
["<fnr>", "<function_results>"],
|
|
52
|
-
["<n>", "<name>"],
|
|
53
|
-
["</n>", "</name>"],
|
|
54
|
-
["<o>", "<output>"],
|
|
55
|
-
["</o>", "</output>"],
|
|
56
|
-
["<e>", "<error>"],
|
|
57
|
-
["</e>", "</error>"],
|
|
58
|
-
["<s>", "<system>"],
|
|
59
|
-
["</s>", "</system>"],
|
|
60
|
-
["<r>", "<result>"],
|
|
61
|
-
["</r>", "</result>"],
|
|
62
|
-
["< META_START >", "<META_START>"],
|
|
63
|
-
["< META_END >", "<META_END>"],
|
|
64
|
-
["< EOT >", "<EOT>"],
|
|
65
|
-
["< META >", "<META>"],
|
|
66
|
-
["< SOS >", "<SOS>"],
|
|
67
|
-
["\n\nH:", "\n\nHuman:"],
|
|
68
|
-
["\n\nA:", "\n\nAssistant:"]
|
|
69
|
-
];
|
|
70
|
-
function desanitize(s) {
|
|
71
|
-
let out = s;
|
|
72
|
-
for (const [from, to] of DESANITIZATIONS)
|
|
73
|
-
out = out.replaceAll(from, to);
|
|
74
|
-
return out;
|
|
75
|
-
}
|
|
76
|
-
var LINE_NUMBER_PREFIX_RE = /^[ \t]*\d{1,9}[\t|\u2192]/gm;
|
|
77
|
-
function stripLineNumberPrefixes(s) {
|
|
78
|
-
return s.replace(LINE_NUMBER_PREFIX_RE, "");
|
|
79
|
-
}
|
|
80
|
-
function locateAndCount(haystack, normFile, target, via) {
|
|
81
|
-
const idx = normFile.indexOf(target);
|
|
82
|
-
if (idx === -1)
|
|
83
|
-
return null;
|
|
84
|
-
const actual = haystack.slice(idx, idx + target.length);
|
|
85
|
-
let occ = 0;
|
|
86
|
-
let cursor = 0;
|
|
87
|
-
while (true) {
|
|
88
|
-
const next = normFile.indexOf(target, cursor);
|
|
89
|
-
if (next === -1)
|
|
90
|
-
break;
|
|
91
|
-
occ++;
|
|
92
|
-
cursor = next + target.length;
|
|
93
|
-
}
|
|
94
|
-
return { actual, occurrences: occ, via };
|
|
95
|
-
}
|
|
96
|
-
function resolveOldString(haystack, needle) {
|
|
97
|
-
const exact = countExactMatches(haystack, needle);
|
|
98
|
-
if (exact > 0)
|
|
99
|
-
return { actual: needle, occurrences: exact, via: "exact" };
|
|
100
|
-
const normNeedle = normalizeQuotes(needle);
|
|
101
|
-
const normFile = normalizeQuotes(haystack);
|
|
102
|
-
if (normNeedle !== needle || normFile !== haystack) {
|
|
103
|
-
const m = locateAndCount(haystack, normFile, normNeedle, "quotes");
|
|
104
|
-
if (m)
|
|
105
|
-
return m;
|
|
106
|
-
}
|
|
107
|
-
const desan = desanitize(needle);
|
|
108
|
-
if (desan !== needle) {
|
|
109
|
-
const desanCount = countExactMatches(haystack, desan);
|
|
110
|
-
if (desanCount > 0)
|
|
111
|
-
return { actual: desan, occurrences: desanCount, via: "desanitize" };
|
|
112
|
-
}
|
|
113
|
-
const combo = desanitize(normNeedle);
|
|
114
|
-
if (combo !== needle) {
|
|
115
|
-
const m = locateAndCount(haystack, normFile, combo, "quotes+desanitize");
|
|
116
|
-
if (m)
|
|
117
|
-
return m;
|
|
118
|
-
}
|
|
119
|
-
const stripped = stripLineNumberPrefixes(needle);
|
|
120
|
-
if (stripped !== needle && stripped.trim().length > 0) {
|
|
121
|
-
const count = countExactMatches(haystack, stripped);
|
|
122
|
-
if (count > 0)
|
|
123
|
-
return { actual: stripped, occurrences: count, via: "line-numbers" };
|
|
124
|
-
const strippedNorm = normalizeQuotes(stripped);
|
|
125
|
-
if (strippedNorm !== stripped || normFile !== haystack) {
|
|
126
|
-
const m = locateAndCount(haystack, normFile, strippedNorm, "quotes+line-numbers");
|
|
127
|
-
if (m)
|
|
128
|
-
return m;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
function styleReplacementForVia(replacement, via, actual) {
|
|
134
|
-
let out = replacement;
|
|
135
|
-
if (via === "desanitize" || via === "quotes+desanitize")
|
|
136
|
-
out = desanitize(out);
|
|
137
|
-
if (via === "line-numbers" || via === "quotes+line-numbers")
|
|
138
|
-
out = stripLineNumberPrefixes(out);
|
|
139
|
-
if (via === "quotes" || via === "quotes+desanitize" || via === "quotes+line-numbers")
|
|
140
|
-
out = preserveQuoteStyle(actual, out);
|
|
141
|
-
return out;
|
|
142
|
-
}
|
|
143
|
-
function preserveQuoteStyle(actual, replacement) {
|
|
144
|
-
const hasDouble = actual.includes(LEFT_DOUBLE_CURLY_QUOTE) || actual.includes(RIGHT_DOUBLE_CURLY_QUOTE);
|
|
145
|
-
const hasSingle = actual.includes(LEFT_SINGLE_CURLY_QUOTE) || actual.includes(RIGHT_SINGLE_CURLY_QUOTE);
|
|
146
|
-
if (!hasDouble && !hasSingle)
|
|
147
|
-
return replacement;
|
|
148
|
-
let out = replacement;
|
|
149
|
-
if (hasDouble)
|
|
150
|
-
out = applyCurly(out, '"', LEFT_DOUBLE_CURLY_QUOTE, RIGHT_DOUBLE_CURLY_QUOTE, false);
|
|
151
|
-
if (hasSingle)
|
|
152
|
-
out = applyCurly(out, "'", LEFT_SINGLE_CURLY_QUOTE, RIGHT_SINGLE_CURLY_QUOTE, true);
|
|
153
|
-
return out;
|
|
154
|
-
}
|
|
155
|
-
function applyCurly(s, straight, left, right, contractionAware) {
|
|
156
|
-
const chars = [...s];
|
|
157
|
-
const result = [];
|
|
158
|
-
for (let i = 0; i < chars.length; i++) {
|
|
159
|
-
if (chars[i] !== straight) {
|
|
160
|
-
result.push(chars[i]);
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
if (contractionAware) {
|
|
164
|
-
const prev = i > 0 ? chars[i - 1] : "";
|
|
165
|
-
const next = i < chars.length - 1 ? chars[i + 1] : "";
|
|
166
|
-
if (/\p{L}/u.test(prev) && /\p{L}/u.test(next)) {
|
|
167
|
-
result.push(right);
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
result.push(isOpeningContext(chars, i) ? left : right);
|
|
172
|
-
}
|
|
173
|
-
return result.join("");
|
|
174
|
-
}
|
|
175
|
-
function isOpeningContext(chars, i) {
|
|
176
|
-
if (i === 0)
|
|
177
|
-
return true;
|
|
178
|
-
const prev = chars[i - 1];
|
|
179
|
-
return prev === " " || prev === " " || prev === "\n" || prev === "\r" || prev === "(" || prev === "[" || prev === "{" || prev === "\u2014" || prev === "\u2013";
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// src/tools/path-suggest.ts
|
|
183
|
-
async function findSimilarFile(execution, handle, path) {
|
|
184
|
-
const slash = path.lastIndexOf("/");
|
|
185
|
-
const dir = slash === -1 ? "." : path.slice(0, slash) || "/";
|
|
186
|
-
const target = slash === -1 ? path : path.slice(slash + 1);
|
|
187
|
-
const dot = target.lastIndexOf(".");
|
|
188
|
-
const targetBase = dot === -1 ? target : target.slice(0, dot);
|
|
189
|
-
if (targetBase.length === 0)
|
|
190
|
-
return null;
|
|
191
|
-
let entries;
|
|
192
|
-
try {
|
|
193
|
-
entries = await execution.listFiles(handle, dir);
|
|
194
|
-
} catch {
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
for (const entry of entries) {
|
|
198
|
-
if (entry === target)
|
|
199
|
-
continue;
|
|
200
|
-
const entryDot = entry.lastIndexOf(".");
|
|
201
|
-
const entryBase = entryDot === -1 ? entry : entry.slice(0, entryDot);
|
|
202
|
-
if (entryBase === targetBase)
|
|
203
|
-
return entry;
|
|
204
|
-
}
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
207
|
-
async function suggestionFor(execution, handle, path) {
|
|
208
|
-
const sibling = await findSimilarFile(execution, handle, path);
|
|
209
|
-
return sibling ? ` Did you mean ${sibling}?` : "";
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// src/tools/read-state.ts
|
|
213
|
-
var STATE = /* @__PURE__ */ new WeakMap();
|
|
214
|
-
function getReadState(session) {
|
|
215
|
-
if (!session)
|
|
216
|
-
return void 0;
|
|
217
|
-
let map = STATE.get(session);
|
|
218
|
-
if (!map) {
|
|
219
|
-
map = /* @__PURE__ */ new Map();
|
|
220
|
-
STATE.set(session, map);
|
|
221
|
-
}
|
|
222
|
-
return map;
|
|
223
|
-
}
|
|
224
|
-
var TOOL_DEDUP_STATE = /* @__PURE__ */ new WeakMap();
|
|
225
|
-
function getToolDedupState(session) {
|
|
226
|
-
if (!session)
|
|
227
|
-
return void 0;
|
|
228
|
-
let map = TOOL_DEDUP_STATE.get(session);
|
|
229
|
-
if (!map) {
|
|
230
|
-
map = /* @__PURE__ */ new Map();
|
|
231
|
-
TOOL_DEDUP_STATE.set(session, map);
|
|
232
|
-
}
|
|
233
|
-
return map;
|
|
234
|
-
}
|
|
235
|
-
function hashContent(text) {
|
|
236
|
-
let h = 2166136261;
|
|
237
|
-
for (let i = 0; i < text.length; i++) {
|
|
238
|
-
h ^= text.charCodeAt(i);
|
|
239
|
-
h = h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
|
|
240
|
-
}
|
|
241
|
-
return h.toString(16).padStart(8, "0");
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// src/tools/edit.ts
|
|
245
|
-
var edit = {
|
|
246
|
-
spec: {
|
|
247
|
-
name: "edit",
|
|
248
|
-
description: "Replace exact `old_string` with `new_string` in a file. Fails if `old_string` is not unique unless `replace_all: true`. Prefer over `write_file` for surgical changes \u2014 preserves the rest of the file. Tolerates `read_file` line-number prefixes (`<N>\\t\u2026`, `<N>|\u2026`, or `<N>\u2192\u2026`) in `old_string` / `new_string` \u2014 they are stripped before matching/writing, so you can paste a numbered chunk verbatim.",
|
|
249
|
-
inputSchema: {
|
|
250
|
-
type: "object",
|
|
251
|
-
properties: {
|
|
252
|
-
path: { type: "string", description: "Relative file path." },
|
|
253
|
-
old_string: { type: "string", description: "Exact substring to find." },
|
|
254
|
-
new_string: { type: "string", description: "Replacement substring." },
|
|
255
|
-
replace_all: { type: "boolean", description: "Replace every occurrence. Default: false." }
|
|
256
|
-
},
|
|
257
|
-
required: ["path", "old_string", "new_string"]
|
|
258
|
-
}
|
|
259
|
-
},
|
|
260
|
-
async execute({ path, old_string, new_string, replace_all }, ctx) {
|
|
261
|
-
const target = path;
|
|
262
|
-
const find = old_string;
|
|
263
|
-
const replacement = new_string;
|
|
264
|
-
const replaceAll = replace_all === true;
|
|
265
|
-
if (find === replacement)
|
|
266
|
-
return `Edit error: old_string and new_string are identical \u2014 nothing to change in ${target}.`;
|
|
267
|
-
if (find.length === 0)
|
|
268
|
-
return `Edit error: old_string is empty. Use write_file to create or fully overwrite a file.`;
|
|
269
|
-
let original;
|
|
270
|
-
try {
|
|
271
|
-
original = await ctx.execution.readFile(ctx.handle, target);
|
|
272
|
-
} catch {
|
|
273
|
-
const hint = await suggestionFor(ctx.execution, ctx.handle, target);
|
|
274
|
-
return `Edit error: file not found: ${target}.${hint}`;
|
|
275
|
-
}
|
|
276
|
-
if (ctx.behavior?.requireReadBeforeEdit && ctx.session) {
|
|
277
|
-
const readState = getReadState(ctx.session);
|
|
278
|
-
const absKey = `${ctx.handle.cwd}::${target}`;
|
|
279
|
-
const prior = readState?.get(absKey);
|
|
280
|
-
if (!prior)
|
|
281
|
-
return `Edit error: ${target} has not been read in this session. Call read_file first so the edit applies against the current contents.`;
|
|
282
|
-
if (prior.contentHash !== hashContent(original))
|
|
283
|
-
return `Edit error: ${target} has changed on disk since the last read. Re-read the file before editing.`;
|
|
284
|
-
}
|
|
285
|
-
const match = resolveOldString(original, find);
|
|
286
|
-
if (!match) {
|
|
287
|
-
const preview = nearestMatchPreview(original, find);
|
|
288
|
-
return preview ? `Edit error: old_string not found in ${target}. Closest match in the file: ${preview}` : `Edit error: old_string not found in ${target}.`;
|
|
289
|
-
}
|
|
290
|
-
const { actual, occurrences, via } = match;
|
|
291
|
-
if (occurrences > 1 && !replaceAll)
|
|
292
|
-
return `Edit error: old_string appears ${occurrences} times in ${target}. Pass replace_all=true or expand old_string for uniqueness.`;
|
|
293
|
-
const styledReplacement = styleReplacementForVia(replacement, via, actual);
|
|
294
|
-
const updated = replaceAll ? original.split(actual).join(styledReplacement) : original.replace(actual, styledReplacement);
|
|
295
|
-
if (updated === original)
|
|
296
|
-
return `Edit error: replacement produced no change in ${target}.`;
|
|
297
|
-
await ctx.execution.writeFile(ctx.handle, target, updated);
|
|
298
|
-
if (ctx.session) {
|
|
299
|
-
const readState = getReadState(ctx.session);
|
|
300
|
-
const absKey = `${ctx.handle.cwd}::${target}`;
|
|
301
|
-
const prior = readState?.get(absKey);
|
|
302
|
-
if (readState && prior) {
|
|
303
|
-
readState.set(absKey, { ...prior, contentHash: hashContent(updated), mtimeMs: Date.now() });
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
return `Edited ${target}: replaced ${occurrences} occurrence${occurrences === 1 ? "" : "s"}.`;
|
|
307
|
-
}
|
|
308
|
-
};
|
|
309
|
-
function nearestMatchPreview(haystack, needle) {
|
|
310
|
-
const normalizedNeedle = stripLineNumberPrefixes(needle);
|
|
311
|
-
const needleFirstLine = normalizedNeedle.split("\n")[0];
|
|
312
|
-
if (needleFirstLine.length < 3)
|
|
313
|
-
return null;
|
|
314
|
-
const lines = haystack.split("\n");
|
|
315
|
-
let bestScore = 0;
|
|
316
|
-
let bestIdx = -1;
|
|
317
|
-
for (let i = 0; i < lines.length; i++) {
|
|
318
|
-
const score = sharedPrefixLength(lines[i], needleFirstLine);
|
|
319
|
-
if (score > bestScore) {
|
|
320
|
-
bestScore = score;
|
|
321
|
-
bestIdx = i;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
if (bestIdx < 0 || bestScore < Math.min(8, Math.floor(needleFirstLine.length / 2)))
|
|
325
|
-
return null;
|
|
326
|
-
const snippet = lines[bestIdx].slice(0, 80);
|
|
327
|
-
return `line ${bestIdx + 1}: ${JSON.stringify(snippet)}`;
|
|
328
|
-
}
|
|
329
|
-
function sharedPrefixLength(a, b) {
|
|
330
|
-
const max = Math.min(a.length, b.length);
|
|
331
|
-
let i = 0;
|
|
332
|
-
while (i < max && a.charCodeAt(i) === b.charCodeAt(i))
|
|
333
|
-
i++;
|
|
334
|
-
return i;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// src/tools/glob.ts
|
|
338
|
-
import { stat } from "fs/promises";
|
|
339
|
-
import { resolve } from "path";
|
|
340
|
-
var DEFAULT_LIMIT = 1e3;
|
|
341
|
-
var SAFE_GLOB_PATTERN_RE = /^[\w./*?[\]{}!,^@+-]+$/;
|
|
342
|
-
async function globInProcess(pattern, cwd, limit) {
|
|
343
|
-
const glob2 = new Bun.Glob(pattern);
|
|
344
|
-
const results = [];
|
|
345
|
-
for await (const file of glob2.scan({ cwd })) {
|
|
346
|
-
results.push(file);
|
|
347
|
-
if (results.length >= limit)
|
|
348
|
-
break;
|
|
349
|
-
}
|
|
350
|
-
return results.sort();
|
|
351
|
-
}
|
|
352
|
-
async function globViaShell(pattern, ctx, limit) {
|
|
353
|
-
if (!SAFE_GLOB_PATTERN_RE.test(pattern))
|
|
354
|
-
throw new Error("Glob pattern contains unsupported characters (shell fallback only allows path/glob metacharacters)");
|
|
355
|
-
const useBasename = !pattern.includes("/");
|
|
356
|
-
const finder = useBasename ? `find . -type f -name '${pattern}'` : `find . -type f -path './${pattern}'`;
|
|
357
|
-
const searchCmd = `${finder} 2>/dev/null | sed 's|^./||' | sort | head -n ${limit}`;
|
|
358
|
-
const result = await ctx.execution.exec(ctx.handle, searchCmd);
|
|
359
|
-
if (result.exitCode !== 0 && !result.stdout)
|
|
360
|
-
return [];
|
|
361
|
-
return result.stdout.split("\n").filter((line) => line.length > 0);
|
|
362
|
-
}
|
|
363
|
-
var glob = {
|
|
364
|
-
spec: {
|
|
365
|
-
name: "glob",
|
|
366
|
-
description: "Match files by glob pattern (supports **, *, ?). Relative to the execution context cwd. By default each row is `<path>\\t<size-bytes>\\t<mtime-iso>`; set `metadata: false` for a plain newline-separated list of paths. Always sorted.",
|
|
367
|
-
inputSchema: {
|
|
368
|
-
type: "object",
|
|
369
|
-
properties: {
|
|
370
|
-
pattern: {
|
|
371
|
-
type: "string",
|
|
372
|
-
description: 'Glob pattern (e.g. "src/**/*.ts", "*.md", "test/**/fixtures/*").'
|
|
373
|
-
},
|
|
374
|
-
limit: {
|
|
375
|
-
type: "number",
|
|
376
|
-
description: `Maximum number of matches to return. Default: ${DEFAULT_LIMIT}.`
|
|
377
|
-
},
|
|
378
|
-
metadata: {
|
|
379
|
-
type: "boolean",
|
|
380
|
-
description: "Append size (bytes) and mtime (ISO) per row, tab-separated. Default: true. In-process only \u2014 non-process execution contexts always return paths."
|
|
381
|
-
}
|
|
382
|
-
},
|
|
383
|
-
required: ["pattern"]
|
|
384
|
-
}
|
|
385
|
-
},
|
|
386
|
-
async execute({ pattern, limit, metadata }, ctx) {
|
|
387
|
-
const pat = pattern;
|
|
388
|
-
const max = typeof limit === "number" && limit > 0 ? limit : DEFAULT_LIMIT;
|
|
389
|
-
const wantMetadata = metadata !== false;
|
|
390
|
-
try {
|
|
391
|
-
const entries = ctx.execution.type === "process" ? await globInProcess(pat, ctx.handle.cwd, max) : await globViaShell(pat, ctx, max);
|
|
392
|
-
if (entries.length === 0)
|
|
393
|
-
return "(no matches)";
|
|
394
|
-
if (!wantMetadata || ctx.execution.type !== "process")
|
|
395
|
-
return entries.join("\n");
|
|
396
|
-
const rows = await Promise.all(entries.map(async (rel) => {
|
|
397
|
-
try {
|
|
398
|
-
const s = await stat(resolve(ctx.handle.cwd, rel));
|
|
399
|
-
return `${rel} ${s.size} ${new Date(s.mtimeMs).toISOString()}`;
|
|
400
|
-
} catch {
|
|
401
|
-
return `${rel} `;
|
|
402
|
-
}
|
|
403
|
-
}));
|
|
404
|
-
return rows.join("\n");
|
|
405
|
-
} catch (err) {
|
|
406
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
407
|
-
return `Glob error: ${message}`;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
// src/tools/shell-quote.ts
|
|
413
|
-
var SAFE_TOKEN_RE = /^[\w@%+=:,./-]+$/;
|
|
414
|
-
var SINGLE_QUOTE_RE = /'/g;
|
|
415
|
-
function shellQuote(arg) {
|
|
416
|
-
if (SAFE_TOKEN_RE.test(arg))
|
|
417
|
-
return arg;
|
|
418
|
-
return `'${arg.replace(SINGLE_QUOTE_RE, "'\\''")}'`;
|
|
419
|
-
}
|
|
420
|
-
function alwaysQuote(arg) {
|
|
421
|
-
return `'${arg.replace(SINGLE_QUOTE_RE, "'\\''")}'`;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// src/tools/grep.ts
|
|
425
|
-
var DEFAULT_HEAD_LIMIT = 250;
|
|
426
|
-
var DEFAULT_OUTPUT_MODE = "files_with_matches";
|
|
427
|
-
var grep = {
|
|
428
|
-
spec: {
|
|
429
|
-
name: "grep",
|
|
430
|
-
description: "Search file contents by regex. Returns matching paths (default), match content, or per-file counts. Backed by ripgrep when available with a Bun.Glob fallback for in-process runs.",
|
|
431
|
-
inputSchema: {
|
|
432
|
-
type: "object",
|
|
433
|
-
properties: {
|
|
434
|
-
"pattern": { type: "string", description: "Regex (PCRE-flavored via ripgrep, JS regex via fallback)." },
|
|
435
|
-
"path": { type: "string", description: 'File or directory to search. Default: ".".' },
|
|
436
|
-
"glob": { type: "string", description: 'Restrict to files matching this glob, e.g. "**/*.ts".' },
|
|
437
|
-
"type": { type: "string", description: 'rg file type filter, e.g. "ts", "py", "rust". Ignored by the fallback.' },
|
|
438
|
-
"output_mode": { type: "string", enum: ["content", "files_with_matches", "count"], description: 'Default: "files_with_matches".' },
|
|
439
|
-
"-i": { type: "boolean", description: "Case-insensitive match." },
|
|
440
|
-
"-n": { type: "boolean", description: "Show line numbers (content mode). Default: true." },
|
|
441
|
-
"-A": { type: "integer", description: "Lines of trailing context (content mode)." },
|
|
442
|
-
"-B": { type: "integer", description: "Lines of leading context (content mode)." },
|
|
443
|
-
"-C": { type: "integer", description: "Lines of surrounding context (content mode). Overridden by -A/-B if set." },
|
|
444
|
-
"multiline": { type: "boolean", description: "Allow patterns to match across line boundaries." },
|
|
445
|
-
"head_limit": { type: "integer", description: "Cap output entries. Default: 250. Set 0 for unlimited." },
|
|
446
|
-
"offset": { type: "integer", description: "Skip first N entries. Default: 0." }
|
|
447
|
-
},
|
|
448
|
-
required: ["pattern"]
|
|
449
|
-
}
|
|
450
|
-
},
|
|
451
|
-
async execute(rawInput, ctx) {
|
|
452
|
-
const input = rawInput;
|
|
453
|
-
const useRg = await isRipgrepAvailable(ctx);
|
|
454
|
-
if (useRg)
|
|
455
|
-
return runViaRipgrep(input, ctx);
|
|
456
|
-
if (ctx.execution.type === "process")
|
|
457
|
-
return runInProcess(input, ctx);
|
|
458
|
-
return "grep error: ripgrep is not available in the execution context. Install `rg` or use the `shell` tool with grep/awk.";
|
|
459
|
-
}
|
|
460
|
-
};
|
|
461
|
-
async function isRipgrepAvailable(ctx) {
|
|
462
|
-
const result = await ctx.execution.exec(ctx.handle, "rg --version");
|
|
463
|
-
return result.exitCode === 0;
|
|
464
|
-
}
|
|
465
|
-
async function runViaRipgrep(input, ctx) {
|
|
466
|
-
const args = ["rg"];
|
|
467
|
-
const mode = input.output_mode ?? DEFAULT_OUTPUT_MODE;
|
|
468
|
-
if (mode === "files_with_matches")
|
|
469
|
-
args.push("--files-with-matches");
|
|
470
|
-
else if (mode === "count")
|
|
471
|
-
args.push("--count");
|
|
472
|
-
else
|
|
473
|
-
args.push(input["-n"] ?? true ? "--line-number" : "--no-line-number");
|
|
474
|
-
if (input["-i"])
|
|
475
|
-
args.push("-i");
|
|
476
|
-
if (mode === "content") {
|
|
477
|
-
if (typeof input["-A"] === "number")
|
|
478
|
-
args.push("-A", String(input["-A"]));
|
|
479
|
-
if (typeof input["-B"] === "number")
|
|
480
|
-
args.push("-B", String(input["-B"]));
|
|
481
|
-
if (typeof input["-C"] === "number" && typeof input["-A"] !== "number" && typeof input["-B"] !== "number")
|
|
482
|
-
args.push("-C", String(input["-C"]));
|
|
483
|
-
}
|
|
484
|
-
if (input.multiline)
|
|
485
|
-
args.push("--multiline", "--multiline-dotall");
|
|
486
|
-
if (input.glob)
|
|
487
|
-
args.push("--glob", input.glob);
|
|
488
|
-
if (input.type)
|
|
489
|
-
args.push("--type", input.type);
|
|
490
|
-
args.push("--", input.pattern);
|
|
491
|
-
args.push(input.path ?? ".");
|
|
492
|
-
const command = args.map(shellQuote).join(" ");
|
|
493
|
-
const result = await ctx.execution.exec(ctx.handle, command);
|
|
494
|
-
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
495
|
-
return `grep error: ${result.stderr.trim() || `rg exited with code ${result.exitCode}`}`;
|
|
496
|
-
}
|
|
497
|
-
return formatPaginated(result.stdout, input);
|
|
498
|
-
}
|
|
499
|
-
async function runInProcess(input, ctx) {
|
|
500
|
-
const mode = input.output_mode ?? DEFAULT_OUTPUT_MODE;
|
|
501
|
-
const flags = `${input["-i"] ? "i" : ""}${input.multiline ? "s" : ""}${mode !== "content" ? "" : "g"}`;
|
|
502
|
-
let regex;
|
|
503
|
-
try {
|
|
504
|
-
regex = new RegExp(input.pattern, flags || void 0);
|
|
505
|
-
} catch (err) {
|
|
506
|
-
return `grep error: invalid regex: ${err.message}`;
|
|
507
|
-
}
|
|
508
|
-
const files = await enumerateFiles(input, ctx);
|
|
509
|
-
const showLineNumbers = input["-n"] ?? true;
|
|
510
|
-
const before = input["-B"] ?? input["-C"] ?? 0;
|
|
511
|
-
const after = input["-A"] ?? input["-C"] ?? 0;
|
|
512
|
-
const lines = [];
|
|
513
|
-
for (const path of files) {
|
|
514
|
-
let content;
|
|
515
|
-
try {
|
|
516
|
-
content = await ctx.execution.readFile(ctx.handle, path);
|
|
517
|
-
} catch {
|
|
518
|
-
continue;
|
|
519
|
-
}
|
|
520
|
-
if (input.multiline) {
|
|
521
|
-
const allMatches = [...content.matchAll(new RegExp(regex.source, `${flags.replace(/g/, "")}g`))];
|
|
522
|
-
if (allMatches.length === 0)
|
|
523
|
-
continue;
|
|
524
|
-
if (mode === "files_with_matches") {
|
|
525
|
-
lines.push(path);
|
|
526
|
-
continue;
|
|
527
|
-
}
|
|
528
|
-
if (mode === "count") {
|
|
529
|
-
lines.push(`${path}:${allMatches.length}`);
|
|
530
|
-
continue;
|
|
531
|
-
}
|
|
532
|
-
for (const m of allMatches) {
|
|
533
|
-
const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
|
|
534
|
-
const lineEnd = content.indexOf("\n", m.index);
|
|
535
|
-
const snippet = content.slice(lineStart, lineEnd === -1 ? void 0 : lineEnd);
|
|
536
|
-
const lineNo = content.slice(0, m.index).split("\n").length;
|
|
537
|
-
lines.push(formatContentLine(path, lineNo, snippet, showLineNumbers));
|
|
538
|
-
}
|
|
539
|
-
continue;
|
|
540
|
-
}
|
|
541
|
-
const fileLines = content.split("\n");
|
|
542
|
-
const matched = [];
|
|
543
|
-
for (let i = 0; i < fileLines.length; i++) {
|
|
544
|
-
regex.lastIndex = 0;
|
|
545
|
-
if (regex.test(fileLines[i]))
|
|
546
|
-
matched.push(i);
|
|
547
|
-
}
|
|
548
|
-
if (matched.length === 0)
|
|
549
|
-
continue;
|
|
550
|
-
if (mode === "files_with_matches") {
|
|
551
|
-
lines.push(path);
|
|
552
|
-
continue;
|
|
553
|
-
}
|
|
554
|
-
if (mode === "count") {
|
|
555
|
-
lines.push(`${path}:${matched.length}`);
|
|
556
|
-
continue;
|
|
557
|
-
}
|
|
558
|
-
const includeLineNos = /* @__PURE__ */ new Set();
|
|
559
|
-
for (const m of matched) {
|
|
560
|
-
for (let i = Math.max(0, m - before); i <= Math.min(fileLines.length - 1, m + after); i++)
|
|
561
|
-
includeLineNos.add(i);
|
|
562
|
-
}
|
|
563
|
-
const sorted = [...includeLineNos].sort((a, b) => a - b);
|
|
564
|
-
let prev = -2;
|
|
565
|
-
for (const lineNo of sorted) {
|
|
566
|
-
if (lineNo > prev + 1 && lines.length > 0)
|
|
567
|
-
lines.push("--");
|
|
568
|
-
const snippet = fileLines[lineNo];
|
|
569
|
-
lines.push(formatContentLine(path, lineNo + 1, snippet, showLineNumbers));
|
|
570
|
-
prev = lineNo;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
return formatPaginated(lines.join("\n"), input);
|
|
574
|
-
}
|
|
575
|
-
function formatContentLine(path, lineNo, snippet, showLineNumbers) {
|
|
576
|
-
return showLineNumbers ? `${path}:${lineNo}:${snippet}` : `${path}:${snippet}`;
|
|
577
|
-
}
|
|
578
|
-
async function enumerateFiles(input, ctx) {
|
|
579
|
-
const cwd = ctx.handle.cwd;
|
|
580
|
-
const root = input.path ?? ".";
|
|
581
|
-
if (input.path && !input.path.includes("*") && !input.path.includes("?")) {
|
|
582
|
-
try {
|
|
583
|
-
const stat2 = await ctx.execution.exec(ctx.handle, `test -f ${shellQuote(input.path)} && echo file || echo dir`);
|
|
584
|
-
if (stat2.stdout.trim() === "file")
|
|
585
|
-
return [input.path];
|
|
586
|
-
} catch {
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
const pattern = input.glob ?? "**/*";
|
|
590
|
-
const glob2 = new Bun.Glob(pattern);
|
|
591
|
-
const out = [];
|
|
592
|
-
const scanRoot = root === "." ? cwd : `${cwd.replace(/\/$/, "")}/${root.replace(/^\.\//, "")}`;
|
|
593
|
-
for await (const file of glob2.scan({ cwd: scanRoot, onlyFiles: true })) {
|
|
594
|
-
out.push(root === "." ? file : `${root.replace(/\/$/, "")}/${file}`);
|
|
595
|
-
}
|
|
596
|
-
return out.sort();
|
|
597
|
-
}
|
|
598
|
-
function formatPaginated(text, input) {
|
|
599
|
-
const headLimit = typeof input.head_limit === "number" && input.head_limit >= 0 ? input.head_limit : DEFAULT_HEAD_LIMIT;
|
|
600
|
-
const offset = typeof input.offset === "number" && input.offset > 0 ? Math.floor(input.offset) : 0;
|
|
601
|
-
if (!text.trim())
|
|
602
|
-
return "(no matches)";
|
|
603
|
-
const lines = text.split("\n").filter((l) => l.length > 0);
|
|
604
|
-
const total = lines.length;
|
|
605
|
-
const sliced = headLimit === 0 ? lines.slice(offset) : lines.slice(offset, offset + headLimit);
|
|
606
|
-
if (sliced.length === 0)
|
|
607
|
-
return "(no matches in this slice)";
|
|
608
|
-
const truncatedHead = offset > 0;
|
|
609
|
-
const truncatedTail = headLimit > 0 && offset + headLimit < total;
|
|
610
|
-
let out = sliced.join("\n");
|
|
611
|
-
if (truncatedHead)
|
|
612
|
-
out = `\u2026(${offset} earlier matches skipped)\u2026
|
|
613
|
-
${out}`;
|
|
614
|
-
if (truncatedTail)
|
|
615
|
-
out = `${out}
|
|
616
|
-
\u2026(${total - offset - headLimit} more matches; re-run with offset=${offset + headLimit} or larger head_limit)`;
|
|
617
|
-
return out;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// src/tools/interaction.ts
|
|
621
|
-
function createInteractionTool(options) {
|
|
622
|
-
const name = options.name ?? "interaction";
|
|
623
|
-
const description = options.description ?? "Request structured input from the user or external system.";
|
|
624
|
-
return {
|
|
625
|
-
spec: {
|
|
626
|
-
name,
|
|
627
|
-
description,
|
|
628
|
-
inputSchema: options.schema
|
|
629
|
-
},
|
|
630
|
-
async execute(input, ctx) {
|
|
631
|
-
const result = await options.onRequest(input, ctx);
|
|
632
|
-
return typeof result === "string" ? result : JSON.stringify(result);
|
|
633
|
-
}
|
|
634
|
-
};
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// src/tools/list-files.ts
|
|
638
|
-
var listFiles = {
|
|
639
|
-
spec: {
|
|
640
|
-
name: "list_files",
|
|
641
|
-
description: "List files and directories at the given path (relative to project root).",
|
|
642
|
-
inputSchema: {
|
|
643
|
-
type: "object",
|
|
644
|
-
properties: {
|
|
645
|
-
path: { type: "string", description: 'Relative directory path (default: ".")' }
|
|
646
|
-
},
|
|
647
|
-
required: []
|
|
648
|
-
}
|
|
649
|
-
},
|
|
650
|
-
async execute({ path }, ctx) {
|
|
651
|
-
try {
|
|
652
|
-
const entries = await ctx.execution.listFiles(ctx.handle, path || ".");
|
|
653
|
-
return entries.join("\n") || "(empty directory)";
|
|
654
|
-
} catch {
|
|
655
|
-
return `Directory not found: ${path}`;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
};
|
|
659
|
-
|
|
660
|
-
// src/tools/multi-edit.ts
|
|
661
|
-
var multiEdit = {
|
|
662
|
-
spec: {
|
|
663
|
-
name: "multi_edit",
|
|
664
|
-
description: "Apply a sequential list of edits to a file atomically. Each edit operates on the result of the previous edit. All edits must succeed for any to be written. Prefer this over multiple `edit` calls when several non-overlapping changes are needed in the same file. Each step tolerates `read_file` line-number prefixes (`<N>\\t\u2026`, `<N>|\u2026`, or `<N>\u2192\u2026`) in `old_string` / `new_string`.",
|
|
665
|
-
inputSchema: {
|
|
666
|
-
type: "object",
|
|
667
|
-
properties: {
|
|
668
|
-
path: { type: "string", description: "Relative file path." },
|
|
669
|
-
edits: {
|
|
670
|
-
type: "array",
|
|
671
|
-
description: "List of edits applied in order; each operates on the previous edit's output.",
|
|
672
|
-
items: {
|
|
673
|
-
type: "object",
|
|
674
|
-
properties: {
|
|
675
|
-
old_string: { type: "string" },
|
|
676
|
-
new_string: { type: "string" },
|
|
677
|
-
replace_all: { type: "boolean" }
|
|
678
|
-
},
|
|
679
|
-
required: ["old_string", "new_string"]
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
},
|
|
683
|
-
required: ["path", "edits"]
|
|
684
|
-
}
|
|
685
|
-
},
|
|
686
|
-
async execute({ path, edits }, ctx) {
|
|
687
|
-
const target = path;
|
|
688
|
-
const steps = edits;
|
|
689
|
-
if (!Array.isArray(steps) || steps.length === 0)
|
|
690
|
-
return `multi_edit error: edits must be a non-empty array.`;
|
|
691
|
-
let current;
|
|
692
|
-
try {
|
|
693
|
-
current = await ctx.execution.readFile(ctx.handle, target);
|
|
694
|
-
} catch {
|
|
695
|
-
const hint = await suggestionFor(ctx.execution, ctx.handle, target);
|
|
696
|
-
return `multi_edit error: file not found: ${target}.${hint}`;
|
|
697
|
-
}
|
|
698
|
-
if (ctx.behavior?.requireReadBeforeEdit && ctx.session) {
|
|
699
|
-
const readState = getReadState(ctx.session);
|
|
700
|
-
const absKey = `${ctx.handle.cwd}::${target}`;
|
|
701
|
-
const prior = readState?.get(absKey);
|
|
702
|
-
if (!prior)
|
|
703
|
-
return `multi_edit error: ${target} has not been read in this session. Call read_file first so the edits apply against the current contents.`;
|
|
704
|
-
if (prior.contentHash !== hashContent(current))
|
|
705
|
-
return `multi_edit error: ${target} has changed on disk since the last read. Re-read the file before editing.`;
|
|
706
|
-
}
|
|
707
|
-
let applied = 0;
|
|
708
|
-
for (let i = 0; i < steps.length; i++) {
|
|
709
|
-
const step = steps[i];
|
|
710
|
-
const find = step.old_string;
|
|
711
|
-
const replacement = step.new_string;
|
|
712
|
-
const replaceAll = step.replace_all === true;
|
|
713
|
-
if (typeof find !== "string" || typeof replacement !== "string")
|
|
714
|
-
return `multi_edit error: edit #${i + 1} is missing old_string or new_string.`;
|
|
715
|
-
if (find.length === 0)
|
|
716
|
-
return `multi_edit error: edit #${i + 1} has empty old_string. Use write_file to fully replace a file.`;
|
|
717
|
-
if (find === replacement)
|
|
718
|
-
return `multi_edit error: edit #${i + 1} old_string and new_string are identical.`;
|
|
719
|
-
const match = resolveOldString(current, find);
|
|
720
|
-
if (!match)
|
|
721
|
-
return `multi_edit error: edit #${i + 1} old_string not found in ${target}.`;
|
|
722
|
-
const { actual, occurrences, via } = match;
|
|
723
|
-
if (occurrences > 1 && !replaceAll)
|
|
724
|
-
return `multi_edit error: edit #${i + 1} old_string appears ${occurrences} times. Pass replace_all=true on this edit or expand old_string for uniqueness.`;
|
|
725
|
-
const styledReplacement = styleReplacementForVia(replacement, via, actual);
|
|
726
|
-
current = replaceAll ? current.split(actual).join(styledReplacement) : current.replace(actual, styledReplacement);
|
|
727
|
-
applied += occurrences;
|
|
728
|
-
}
|
|
729
|
-
await ctx.execution.writeFile(ctx.handle, target, current);
|
|
730
|
-
if (ctx.session) {
|
|
731
|
-
const readState = getReadState(ctx.session);
|
|
732
|
-
const absKey = `${ctx.handle.cwd}::${target}`;
|
|
733
|
-
const prior = readState?.get(absKey);
|
|
734
|
-
if (readState && prior) {
|
|
735
|
-
readState.set(absKey, { ...prior, contentHash: hashContent(current), mtimeMs: Date.now() });
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
return `Edited ${target}: applied ${steps.length} edit${steps.length === 1 ? "" : "s"} (${applied} replacement${applied === 1 ? "" : "s"}).`;
|
|
739
|
-
}
|
|
740
|
-
};
|
|
741
|
-
|
|
742
|
-
// src/tools/read-file.ts
|
|
743
|
-
import { Buffer as Buffer2 } from "buffer";
|
|
744
|
-
|
|
745
|
-
// src/tools/binary-detect.ts
|
|
746
|
-
var SNIFF_BYTES = 8192;
|
|
747
|
-
var REPLACEMENT_RATIO_THRESHOLD = 0.01;
|
|
748
|
-
var REPLACEMENT_MIN_COUNT = 5;
|
|
749
|
-
function containsNullByte(text, sniffBytes = SNIFF_BYTES) {
|
|
750
|
-
const sample = text.length > sniffBytes ? text.slice(0, sniffBytes) : text;
|
|
751
|
-
for (let i = 0; i < sample.length; i++) {
|
|
752
|
-
if (sample.charCodeAt(i) === 0)
|
|
753
|
-
return true;
|
|
754
|
-
}
|
|
755
|
-
return false;
|
|
756
|
-
}
|
|
757
|
-
function looksBinary(text, sniffBytes = SNIFF_BYTES) {
|
|
758
|
-
const sample = text.length > sniffBytes ? text.slice(0, sniffBytes) : text;
|
|
759
|
-
if (sample.length === 0)
|
|
760
|
-
return false;
|
|
761
|
-
let replacementCount = 0;
|
|
762
|
-
for (let i = 0; i < sample.length; i++) {
|
|
763
|
-
const code = sample.charCodeAt(i);
|
|
764
|
-
if (code === 0)
|
|
765
|
-
return true;
|
|
766
|
-
if (code === 65533)
|
|
767
|
-
replacementCount++;
|
|
768
|
-
}
|
|
769
|
-
return replacementCount >= REPLACEMENT_MIN_COUNT && replacementCount / sample.length > REPLACEMENT_RATIO_THRESHOLD;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
// src/tools/binary-read.ts
|
|
773
|
-
import { Buffer } from "buffer";
|
|
774
|
-
function imageMediaTypeFor(path) {
|
|
775
|
-
const dot = path.lastIndexOf(".");
|
|
776
|
-
if (dot === -1)
|
|
777
|
-
return void 0;
|
|
778
|
-
const ext = path.slice(dot + 1).toLowerCase();
|
|
779
|
-
switch (ext) {
|
|
780
|
-
case "png":
|
|
781
|
-
return "image/png";
|
|
782
|
-
case "jpg":
|
|
783
|
-
case "jpeg":
|
|
784
|
-
return "image/jpeg";
|
|
785
|
-
case "gif":
|
|
786
|
-
return "image/gif";
|
|
787
|
-
case "webp":
|
|
788
|
-
return "image/webp";
|
|
789
|
-
default:
|
|
790
|
-
return void 0;
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
async function readFileAsBase64(execution, handle, path) {
|
|
794
|
-
if (execution.readFileBinary) {
|
|
795
|
-
const bytes = await execution.readFileBinary(handle, path);
|
|
796
|
-
const b642 = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
|
|
797
|
-
return { base64: b642, byteLength: bytes.byteLength };
|
|
798
|
-
}
|
|
799
|
-
const cmd = `base64 < ${alwaysQuote(path)}`;
|
|
800
|
-
const result = await execution.exec(handle, cmd);
|
|
801
|
-
if (result.exitCode !== 0)
|
|
802
|
-
throw new Error(`base64 read failed: ${result.stderr || `exit ${result.exitCode}`}`);
|
|
803
|
-
const b64 = result.stdout.replace(/\s+/g, "");
|
|
804
|
-
return { base64: b64, byteLength: decodedBase64ByteLength(b64) };
|
|
805
|
-
}
|
|
806
|
-
function decodedBase64ByteLength(b64) {
|
|
807
|
-
if (b64.length === 0)
|
|
808
|
-
return 0;
|
|
809
|
-
let pad = 0;
|
|
810
|
-
if (b64.endsWith("=="))
|
|
811
|
-
pad = 2;
|
|
812
|
-
else if (b64.endsWith("="))
|
|
813
|
-
pad = 1;
|
|
814
|
-
return Math.max(0, b64.length * 3 / 4 - pad);
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
// src/tools/read-file.ts
|
|
818
|
-
var DEFAULT_LINE_LIMIT = 2e3;
|
|
819
|
-
var DEFAULT_BYTE_CAP = 262144;
|
|
820
|
-
var DEFAULT_IMAGE_BYTE_CAP = 5 * 1024 * 1024;
|
|
821
|
-
var readFile = {
|
|
822
|
-
spec: {
|
|
823
|
-
name: "read_file",
|
|
824
|
-
description: "Read a file by path. Returns lines [offset..offset+limit). Default offset=1, limit=2000. Each line is prefixed with its 1-indexed line number followed by a tab (e.g. `42\\tconst foo = bar`); the prefix is metadata, not part of the file. Mirrors Claude Code's `cat -n`-style compact output for token efficiency. A trailing footer explains how to read the rest when truncated. Binary files return a short marker rather than mojibake.",
|
|
825
|
-
inputSchema: {
|
|
826
|
-
type: "object",
|
|
827
|
-
properties: {
|
|
828
|
-
path: { type: "string", description: "Relative file path." },
|
|
829
|
-
offset: { type: "integer", description: "1-indexed line number to start from. Default: 1." },
|
|
830
|
-
limit: { type: "integer", description: "Max lines to return. Default: 2000. Set 0 for unlimited." },
|
|
831
|
-
maxBytes: { type: "integer", description: "Hard byte cap on file content read, regardless of line count. Default: 262144. Set 0 for unlimited. The rendered output may be slightly larger than this cap when `lineNumbers` is on (each line carries a `<N>\\t` prefix)." },
|
|
832
|
-
lineNumbers: { type: "boolean", description: "Prefix each line with its 1-indexed line number. Default: true. Override the agent-wide `behavior.readLineNumbers` for this call." }
|
|
833
|
-
},
|
|
834
|
-
required: ["path"]
|
|
835
|
-
}
|
|
836
|
-
},
|
|
837
|
-
async execute({ path, offset, limit, maxBytes, lineNumbers }, ctx) {
|
|
838
|
-
const imgMedia = imageMediaTypeFor(path);
|
|
839
|
-
if (imgMedia) {
|
|
840
|
-
const sizeCap = maxBytes !== void 0 ? normalizeInteger(maxBytes, DEFAULT_IMAGE_BYTE_CAP) : DEFAULT_IMAGE_BYTE_CAP;
|
|
841
|
-
try {
|
|
842
|
-
const { base64, byteLength } = await readFileAsBase64(ctx.execution, ctx.handle, path);
|
|
843
|
-
if (sizeCap > 0 && byteLength > sizeCap) {
|
|
844
|
-
return `[image too large to inline: ${path}, ${byteLength} bytes (cap ${sizeCap}). Raise maxBytes, or use shell to inspect.]`;
|
|
845
|
-
}
|
|
846
|
-
const content = [
|
|
847
|
-
{ type: "text", text: `Image: ${path} (${byteLength} bytes, ${imgMedia})` },
|
|
848
|
-
{ type: "image", mediaType: imgMedia, data: base64 }
|
|
849
|
-
];
|
|
850
|
-
return content;
|
|
851
|
-
} catch (err) {
|
|
852
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
853
|
-
const hint = await suggestionFor(ctx.execution, ctx.handle, path);
|
|
854
|
-
return `Image read failed: ${path} \u2014 ${msg}.${hint}`;
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
let raw;
|
|
858
|
-
try {
|
|
859
|
-
raw = await ctx.execution.readFile(ctx.handle, path);
|
|
860
|
-
} catch {
|
|
861
|
-
const hint = await suggestionFor(ctx.execution, ctx.handle, path);
|
|
862
|
-
return `File not found: ${path}.${hint}`;
|
|
863
|
-
}
|
|
864
|
-
const totalBytes = Buffer2.byteLength(raw);
|
|
865
|
-
const dedupEnabled = ctx.behavior?.dedupReads !== false;
|
|
866
|
-
const readState = dedupEnabled ? getReadState(ctx.session) : void 0;
|
|
867
|
-
const absKey = `${ctx.handle.cwd}::${path}`;
|
|
868
|
-
const offsetForKey = normalizeInteger(offset, 1);
|
|
869
|
-
const limitForKey = normalizeInteger(limit, DEFAULT_LINE_LIMIT);
|
|
870
|
-
const maxBytesForKey = normalizeInteger(maxBytes, DEFAULT_BYTE_CAP);
|
|
871
|
-
const showLineNumbers = typeof lineNumbers === "boolean" ? lineNumbers : ctx.behavior?.readLineNumbers ?? true;
|
|
872
|
-
const currentHash = readState ? hashContent(raw) : "";
|
|
873
|
-
if (readState) {
|
|
874
|
-
const prior = readState.get(absKey);
|
|
875
|
-
if (prior && prior.contentHash === currentHash && prior.offset === offsetForKey && prior.limit === limitForKey && prior.maxBytes === maxBytesForKey && prior.lineNumbers === showLineNumbers) {
|
|
876
|
-
return `File ${path} unchanged since the previous read in this session \u2014 the prior result is still current.`;
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
if (looksBinary(raw)) {
|
|
880
|
-
return `[binary file: ${path}, ${totalBytes} bytes; use shell with hexdump | xxd | od to inspect]`;
|
|
881
|
-
}
|
|
882
|
-
const offsetN = offsetForKey;
|
|
883
|
-
const limitN = limitForKey;
|
|
884
|
-
const maxBytesN = maxBytesForKey;
|
|
885
|
-
const lines = raw.split("\n");
|
|
886
|
-
const totalLines = lines.length;
|
|
887
|
-
const startIdx = Math.max(0, offsetN - 1);
|
|
888
|
-
const endIdx = limitN > 0 ? Math.min(totalLines, startIdx + limitN) : totalLines;
|
|
889
|
-
let slice = lines.slice(startIdx, endIdx);
|
|
890
|
-
let bytesCut = false;
|
|
891
|
-
if (maxBytesN > 0) {
|
|
892
|
-
const truncatedSlice = [];
|
|
893
|
-
let bytesUsed = 0;
|
|
894
|
-
for (const line of slice) {
|
|
895
|
-
const lineBytes = Buffer2.byteLength(line) + 1;
|
|
896
|
-
if (bytesUsed + lineBytes > maxBytesN && truncatedSlice.length > 0) {
|
|
897
|
-
bytesCut = true;
|
|
898
|
-
break;
|
|
899
|
-
}
|
|
900
|
-
truncatedSlice.push(line);
|
|
901
|
-
bytesUsed += lineBytes;
|
|
902
|
-
if (bytesUsed >= maxBytesN) {
|
|
903
|
-
break;
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
if (truncatedSlice.length < slice.length)
|
|
907
|
-
bytesCut = true;
|
|
908
|
-
slice = truncatedSlice;
|
|
909
|
-
}
|
|
910
|
-
let midLineCut = false;
|
|
911
|
-
if (maxBytesN > 0 && slice.length > 0) {
|
|
912
|
-
const bodyBytes = Buffer2.byteLength(slice.join("\n"));
|
|
913
|
-
if (bodyBytes > maxBytesN) {
|
|
914
|
-
const lastIdx = slice.length - 1;
|
|
915
|
-
const lastLine = slice[lastIdx];
|
|
916
|
-
const otherBytes = lastIdx > 0 ? Buffer2.byteLength(slice.slice(0, lastIdx).join("\n")) + 1 : 0;
|
|
917
|
-
const budgetForLast = Math.max(0, maxBytesN - otherBytes);
|
|
918
|
-
let cut = Math.min(lastLine.length, budgetForLast);
|
|
919
|
-
while (cut > 0 && Buffer2.byteLength(lastLine.slice(0, cut)) > budgetForLast)
|
|
920
|
-
cut--;
|
|
921
|
-
slice[lastIdx] = lastLine.slice(0, cut);
|
|
922
|
-
midLineCut = true;
|
|
923
|
-
bytesCut = true;
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
const linesReturned = slice.length;
|
|
927
|
-
const lastLineRead = startIdx + linesReturned;
|
|
928
|
-
const body = showLineNumbers ? slice.map((line, i) => `${startIdx + i + 1} ${line}`).join("\n") : slice.join("\n");
|
|
929
|
-
if (readState) {
|
|
930
|
-
readState.set(absKey, {
|
|
931
|
-
contentHash: currentHash,
|
|
932
|
-
offset: offsetN,
|
|
933
|
-
limit: limitN,
|
|
934
|
-
maxBytes: maxBytesN,
|
|
935
|
-
lineNumbers: showLineNumbers,
|
|
936
|
-
mtimeMs: Date.now()
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
const linesTruncated = endIdx < totalLines || bytesCut;
|
|
940
|
-
if (!linesTruncated && offsetN === 1)
|
|
941
|
-
return body;
|
|
942
|
-
if (!linesTruncated) {
|
|
943
|
-
return `${body}
|
|
944
|
-
|
|
945
|
-
\u2026read lines ${offsetN}-${lastLineRead} of ${totalLines}.`;
|
|
946
|
-
}
|
|
947
|
-
if (midLineCut) {
|
|
948
|
-
return `${body}
|
|
949
|
-
|
|
950
|
-
\u2026truncated mid-line at line ${lastLineRead} (byte cap ${maxBytesN} reached). File has ${totalLines} lines, ${totalBytes} bytes total. Raise maxBytes to read the full line.`;
|
|
951
|
-
}
|
|
952
|
-
const reason = bytesCut ? `byte cap (${maxBytesN}) reached` : `line limit (${limitN}) reached`;
|
|
953
|
-
return `${body}
|
|
954
|
-
|
|
955
|
-
\u2026truncated at line ${lastLineRead} (${reason}). File has ${totalLines} lines, ${totalBytes} bytes total \u2014 re-read with offset=${lastLineRead + 1} to continue.`;
|
|
956
|
-
}
|
|
957
|
-
};
|
|
958
|
-
function normalizeInteger(value, fallback) {
|
|
959
|
-
if (typeof value !== "number" || !Number.isFinite(value))
|
|
960
|
-
return fallback;
|
|
961
|
-
if (value < 0)
|
|
962
|
-
return fallback;
|
|
963
|
-
return Math.floor(value);
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
// src/tools/shell.ts
|
|
967
|
-
import { Buffer as Buffer3 } from "buffer";
|
|
968
|
-
|
|
969
|
-
// src/tools/shell-semantics.ts
|
|
970
|
-
var DEFAULT_SEMANTIC = (exitCode) => ({
|
|
971
|
-
isError: exitCode !== 0,
|
|
972
|
-
message: exitCode !== 0 ? `Command failed with exit code ${exitCode}` : void 0
|
|
973
|
-
});
|
|
974
|
-
var COMMAND_SEMANTICS = /* @__PURE__ */ new Map([
|
|
975
|
-
// grep / ripgrep: 0 = matches, 1 = no matches, ≥2 = error.
|
|
976
|
-
["grep", (exit) => ({ isError: exit >= 2, message: exit === 1 ? "No matches found" : void 0 })],
|
|
977
|
-
["rg", (exit) => ({ isError: exit >= 2, message: exit === 1 ? "No matches found" : void 0 })],
|
|
978
|
-
// diff: 0 = identical, 1 = differ, ≥2 = error.
|
|
979
|
-
["diff", (exit) => ({ isError: exit >= 2, message: exit === 1 ? "Files differ" : void 0 })],
|
|
980
|
-
// find: 0 = ok, 1 = some dirs inaccessible (warning), ≥2 = error.
|
|
981
|
-
["find", (exit) => ({ isError: exit >= 2, message: exit === 1 ? "Some directories were inaccessible" : void 0 })],
|
|
982
|
-
// test / [: 0 = condition true, 1 = condition false, ≥2 = error.
|
|
983
|
-
["test", (exit) => ({ isError: exit >= 2, message: exit === 1 ? "Condition is false" : void 0 })],
|
|
984
|
-
["[", (exit) => ({ isError: exit >= 2, message: exit === 1 ? "Condition is false" : void 0 })]
|
|
985
|
-
]);
|
|
986
|
-
function interpretShellResult(command, exitCode) {
|
|
987
|
-
const base = extractTrailingCommand(command);
|
|
988
|
-
const semantic = COMMAND_SEMANTICS.get(base) ?? DEFAULT_SEMANTIC;
|
|
989
|
-
return semantic(exitCode);
|
|
990
|
-
}
|
|
991
|
-
function extractTrailingCommand(command) {
|
|
992
|
-
const segments = command.split(/\|\||&&|[;|\n]/);
|
|
993
|
-
const last = segments[segments.length - 1]?.trim() ?? command;
|
|
994
|
-
const tokens = last.split(/\s+/).filter((t) => !/^[A-Z_]\w*=/i.test(t));
|
|
995
|
-
return tokens[0] ?? "";
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
// src/tools/shell.ts
|
|
999
|
-
var DEFAULT_MAX_OUTPUT_BYTES = 32768;
|
|
1000
|
-
var shell = {
|
|
1001
|
-
spec: {
|
|
1002
|
-
name: "shell",
|
|
1003
|
-
description: "Execute a shell command in the project root and return its combined stdout/stderr. Output is tail-priority truncated at 32 KiB by default; errors and exit-code summaries live in the tail. By default each call appends a `(exit N, Nms)` footer and surfaces non-empty stderr in a separate section even on success \u2014 set `metadata: false` to return only stdout. Set maxOutputBytes=0 to disable truncation.",
|
|
1004
|
-
inputSchema: {
|
|
1005
|
-
type: "object",
|
|
1006
|
-
properties: {
|
|
1007
|
-
command: { type: "string", description: "Shell command to run." },
|
|
1008
|
-
timeout: { type: "integer", description: "Per-call timeout in milliseconds." },
|
|
1009
|
-
maxOutputBytes: { type: "integer", description: "Truncate combined stdout+stderr beyond this many bytes. Default: 32768. Set 0 for unlimited." },
|
|
1010
|
-
metadata: { type: "boolean", description: "Append `(exit N, Nms)` footer and surface non-empty stderr on success. Default: true." }
|
|
1011
|
-
},
|
|
1012
|
-
required: ["command"]
|
|
1013
|
-
}
|
|
1014
|
-
},
|
|
1015
|
-
async execute({ command, timeout, maxOutputBytes, metadata }, ctx) {
|
|
1016
|
-
const execOpts = {};
|
|
1017
|
-
if (typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0)
|
|
1018
|
-
execOpts.timeout = Math.max(1, Math.ceil(timeout / 1e3));
|
|
1019
|
-
const cmd = command;
|
|
1020
|
-
const wantMetadata = metadata !== false;
|
|
1021
|
-
const startedAt = Date.now();
|
|
1022
|
-
const result = await ctx.execution.exec(ctx.handle, cmd, execOpts);
|
|
1023
|
-
const durationMs = Date.now() - startedAt;
|
|
1024
|
-
const cap = normalizeCap(maxOutputBytes);
|
|
1025
|
-
const semantic = interpretShellResult(cmd, result.exitCode);
|
|
1026
|
-
if (result.exitCode === 0) {
|
|
1027
|
-
const stdoutTail = truncateTail(result.stdout || "(no output)", cap);
|
|
1028
|
-
if (!wantMetadata)
|
|
1029
|
-
return stdoutTail;
|
|
1030
|
-
const stderrTrimmed = result.stderr.trim();
|
|
1031
|
-
const stderrSection = stderrTrimmed ? `
|
|
1032
|
-
[stderr]
|
|
1033
|
-
${truncateTail(stderrTrimmed, Math.min(cap, 2048))}` : "";
|
|
1034
|
-
return `${stdoutTail}${stderrSection}
|
|
1035
|
-
(exit 0, ${durationMs}ms)`;
|
|
1036
|
-
}
|
|
1037
|
-
if (!semantic.isError) {
|
|
1038
|
-
const body = (result.stdout || result.stderr || "").trim();
|
|
1039
|
-
const tail = truncateTail(body, cap);
|
|
1040
|
-
const semanticFooter = semantic.message ? `
|
|
1041
|
-
(${semantic.message})` : "";
|
|
1042
|
-
const timingFooter = wantMetadata ? `
|
|
1043
|
-
(exit ${result.exitCode}, ${durationMs}ms)` : "";
|
|
1044
|
-
const head = tail.length > 0 ? tail : semantic.message ?? "(no output)";
|
|
1045
|
-
return `${head}${semanticFooter}${timingFooter}`;
|
|
1046
|
-
}
|
|
1047
|
-
const combined = `${result.stdout}
|
|
1048
|
-
${result.stderr}`.trim();
|
|
1049
|
-
const header = wantMetadata ? `Exit code ${result.exitCode} (${durationMs}ms)` : `Exit code ${result.exitCode}`;
|
|
1050
|
-
return `${header}
|
|
1051
|
-
${truncateTail(combined, cap)}`;
|
|
1052
|
-
}
|
|
1053
|
-
};
|
|
1054
|
-
function normalizeCap(value) {
|
|
1055
|
-
if (typeof value !== "number" || !Number.isFinite(value))
|
|
1056
|
-
return DEFAULT_MAX_OUTPUT_BYTES;
|
|
1057
|
-
if (value < 0)
|
|
1058
|
-
return DEFAULT_MAX_OUTPUT_BYTES;
|
|
1059
|
-
return Math.floor(value);
|
|
1060
|
-
}
|
|
1061
|
-
function truncateTail(text, cap) {
|
|
1062
|
-
if (cap === 0)
|
|
1063
|
-
return text;
|
|
1064
|
-
const totalBytes = Buffer3.byteLength(text);
|
|
1065
|
-
if (totalBytes <= cap)
|
|
1066
|
-
return text;
|
|
1067
|
-
let bytes = 0;
|
|
1068
|
-
let charIdx = text.length;
|
|
1069
|
-
while (charIdx > 0) {
|
|
1070
|
-
const ch = text[charIdx - 1];
|
|
1071
|
-
const chBytes = Buffer3.byteLength(ch);
|
|
1072
|
-
if (bytes + chBytes > cap)
|
|
1073
|
-
break;
|
|
1074
|
-
bytes += chBytes;
|
|
1075
|
-
charIdx--;
|
|
1076
|
-
}
|
|
1077
|
-
const tail = text.slice(charIdx);
|
|
1078
|
-
const droppedBytes = totalBytes - Buffer3.byteLength(tail);
|
|
1079
|
-
return `\u2026(${droppedBytes} bytes truncated from head)\u2026
|
|
1080
|
-
${tail}`;
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
// src/tools/skills-read.ts
|
|
1084
|
-
function createSkillsReadTool(options) {
|
|
1085
|
-
const byName = new Map(options.catalog.map((s) => [s.name, s]));
|
|
1086
|
-
return {
|
|
1087
|
-
spec: {
|
|
1088
|
-
name: "skills_read",
|
|
1089
|
-
description: `Read a bundled resource file from an active skill. The skill must have been activated via skills_use first. Path is relative to the skill's directory (e.g. "references/REFERENCE.md", "assets/template.txt").`,
|
|
1090
|
-
inputSchema: {
|
|
1091
|
-
type: "object",
|
|
1092
|
-
properties: {
|
|
1093
|
-
name: {
|
|
1094
|
-
type: "string",
|
|
1095
|
-
enum: options.catalog.map((s) => s.name),
|
|
1096
|
-
description: "The name of the active skill."
|
|
1097
|
-
},
|
|
1098
|
-
path: {
|
|
1099
|
-
type: "string",
|
|
1100
|
-
description: "Path to the resource, relative to the skill root. Cannot escape the skill directory."
|
|
1101
|
-
}
|
|
1102
|
-
},
|
|
1103
|
-
required: ["name", "path"],
|
|
1104
|
-
additionalProperties: false
|
|
1105
|
-
}
|
|
1106
|
-
},
|
|
1107
|
-
async execute(input, ctx) {
|
|
1108
|
-
const skillName = input.name;
|
|
1109
|
-
const relPath = input.path;
|
|
1110
|
-
const skill = byName.get(skillName);
|
|
1111
|
-
if (!skill)
|
|
1112
|
-
return `Error: unknown skill "${skillName}".`;
|
|
1113
|
-
if (!options.state.isActive(skillName))
|
|
1114
|
-
return `Error: skill "${skillName}" is not active. Call skills_use with name: "${skillName}" first.`;
|
|
1115
|
-
if (!skill.baseDir) {
|
|
1116
|
-
return `Error: skill "${skillName}" has no base directory (likely an inline skill without bundled resources); cannot read files.`;
|
|
1117
|
-
}
|
|
1118
|
-
const validated = validateResourcePath(relPath, skill.baseDir);
|
|
1119
|
-
if (!validated.valid)
|
|
1120
|
-
return `Error: ${validated.error}`;
|
|
1121
|
-
let content;
|
|
1122
|
-
try {
|
|
1123
|
-
content = await ctx.execution.readFile(ctx.handle, validated.absolutePath);
|
|
1124
|
-
} catch (err) {
|
|
1125
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1126
|
-
return `Error reading "${relPath}" in skill "${skillName}": ${message}`;
|
|
1127
|
-
}
|
|
1128
|
-
if (containsNullByte(content)) {
|
|
1129
|
-
return JSON.stringify({
|
|
1130
|
-
kind: "binary-unsupported",
|
|
1131
|
-
path: validated.absolutePath,
|
|
1132
|
-
note: "This file appears to be binary. The skills_read tool returns text only; binary files are not delivered through the execution context's text-based readFile API."
|
|
1133
|
-
});
|
|
1134
|
-
}
|
|
1135
|
-
return content;
|
|
1136
|
-
}
|
|
1137
|
-
};
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
// src/tools/skills-run-script.ts
|
|
1141
|
-
var ABS_WINDOWS_RE = /^[a-z]:[\\/]/i;
|
|
1142
|
-
var COLLAPSE_SLASHES_RE = /\/+/g;
|
|
1143
|
-
function createSkillsRunScriptTool(options) {
|
|
1144
|
-
const byName = new Map(options.catalog.map((s) => [s.name, s]));
|
|
1145
|
-
const timeoutMs = options.scriptTimeoutMs ?? 6e4;
|
|
1146
|
-
return {
|
|
1147
|
-
spec: {
|
|
1148
|
-
name: "skills_run_script",
|
|
1149
|
-
description: "Execute a script bundled with an active skill (from its scripts/ directory). The skill must have been activated via skills_use first. Returns stdout, stderr, and the exit code. Honors the script's shebang.",
|
|
1150
|
-
inputSchema: {
|
|
1151
|
-
type: "object",
|
|
1152
|
-
properties: {
|
|
1153
|
-
name: {
|
|
1154
|
-
type: "string",
|
|
1155
|
-
enum: options.catalog.map((s) => s.name),
|
|
1156
|
-
description: "The name of the active skill."
|
|
1157
|
-
},
|
|
1158
|
-
script: {
|
|
1159
|
-
type: "string",
|
|
1160
|
-
description: `Path to the script relative to the skill's scripts/ directory (e.g. "extract.py", "merge.sh").`
|
|
1161
|
-
},
|
|
1162
|
-
args: {
|
|
1163
|
-
type: "array",
|
|
1164
|
-
items: { type: "string" },
|
|
1165
|
-
description: "Optional argv array passed to the script."
|
|
1166
|
-
}
|
|
1167
|
-
},
|
|
1168
|
-
required: ["name", "script"],
|
|
1169
|
-
additionalProperties: false
|
|
1170
|
-
}
|
|
1171
|
-
},
|
|
1172
|
-
async execute(input, ctx) {
|
|
1173
|
-
const skillName = input.name;
|
|
1174
|
-
const scriptRel = input.script;
|
|
1175
|
-
const args = input.args ?? [];
|
|
1176
|
-
const skill = byName.get(skillName);
|
|
1177
|
-
if (!skill)
|
|
1178
|
-
return `Error: unknown skill "${skillName}".`;
|
|
1179
|
-
if (!options.state.isActive(skillName))
|
|
1180
|
-
return `Error: skill "${skillName}" is not active. Call skills_use with name: "${skillName}" first.`;
|
|
1181
|
-
if (!skill.baseDir)
|
|
1182
|
-
return `Error: skill "${skillName}" has no base directory (likely an inline skill); cannot run scripts.`;
|
|
1183
|
-
if (scriptRel.startsWith("/") || ABS_WINDOWS_RE.test(scriptRel))
|
|
1184
|
-
return `Error: Absolute paths are not allowed ("${scriptRel}").`;
|
|
1185
|
-
const joinedPath = `scripts/${scriptRel}`.replace(COLLAPSE_SLASHES_RE, "/");
|
|
1186
|
-
const validated = validateResourcePath(joinedPath, skill.baseDir);
|
|
1187
|
-
if (!validated.valid)
|
|
1188
|
-
return `Error: ${validated.error}`;
|
|
1189
|
-
const cmd = [validated.absolutePath, ...args].map(alwaysQuote).join(" ");
|
|
1190
|
-
try {
|
|
1191
|
-
const result = await ctx.execution.exec(ctx.handle, cmd, {
|
|
1192
|
-
timeout: Math.max(1, Math.round(timeoutMs / 1e3))
|
|
1193
|
-
});
|
|
1194
|
-
return JSON.stringify({
|
|
1195
|
-
exitCode: result.exitCode,
|
|
1196
|
-
stdout: result.stdout,
|
|
1197
|
-
stderr: result.stderr
|
|
1198
|
-
});
|
|
1199
|
-
} catch (err) {
|
|
1200
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1201
|
-
return `Error running script "${scriptRel}" for skill "${skillName}": ${message}`;
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
};
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
// src/tools/skills-use.ts
|
|
1208
|
-
var MAX_RESOURCE_LIST = 50;
|
|
1209
|
-
function buildSkillContentWrapper(skill, body) {
|
|
1210
|
-
const parts = [];
|
|
1211
|
-
parts.push(`<skill_content name="${escapeXml(skill.name)}" spec_version="0.1">`);
|
|
1212
|
-
parts.push(body);
|
|
1213
|
-
if (skill.baseDir) {
|
|
1214
|
-
parts.push("");
|
|
1215
|
-
parts.push(`Skill directory: ${skill.baseDir}`);
|
|
1216
|
-
parts.push("Relative paths resolve against this directory.");
|
|
1217
|
-
}
|
|
1218
|
-
if (skill.resources?.length) {
|
|
1219
|
-
parts.push("");
|
|
1220
|
-
parts.push("<skill_resources>");
|
|
1221
|
-
const shown = skill.resources.slice(0, MAX_RESOURCE_LIST);
|
|
1222
|
-
for (const res of shown) {
|
|
1223
|
-
parts.push(` <file type="${res.type}">${escapeXml(res.path)}</file>`);
|
|
1224
|
-
}
|
|
1225
|
-
if (skill.resources.length > MAX_RESOURCE_LIST) {
|
|
1226
|
-
parts.push(` <!-- \u2026(${skill.resources.length - MAX_RESOURCE_LIST} more) -->`);
|
|
1227
|
-
}
|
|
1228
|
-
parts.push("</skill_resources>");
|
|
1229
|
-
}
|
|
1230
|
-
if (skill.compatibility) {
|
|
1231
|
-
parts.push("");
|
|
1232
|
-
parts.push(`Compatibility: ${skill.compatibility}`);
|
|
1233
|
-
}
|
|
1234
|
-
if (skill.allowedTools?.length) {
|
|
1235
|
-
parts.push(`Allowed tools: ${skill.allowedTools.join(" ")}`);
|
|
1236
|
-
}
|
|
1237
|
-
parts.push("</skill_content>");
|
|
1238
|
-
return parts.join("\n");
|
|
1239
|
-
}
|
|
1240
|
-
function createSkillsUseTool(options) {
|
|
1241
|
-
const byName = new Map(options.catalog.map((s) => [s.name, s]));
|
|
1242
|
-
const interpolatedBodyCache = /* @__PURE__ */ new Map();
|
|
1243
|
-
return {
|
|
1244
|
-
spec: {
|
|
1245
|
-
name: "skills_use",
|
|
1246
|
-
description: "Activate a specialized skill and load its full instructions. Call this when a task matches a skill's description from the catalog. After calling, follow the returned instructions; use skills_read to load referenced files and skills_run_script to execute bundled scripts.",
|
|
1247
|
-
inputSchema: {
|
|
1248
|
-
type: "object",
|
|
1249
|
-
properties: {
|
|
1250
|
-
name: {
|
|
1251
|
-
type: "string",
|
|
1252
|
-
enum: options.catalog.map((s) => s.name),
|
|
1253
|
-
description: "The name of the skill to activate (must be in the available skills catalog)."
|
|
1254
|
-
}
|
|
1255
|
-
},
|
|
1256
|
-
required: ["name"],
|
|
1257
|
-
additionalProperties: false
|
|
1258
|
-
}
|
|
1259
|
-
},
|
|
1260
|
-
async execute(input, ctx) {
|
|
1261
|
-
const skillName = input.name;
|
|
1262
|
-
const skill = byName.get(skillName);
|
|
1263
|
-
if (!skill) {
|
|
1264
|
-
const available = [...byName.keys()].join(", ") || "<none>";
|
|
1265
|
-
return `Error: unknown skill "${skillName}". Available skills: ${available}.`;
|
|
1266
|
-
}
|
|
1267
|
-
const wasActive = options.state.isActive(skillName);
|
|
1268
|
-
if (!wasActive) {
|
|
1269
|
-
const outcome = options.state.activate(skill, "model");
|
|
1270
|
-
if (outcome === "cap-reached") {
|
|
1271
|
-
const activeNames = options.state.active().map((a) => a.skill.name).join(", ");
|
|
1272
|
-
return `Error: cannot activate "${skillName}" \u2014 the maxActive skill cap has been reached. Currently active: ${activeNames}. Deactivate an existing skill first.`;
|
|
1273
|
-
}
|
|
1274
|
-
await options.hooks.callHook("skills:activate", { skill, via: "model" });
|
|
1275
|
-
}
|
|
1276
|
-
let body = interpolatedBodyCache.get(skillName);
|
|
1277
|
-
if (body === void 0) {
|
|
1278
|
-
body = skill.instructions.includes("!`") ? await interpolateShellCommands(skill.instructions, ctx.execution, ctx.handle) : skill.instructions;
|
|
1279
|
-
interpolatedBodyCache.set(skillName, body);
|
|
1280
|
-
}
|
|
1281
|
-
return buildSkillContentWrapper(skill, body);
|
|
1282
|
-
}
|
|
1283
|
-
};
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
// src/agent.ts
|
|
1287
|
-
import { createHooks } from "hookable";
|
|
1288
|
-
|
|
1289
|
-
// src/aliasing.ts
|
|
1290
|
-
function buildAliasMaps(aliases, canonicalNames) {
|
|
1291
|
-
const aliasByCanonical = /* @__PURE__ */ new Map();
|
|
1292
|
-
const canonicalByAlias = /* @__PURE__ */ new Map();
|
|
1293
|
-
if (!aliases) {
|
|
1294
|
-
return { aliasByCanonical, canonicalByAlias };
|
|
1295
|
-
}
|
|
1296
|
-
const canonicalSet = new Set(canonicalNames);
|
|
1297
|
-
function isRemappedAway(canonical) {
|
|
1298
|
-
const mapped = aliases[canonical];
|
|
1299
|
-
return typeof mapped === "string" && mapped.length > 0 && mapped !== canonical;
|
|
1300
|
-
}
|
|
1301
|
-
for (const [canonical, alias] of Object.entries(aliases)) {
|
|
1302
|
-
if (typeof alias !== "string" || alias.length === 0)
|
|
1303
|
-
throw new Error(`Tool alias for "${canonical}" must be a non-empty string`);
|
|
1304
|
-
if (alias === canonical)
|
|
1305
|
-
continue;
|
|
1306
|
-
if (!canonicalSet.has(canonical))
|
|
1307
|
-
continue;
|
|
1308
|
-
if (canonicalSet.has(alias) && !isRemappedAway(alias)) {
|
|
1309
|
-
throw new Error(`Tool alias "${canonical}" -> "${alias}" collides with an existing canonical tool name`);
|
|
1310
|
-
}
|
|
1311
|
-
const existingCanonical = canonicalByAlias.get(alias);
|
|
1312
|
-
if (existingCanonical && existingCanonical !== canonical) {
|
|
1313
|
-
throw new Error(
|
|
1314
|
-
`Tool alias collision: both "${existingCanonical}" and "${canonical}" map to alias "${alias}"`
|
|
1315
|
-
);
|
|
1316
|
-
}
|
|
1317
|
-
aliasByCanonical.set(canonical, alias);
|
|
1318
|
-
canonicalByAlias.set(alias, canonical);
|
|
1319
|
-
}
|
|
1320
|
-
return { aliasByCanonical, canonicalByAlias };
|
|
1321
|
-
}
|
|
1322
|
-
function toWireName(canonical, maps) {
|
|
1323
|
-
return maps.aliasByCanonical.get(canonical) ?? canonical;
|
|
1324
|
-
}
|
|
1325
|
-
function toCanonicalName(wire, maps) {
|
|
1326
|
-
return maps.canonicalByAlias.get(wire) ?? wire;
|
|
1327
|
-
}
|
|
1328
|
-
function rewriteContentToWire(content, maps) {
|
|
1329
|
-
if (maps.aliasByCanonical.size === 0)
|
|
1330
|
-
return content;
|
|
1331
|
-
return content.map((block) => {
|
|
1332
|
-
if (block.type !== "tool_call")
|
|
1333
|
-
return block;
|
|
1334
|
-
const wire = maps.aliasByCanonical.get(block.name);
|
|
1335
|
-
if (!wire || wire === block.name)
|
|
1336
|
-
return block;
|
|
1337
|
-
return { ...block, name: wire };
|
|
1338
|
-
});
|
|
1339
|
-
}
|
|
1340
|
-
function rewriteContentToCanonical(content, maps) {
|
|
1341
|
-
if (maps.canonicalByAlias.size === 0)
|
|
1342
|
-
return content;
|
|
1343
|
-
return content.map((block) => {
|
|
1344
|
-
if (block.type !== "tool_call")
|
|
1345
|
-
return block;
|
|
1346
|
-
const canonical = maps.canonicalByAlias.get(block.name);
|
|
1347
|
-
if (!canonical || canonical === block.name)
|
|
1348
|
-
return block;
|
|
1349
|
-
return { ...block, name: canonical };
|
|
1350
|
-
});
|
|
1351
|
-
}
|
|
1352
|
-
function rewriteMessagesToWire(messages, maps) {
|
|
1353
|
-
if (maps.aliasByCanonical.size === 0)
|
|
1354
|
-
return messages;
|
|
1355
|
-
return messages.map((msg) => ({ ...msg, content: rewriteContentToWire(msg.content, maps) }));
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
// src/dedup-tools.ts
|
|
1359
|
-
function installDedupToolsGate(hooks, getDedupTools, getSession) {
|
|
1360
|
-
const pending = /* @__PURE__ */ new Map();
|
|
1361
|
-
function pendingKey(callId, name) {
|
|
1362
|
-
return `${callId}::${name}`;
|
|
1363
|
-
}
|
|
1364
|
-
function gateHandler(ctx) {
|
|
1365
|
-
if (ctx.block || ctx.result !== void 0)
|
|
1366
|
-
return;
|
|
1367
|
-
const dedupTools = getDedupTools();
|
|
1368
|
-
const hasher = dedupTools?.[ctx.name];
|
|
1369
|
-
if (!hasher)
|
|
1370
|
-
return;
|
|
1371
|
-
const session = getSession();
|
|
1372
|
-
const state = getToolDedupState(session);
|
|
1373
|
-
if (!state)
|
|
1374
|
-
return;
|
|
1375
|
-
let hash;
|
|
1376
|
-
try {
|
|
1377
|
-
hash = hasher(ctx.input);
|
|
1378
|
-
} catch {
|
|
1379
|
-
return;
|
|
1380
|
-
}
|
|
1381
|
-
if (typeof hash !== "string" || hash.length === 0)
|
|
1382
|
-
return;
|
|
1383
|
-
const prior = state.get(ctx.name);
|
|
1384
|
-
if (prior && prior.hash === hash) {
|
|
1385
|
-
ctx.result = prior.result;
|
|
1386
|
-
return;
|
|
1387
|
-
}
|
|
1388
|
-
pending.set(pendingKey(ctx.callId, ctx.name), hash);
|
|
1389
|
-
}
|
|
1390
|
-
function afterHandler(ctx) {
|
|
1391
|
-
const key = pendingKey(ctx.callId, ctx.name);
|
|
1392
|
-
const hash = pending.get(key);
|
|
1393
|
-
if (hash === void 0)
|
|
1394
|
-
return;
|
|
1395
|
-
pending.delete(key);
|
|
1396
|
-
const session = getSession();
|
|
1397
|
-
const state = getToolDedupState(session);
|
|
1398
|
-
if (!state)
|
|
1399
|
-
return;
|
|
1400
|
-
state.set(ctx.name, { hash, result: ctx.result });
|
|
1401
|
-
}
|
|
1402
|
-
const unregisterGate = hooks.hook("tool:gate", gateHandler);
|
|
1403
|
-
const unregisterAfter = hooks.hook("tool:after", afterHandler);
|
|
1404
|
-
return function uninstall() {
|
|
1405
|
-
unregisterGate();
|
|
1406
|
-
unregisterAfter();
|
|
1407
|
-
pending.clear();
|
|
1408
|
-
};
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
// src/tools/validation.ts
|
|
1412
|
-
var TRUE_STRINGS = /* @__PURE__ */ new Set(["true", "True", "TRUE", "1", "yes", "Yes", "YES"]);
|
|
1413
|
-
var FALSE_STRINGS = /* @__PURE__ */ new Set(["false", "False", "FALSE", "0", "no", "No", "NO"]);
|
|
1414
|
-
function validateToolArgs(input, schema) {
|
|
1415
|
-
const required = schema.required ?? [];
|
|
1416
|
-
const properties = schema.properties ?? {};
|
|
1417
|
-
for (const field of required) {
|
|
1418
|
-
if (!(field in input) || input[field] === void 0 || input[field] === null) {
|
|
1419
|
-
return { valid: false, error: `Missing required field: ${field}` };
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
let coerced;
|
|
1423
|
-
const coercions = [];
|
|
1424
|
-
for (const [key, value] of Object.entries(input)) {
|
|
1425
|
-
const propSchema = properties[key];
|
|
1426
|
-
if (!propSchema?.type)
|
|
1427
|
-
continue;
|
|
1428
|
-
if (value === void 0 || value === null)
|
|
1429
|
-
continue;
|
|
1430
|
-
const outcome = coerceValue(value, propSchema);
|
|
1431
|
-
if (outcome.error) {
|
|
1432
|
-
return { valid: false, error: `Field "${key}": ${outcome.error}` };
|
|
1433
|
-
}
|
|
1434
|
-
if (outcome.changed) {
|
|
1435
|
-
if (!coerced)
|
|
1436
|
-
coerced = { ...input };
|
|
1437
|
-
coerced[key] = outcome.value;
|
|
1438
|
-
coercions.push(key);
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
return {
|
|
1442
|
-
valid: true,
|
|
1443
|
-
coercedInput: coerced ?? input,
|
|
1444
|
-
coercions
|
|
1445
|
-
};
|
|
1446
|
-
}
|
|
1447
|
-
function coerceValue(value, schema) {
|
|
1448
|
-
const declaredTypes = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
1449
|
-
for (const t of declaredTypes) {
|
|
1450
|
-
if (matchesType(value, t)) {
|
|
1451
|
-
if (schema.enum && !schema.enum.includes(value)) {
|
|
1452
|
-
return {
|
|
1453
|
-
value,
|
|
1454
|
-
changed: false,
|
|
1455
|
-
error: `must be one of ${JSON.stringify(schema.enum)}, got ${formatValue(value)}`
|
|
1456
|
-
};
|
|
1457
|
-
}
|
|
1458
|
-
return { value, changed: false };
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
for (const t of declaredTypes) {
|
|
1462
|
-
const coerced = tryCoerce(value, t);
|
|
1463
|
-
if (coerced.ok) {
|
|
1464
|
-
if (schema.enum && !schema.enum.includes(coerced.value)) {
|
|
1465
|
-
return {
|
|
1466
|
-
value,
|
|
1467
|
-
changed: false,
|
|
1468
|
-
error: `must be one of ${JSON.stringify(schema.enum)}, got ${formatValue(coerced.value)}`
|
|
1469
|
-
};
|
|
1470
|
-
}
|
|
1471
|
-
return { value: coerced.value, changed: true };
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
const expected = declaredTypes.join(" | ");
|
|
1475
|
-
return {
|
|
1476
|
-
value,
|
|
1477
|
-
changed: false,
|
|
1478
|
-
error: `expected ${expected}, got ${jsonType(value)} ${formatValue(value)}`
|
|
1479
|
-
};
|
|
1480
|
-
}
|
|
1481
|
-
function matchesType(value, type) {
|
|
1482
|
-
switch (type) {
|
|
1483
|
-
case "string":
|
|
1484
|
-
return typeof value === "string";
|
|
1485
|
-
case "number":
|
|
1486
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
1487
|
-
case "integer":
|
|
1488
|
-
return typeof value === "number" && Number.isInteger(value);
|
|
1489
|
-
case "boolean":
|
|
1490
|
-
return typeof value === "boolean";
|
|
1491
|
-
case "array":
|
|
1492
|
-
return Array.isArray(value);
|
|
1493
|
-
case "object":
|
|
1494
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1495
|
-
case "null":
|
|
1496
|
-
return value === null;
|
|
1497
|
-
default:
|
|
1498
|
-
return true;
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
function tryCoerce(value, type) {
|
|
1502
|
-
if (typeof value === "string") {
|
|
1503
|
-
if (type === "boolean") {
|
|
1504
|
-
const trimmed = value.trim();
|
|
1505
|
-
if (TRUE_STRINGS.has(trimmed))
|
|
1506
|
-
return { ok: true, value: true };
|
|
1507
|
-
if (FALSE_STRINGS.has(trimmed))
|
|
1508
|
-
return { ok: true, value: false };
|
|
1509
|
-
return { ok: false };
|
|
1510
|
-
}
|
|
1511
|
-
if (type === "number") {
|
|
1512
|
-
const n = Number(value.trim());
|
|
1513
|
-
return Number.isFinite(n) ? { ok: true, value: n } : { ok: false };
|
|
1514
|
-
}
|
|
1515
|
-
if (type === "integer") {
|
|
1516
|
-
const n = Number(value.trim());
|
|
1517
|
-
return Number.isInteger(n) ? { ok: true, value: n } : { ok: false };
|
|
1518
|
-
}
|
|
1519
|
-
if (type === "array" || type === "object") {
|
|
1520
|
-
try {
|
|
1521
|
-
const parsed = JSON.parse(value);
|
|
1522
|
-
if (type === "array" && Array.isArray(parsed))
|
|
1523
|
-
return { ok: true, value: parsed };
|
|
1524
|
-
if (type === "object" && parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
|
|
1525
|
-
return { ok: true, value: parsed };
|
|
1526
|
-
return { ok: false };
|
|
1527
|
-
} catch {
|
|
1528
|
-
return { ok: false };
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
if (type === "null") {
|
|
1532
|
-
return value === "" || value === "null" ? { ok: true, value: null } : { ok: false };
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1536
|
-
if (type === "string")
|
|
1537
|
-
return { ok: true, value: String(value) };
|
|
1538
|
-
if (type === "integer" && Number.isInteger(value))
|
|
1539
|
-
return { ok: true, value };
|
|
1540
|
-
}
|
|
1541
|
-
if (typeof value === "boolean" && type === "string")
|
|
1542
|
-
return { ok: true, value: String(value) };
|
|
1543
|
-
return { ok: false };
|
|
1544
|
-
}
|
|
1545
|
-
function jsonType(value) {
|
|
1546
|
-
if (value === null)
|
|
1547
|
-
return "null";
|
|
1548
|
-
if (Array.isArray(value))
|
|
1549
|
-
return "array";
|
|
1550
|
-
return typeof value;
|
|
1551
|
-
}
|
|
1552
|
-
function formatValue(value) {
|
|
1553
|
-
let s;
|
|
1554
|
-
try {
|
|
1555
|
-
s = JSON.stringify(value);
|
|
1556
|
-
} catch {
|
|
1557
|
-
s = String(value);
|
|
1558
|
-
}
|
|
1559
|
-
if (s === void 0)
|
|
1560
|
-
s = String(value);
|
|
1561
|
-
return s.length > 80 ? `${s.slice(0, 77)}...` : s;
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
// src/loop.ts
|
|
1565
|
-
var IMAGE_OMITTED_MARKER = "[image omitted \u2014 model does not support vision]";
|
|
1566
|
-
function applyThinkingDecay(baseBudget, decay, turn) {
|
|
1567
|
-
if (typeof baseBudget !== "number" || baseBudget <= 0)
|
|
1568
|
-
return baseBudget;
|
|
1569
|
-
if (!decay)
|
|
1570
|
-
return baseBudget;
|
|
1571
|
-
let raw;
|
|
1572
|
-
if (typeof decay === "function") {
|
|
1573
|
-
raw = decay(turn, baseBudget);
|
|
1574
|
-
} else {
|
|
1575
|
-
if (turn <= decay.afterTurn)
|
|
1576
|
-
return baseBudget;
|
|
1577
|
-
const k = turn - decay.afterTurn;
|
|
1578
|
-
raw = Math.max(decay.floor, baseBudget * decay.factor ** k);
|
|
1579
|
-
}
|
|
1580
|
-
if (Number.isNaN(raw) || raw <= 0)
|
|
1581
|
-
return 0;
|
|
1582
|
-
return Math.round(Math.min(baseBudget, raw));
|
|
1583
|
-
}
|
|
1584
|
-
function turnsToMessages(turns) {
|
|
1585
|
-
return turns.filter((t) => t.role !== "system").map((t) => ({ role: t.role, content: t.content }));
|
|
1586
|
-
}
|
|
1587
|
-
var COMPACTION_STUB = "[\u2026elided by client-side tail compaction; ask the user or re-run the tool to retrieve.]";
|
|
1588
|
-
function applyTailCompaction(messages, threshold, keepTurns) {
|
|
1589
|
-
if (messages.length === 0)
|
|
1590
|
-
return messages;
|
|
1591
|
-
let totalBytes = 0;
|
|
1592
|
-
for (const msg of messages) {
|
|
1593
|
-
for (const block of msg.content) {
|
|
1594
|
-
if (block.type === "tool_result")
|
|
1595
|
-
totalBytes += toolOutputByteLength(block.output);
|
|
1596
|
-
}
|
|
1597
|
-
}
|
|
1598
|
-
if (totalBytes <= threshold)
|
|
1599
|
-
return messages;
|
|
1600
|
-
const keep = Math.max(0, keepTurns);
|
|
1601
|
-
const cutoff = messages.length - keep;
|
|
1602
|
-
if (cutoff <= 0)
|
|
1603
|
-
return messages;
|
|
1604
|
-
let changed = false;
|
|
1605
|
-
const out = messages.slice();
|
|
1606
|
-
for (let i = 0; i < cutoff; i++) {
|
|
1607
|
-
const msg = out[i];
|
|
1608
|
-
let msgChanged = false;
|
|
1609
|
-
const newContent = msg.content.map((block) => {
|
|
1610
|
-
if (block.type !== "tool_result")
|
|
1611
|
-
return block;
|
|
1612
|
-
const existingBytes = toolOutputByteLength(block.output);
|
|
1613
|
-
if (existingBytes <= COMPACTION_STUB.length)
|
|
1614
|
-
return block;
|
|
1615
|
-
msgChanged = true;
|
|
1616
|
-
changed = true;
|
|
1617
|
-
return { ...block, output: COMPACTION_STUB };
|
|
1618
|
-
});
|
|
1619
|
-
if (msgChanged)
|
|
1620
|
-
out[i] = { ...msg, content: newContent };
|
|
1621
|
-
}
|
|
1622
|
-
return changed ? out : messages;
|
|
1623
|
-
}
|
|
1624
|
-
var STALE_READ_STUB = "[\u2026elided: file edited later in this run; re-read if still needed.]";
|
|
1625
|
-
function applyStaleReadElision(messages) {
|
|
1626
|
-
if (messages.length === 0)
|
|
1627
|
-
return messages;
|
|
1628
|
-
const resultByCallId = /* @__PURE__ */ new Map();
|
|
1629
|
-
for (const msg of messages) {
|
|
1630
|
-
for (const block of msg.content) {
|
|
1631
|
-
if (block.type === "tool_result" && typeof block.output === "string")
|
|
1632
|
-
resultByCallId.set(block.callId, block.output);
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
const maxMutationIdxByPath = /* @__PURE__ */ new Map();
|
|
1636
|
-
const readCallInfo = /* @__PURE__ */ new Map();
|
|
1637
|
-
for (let i = 0; i < messages.length; i++) {
|
|
1638
|
-
for (const block of messages[i].content) {
|
|
1639
|
-
if (block.type !== "tool_call")
|
|
1640
|
-
continue;
|
|
1641
|
-
const path = block.input.path;
|
|
1642
|
-
if (typeof path !== "string")
|
|
1643
|
-
continue;
|
|
1644
|
-
if (block.name === "read_file") {
|
|
1645
|
-
readCallInfo.set(block.id, { path, msgIdx: i });
|
|
1646
|
-
continue;
|
|
1647
|
-
}
|
|
1648
|
-
const isEdit = block.name === "edit" || block.name === "multi_edit";
|
|
1649
|
-
const isWrite = block.name === "write_file";
|
|
1650
|
-
if (!isEdit && !isWrite)
|
|
1651
|
-
continue;
|
|
1652
|
-
const result = resultByCallId.get(block.id);
|
|
1653
|
-
if (typeof result !== "string")
|
|
1654
|
-
continue;
|
|
1655
|
-
const succeeded = isEdit ? result.startsWith("Edited ") : result.startsWith("Created ") || result.startsWith("Updated ");
|
|
1656
|
-
if (!succeeded)
|
|
1657
|
-
continue;
|
|
1658
|
-
const prior = maxMutationIdxByPath.get(path);
|
|
1659
|
-
if (prior === void 0 || i > prior)
|
|
1660
|
-
maxMutationIdxByPath.set(path, i);
|
|
1661
|
-
}
|
|
1662
|
-
}
|
|
1663
|
-
if (maxMutationIdxByPath.size === 0)
|
|
1664
|
-
return messages;
|
|
1665
|
-
const staleCallIds = /* @__PURE__ */ new Set();
|
|
1666
|
-
for (const [callId, info] of readCallInfo) {
|
|
1667
|
-
const lastMutationIdx = maxMutationIdxByPath.get(info.path);
|
|
1668
|
-
if (typeof lastMutationIdx === "number" && info.msgIdx < lastMutationIdx)
|
|
1669
|
-
staleCallIds.add(callId);
|
|
1670
|
-
}
|
|
1671
|
-
if (staleCallIds.size === 0)
|
|
1672
|
-
return messages;
|
|
1673
|
-
let changed = false;
|
|
1674
|
-
const out = messages.slice();
|
|
1675
|
-
for (let i = 0; i < out.length; i++) {
|
|
1676
|
-
const msg = out[i];
|
|
1677
|
-
let msgChanged = false;
|
|
1678
|
-
const newContent = msg.content.map((block) => {
|
|
1679
|
-
if (block.type !== "tool_result" || !staleCallIds.has(block.callId))
|
|
1680
|
-
return block;
|
|
1681
|
-
if (block.output === STALE_READ_STUB)
|
|
1682
|
-
return block;
|
|
1683
|
-
msgChanged = true;
|
|
1684
|
-
changed = true;
|
|
1685
|
-
return { ...block, output: STALE_READ_STUB };
|
|
1686
|
-
});
|
|
1687
|
-
if (msgChanged)
|
|
1688
|
-
out[i] = { ...msg, content: newContent };
|
|
1689
|
-
}
|
|
1690
|
-
return changed ? out : messages;
|
|
1691
|
-
}
|
|
1692
|
-
function sanitizeStoredToolResults(provider, messages) {
|
|
1693
|
-
if (provider.meta.capabilities?.vision !== false)
|
|
1694
|
-
return messages;
|
|
1695
|
-
return messages.map((msg) => {
|
|
1696
|
-
let changed = false;
|
|
1697
|
-
const newContent = msg.content.map((block) => {
|
|
1698
|
-
if (block.type !== "tool_result" || typeof block.output === "string")
|
|
1699
|
-
return block;
|
|
1700
|
-
changed = true;
|
|
1701
|
-
const flattened = block.output.map((b) => b.type === "image" ? IMAGE_OMITTED_MARKER : b.text).join("\n");
|
|
1702
|
-
return { ...block, output: flattened };
|
|
1703
|
-
});
|
|
1704
|
-
return changed ? { ...msg, content: newContent } : msg;
|
|
1705
|
-
});
|
|
1706
|
-
}
|
|
1707
|
-
async function runLoop(ctx) {
|
|
1708
|
-
let totalIn = 0;
|
|
1709
|
-
let totalOut = 0;
|
|
1710
|
-
let totalCacheRead = 0;
|
|
1711
|
-
let totalCacheCreation = 0;
|
|
1712
|
-
const turnUsages = [];
|
|
1713
|
-
const startTime = Date.now();
|
|
1714
|
-
const maxTurns = ctx.maxTurns ?? Number.POSITIVE_INFINITY;
|
|
1715
|
-
let turnsCompleted = 0;
|
|
1716
|
-
const ttft = { mark: void 0 };
|
|
1717
|
-
const markTtft = () => {
|
|
1718
|
-
if (ttft.mark === void 0)
|
|
1719
|
-
ttft.mark = Date.now() - ctx.runStartMs;
|
|
1720
|
-
};
|
|
1721
|
-
const unregisterTtftText = ctx.hooks.hook("stream:text", markTtft);
|
|
1722
|
-
const unregisterTtftThinking = ctx.hooks.hook("stream:thinking", markTtft);
|
|
1723
|
-
const unregisterTtftTool = ctx.hooks.hook("tool:before", markTtft);
|
|
1724
|
-
try {
|
|
1725
|
-
for (let turn = 0; turn < maxTurns; turn++) {
|
|
1726
|
-
if (ctx.signal.aborted) {
|
|
1727
|
-
await ctx.hooks.callHook("agent:abort", {});
|
|
1728
|
-
break;
|
|
1729
|
-
}
|
|
1730
|
-
const result = await executeTurn(ctx, turn);
|
|
1731
|
-
turnsCompleted = turn + 1;
|
|
1732
|
-
totalIn += result.usage.input;
|
|
1733
|
-
totalOut += result.usage.output;
|
|
1734
|
-
totalCacheRead += result.usage.cacheRead ?? 0;
|
|
1735
|
-
totalCacheCreation += result.usage.cacheCreation ?? 0;
|
|
1736
|
-
turnUsages.push(result.usage);
|
|
1737
|
-
await ctx.hooks.callHook("usage", { turn, turnId: result.turnId, usage: result.usage, totalIn, totalOut });
|
|
1738
|
-
if (ctx.signal.aborted) {
|
|
1739
|
-
await ctx.hooks.callHook("agent:abort", {});
|
|
1740
|
-
break;
|
|
1741
|
-
}
|
|
1742
|
-
if (ctx.steeringQueue.length > 0) {
|
|
1743
|
-
const steerMsg = ctx.steeringQueue.shift();
|
|
1744
|
-
await ctx.hooks.callHook("steer:inject", { message: steerMsg });
|
|
1745
|
-
const steerUserMsg = ctx.provider.userMessage(steerMsg);
|
|
1746
|
-
ctx.turns.push({
|
|
1747
|
-
id: await ctx.generateTurnId(),
|
|
1748
|
-
runId: ctx.runId,
|
|
1749
|
-
role: steerUserMsg.role,
|
|
1750
|
-
content: steerUserMsg.content,
|
|
1751
|
-
createdAt: Date.now()
|
|
1752
|
-
});
|
|
1753
|
-
continue;
|
|
1754
|
-
}
|
|
1755
|
-
if (result.ended) {
|
|
1756
|
-
if (ctx.followUpQueue.length > 0) {
|
|
1757
|
-
const followUp = ctx.followUpQueue.shift();
|
|
1758
|
-
await ctx.hooks.callHook("steer:inject", { message: followUp });
|
|
1759
|
-
const followUpMsg = ctx.provider.userMessage(followUp);
|
|
1760
|
-
ctx.turns.push({
|
|
1761
|
-
id: await ctx.generateTurnId(),
|
|
1762
|
-
runId: ctx.runId,
|
|
1763
|
-
role: followUpMsg.role,
|
|
1764
|
-
content: followUpMsg.content,
|
|
1765
|
-
createdAt: Date.now()
|
|
1766
|
-
});
|
|
1767
|
-
continue;
|
|
1768
|
-
}
|
|
1769
|
-
return {
|
|
1770
|
-
totalIn,
|
|
1771
|
-
totalOut,
|
|
1772
|
-
totalCacheRead,
|
|
1773
|
-
totalCacheCreation,
|
|
1774
|
-
turns: turn + 1,
|
|
1775
|
-
elapsed: Date.now() - startTime,
|
|
1776
|
-
turnUsage: turnUsages,
|
|
1777
|
-
output: result.output,
|
|
1778
|
-
...ttft.mark !== void 0 ? { timeTillFirstTokenMs: ttft.mark } : {}
|
|
1779
|
-
};
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
return {
|
|
1783
|
-
totalIn,
|
|
1784
|
-
totalOut,
|
|
1785
|
-
totalCacheRead,
|
|
1786
|
-
totalCacheCreation,
|
|
1787
|
-
turns: turnsCompleted,
|
|
1788
|
-
elapsed: Date.now() - startTime,
|
|
1789
|
-
turnUsage: turnUsages,
|
|
1790
|
-
...ttft.mark !== void 0 ? { timeTillFirstTokenMs: ttft.mark } : {}
|
|
1791
|
-
};
|
|
1792
|
-
} finally {
|
|
1793
|
-
unregisterTtftText();
|
|
1794
|
-
unregisterTtftThinking();
|
|
1795
|
-
unregisterTtftTool();
|
|
1796
|
-
}
|
|
1797
|
-
}
|
|
1798
|
-
function wrapProviderError(err, ctx) {
|
|
1799
|
-
if (ctx.signal.aborted || err instanceof Error && err.name === "AbortError")
|
|
1800
|
-
return new AgentAbortedError("Agent run aborted", { cause: err });
|
|
1801
|
-
const classification = ctx.provider.classifyError?.(err);
|
|
1802
|
-
if (classification)
|
|
1803
|
-
return toTypedError(classification, ctx.provider.name, err);
|
|
1804
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1805
|
-
return new AgentProviderError(message, { provider: ctx.provider.name, cause: err });
|
|
1806
|
-
}
|
|
1807
|
-
async function executeTurn(ctx, turn) {
|
|
1808
|
-
const turnId = await ctx.generateTurnId();
|
|
1809
|
-
let canonicalMessages = turnsToMessages(ctx.turns);
|
|
1810
|
-
if (ctx.elideStaleReads === true)
|
|
1811
|
-
canonicalMessages = applyStaleReadElision(canonicalMessages);
|
|
1812
|
-
const wireMessages = rewriteMessagesToWire(canonicalMessages, ctx.aliasMaps);
|
|
1813
|
-
let sanitizedMessages = sanitizeStoredToolResults(ctx.provider, wireMessages);
|
|
1814
|
-
if (ctx.compactStrategy === "tail") {
|
|
1815
|
-
const threshold = typeof ctx.compactThreshold === "number" && ctx.compactThreshold > 0 ? ctx.compactThreshold : 131072;
|
|
1816
|
-
const keep = typeof ctx.compactKeepTurns === "number" && ctx.compactKeepTurns >= 0 ? ctx.compactKeepTurns : 4;
|
|
1817
|
-
sanitizedMessages = applyTailCompaction(sanitizedMessages, threshold, keep);
|
|
1818
|
-
}
|
|
1819
|
-
const effectiveThinkingBudget = applyThinkingDecay(ctx.thinkingBudget, ctx.thinkingDecay, turn);
|
|
1820
|
-
const formattedTools = ctx.rebuildFormattedTools ? ctx.rebuildFormattedTools() : ctx.formattedTools;
|
|
1821
|
-
const streamOptions = {
|
|
1822
|
-
model: ctx.model,
|
|
1823
|
-
system: ctx.system,
|
|
1824
|
-
tools: formattedTools,
|
|
1825
|
-
messages: sanitizedMessages,
|
|
1826
|
-
maxTokens: ctx.maxTokens ?? 16384,
|
|
1827
|
-
thinking: ctx.thinking,
|
|
1828
|
-
thinkingBudget: effectiveThinkingBudget,
|
|
1829
|
-
cache: ctx.cache ?? true,
|
|
1830
|
-
signal: ctx.signal
|
|
1831
|
-
};
|
|
1832
|
-
const transformCtx = { messages: streamOptions.messages };
|
|
1833
|
-
await ctx.hooks.callHook("context:transform", transformCtx);
|
|
1834
|
-
streamOptions.messages = transformCtx.messages;
|
|
1835
|
-
const systemCtx = {
|
|
1836
|
-
system: streamOptions.system,
|
|
1837
|
-
messages: streamOptions.messages,
|
|
1838
|
-
turn,
|
|
1839
|
-
turnId,
|
|
1840
|
-
...ctx.session ? { session: ctx.session } : {}
|
|
1841
|
-
};
|
|
1842
|
-
await ctx.hooks.callHook("system:transform", systemCtx);
|
|
1843
|
-
streamOptions.system = systemCtx.system;
|
|
1844
|
-
await ctx.hooks.callHook("turn:before", { turn, turnId, options: streamOptions });
|
|
1845
|
-
let currentText = "";
|
|
1846
|
-
let currentThinking = "";
|
|
1847
|
-
let result;
|
|
1848
|
-
try {
|
|
1849
|
-
result = await ctx.provider.stream(
|
|
1850
|
-
streamOptions,
|
|
1851
|
-
{
|
|
1852
|
-
onText(delta) {
|
|
1853
|
-
currentText += delta;
|
|
1854
|
-
ctx.hooks.callHook("stream:text", { delta, text: currentText, turnId });
|
|
1855
|
-
},
|
|
1856
|
-
onThinking(delta) {
|
|
1857
|
-
currentThinking += delta;
|
|
1858
|
-
ctx.hooks.callHook("stream:thinking", { delta, thinking: currentThinking, turnId });
|
|
1859
|
-
},
|
|
1860
|
-
onOAuthRefresh(refreshCtx) {
|
|
1861
|
-
return ctx.hooks.callHook("oauth:refresh", refreshCtx);
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
);
|
|
1865
|
-
} catch (err) {
|
|
1866
|
-
const errorUsage = { input: 0, output: 0 };
|
|
1867
|
-
const errorContent = currentText ? [{ type: "text", text: currentText }] : [{ type: "text", text: "[provider error before any output]" }];
|
|
1868
|
-
const errorTurn = {
|
|
1869
|
-
id: turnId,
|
|
1870
|
-
runId: ctx.runId,
|
|
1871
|
-
role: "assistant",
|
|
1872
|
-
content: errorContent,
|
|
1873
|
-
usage: errorUsage,
|
|
1874
|
-
createdAt: Date.now()
|
|
1875
|
-
};
|
|
1876
|
-
ctx.turns.push(errorTurn);
|
|
1877
|
-
await ctx.hooks.callHook("turn:after", {
|
|
1878
|
-
turn,
|
|
1879
|
-
turnId,
|
|
1880
|
-
usage: errorUsage,
|
|
1881
|
-
message: errorTurn,
|
|
1882
|
-
toolCounts: { turn: Object.freeze({}), run: Object.freeze({ ...ctx.runToolCounts }) }
|
|
1883
|
-
});
|
|
1884
|
-
throw wrapProviderError(err, ctx);
|
|
1885
|
-
}
|
|
1886
|
-
if (currentText) {
|
|
1887
|
-
await ctx.hooks.callHook("stream:end", { text: currentText, turnId });
|
|
1888
|
-
}
|
|
1889
|
-
const canonicalToolCalls = result.toolCalls.map((tc) => ({
|
|
1890
|
-
...tc,
|
|
1891
|
-
name: toCanonicalName(tc.name, ctx.aliasMaps)
|
|
1892
|
-
}));
|
|
1893
|
-
const canonicalContent = rewriteContentToCanonical(
|
|
1894
|
-
result.assistantMessage?.content ?? [],
|
|
1895
|
-
ctx.aliasMaps
|
|
1896
|
-
);
|
|
1897
|
-
const assistantTurn = {
|
|
1898
|
-
id: turnId,
|
|
1899
|
-
runId: ctx.runId,
|
|
1900
|
-
role: "assistant",
|
|
1901
|
-
content: result.done ? canonicalContent.length > 0 ? canonicalContent : [{ type: "text", text: currentText }] : canonicalContent,
|
|
1902
|
-
usage: result.usage,
|
|
1903
|
-
createdAt: Date.now()
|
|
1904
|
-
};
|
|
1905
|
-
ctx.turns.push(assistantTurn);
|
|
1906
|
-
const turnCounts = {};
|
|
1907
|
-
for (const tc of canonicalToolCalls)
|
|
1908
|
-
turnCounts[tc.name] = (turnCounts[tc.name] ?? 0) + 1;
|
|
1909
|
-
await ctx.hooks.callHook("turn:after", {
|
|
1910
|
-
turn,
|
|
1911
|
-
turnId,
|
|
1912
|
-
usage: result.usage,
|
|
1913
|
-
message: assistantTurn,
|
|
1914
|
-
toolCounts: { turn: Object.freeze(turnCounts), run: Object.freeze({ ...ctx.runToolCounts }) }
|
|
1915
|
-
});
|
|
1916
|
-
if (result.done) {
|
|
1917
|
-
if (ctx.schema && !ctx.signal.aborted) {
|
|
1918
|
-
const outputSpec = {
|
|
1919
|
-
name: "__output__",
|
|
1920
|
-
description: "Return the final structured output matching the required schema.",
|
|
1921
|
-
inputSchema: ctx.schema
|
|
1922
|
-
};
|
|
1923
|
-
const schemaMessages = rewriteMessagesToWire(turnsToMessages(ctx.turns), ctx.aliasMaps);
|
|
1924
|
-
let schemaResult;
|
|
1925
|
-
try {
|
|
1926
|
-
schemaResult = await ctx.provider.stream(
|
|
1927
|
-
{
|
|
1928
|
-
model: ctx.model,
|
|
1929
|
-
system: ctx.system,
|
|
1930
|
-
tools: ctx.provider.formatTools([outputSpec]),
|
|
1931
|
-
messages: schemaMessages,
|
|
1932
|
-
maxTokens: ctx.maxTokens ?? 16384,
|
|
1933
|
-
signal: ctx.signal,
|
|
1934
|
-
toolChoice: { type: "tool", name: "__output__" }
|
|
1935
|
-
},
|
|
1936
|
-
{
|
|
1937
|
-
onText: () => {
|
|
1938
|
-
},
|
|
1939
|
-
onOAuthRefresh(refreshCtx) {
|
|
1940
|
-
return ctx.hooks.callHook("oauth:refresh", refreshCtx);
|
|
1941
|
-
}
|
|
1942
|
-
}
|
|
1943
|
-
);
|
|
1944
|
-
} catch (err) {
|
|
1945
|
-
throw wrapProviderError(err, ctx);
|
|
1946
|
-
}
|
|
1947
|
-
const output = schemaResult.toolCalls.find((tc) => tc.name === "__output__")?.input;
|
|
1948
|
-
if (output) {
|
|
1949
|
-
await ctx.hooks.callHook("output", { output, schema: ctx.schema });
|
|
1950
|
-
}
|
|
1951
|
-
const schemaTurn = {
|
|
1952
|
-
id: await ctx.generateTurnId(),
|
|
1953
|
-
runId: ctx.runId,
|
|
1954
|
-
role: "assistant",
|
|
1955
|
-
content: schemaResult.assistantMessage.content,
|
|
1956
|
-
usage: schemaResult.usage,
|
|
1957
|
-
createdAt: Date.now()
|
|
1958
|
-
};
|
|
1959
|
-
ctx.turns.push(schemaTurn);
|
|
1960
|
-
return {
|
|
1961
|
-
ended: true,
|
|
1962
|
-
turnId,
|
|
1963
|
-
usage: {
|
|
1964
|
-
input: result.usage.input + schemaResult.usage.input,
|
|
1965
|
-
output: result.usage.output + schemaResult.usage.output
|
|
1966
|
-
},
|
|
1967
|
-
output
|
|
1968
|
-
};
|
|
1969
|
-
}
|
|
1970
|
-
return { ended: true, turnId, usage: result.usage };
|
|
1971
|
-
}
|
|
1972
|
-
if (canonicalToolCalls.length === 0 && result.usage.finishReason === "pause") {
|
|
1973
|
-
const continueMsg = ctx.provider.userMessage("Please continue.");
|
|
1974
|
-
ctx.turns.push({
|
|
1975
|
-
id: await ctx.generateTurnId(),
|
|
1976
|
-
runId: ctx.runId,
|
|
1977
|
-
role: continueMsg.role,
|
|
1978
|
-
content: continueMsg.content,
|
|
1979
|
-
createdAt: Date.now()
|
|
1980
|
-
});
|
|
1981
|
-
return { ended: false, turnId, usage: result.usage };
|
|
1982
|
-
}
|
|
1983
|
-
const toolResults = ctx.toolExecution === "parallel" ? await executeToolsParallel(ctx, canonicalToolCalls, turnId) : await executeToolsSequential(ctx, canonicalToolCalls, turnId);
|
|
1984
|
-
const toolResultMsg = ctx.provider.toolResultsMessage(toolResults);
|
|
1985
|
-
ctx.turns.push({
|
|
1986
|
-
id: await ctx.generateTurnId(),
|
|
1987
|
-
runId: ctx.runId,
|
|
1988
|
-
role: toolResultMsg.role,
|
|
1989
|
-
content: toolResultMsg.content,
|
|
1990
|
-
createdAt: Date.now()
|
|
1991
|
-
});
|
|
1992
|
-
if (typeof ctx.toolOutputBudget === "number" && ctx.toolOutputBudget > 0) {
|
|
1993
|
-
const totalBytes = toolResults.reduce(
|
|
1994
|
-
(sum, r) => sum + toolOutputByteLength(r.content),
|
|
1995
|
-
0
|
|
1996
|
-
);
|
|
1997
|
-
if (totalBytes > ctx.toolOutputBudget) {
|
|
1998
|
-
const warning = `[Tool output budget exceeded: ${totalBytes} bytes returned in this turn (cap: ${ctx.toolOutputBudget}). Summarize the salient findings before calling more tools.]`;
|
|
1999
|
-
const userMsg = ctx.provider.userMessage(warning);
|
|
2000
|
-
ctx.turns.push({
|
|
2001
|
-
id: await ctx.generateTurnId(),
|
|
2002
|
-
runId: ctx.runId,
|
|
2003
|
-
role: userMsg.role,
|
|
2004
|
-
content: userMsg.content,
|
|
2005
|
-
createdAt: Date.now()
|
|
2006
|
-
});
|
|
2007
|
-
await ctx.hooks.callHook("budget:exceeded", {
|
|
2008
|
-
turn,
|
|
2009
|
-
turnId,
|
|
2010
|
-
bytes: totalBytes,
|
|
2011
|
-
budget: ctx.toolOutputBudget
|
|
2012
|
-
});
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
2015
|
-
return { ended: false, turnId, usage: result.usage };
|
|
2016
|
-
}
|
|
2017
|
-
function stripImagesForNonVision(provider, output) {
|
|
2018
|
-
if (typeof output === "string")
|
|
2019
|
-
return output;
|
|
2020
|
-
if (provider.meta.capabilities?.vision !== false)
|
|
2021
|
-
return output;
|
|
2022
|
-
return output.map((b) => b.type === "image" ? IMAGE_OMITTED_MARKER : b.text).join("\n");
|
|
2023
|
-
}
|
|
2024
|
-
async function executeSingleTool(ctx, call, turnId) {
|
|
2025
|
-
const toolDef = ctx.tools[call.name];
|
|
2026
|
-
const callId = call.id;
|
|
2027
|
-
const displayName = toWireName(call.name, ctx.aliasMaps);
|
|
2028
|
-
const runToolCounts = Object.freeze({ ...ctx.runToolCounts });
|
|
2029
|
-
const gateCtx = {
|
|
2030
|
-
turnId,
|
|
2031
|
-
callId,
|
|
2032
|
-
name: call.name,
|
|
2033
|
-
displayName,
|
|
2034
|
-
input: call.input,
|
|
2035
|
-
block: false,
|
|
2036
|
-
reason: "Tool execution was blocked",
|
|
2037
|
-
runToolCounts
|
|
2038
|
-
};
|
|
2039
|
-
await ctx.hooks.callHook("tool:gate", gateCtx);
|
|
2040
|
-
if (gateCtx.block) {
|
|
2041
|
-
return { result: { id: callId, content: `Blocked: ${gateCtx.reason}` } };
|
|
2042
|
-
}
|
|
2043
|
-
ctx.runToolCounts[call.name] = (ctx.runToolCounts[call.name] ?? 0) + 1;
|
|
2044
|
-
if (gateCtx.result !== void 0) {
|
|
2045
|
-
const substitute = await emitToolResult(ctx, {
|
|
2046
|
-
turnId,
|
|
2047
|
-
callId,
|
|
2048
|
-
name: call.name,
|
|
2049
|
-
displayName,
|
|
2050
|
-
input: gateCtx.input,
|
|
2051
|
-
output: gateCtx.result,
|
|
2052
|
-
isError: false,
|
|
2053
|
-
runToolCounts
|
|
2054
|
-
});
|
|
2055
|
-
return { result: { id: callId, content: substitute } };
|
|
2056
|
-
}
|
|
2057
|
-
let effectiveInput = gateCtx.input;
|
|
2058
|
-
if (!toolDef) {
|
|
2059
|
-
const unknownCtx = {
|
|
2060
|
-
turnId,
|
|
2061
|
-
callId,
|
|
2062
|
-
name: call.name,
|
|
2063
|
-
displayName,
|
|
2064
|
-
input: effectiveInput,
|
|
2065
|
-
suppressError: false
|
|
2066
|
-
};
|
|
2067
|
-
await ctx.hooks.callHook("tool:unknown", unknownCtx);
|
|
2068
|
-
const content = unknownCtx.result ?? `Tool error: Unknown tool: ${call.name}`;
|
|
2069
|
-
if (!unknownCtx.suppressError) {
|
|
2070
|
-
const err = new Error(`Unknown tool: ${call.name}`);
|
|
2071
|
-
await ctx.hooks.callHook("tool:error", {
|
|
2072
|
-
turnId,
|
|
2073
|
-
callId,
|
|
2074
|
-
name: call.name,
|
|
2075
|
-
displayName,
|
|
2076
|
-
input: effectiveInput,
|
|
2077
|
-
error: err
|
|
2078
|
-
});
|
|
2079
|
-
}
|
|
2080
|
-
return { result: { id: callId, content } };
|
|
2081
|
-
}
|
|
2082
|
-
const validation = validateToolArgs(effectiveInput, toolDef.spec.inputSchema);
|
|
2083
|
-
if (!validation.valid) {
|
|
2084
|
-
await ctx.hooks.callHook("validation:reject", {
|
|
2085
|
-
turnId,
|
|
2086
|
-
callId,
|
|
2087
|
-
name: call.name,
|
|
2088
|
-
displayName,
|
|
2089
|
-
input: effectiveInput,
|
|
2090
|
-
reason: validation.error ?? "invalid input",
|
|
2091
|
-
schema: toolDef.spec.inputSchema
|
|
2092
|
-
});
|
|
2093
|
-
return { result: { id: callId, content: `Validation error: ${validation.error}` } };
|
|
2094
|
-
}
|
|
2095
|
-
effectiveInput = validation.coercedInput ?? effectiveInput;
|
|
2096
|
-
const coercions = validation.coercions && validation.coercions.length > 0 ? validation.coercions : void 0;
|
|
2097
|
-
if (coercions) {
|
|
2098
|
-
await ctx.hooks.callHook("validation:coerce", {
|
|
2099
|
-
turnId,
|
|
2100
|
-
callId,
|
|
2101
|
-
name: call.name,
|
|
2102
|
-
displayName,
|
|
2103
|
-
input: effectiveInput,
|
|
2104
|
-
coercions,
|
|
2105
|
-
schema: toolDef.spec.inputSchema
|
|
2106
|
-
});
|
|
2107
|
-
}
|
|
2108
|
-
await ctx.hooks.callHook("tool:before", {
|
|
2109
|
-
turnId,
|
|
2110
|
-
callId,
|
|
2111
|
-
name: call.name,
|
|
2112
|
-
displayName,
|
|
2113
|
-
input: effectiveInput,
|
|
2114
|
-
runToolCounts,
|
|
2115
|
-
...coercions ? { coercions } : {}
|
|
2116
|
-
});
|
|
2117
|
-
let output;
|
|
2118
|
-
let isError = false;
|
|
2119
|
-
try {
|
|
2120
|
-
const toolCtx = {
|
|
2121
|
-
provider: ctx.provider,
|
|
2122
|
-
signal: ctx.signal,
|
|
2123
|
-
execution: ctx.execution,
|
|
2124
|
-
handle: ctx.handle,
|
|
2125
|
-
hooks: ctx.hooks,
|
|
2126
|
-
tools: ctx.agentTools,
|
|
2127
|
-
...ctx.agentName !== void 0 ? { name: ctx.agentName } : {},
|
|
2128
|
-
...ctx.agentSystem !== void 0 ? { system: ctx.agentSystem } : {},
|
|
2129
|
-
...ctx.agentToolAliases !== void 0 ? { toolAliases: ctx.agentToolAliases } : {},
|
|
2130
|
-
...ctx.agentMcpServers !== void 0 ? { mcpServers: ctx.agentMcpServers } : {},
|
|
2131
|
-
...ctx.agentSkills !== void 0 ? { skills: ctx.agentSkills } : {},
|
|
2132
|
-
...ctx.agentBehavior !== void 0 ? { behavior: ctx.agentBehavior } : {},
|
|
2133
|
-
turnId,
|
|
2134
|
-
callId,
|
|
2135
|
-
runId: ctx.runId,
|
|
2136
|
-
...ctx.session ? { session: ctx.session } : {},
|
|
2137
|
-
...typeof ctx.depth === "number" ? { depth: ctx.depth } : {}
|
|
2138
|
-
};
|
|
2139
|
-
output = await toolDef.execute(effectiveInput, toolCtx);
|
|
2140
|
-
} catch (err) {
|
|
2141
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
2142
|
-
const errorCtx = {
|
|
2143
|
-
turnId,
|
|
2144
|
-
callId,
|
|
2145
|
-
name: call.name,
|
|
2146
|
-
displayName,
|
|
2147
|
-
input: effectiveInput,
|
|
2148
|
-
error
|
|
2149
|
-
};
|
|
2150
|
-
await ctx.hooks.callHook("tool:error", errorCtx);
|
|
2151
|
-
output = errorCtx.result ?? `Tool error: ${error.message}`;
|
|
2152
|
-
isError = true;
|
|
2153
|
-
}
|
|
2154
|
-
const finalOutput = await emitToolResult(ctx, {
|
|
2155
|
-
turnId,
|
|
2156
|
-
callId,
|
|
2157
|
-
name: call.name,
|
|
2158
|
-
displayName,
|
|
2159
|
-
input: effectiveInput,
|
|
2160
|
-
output,
|
|
2161
|
-
isError,
|
|
2162
|
-
runToolCounts,
|
|
2163
|
-
...coercions ? { coercions } : {}
|
|
2164
|
-
});
|
|
2165
|
-
return { result: { id: callId, content: finalOutput } };
|
|
2166
|
-
}
|
|
2167
|
-
async function emitToolResult(ctx, params) {
|
|
2168
|
-
const { turnId, callId, name, displayName, input, runToolCounts, coercions } = params;
|
|
2169
|
-
let output = params.output;
|
|
2170
|
-
let isError = params.isError;
|
|
2171
|
-
const transformCtx = {
|
|
2172
|
-
turnId,
|
|
2173
|
-
callId,
|
|
2174
|
-
name,
|
|
2175
|
-
displayName,
|
|
2176
|
-
input,
|
|
2177
|
-
result: output,
|
|
2178
|
-
isError,
|
|
2179
|
-
outputBytes: toolOutputByteLength(output),
|
|
2180
|
-
...coercions ? { coercions } : {}
|
|
2181
|
-
};
|
|
2182
|
-
await ctx.hooks.callHook("tool:transform", transformCtx);
|
|
2183
|
-
output = transformCtx.result;
|
|
2184
|
-
isError = transformCtx.isError;
|
|
2185
|
-
output = stripImagesForNonVision(ctx.provider, output);
|
|
2186
|
-
await ctx.hooks.callHook("tool:after", {
|
|
2187
|
-
turnId,
|
|
2188
|
-
callId,
|
|
2189
|
-
name,
|
|
2190
|
-
displayName,
|
|
2191
|
-
input,
|
|
2192
|
-
result: output,
|
|
2193
|
-
outputBytes: toolOutputByteLength(output),
|
|
2194
|
-
runToolCounts,
|
|
2195
|
-
...coercions ? { coercions } : {}
|
|
2196
|
-
});
|
|
2197
|
-
return output;
|
|
2198
|
-
}
|
|
2199
|
-
async function executeToolsSequential(ctx, toolCalls, turnId) {
|
|
2200
|
-
const results = [];
|
|
2201
|
-
for (const call of toolCalls) {
|
|
2202
|
-
if (ctx.signal.aborted)
|
|
2203
|
-
break;
|
|
2204
|
-
if (ctx.steeringQueue.length > 0) {
|
|
2205
|
-
const fromIdx = toolCalls.indexOf(call);
|
|
2206
|
-
for (let i = fromIdx; i < toolCalls.length; i++)
|
|
2207
|
-
results.push({ id: toolCalls[i].id, content: "Skipped: steering message received" });
|
|
2208
|
-
return results;
|
|
2209
|
-
}
|
|
2210
|
-
const { result } = await executeSingleTool(ctx, call, turnId);
|
|
2211
|
-
results.push(result);
|
|
2212
|
-
}
|
|
2213
|
-
return results;
|
|
2214
|
-
}
|
|
2215
|
-
async function executeToolsParallel(ctx, toolCalls, turnId) {
|
|
2216
|
-
const executions = toolCalls.map((call) => executeSingleTool(ctx, call, turnId));
|
|
2217
|
-
const settled = await Promise.allSettled(executions);
|
|
2218
|
-
return settled.map((s, i) => {
|
|
2219
|
-
if (s.status === "fulfilled")
|
|
2220
|
-
return s.value.result;
|
|
2221
|
-
return {
|
|
2222
|
-
id: toolCalls[i].id,
|
|
2223
|
-
content: `Error: ${s.reason instanceof Error ? s.reason.message : String(s.reason)}`
|
|
2224
|
-
};
|
|
2225
|
-
});
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
|
-
// src/prompt.ts
|
|
2229
|
-
function canonicalizePrompt(prompt) {
|
|
2230
|
-
if (prompt === void 0)
|
|
2231
|
-
return void 0;
|
|
2232
|
-
if (typeof prompt === "string") {
|
|
2233
|
-
if (prompt.length === 0)
|
|
2234
|
-
return void 0;
|
|
2235
|
-
return [{ type: "text", text: prompt }];
|
|
2236
|
-
}
|
|
2237
|
-
if (prompt.length === 0)
|
|
2238
|
-
return void 0;
|
|
2239
|
-
for (const part of prompt) {
|
|
2240
|
-
if (!part || typeof part !== "object" || typeof part.type !== "string") {
|
|
2241
|
-
throw new Error("Invalid PromptPart: each part must be an object with a `type` field.");
|
|
2242
|
-
}
|
|
2243
|
-
const type = part.type;
|
|
2244
|
-
if (type !== "text" && type !== "image" && type !== "document") {
|
|
2245
|
-
throw new Error(`Invalid PromptPart type "${type}". Expected "text" | "image" | "document".`);
|
|
2246
|
-
}
|
|
2247
|
-
}
|
|
2248
|
-
const hasMeaningfulPart = prompt.some((part) => part.type === "text" && part.text.length > 0 || part.type === "image" || part.type === "document");
|
|
2249
|
-
if (!hasMeaningfulPart)
|
|
2250
|
-
return void 0;
|
|
2251
|
-
return prompt;
|
|
2252
|
-
}
|
|
2253
|
-
function defaultPromptMessage(parts) {
|
|
2254
|
-
const content = [];
|
|
2255
|
-
for (const part of parts) {
|
|
2256
|
-
if (part.type === "text") {
|
|
2257
|
-
if (part.text.length > 0)
|
|
2258
|
-
content.push({ type: "text", text: part.text });
|
|
2259
|
-
continue;
|
|
2260
|
-
}
|
|
2261
|
-
if (part.type === "image") {
|
|
2262
|
-
content.push({ type: "image", mediaType: part.mediaType, data: part.data });
|
|
2263
|
-
continue;
|
|
2264
|
-
}
|
|
2265
|
-
if (part.encoding === "text") {
|
|
2266
|
-
const header = part.name ? `<attachment name="${part.name}" media_type="${part.mediaType}">` : `<attachment media_type="${part.mediaType}">`;
|
|
2267
|
-
content.push({ type: "text", text: `${header}
|
|
2268
|
-
${part.data}
|
|
2269
|
-
</attachment>` });
|
|
2270
|
-
continue;
|
|
2271
|
-
}
|
|
2272
|
-
throw new Error(
|
|
2273
|
-
`Provider does not support base64 document parts (mediaType: ${part.mediaType}). Use a text-encoded document or a provider that implements promptMessage (e.g. Anthropic).`
|
|
2274
|
-
);
|
|
2275
|
-
}
|
|
2276
|
-
return { role: "user", content };
|
|
2277
|
-
}
|
|
2278
|
-
function buildPromptMessage(provider, parts) {
|
|
2279
|
-
if (provider.promptMessage)
|
|
2280
|
-
return provider.promptMessage(parts);
|
|
2281
|
-
return defaultPromptMessage(parts);
|
|
2282
|
-
}
|
|
2283
|
-
|
|
2284
|
-
// src/tool-budgets.ts
|
|
2285
|
-
function installToolBudgetsGate(hooks, getToolBudgets, enqueueSteer) {
|
|
2286
|
-
const steeredOnce = /* @__PURE__ */ new Set();
|
|
2287
|
-
const approvedCounts = {};
|
|
2288
|
-
async function gateHandler(ctx) {
|
|
2289
|
-
if (ctx.block || ctx.result !== void 0)
|
|
2290
|
-
return;
|
|
2291
|
-
const toolBudgets = getToolBudgets();
|
|
2292
|
-
const budget = toolBudgets?.[ctx.name];
|
|
2293
|
-
if (!budget)
|
|
2294
|
-
return;
|
|
2295
|
-
const max = budget.max;
|
|
2296
|
-
if (typeof max !== "number" || max <= 0)
|
|
2297
|
-
return;
|
|
2298
|
-
const count = approvedCounts[ctx.name] ?? 0;
|
|
2299
|
-
if (count < max) {
|
|
2300
|
-
approvedCounts[ctx.name] = count + 1;
|
|
2301
|
-
return;
|
|
2302
|
-
}
|
|
2303
|
-
const onExceed = budget.onExceed ?? "steer";
|
|
2304
|
-
let mode;
|
|
2305
|
-
let message;
|
|
2306
|
-
if (typeof onExceed === "function") {
|
|
2307
|
-
try {
|
|
2308
|
-
const out = onExceed({ tool: ctx.name, count, max });
|
|
2309
|
-
mode = out.mode;
|
|
2310
|
-
message = out.message;
|
|
2311
|
-
} catch {
|
|
2312
|
-
mode = "steer";
|
|
2313
|
-
message = defaultSteerMessage(ctx.name, count, max);
|
|
2314
|
-
}
|
|
2315
|
-
} else if (onExceed === "block") {
|
|
2316
|
-
mode = "block";
|
|
2317
|
-
message = defaultBlockMessage(ctx.name, max);
|
|
2318
|
-
} else {
|
|
2319
|
-
mode = "steer";
|
|
2320
|
-
message = defaultSteerMessage(ctx.name, count, max);
|
|
2321
|
-
}
|
|
2322
|
-
if (mode === "block") {
|
|
2323
|
-
ctx.block = true;
|
|
2324
|
-
ctx.reason = message;
|
|
2325
|
-
await hooks.callHook("tool-budget:exceeded", {
|
|
2326
|
-
tool: ctx.name,
|
|
2327
|
-
count,
|
|
2328
|
-
max,
|
|
2329
|
-
turnId: ctx.turnId,
|
|
2330
|
-
mode: "block"
|
|
2331
|
-
});
|
|
2332
|
-
return;
|
|
2333
|
-
}
|
|
2334
|
-
if (!steeredOnce.has(ctx.name)) {
|
|
2335
|
-
steeredOnce.add(ctx.name);
|
|
2336
|
-
enqueueSteer(message);
|
|
2337
|
-
await hooks.callHook("tool-budget:exceeded", {
|
|
2338
|
-
tool: ctx.name,
|
|
2339
|
-
count,
|
|
2340
|
-
max,
|
|
2341
|
-
turnId: ctx.turnId,
|
|
2342
|
-
mode: "steer"
|
|
2343
|
-
});
|
|
2344
|
-
}
|
|
2345
|
-
}
|
|
2346
|
-
const unregister = hooks.hook("tool:gate", gateHandler);
|
|
2347
|
-
return function uninstall() {
|
|
2348
|
-
unregister();
|
|
2349
|
-
steeredOnce.clear();
|
|
2350
|
-
};
|
|
2351
|
-
}
|
|
2352
|
-
function defaultSteerMessage(tool, count, max) {
|
|
2353
|
-
return `[Tool budget reached: '${tool}' has been called ${count} times this run (cap: ${max}). Avoid calling it again unless strictly necessary; commit to a result and move on.]`;
|
|
2354
|
-
}
|
|
2355
|
-
function defaultBlockMessage(tool, max) {
|
|
2356
|
-
return `Tool '${tool}' has reached its per-run budget of ${max} calls; further invocations are refused.`;
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
// src/tools/tool-search.ts
|
|
2360
|
-
var DEFAULT_LIMIT2 = 20;
|
|
2361
|
-
function rankByQuery(catalog, query) {
|
|
2362
|
-
const q = query.trim().toLowerCase();
|
|
2363
|
-
if (!q)
|
|
2364
|
-
return [...catalog];
|
|
2365
|
-
const nameHits = [];
|
|
2366
|
-
const descHits = [];
|
|
2367
|
-
for (const entry of catalog) {
|
|
2368
|
-
if (entry.name.toLowerCase().includes(q))
|
|
2369
|
-
nameHits.push(entry);
|
|
2370
|
-
else if (entry.description.toLowerCase().includes(q))
|
|
2371
|
-
descHits.push(entry);
|
|
2372
|
-
}
|
|
2373
|
-
return [...nameHits, ...descHits];
|
|
2374
|
-
}
|
|
2375
|
-
function sanitiseSchemaForXml(schemaJson) {
|
|
2376
|
-
return schemaJson.replace(/</g, "\\u003c");
|
|
2377
|
-
}
|
|
2378
|
-
function formatMatch(entry) {
|
|
2379
|
-
const schema = sanitiseSchemaForXml(JSON.stringify(entry.inputSchema));
|
|
2380
|
-
const serverAttr = entry.server ? ` server="${escapeXml(entry.server)}"` : "";
|
|
2381
|
-
return [
|
|
2382
|
-
` <tool name="${escapeXml(entry.name)}"${serverAttr}>`,
|
|
2383
|
-
` <description>${escapeXml(entry.description)}</description>`,
|
|
2384
|
-
` <input_schema>${schema}</input_schema>`,
|
|
2385
|
-
` </tool>`
|
|
2386
|
-
].join("\n");
|
|
2387
|
-
}
|
|
2388
|
-
function createToolSearchTool(options) {
|
|
2389
|
-
const defaultLimit = options.defaultLimit ?? DEFAULT_LIMIT2;
|
|
2390
|
-
const byName = new Map(options.catalog.map((e) => [e.name, e]));
|
|
2391
|
-
const byServer = /* @__PURE__ */ new Map();
|
|
2392
|
-
for (const entry of options.catalog) {
|
|
2393
|
-
if (!entry.server)
|
|
2394
|
-
continue;
|
|
2395
|
-
const list = byServer.get(entry.server) ?? [];
|
|
2396
|
-
list.push(entry);
|
|
2397
|
-
byServer.set(entry.server, list);
|
|
2398
|
-
}
|
|
2399
|
-
const maxLimit = Math.max(options.catalog.length, 1);
|
|
2400
|
-
return {
|
|
2401
|
-
spec: {
|
|
2402
|
-
name: "tool_search",
|
|
2403
|
-
description: "Discover and load schemas for additional tools listed in <searchable_tools>. Tools listed there are advertised by name + description only \u2014 their input schemas are not loaded into context until you surface them through this tool. Pass `query` for a substring search, `names` to load specific tools, or `server` to load every tool from one MCP server. Returned tools become callable for the rest of this run.",
|
|
2404
|
-
inputSchema: {
|
|
2405
|
-
type: "object",
|
|
2406
|
-
properties: {
|
|
2407
|
-
query: {
|
|
2408
|
-
type: "string",
|
|
2409
|
-
description: "Substring to match against tool name + description (case-insensitive)."
|
|
2410
|
-
},
|
|
2411
|
-
names: {
|
|
2412
|
-
type: "array",
|
|
2413
|
-
items: { type: "string" },
|
|
2414
|
-
description: "Explicit tool names to load (bypasses ranking). Use the names shown in <searchable_tools>."
|
|
2415
|
-
},
|
|
2416
|
-
server: {
|
|
2417
|
-
type: "string",
|
|
2418
|
-
description: "MCP server name \u2014 load every tool from this server."
|
|
2419
|
-
},
|
|
2420
|
-
limit: {
|
|
2421
|
-
type: "integer",
|
|
2422
|
-
minimum: 1,
|
|
2423
|
-
description: `Cap on returned matches. Default: ${defaultLimit}.`
|
|
2424
|
-
}
|
|
2425
|
-
},
|
|
2426
|
-
additionalProperties: false
|
|
2427
|
-
}
|
|
2428
|
-
},
|
|
2429
|
-
async execute(input, ctx) {
|
|
2430
|
-
if (ctx.signal?.aborted)
|
|
2431
|
-
return '<tool_search_results matches="0" aborted="true">Run aborted.</tool_search_results>';
|
|
2432
|
-
const rawQuery = typeof input.query === "string" ? input.query.trim() : void 0;
|
|
2433
|
-
const query = rawQuery || void 0;
|
|
2434
|
-
const namesIn = Array.isArray(input.names) ? input.names.filter((n) => typeof n === "string" && n.length > 0) : void 0;
|
|
2435
|
-
const server = typeof input.server === "string" && input.server.length > 0 ? input.server : void 0;
|
|
2436
|
-
const limitIn = typeof input.limit === "number" && Number.isFinite(input.limit) && input.limit > 0 ? Math.floor(input.limit) : defaultLimit;
|
|
2437
|
-
const limit = Math.min(limitIn, maxLimit);
|
|
2438
|
-
if (options.catalog.length === 0)
|
|
2439
|
-
return '<tool_search_results matches="0">No lazy tools registered for this run.</tool_search_results>';
|
|
2440
|
-
const matches = [];
|
|
2441
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2442
|
-
const misses = [];
|
|
2443
|
-
if (namesIn && namesIn.length > 0) {
|
|
2444
|
-
for (const n of namesIn) {
|
|
2445
|
-
if (seen.has(n))
|
|
2446
|
-
continue;
|
|
2447
|
-
const entry = byName.get(n);
|
|
2448
|
-
if (entry) {
|
|
2449
|
-
matches.push(entry);
|
|
2450
|
-
seen.add(n);
|
|
2451
|
-
} else {
|
|
2452
|
-
misses.push(n);
|
|
2453
|
-
}
|
|
2454
|
-
}
|
|
2455
|
-
}
|
|
2456
|
-
if (server) {
|
|
2457
|
-
const list = byServer.get(server) ?? [];
|
|
2458
|
-
for (const entry of list) {
|
|
2459
|
-
if (seen.has(entry.name))
|
|
2460
|
-
continue;
|
|
2461
|
-
matches.push(entry);
|
|
2462
|
-
seen.add(entry.name);
|
|
2463
|
-
}
|
|
2464
|
-
}
|
|
2465
|
-
if (query !== void 0) {
|
|
2466
|
-
for (const entry of rankByQuery(options.catalog, query)) {
|
|
2467
|
-
if (seen.has(entry.name))
|
|
2468
|
-
continue;
|
|
2469
|
-
matches.push(entry);
|
|
2470
|
-
seen.add(entry.name);
|
|
2471
|
-
}
|
|
2472
|
-
}
|
|
2473
|
-
if (!namesIn?.length && !server && query === void 0) {
|
|
2474
|
-
for (const entry of options.catalog) {
|
|
2475
|
-
matches.push(entry);
|
|
2476
|
-
seen.add(entry.name);
|
|
2477
|
-
}
|
|
2478
|
-
}
|
|
2479
|
-
const truncated = matches.length > limit;
|
|
2480
|
-
const shown = truncated ? matches.slice(0, limit) : matches;
|
|
2481
|
-
for (const entry of shown)
|
|
2482
|
-
options.unlocked.add(entry.canonicalName);
|
|
2483
|
-
const parts = [];
|
|
2484
|
-
const queryAttr = query ? ` query="${escapeXml(query)}"` : "";
|
|
2485
|
-
const serverAttr = server ? ` server="${escapeXml(server)}"` : "";
|
|
2486
|
-
parts.push(`<tool_search_results matches="${shown.length}" total="${matches.length}"${queryAttr}${serverAttr}>`);
|
|
2487
|
-
if (shown.length === 0) {
|
|
2488
|
-
parts.push(" No matches. Try a broader query, or omit all parameters to list everything.");
|
|
2489
|
-
} else {
|
|
2490
|
-
for (const entry of shown)
|
|
2491
|
-
parts.push(formatMatch(entry));
|
|
2492
|
-
parts.push("");
|
|
2493
|
-
parts.push(" These tools are now callable. Invoke them by name in subsequent turns.");
|
|
2494
|
-
if (truncated) {
|
|
2495
|
-
parts.push(` ${matches.length - shown.length} additional matches were truncated \u2014 refine the query or raise \`limit\`.`);
|
|
2496
|
-
}
|
|
2497
|
-
}
|
|
2498
|
-
if (misses.length > 0) {
|
|
2499
|
-
parts.push(` <misses>${misses.map(escapeXml).join(", ")}</misses>`);
|
|
2500
|
-
}
|
|
2501
|
-
parts.push("</tool_search_results>");
|
|
2502
|
-
return parts.join("\n");
|
|
2503
|
-
}
|
|
2504
|
-
};
|
|
2505
|
-
}
|
|
2506
|
-
|
|
2507
|
-
// src/agent.ts
|
|
2508
|
-
var HOOK_EVENT_NAMES = [
|
|
2509
|
-
"system:before",
|
|
2510
|
-
"turn:before",
|
|
2511
|
-
"turn:after",
|
|
2512
|
-
"stream:text",
|
|
2513
|
-
"stream:end",
|
|
2514
|
-
"stream:thinking",
|
|
2515
|
-
"oauth:refresh",
|
|
2516
|
-
"tool:gate",
|
|
2517
|
-
"tool:before",
|
|
2518
|
-
"tool:after",
|
|
2519
|
-
"tool:error",
|
|
2520
|
-
"tool:transform",
|
|
2521
|
-
"tool:unknown",
|
|
2522
|
-
"validation:reject",
|
|
2523
|
-
"validation:coerce",
|
|
2524
|
-
"context:transform",
|
|
2525
|
-
"system:transform",
|
|
2526
|
-
"steer:inject",
|
|
2527
|
-
"spawn:before",
|
|
2528
|
-
"spawn:complete",
|
|
2529
|
-
"spawn:error",
|
|
2530
|
-
"child:stream:text",
|
|
2531
|
-
"child:stream:thinking",
|
|
2532
|
-
"child:stream:end",
|
|
2533
|
-
"child:tool:before",
|
|
2534
|
-
"child:tool:after",
|
|
2535
|
-
"child:tool:error",
|
|
2536
|
-
"child:turn:after",
|
|
2537
|
-
"mcp:connect",
|
|
2538
|
-
"mcp:error",
|
|
2539
|
-
"mcp:close",
|
|
2540
|
-
"mcp:bootstrap:start",
|
|
2541
|
-
"mcp:bootstrap:end",
|
|
2542
|
-
"mcp:tools:filter",
|
|
2543
|
-
"mcp:tool:gate",
|
|
2544
|
-
"mcp:tool:before",
|
|
2545
|
-
"mcp:tool:after",
|
|
2546
|
-
"mcp:tool:transform",
|
|
2547
|
-
"mcp:tool:error",
|
|
2548
|
-
"skills:resolve",
|
|
2549
|
-
"skills:catalog",
|
|
2550
|
-
"skills:activate",
|
|
2551
|
-
"skills:deactivate",
|
|
2552
|
-
"usage",
|
|
2553
|
-
"output",
|
|
2554
|
-
"budget:exceeded",
|
|
2555
|
-
"tool-budget:exceeded",
|
|
2556
|
-
"agent:abort",
|
|
2557
|
-
"agent:done",
|
|
2558
|
-
"session:start",
|
|
2559
|
-
"session:end",
|
|
2560
|
-
"session:turns",
|
|
2561
|
-
"session:meta",
|
|
2562
|
-
"session:save"
|
|
2563
|
-
];
|
|
2564
|
-
var HOOK_EVENT_SET = new Set(HOOK_EVENT_NAMES);
|
|
2565
|
-
function isKnownHookEvent(event) {
|
|
2566
|
-
return HOOK_EVENT_SET.has(event);
|
|
2567
|
-
}
|
|
2568
|
-
function resolveBehavior(agentBehavior, runBehavior) {
|
|
2569
|
-
return {
|
|
2570
|
-
toolExecution: runBehavior?.toolExecution ?? agentBehavior?.toolExecution ?? "parallel",
|
|
2571
|
-
maxTurns: runBehavior?.maxTurns ?? agentBehavior?.maxTurns,
|
|
2572
|
-
maxTokens: runBehavior?.maxTokens ?? agentBehavior?.maxTokens,
|
|
2573
|
-
thinkingBudget: runBehavior?.thinkingBudget ?? agentBehavior?.thinkingBudget,
|
|
2574
|
-
schema: runBehavior?.schema ?? agentBehavior?.schema,
|
|
2575
|
-
cache: runBehavior?.cache ?? agentBehavior?.cache ?? true,
|
|
2576
|
-
toolOutputBudget: runBehavior?.toolOutputBudget ?? agentBehavior?.toolOutputBudget,
|
|
2577
|
-
compactStrategy: runBehavior?.compactStrategy ?? agentBehavior?.compactStrategy ?? "off",
|
|
2578
|
-
compactThreshold: runBehavior?.compactThreshold ?? agentBehavior?.compactThreshold,
|
|
2579
|
-
compactKeepTurns: runBehavior?.compactKeepTurns ?? agentBehavior?.compactKeepTurns,
|
|
2580
|
-
thinkingDecay: runBehavior?.thinkingDecay ?? agentBehavior?.thinkingDecay,
|
|
2581
|
-
dedupReads: runBehavior?.dedupReads ?? agentBehavior?.dedupReads,
|
|
2582
|
-
dedupTools: runBehavior?.dedupTools ?? agentBehavior?.dedupTools,
|
|
2583
|
-
requireReadBeforeEdit: runBehavior?.requireReadBeforeEdit ?? agentBehavior?.requireReadBeforeEdit,
|
|
2584
|
-
toolBudgets: runBehavior?.toolBudgets ?? agentBehavior?.toolBudgets,
|
|
2585
|
-
readLineNumbers: runBehavior?.readLineNumbers ?? agentBehavior?.readLineNumbers,
|
|
2586
|
-
elideStaleReads: runBehavior?.elideStaleReads ?? agentBehavior?.elideStaleReads,
|
|
2587
|
-
toolDisclosure: runBehavior?.toolDisclosure ?? agentBehavior?.toolDisclosure ?? "eager",
|
|
2588
|
-
toolSearch: runBehavior?.toolSearch ?? agentBehavior?.toolSearch
|
|
2589
|
-
};
|
|
2590
|
-
}
|
|
2591
|
-
function resolveServerForTool(toolName, servers) {
|
|
2592
|
-
if (!servers || servers.length === 0)
|
|
2593
|
-
return void 0;
|
|
2594
|
-
let best;
|
|
2595
|
-
let bestLen = -1;
|
|
2596
|
-
for (const server of servers) {
|
|
2597
|
-
const prefix = `mcp_${server.name}_`;
|
|
2598
|
-
if (toolName.startsWith(prefix) && server.name.length > bestLen) {
|
|
2599
|
-
best = server;
|
|
2600
|
-
bestLen = server.name.length;
|
|
2601
|
-
}
|
|
2602
|
-
}
|
|
2603
|
-
return best;
|
|
2604
|
-
}
|
|
2605
|
-
function partitionToolDisclosure(toolsBySpecName, mcpToolNames, servers, globalMode, toolAliases) {
|
|
2606
|
-
const eagerCanonicalNames = /* @__PURE__ */ new Set();
|
|
2607
|
-
const lazyCanonicalNames = /* @__PURE__ */ new Set();
|
|
2608
|
-
const lazyEntries = [];
|
|
2609
|
-
function wireFor(canonical) {
|
|
2610
|
-
const aliased = toolAliases?.[canonical];
|
|
2611
|
-
return typeof aliased === "string" && aliased.length > 0 ? aliased : canonical;
|
|
2612
|
-
}
|
|
2613
|
-
for (const [canonicalName, def] of Object.entries(toolsBySpecName)) {
|
|
2614
|
-
if (!mcpToolNames.has(canonicalName)) {
|
|
2615
|
-
eagerCanonicalNames.add(canonicalName);
|
|
2616
|
-
continue;
|
|
2617
|
-
}
|
|
2618
|
-
const server = resolveServerForTool(canonicalName, servers);
|
|
2619
|
-
const mode = server?.disclosure ?? globalMode;
|
|
2620
|
-
if (mode === "lazy") {
|
|
2621
|
-
lazyCanonicalNames.add(canonicalName);
|
|
2622
|
-
lazyEntries.push({
|
|
2623
|
-
name: wireFor(canonicalName),
|
|
2624
|
-
canonicalName,
|
|
2625
|
-
description: def.spec.description || "",
|
|
2626
|
-
inputSchema: def.spec.inputSchema ?? { type: "object", properties: {} },
|
|
2627
|
-
...server ? { server: server.name } : {}
|
|
2628
|
-
});
|
|
2629
|
-
} else {
|
|
2630
|
-
eagerCanonicalNames.add(canonicalName);
|
|
2631
|
-
}
|
|
2632
|
-
}
|
|
2633
|
-
return { eagerCanonicalNames, lazyCanonicalNames, lazyEntries };
|
|
2634
|
-
}
|
|
2635
|
-
function buildSearchableCatalog(entries, options) {
|
|
2636
|
-
const byServer = /* @__PURE__ */ new Map();
|
|
2637
|
-
const ungrouped = [];
|
|
2638
|
-
for (const entry of entries) {
|
|
2639
|
-
if (!entry.server) {
|
|
2640
|
-
ungrouped.push(entry);
|
|
2641
|
-
continue;
|
|
2642
|
-
}
|
|
2643
|
-
const list = byServer.get(entry.server) ?? [];
|
|
2644
|
-
list.push(entry);
|
|
2645
|
-
byServer.set(entry.server, list);
|
|
2646
|
-
}
|
|
2647
|
-
const serverNames = [...byServer.keys()].sort();
|
|
2648
|
-
const parts = [];
|
|
2649
|
-
if (options.discoveryToolName) {
|
|
2650
|
-
parts.push(
|
|
2651
|
-
"The following tools are available but their input schemas are NOT loaded in your context.",
|
|
2652
|
-
`Call the \`${options.discoveryToolName}\` tool to load schemas before invoking them. Surfaced tools persist for the rest of the run.`,
|
|
2653
|
-
""
|
|
2654
|
-
);
|
|
2655
|
-
}
|
|
2656
|
-
parts.push("<searchable_tools>");
|
|
2657
|
-
for (const server of serverNames) {
|
|
2658
|
-
parts.push(` <server name="${escapeXml(server)}">`);
|
|
2659
|
-
for (const entry of byServer.get(server))
|
|
2660
|
-
parts.push(` <tool name="${escapeXml(entry.name)}">${escapeXml(entry.description)}</tool>`);
|
|
2661
|
-
parts.push(" </server>");
|
|
2662
|
-
}
|
|
2663
|
-
for (const entry of ungrouped)
|
|
2664
|
-
parts.push(` <tool name="${escapeXml(entry.name)}">${escapeXml(entry.description)}</tool>`);
|
|
2665
|
-
parts.push("</searchable_tools>");
|
|
2666
|
-
return parts.join("\n");
|
|
2667
|
-
}
|
|
2668
|
-
function installLazyDisclosureGate(hooks, lazyCanonicalNames, unlocked, discoveryToolName) {
|
|
2669
|
-
if (lazyCanonicalNames.size === 0)
|
|
2670
|
-
return () => {
|
|
2671
|
-
};
|
|
2672
|
-
return hooks.hook("tool:gate", (ctx) => {
|
|
2673
|
-
if (ctx.block)
|
|
2674
|
-
return;
|
|
2675
|
-
if (!lazyCanonicalNames.has(ctx.name))
|
|
2676
|
-
return;
|
|
2677
|
-
if (unlocked.has(ctx.name))
|
|
2678
|
-
return;
|
|
2679
|
-
ctx.block = true;
|
|
2680
|
-
ctx.reason = discoveryToolName ? `Tool "${ctx.name}" is listed in <searchable_tools> but its schema has not been loaded. Call the \`${discoveryToolName}\` tool with names: ["${ctx.name}"] first, then re-issue the call.` : `Tool "${ctx.name}" is listed in <searchable_tools> but its schema has not been loaded.`;
|
|
2681
|
-
});
|
|
2682
|
-
}
|
|
2683
|
-
function createAgent({ provider, name: agentName, system: agentSystem, tools: agentTools, toolAliases, behavior: agentBehavior, execution, mcpServers, session, skills: agentSkills, mcpConnector, eager }) {
|
|
2684
|
-
const hooks = createHooks();
|
|
2685
|
-
const executionContext = execution ?? createProcessContext();
|
|
2686
|
-
const sourceTools = agentTools ?? {};
|
|
2687
|
-
let abortController;
|
|
2688
|
-
let running = false;
|
|
2689
|
-
let idleResolve;
|
|
2690
|
-
let idlePromise;
|
|
2691
|
-
let executionHandle = null;
|
|
2692
|
-
let mcpConnection = null;
|
|
2693
|
-
let mcpWarmupPromise = null;
|
|
2694
|
-
const allMcpServers = mcpServers ?? [];
|
|
2695
|
-
const steeringQueue = [];
|
|
2696
|
-
const followUpQueue = [];
|
|
2697
|
-
let conversationTurns = session?.turns.slice() ?? [];
|
|
2698
|
-
let runCounter = session?.runs.length ?? 0;
|
|
2699
|
-
const skillsConfig = agentSkills;
|
|
2700
|
-
const skillsEnabledValue = skillsConfig?.enabled;
|
|
2701
|
-
const skillsDisabled = skillsEnabledValue === false || Array.isArray(skillsEnabledValue) && skillsEnabledValue.length === 0;
|
|
2702
|
-
let resolvedSkills = null;
|
|
2703
|
-
let skillsCatalog = null;
|
|
2704
|
-
let skillsCleanup = () => {
|
|
2705
|
-
};
|
|
2706
|
-
const skillActivationState = createSkillActivationState({
|
|
2707
|
-
maxActive: skillsConfig?.maxActive
|
|
2708
|
-
});
|
|
2709
|
-
async function run(options) {
|
|
2710
|
-
if (running) {
|
|
2711
|
-
throw new Error("Agent is already running. Use steer() or followUp() to queue messages, or waitForIdle().");
|
|
2712
|
-
}
|
|
2713
|
-
const hasSessionTurns = session && session.turns.length > 0;
|
|
2714
|
-
if (!options.prompt && !hasSessionTurns) {
|
|
2715
|
-
throw new Error("prompt is required when no session with existing turns is provided");
|
|
2716
|
-
}
|
|
2717
|
-
if (!options.prompt && hasSessionTurns) {
|
|
2718
|
-
const lastTurn = session.turns.at(-1);
|
|
2719
|
-
if (lastTurn && lastTurn.role !== "user") {
|
|
2720
|
-
throw new Error("cannot resume without prompt: last session turn must be a user message");
|
|
2721
|
-
}
|
|
2722
|
-
}
|
|
2723
|
-
running = true;
|
|
2724
|
-
abortController = new AbortController();
|
|
2725
|
-
const runId = `run_${++runCounter}`;
|
|
2726
|
-
const promptLabel = typeof options.prompt === "string" ? options.prompt : Array.isArray(options.prompt) ? options.prompt.filter((p) => p.type === "text").map((p) => p.text).join("\n") : "";
|
|
2727
|
-
session?.startRun(runId, promptLabel, {
|
|
2728
|
-
...options.parentRunId ? { parentRunId: options.parentRunId } : {},
|
|
2729
|
-
depth: typeof options.depth === "number" ? options.depth : 0
|
|
2730
|
-
});
|
|
2731
|
-
if (session) {
|
|
2732
|
-
await session.updateStatus("running");
|
|
2733
|
-
await hooks.callHook("session:start", { sessionId: session.id, runId, prompt: promptLabel });
|
|
2734
|
-
}
|
|
2735
|
-
if (options.signal) {
|
|
2736
|
-
if (options.signal.aborted) {
|
|
2737
|
-
abortController.abort();
|
|
2738
|
-
} else {
|
|
2739
|
-
const onExternalAbort = () => abortController?.abort();
|
|
2740
|
-
options.signal.addEventListener("abort", onExternalAbort, { once: true });
|
|
2741
|
-
}
|
|
2742
|
-
}
|
|
2743
|
-
idlePromise = new Promise((resolve2) => {
|
|
2744
|
-
idleResolve = resolve2;
|
|
2745
|
-
});
|
|
2746
|
-
const childrenStats = [];
|
|
2747
|
-
const unregisterSpawnHook = hooks.hook("spawn:complete", (ctx) => {
|
|
2748
|
-
childrenStats.push(ctx);
|
|
2749
|
-
});
|
|
2750
|
-
const perRunUnregisters = [];
|
|
2751
|
-
if (options.hooks) {
|
|
2752
|
-
for (const [event, handler] of Object.entries(options.hooks)) {
|
|
2753
|
-
if (!isKnownHookEvent(event)) {
|
|
2754
|
-
throw new Error(
|
|
2755
|
-
`Unknown hook event "${event}" passed to run(). See AgentHooks for valid events.`
|
|
2756
|
-
);
|
|
2757
|
-
}
|
|
2758
|
-
const handlerList = Array.isArray(handler) ? handler : [handler];
|
|
2759
|
-
for (const fn of handlerList) {
|
|
2760
|
-
if (typeof fn !== "function")
|
|
2761
|
-
continue;
|
|
2762
|
-
perRunUnregisters.push(hooks.hook(event, fn));
|
|
2763
|
-
}
|
|
2764
|
-
}
|
|
2765
|
-
}
|
|
2766
|
-
if (!executionHandle) {
|
|
2767
|
-
executionHandle = await executionContext.spawn();
|
|
2768
|
-
}
|
|
2769
|
-
if (allMcpServers.length > 0 && !mcpConnection) {
|
|
2770
|
-
await warmup();
|
|
2771
|
-
}
|
|
2772
|
-
if (!skillsDisabled && skillsConfig && !resolvedSkills) {
|
|
2773
|
-
const bundle = await resolveSkills(skillsConfig);
|
|
2774
|
-
resolvedSkills = bundle.skills;
|
|
2775
|
-
skillsCleanup = bundle.cleanup;
|
|
2776
|
-
await hooks.callHook("skills:resolve", { skills: resolvedSkills });
|
|
2777
|
-
const skillsToolRegistered = skillsConfig?.tool !== false && resolvedSkills.length > 0;
|
|
2778
|
-
const catalogCtx = {
|
|
2779
|
-
catalog: buildCatalog(resolvedSkills, { skillsToolRegistered }),
|
|
2780
|
-
skills: resolvedSkills
|
|
2781
|
-
};
|
|
2782
|
-
await hooks.callHook("skills:catalog", catalogCtx);
|
|
2783
|
-
skillsCatalog = catalogCtx.catalog;
|
|
2784
|
-
}
|
|
2785
|
-
if (resolvedSkills && session && session.turns.length > 0 && skillActivationState.active().length === 0) {
|
|
2786
|
-
const skillsByName = new Map(resolvedSkills.map((s) => [s.name, s]));
|
|
2787
|
-
for (const turn of session.turns) {
|
|
2788
|
-
if (turn.role !== "assistant")
|
|
2789
|
-
continue;
|
|
2790
|
-
for (const block of turn.content) {
|
|
2791
|
-
if (block.type !== "tool_call" || block.name !== "skills_use")
|
|
2792
|
-
continue;
|
|
2793
|
-
const skillName = block.input?.name;
|
|
2794
|
-
if (!skillName)
|
|
2795
|
-
continue;
|
|
2796
|
-
const skill = skillsByName.get(skillName);
|
|
2797
|
-
if (!skill)
|
|
2798
|
-
continue;
|
|
2799
|
-
if (skillActivationState.activate(skill, "resume") === "ok") {
|
|
2800
|
-
await hooks.callHook("skills:activate", { skill, via: "resume" });
|
|
2801
|
-
}
|
|
2802
|
-
}
|
|
2803
|
-
}
|
|
2804
|
-
}
|
|
2805
|
-
const thinking = options.thinking ?? "off";
|
|
2806
|
-
const model = options.model ?? provider.meta.defaultModel;
|
|
2807
|
-
const resolvedBehavior = resolveBehavior(agentBehavior, options.behavior);
|
|
2808
|
-
const { toolExecution, maxTurns, maxTokens, thinkingBudget, schema, cache, toolOutputBudget, compactStrategy, compactThreshold, compactKeepTurns, thinkingDecay, dedupTools, toolBudgets, elideStaleReads, toolDisclosure, toolSearch } = resolvedBehavior;
|
|
2809
|
-
let system = options.system || agentSystem || "You are a helpful assistant.";
|
|
2810
|
-
if (skillsCatalog) {
|
|
2811
|
-
system = `${system}
|
|
2812
|
-
|
|
2813
|
-
${skillsCatalog}`;
|
|
2814
|
-
}
|
|
2815
|
-
const runBaseTools = options.tools !== void 0 ? options.tools : mcpConnection ? { ...sourceTools, ...mcpConnection.tools } : sourceTools;
|
|
2816
|
-
const mcpToolNames = options.tools === void 0 && mcpConnection ? new Set(Object.keys(mcpConnection.tools)) : /* @__PURE__ */ new Set();
|
|
2817
|
-
const shouldInjectSkillTools = options.tools === void 0 && !!resolvedSkills && resolvedSkills.length > 0 && skillsConfig?.tool !== false;
|
|
2818
|
-
const mergedWithSkills = shouldInjectSkillTools ? {
|
|
2819
|
-
// Auto-injected first so agent + MCP tools can override by name.
|
|
2820
|
-
skills_use: createSkillsUseTool({
|
|
2821
|
-
catalog: resolvedSkills,
|
|
2822
|
-
state: skillActivationState,
|
|
2823
|
-
hooks
|
|
2824
|
-
}),
|
|
2825
|
-
skills_read: createSkillsReadTool({
|
|
2826
|
-
catalog: resolvedSkills,
|
|
2827
|
-
state: skillActivationState
|
|
2828
|
-
}),
|
|
2829
|
-
skills_run_script: createSkillsRunScriptTool({
|
|
2830
|
-
catalog: resolvedSkills,
|
|
2831
|
-
state: skillActivationState,
|
|
2832
|
-
scriptTimeoutMs: skillsConfig?.scriptTimeoutMs
|
|
2833
|
-
}),
|
|
2834
|
-
...runBaseTools
|
|
2835
|
-
} : runBaseTools;
|
|
2836
|
-
const toolsPreSearch = {};
|
|
2837
|
-
for (const tool of Object.values(mergedWithSkills)) {
|
|
2838
|
-
toolsPreSearch[tool.spec.name] = tool;
|
|
2839
|
-
}
|
|
2840
|
-
const disclosure = partitionToolDisclosure(toolsPreSearch, mcpToolNames, mcpServers, toolDisclosure, toolAliases);
|
|
2841
|
-
const unlocked = new Set(disclosure.eagerCanonicalNames);
|
|
2842
|
-
const hostDefinedToolSearch = !!toolsPreSearch.tool_search;
|
|
2843
|
-
const shouldInjectToolSearch = disclosure.lazyEntries.length > 0 && toolSearch?.tool !== false && !hostDefinedToolSearch;
|
|
2844
|
-
let tools = toolsPreSearch;
|
|
2845
|
-
if (shouldInjectToolSearch) {
|
|
2846
|
-
const toolSearchTool = createToolSearchTool({
|
|
2847
|
-
catalog: disclosure.lazyEntries,
|
|
2848
|
-
unlocked,
|
|
2849
|
-
...toolSearch?.limit !== void 0 ? { defaultLimit: toolSearch.limit } : {}
|
|
2850
|
-
});
|
|
2851
|
-
tools = { ...toolsPreSearch, [toolSearchTool.spec.name]: toolSearchTool };
|
|
2852
|
-
unlocked.add(toolSearchTool.spec.name);
|
|
2853
|
-
}
|
|
2854
|
-
const discoveryToolName = shouldInjectToolSearch ? "tool_search" : hostDefinedToolSearch ? toolAliases?.tool_search ?? "tool_search" : null;
|
|
2855
|
-
if (disclosure.lazyEntries.length > 0) {
|
|
2856
|
-
system = `${system}
|
|
2857
|
-
|
|
2858
|
-
${buildSearchableCatalog(disclosure.lazyEntries, { discoveryToolName })}`;
|
|
2859
|
-
}
|
|
2860
|
-
const aliasMaps = buildAliasMaps(toolAliases, Object.keys(tools));
|
|
2861
|
-
const uninstallLazyDisclosureGate = installLazyDisclosureGate(
|
|
2862
|
-
hooks,
|
|
2863
|
-
disclosure.lazyCanonicalNames,
|
|
2864
|
-
unlocked,
|
|
2865
|
-
discoveryToolName
|
|
2866
|
-
);
|
|
2867
|
-
function buildFormattedTools() {
|
|
2868
|
-
const specs = [];
|
|
2869
|
-
for (const t of Object.values(tools)) {
|
|
2870
|
-
if (!unlocked.has(t.spec.name))
|
|
2871
|
-
continue;
|
|
2872
|
-
specs.push({
|
|
2873
|
-
name: aliasMaps.aliasByCanonical.get(t.spec.name) ?? t.spec.name,
|
|
2874
|
-
description: t.spec.description || "",
|
|
2875
|
-
inputSchema: t.spec.inputSchema
|
|
2876
|
-
});
|
|
2877
|
-
}
|
|
2878
|
-
return specs.length > 0 ? provider.formatTools(specs) : [];
|
|
2879
|
-
}
|
|
2880
|
-
const formattedTools = buildFormattedTools();
|
|
2881
|
-
const turns = [];
|
|
2882
|
-
const isResume = session && session.turns.length > 0 && (session.runs.length > 0 || !options.prompt);
|
|
2883
|
-
if (isResume) {
|
|
2884
|
-
turns.push(...session.turns);
|
|
2885
|
-
}
|
|
2886
|
-
const runTurnStart = turns.length;
|
|
2887
|
-
if (options.system) {
|
|
2888
|
-
await hooks.callHook("system:before", { system: options.system });
|
|
2889
|
-
}
|
|
2890
|
-
const promptParts = canonicalizePrompt(options.prompt);
|
|
2891
|
-
if (promptParts) {
|
|
2892
|
-
const promptMsg = buildPromptMessage(provider, promptParts);
|
|
2893
|
-
turns.push({
|
|
2894
|
-
id: crypto.randomUUID(),
|
|
2895
|
-
runId,
|
|
2896
|
-
role: promptMsg.role,
|
|
2897
|
-
content: promptMsg.content,
|
|
2898
|
-
createdAt: Date.now()
|
|
2899
|
-
});
|
|
2900
|
-
}
|
|
2901
|
-
conversationTurns = turns;
|
|
2902
|
-
let lastPersistedTurnCount = isResume ? session.turns.length : 0;
|
|
2903
|
-
if (session && turns.length > lastPersistedTurnCount) {
|
|
2904
|
-
const seededTurns = turns.slice(lastPersistedTurnCount);
|
|
2905
|
-
await session.appendTurns(seededTurns);
|
|
2906
|
-
lastPersistedTurnCount = turns.length;
|
|
2907
|
-
await hooks.callHook("session:turns", { sessionId: session.id, turns: seededTurns, count: turns.length });
|
|
2908
|
-
}
|
|
2909
|
-
const unregisterSessionSync = session ? hooks.hook("turn:after", async () => {
|
|
2910
|
-
const newTurns = turns.slice(lastPersistedTurnCount);
|
|
2911
|
-
if (newTurns.length > 0) {
|
|
2912
|
-
await session.appendTurns(newTurns);
|
|
2913
|
-
lastPersistedTurnCount = turns.length;
|
|
2914
|
-
await hooks.callHook("session:turns", { sessionId: session.id, turns: newTurns, count: turns.length });
|
|
2915
|
-
}
|
|
2916
|
-
}) : void 0;
|
|
2917
|
-
async function flushTurns() {
|
|
2918
|
-
if (!session)
|
|
2919
|
-
return;
|
|
2920
|
-
const remaining = turns.slice(lastPersistedTurnCount);
|
|
2921
|
-
if (remaining.length > 0) {
|
|
2922
|
-
await session.appendTurns(remaining);
|
|
2923
|
-
lastPersistedTurnCount = turns.length;
|
|
2924
|
-
await hooks.callHook("session:turns", { sessionId: session.id, turns: remaining, count: turns.length });
|
|
2925
|
-
}
|
|
2926
|
-
}
|
|
2927
|
-
async function deactivateAllSkills() {
|
|
2928
|
-
for (const record of skillActivationState.clear())
|
|
2929
|
-
await hooks.callHook("skills:deactivate", { skill: record.skill, reason: "run-end" });
|
|
2930
|
-
}
|
|
2931
|
-
async function finalizeSession(status) {
|
|
2932
|
-
if (!session)
|
|
2933
|
-
return;
|
|
2934
|
-
const run2 = session.runs.find((r) => r.id === runId);
|
|
2935
|
-
if (run2)
|
|
2936
|
-
await session.updateRun(run2);
|
|
2937
|
-
await session.updateStatus(status === "aborted" ? "idle" : status);
|
|
2938
|
-
await hooks.callHook("session:end", { sessionId: session.id, runId, status, turnRange: [runTurnStart, turns.length - 1] });
|
|
2939
|
-
}
|
|
2940
|
-
const uninstallAllowedToolsGate = installAllowedToolsGate(hooks, skillActivationState);
|
|
2941
|
-
const uninstallToolBudgets = installToolBudgetsGate(
|
|
2942
|
-
hooks,
|
|
2943
|
-
() => toolBudgets,
|
|
2944
|
-
(msg) => steeringQueue.push(msg)
|
|
2945
|
-
);
|
|
2946
|
-
const uninstallDedupTools = installDedupToolsGate(
|
|
2947
|
-
hooks,
|
|
2948
|
-
() => dedupTools,
|
|
2949
|
-
() => session ?? void 0
|
|
2950
|
-
);
|
|
2951
|
-
const runStartMs = Date.now();
|
|
2952
|
-
const runDepth = typeof options.depth === "number" ? options.depth : 0;
|
|
2953
|
-
try {
|
|
2954
|
-
const stats = await runLoop({
|
|
2955
|
-
provider,
|
|
2956
|
-
hooks,
|
|
2957
|
-
agentName,
|
|
2958
|
-
agentSystem,
|
|
2959
|
-
agentTools: sourceTools,
|
|
2960
|
-
agentToolAliases: toolAliases,
|
|
2961
|
-
agentMcpServers: mcpServers,
|
|
2962
|
-
agentSkills,
|
|
2963
|
-
// Forward the resolved view (agent + run merged) so per-run overrides
|
|
2964
|
-
// of `dedupReads` / `requireReadBeforeEdit` / etc. are visible to
|
|
2965
|
-
// tools via `ToolContext.behavior`.
|
|
2966
|
-
agentBehavior: resolvedBehavior,
|
|
2967
|
-
tools,
|
|
2968
|
-
formattedTools,
|
|
2969
|
-
rebuildFormattedTools: disclosure.lazyEntries.length > 0 ? buildFormattedTools : void 0,
|
|
2970
|
-
aliasMaps,
|
|
2971
|
-
model,
|
|
2972
|
-
system,
|
|
2973
|
-
thinking,
|
|
2974
|
-
toolExecution,
|
|
2975
|
-
signal: abortController.signal,
|
|
2976
|
-
execution: executionContext,
|
|
2977
|
-
handle: executionHandle,
|
|
2978
|
-
steeringQueue,
|
|
2979
|
-
followUpQueue,
|
|
2980
|
-
turns,
|
|
2981
|
-
runId,
|
|
2982
|
-
generateTurnId: () => session?.generateTurnId() ?? crypto.randomUUID(),
|
|
2983
|
-
maxTurns,
|
|
2984
|
-
maxTokens,
|
|
2985
|
-
...session ? { session } : {},
|
|
2986
|
-
depth: runDepth,
|
|
2987
|
-
thinkingBudget,
|
|
2988
|
-
schema,
|
|
2989
|
-
cache,
|
|
2990
|
-
toolOutputBudget,
|
|
2991
|
-
compactStrategy,
|
|
2992
|
-
compactThreshold,
|
|
2993
|
-
compactKeepTurns,
|
|
2994
|
-
...elideStaleReads !== void 0 ? { elideStaleReads } : {},
|
|
2995
|
-
...thinkingDecay !== void 0 ? { thinkingDecay } : {},
|
|
2996
|
-
runStartMs,
|
|
2997
|
-
runToolCounts: {}
|
|
2998
|
-
});
|
|
2999
|
-
const parentTurnCost = stats.turnUsage?.reduce((sum, t) => sum + (t.cost ?? 0), 0) ?? 0;
|
|
3000
|
-
let childrenIn = 0;
|
|
3001
|
-
let childrenOut = 0;
|
|
3002
|
-
let childrenCost = 0;
|
|
3003
|
-
let childrenCacheRead = 0;
|
|
3004
|
-
let childrenCacheCreation = 0;
|
|
3005
|
-
for (const c of childrenStats) {
|
|
3006
|
-
childrenIn += c.stats.totalIn;
|
|
3007
|
-
childrenOut += c.stats.totalOut;
|
|
3008
|
-
childrenCost += c.stats.cost ?? 0;
|
|
3009
|
-
childrenCacheRead += c.stats.totalCacheRead;
|
|
3010
|
-
childrenCacheCreation += c.stats.totalCacheCreation;
|
|
3011
|
-
}
|
|
3012
|
-
const cumulativeCost = parentTurnCost + childrenCost;
|
|
3013
|
-
const finalStats = {
|
|
3014
|
-
...stats,
|
|
3015
|
-
totalIn: stats.totalIn + childrenIn,
|
|
3016
|
-
totalOut: stats.totalOut + childrenOut,
|
|
3017
|
-
totalCacheRead: stats.totalCacheRead + childrenCacheRead,
|
|
3018
|
-
totalCacheCreation: stats.totalCacheCreation + childrenCacheCreation,
|
|
3019
|
-
...cumulativeCost > 0 ? { cost: cumulativeCost } : {},
|
|
3020
|
-
children: childrenStats.length > 0 ? childrenStats : void 0
|
|
3021
|
-
};
|
|
3022
|
-
await flushTurns();
|
|
3023
|
-
if (abortController.signal.aborted) {
|
|
3024
|
-
session?.abortRun(runId);
|
|
3025
|
-
await finalizeSession("aborted");
|
|
3026
|
-
await hooks.callHook("agent:done", finalStats);
|
|
3027
|
-
return finalStats;
|
|
3028
|
-
}
|
|
3029
|
-
session?.completeRun(runId, {
|
|
3030
|
-
turns: stats.turns,
|
|
3031
|
-
tokensIn: stats.totalIn,
|
|
3032
|
-
tokensOut: stats.totalOut,
|
|
3033
|
-
turnUsage: stats.turnUsage,
|
|
3034
|
-
cost: parentTurnCost > 0 ? parentTurnCost : void 0
|
|
3035
|
-
});
|
|
3036
|
-
await finalizeSession("completed");
|
|
3037
|
-
await hooks.callHook("agent:done", finalStats);
|
|
3038
|
-
return finalStats;
|
|
3039
|
-
} catch (err) {
|
|
3040
|
-
await flushTurns();
|
|
3041
|
-
if (abortController.signal.aborted) {
|
|
3042
|
-
session?.abortRun(runId);
|
|
3043
|
-
await finalizeSession("aborted");
|
|
3044
|
-
const stats = {
|
|
3045
|
-
totalIn: 0,
|
|
3046
|
-
totalOut: 0,
|
|
3047
|
-
totalCacheRead: 0,
|
|
3048
|
-
totalCacheCreation: 0,
|
|
3049
|
-
turns: 0,
|
|
3050
|
-
elapsed: 0
|
|
3051
|
-
};
|
|
3052
|
-
await hooks.callHook("agent:done", stats);
|
|
3053
|
-
return stats;
|
|
3054
|
-
}
|
|
3055
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
3056
|
-
session?.errorRun(runId, message);
|
|
3057
|
-
await finalizeSession("error");
|
|
3058
|
-
throw err;
|
|
3059
|
-
} finally {
|
|
3060
|
-
await deactivateAllSkills();
|
|
3061
|
-
uninstallAllowedToolsGate();
|
|
3062
|
-
uninstallDedupTools();
|
|
3063
|
-
uninstallToolBudgets();
|
|
3064
|
-
uninstallLazyDisclosureGate();
|
|
3065
|
-
unregisterSpawnHook();
|
|
3066
|
-
unregisterSessionSync?.();
|
|
3067
|
-
for (const unregister of perRunUnregisters)
|
|
3068
|
-
unregister();
|
|
3069
|
-
running = false;
|
|
3070
|
-
abortController = void 0;
|
|
3071
|
-
steeringQueue.length = 0;
|
|
3072
|
-
followUpQueue.length = 0;
|
|
3073
|
-
idleResolve?.();
|
|
3074
|
-
idlePromise = void 0;
|
|
3075
|
-
idleResolve = void 0;
|
|
3076
|
-
}
|
|
3077
|
-
}
|
|
3078
|
-
function abort() {
|
|
3079
|
-
abortController?.abort();
|
|
3080
|
-
}
|
|
3081
|
-
function steer(message) {
|
|
3082
|
-
steeringQueue.push(message);
|
|
3083
|
-
}
|
|
3084
|
-
function followUpFn(message) {
|
|
3085
|
-
followUpQueue.push(message);
|
|
3086
|
-
}
|
|
3087
|
-
function waitForIdle() {
|
|
3088
|
-
return idlePromise ?? Promise.resolve();
|
|
3089
|
-
}
|
|
3090
|
-
async function reset() {
|
|
3091
|
-
if (running) {
|
|
3092
|
-
throw new Error(
|
|
3093
|
-
"Cannot reset() while the agent is running. Call `agent.abort()` and `await agent.waitForIdle()` first."
|
|
3094
|
-
);
|
|
3095
|
-
}
|
|
3096
|
-
conversationTurns = [];
|
|
3097
|
-
steeringQueue.length = 0;
|
|
3098
|
-
followUpQueue.length = 0;
|
|
3099
|
-
const cleared = skillActivationState.clear();
|
|
3100
|
-
for (const record of cleared)
|
|
3101
|
-
await hooks.callHook("skills:deactivate", { skill: record.skill, reason: "reset" });
|
|
3102
|
-
}
|
|
3103
|
-
async function activateSkill(name) {
|
|
3104
|
-
if (!resolvedSkills) {
|
|
3105
|
-
throw new Error(
|
|
3106
|
-
`Cannot activate skill "${name}" \u2014 skills have not been resolved yet. Call activateSkill after the first \`run()\`, or pass a skills config that resolves at agent-creation time.`
|
|
3107
|
-
);
|
|
3108
|
-
}
|
|
3109
|
-
const skill = resolvedSkills.find((s) => s.name === name);
|
|
3110
|
-
if (!skill) {
|
|
3111
|
-
const available = resolvedSkills.map((s) => s.name).join(", ") || "<none>";
|
|
3112
|
-
throw new Error(`Unknown skill "${name}". Available skills: ${available}.`);
|
|
3113
|
-
}
|
|
3114
|
-
const outcome = skillActivationState.activate(skill, "explicit");
|
|
3115
|
-
if (outcome === "cap-reached") {
|
|
3116
|
-
throw new Error(
|
|
3117
|
-
`Cannot activate skill "${name}" \u2014 the maxActive cap of ${skillsConfig?.maxActive} has been reached.`
|
|
3118
|
-
);
|
|
3119
|
-
}
|
|
3120
|
-
if (outcome === "ok")
|
|
3121
|
-
await hooks.callHook("skills:activate", { skill, via: "explicit" });
|
|
3122
|
-
}
|
|
3123
|
-
async function deactivateSkill(name) {
|
|
3124
|
-
const removed = skillActivationState.deactivate(name);
|
|
3125
|
-
if (removed)
|
|
3126
|
-
await hooks.callHook("skills:deactivate", { skill: removed.skill, reason: "explicit" });
|
|
3127
|
-
}
|
|
3128
|
-
if (session) {
|
|
3129
|
-
const originalSave = session.save.bind(session);
|
|
3130
|
-
const originalSetMeta = session.setMeta.bind(session);
|
|
3131
|
-
session.save = async () => {
|
|
3132
|
-
await originalSave();
|
|
3133
|
-
await hooks.callHook("session:save", { sessionId: session.id });
|
|
3134
|
-
};
|
|
3135
|
-
session.setMeta = (key, value) => {
|
|
3136
|
-
originalSetMeta(key, value);
|
|
3137
|
-
void Promise.resolve(hooks.callHook("session:meta", { sessionId: session.id, key, value })).catch((err) => {
|
|
3138
|
-
console.error("[zidane] session:meta listener rejected:", err);
|
|
3139
|
-
});
|
|
3140
|
-
};
|
|
3141
|
-
}
|
|
3142
|
-
let destroyed = false;
|
|
3143
|
-
async function warmup() {
|
|
3144
|
-
if (destroyed)
|
|
3145
|
-
return;
|
|
3146
|
-
if (mcpConnection || allMcpServers.length === 0)
|
|
3147
|
-
return;
|
|
3148
|
-
if (mcpWarmupPromise)
|
|
3149
|
-
return mcpWarmupPromise;
|
|
3150
|
-
mcpWarmupPromise = (async () => {
|
|
3151
|
-
const connection = mcpConnector ? await mcpConnector(allMcpServers) : await connectMcpServers(allMcpServers, void 0, hooks);
|
|
3152
|
-
if (destroyed) {
|
|
3153
|
-
await connection.close().catch(() => {
|
|
3154
|
-
});
|
|
3155
|
-
return;
|
|
3156
|
-
}
|
|
3157
|
-
mcpConnection = connection;
|
|
3158
|
-
})();
|
|
3159
|
-
try {
|
|
3160
|
-
await mcpWarmupPromise;
|
|
3161
|
-
} catch (err) {
|
|
3162
|
-
mcpWarmupPromise = null;
|
|
3163
|
-
throw err;
|
|
3164
|
-
}
|
|
3165
|
-
}
|
|
3166
|
-
async function destroy() {
|
|
3167
|
-
if (destroyed)
|
|
3168
|
-
return;
|
|
3169
|
-
destroyed = true;
|
|
3170
|
-
if (mcpWarmupPromise) {
|
|
3171
|
-
try {
|
|
3172
|
-
await mcpWarmupPromise;
|
|
3173
|
-
} catch {
|
|
3174
|
-
}
|
|
3175
|
-
}
|
|
3176
|
-
if (mcpConnection) {
|
|
3177
|
-
await mcpConnection.close();
|
|
3178
|
-
mcpConnection = null;
|
|
3179
|
-
}
|
|
3180
|
-
if (executionHandle) {
|
|
3181
|
-
await executionContext.destroy(executionHandle);
|
|
3182
|
-
executionHandle = null;
|
|
3183
|
-
}
|
|
3184
|
-
skillsCleanup();
|
|
3185
|
-
skillsCleanup = () => {
|
|
3186
|
-
};
|
|
3187
|
-
}
|
|
3188
|
-
if (eager && allMcpServers.length > 0) {
|
|
3189
|
-
void warmup().catch(() => {
|
|
3190
|
-
});
|
|
3191
|
-
}
|
|
3192
|
-
return {
|
|
3193
|
-
hooks,
|
|
3194
|
-
run,
|
|
3195
|
-
abort,
|
|
3196
|
-
steer,
|
|
3197
|
-
followUp: followUpFn,
|
|
3198
|
-
waitForIdle,
|
|
3199
|
-
reset,
|
|
3200
|
-
destroy,
|
|
3201
|
-
warmup,
|
|
3202
|
-
activateSkill,
|
|
3203
|
-
deactivateSkill,
|
|
3204
|
-
get isRunning() {
|
|
3205
|
-
return running;
|
|
3206
|
-
},
|
|
3207
|
-
get turns() {
|
|
3208
|
-
return conversationTurns;
|
|
3209
|
-
},
|
|
3210
|
-
get execution() {
|
|
3211
|
-
return executionContext;
|
|
3212
|
-
},
|
|
3213
|
-
get handle() {
|
|
3214
|
-
return executionHandle;
|
|
3215
|
-
},
|
|
3216
|
-
get session() {
|
|
3217
|
-
return session ?? null;
|
|
3218
|
-
},
|
|
3219
|
-
get activeSkills() {
|
|
3220
|
-
return skillActivationState.active();
|
|
3221
|
-
},
|
|
3222
|
-
// Expose a frozen view of provider.meta. Hosts previously could mutate
|
|
3223
|
-
// the underlying provider meta (e.g. via `agent.meta.defaultModel = …`),
|
|
3224
|
-
// which quietly affected every other agent sharing the same provider
|
|
3225
|
-
// instance. Freezing forces callers to construct a new provider when
|
|
3226
|
-
// they want to override model/capabilities.
|
|
3227
|
-
meta: Object.freeze({ ...provider.meta })
|
|
3228
|
-
};
|
|
3229
|
-
}
|
|
3230
|
-
|
|
3231
|
-
// src/tools/spawn.ts
|
|
3232
|
-
var BUBBLED_EVENTS = [
|
|
3233
|
-
"stream:text",
|
|
3234
|
-
"stream:thinking",
|
|
3235
|
-
"stream:end",
|
|
3236
|
-
"tool:before",
|
|
3237
|
-
"tool:after",
|
|
3238
|
-
"tool:error",
|
|
3239
|
-
"turn:after"
|
|
3240
|
-
];
|
|
3241
|
-
var CHILD_EVENT_NAME = {
|
|
3242
|
-
"stream:text": "child:stream:text",
|
|
3243
|
-
"stream:thinking": "child:stream:thinking",
|
|
3244
|
-
"stream:end": "child:stream:end",
|
|
3245
|
-
"tool:before": "child:tool:before",
|
|
3246
|
-
"tool:after": "child:tool:after",
|
|
3247
|
-
"tool:error": "child:tool:error",
|
|
3248
|
-
"turn:after": "child:turn:after"
|
|
3249
|
-
};
|
|
3250
|
-
function extractText(message) {
|
|
3251
|
-
if (!message || typeof message !== "object")
|
|
3252
|
-
return "";
|
|
3253
|
-
const msg = message;
|
|
3254
|
-
if (typeof msg.content === "string")
|
|
3255
|
-
return msg.content;
|
|
3256
|
-
if (Array.isArray(msg.content)) {
|
|
3257
|
-
return msg.content.filter((block) => !!block && typeof block === "object" && block.type === "text").map((block) => block.text).join("\n");
|
|
3258
|
-
}
|
|
3259
|
-
return "";
|
|
3260
|
-
}
|
|
3261
|
-
async function raceWithTimeout(task, timeoutMs) {
|
|
3262
|
-
if (!timeoutMs || timeoutMs <= 0)
|
|
3263
|
-
return task;
|
|
3264
|
-
let timer;
|
|
3265
|
-
try {
|
|
3266
|
-
return await new Promise((resolve2, reject) => {
|
|
3267
|
-
timer = setTimeout(() => reject(new SpawnTimeoutError(timeoutMs)), timeoutMs);
|
|
3268
|
-
task.then(resolve2, reject);
|
|
3269
|
-
});
|
|
3270
|
-
} finally {
|
|
3271
|
-
if (timer)
|
|
3272
|
-
clearTimeout(timer);
|
|
3273
|
-
}
|
|
3274
|
-
}
|
|
3275
|
-
var SpawnTimeoutError = class extends Error {
|
|
3276
|
-
timeoutMs;
|
|
3277
|
-
constructor(timeoutMs) {
|
|
3278
|
-
super(`Child agent timed out after ${timeoutMs}ms`);
|
|
3279
|
-
this.name = "SpawnTimeoutError";
|
|
3280
|
-
this.timeoutMs = timeoutMs;
|
|
3281
|
-
}
|
|
3282
|
-
};
|
|
3283
|
-
function bubbleHooks(childHooks, parentHooks, childId, depth) {
|
|
3284
|
-
const unregisters = [];
|
|
3285
|
-
const fire = parentHooks.callHook;
|
|
3286
|
-
for (const evt of BUBBLED_EVENTS) {
|
|
3287
|
-
const parentEvt = CHILD_EVENT_NAME[evt];
|
|
3288
|
-
const unregister = childHooks.hook(evt, (ctx) => {
|
|
3289
|
-
void fire(parentEvt, { ...ctx, childId, depth });
|
|
3290
|
-
});
|
|
3291
|
-
unregisters.push(unregister);
|
|
3292
|
-
}
|
|
3293
|
-
for (const evt of BUBBLED_EVENTS) {
|
|
3294
|
-
const parentEvt = CHILD_EVENT_NAME[evt];
|
|
3295
|
-
const unregister = childHooks.hook(parentEvt, (ctx) => {
|
|
3296
|
-
void fire(parentEvt, ctx);
|
|
3297
|
-
});
|
|
3298
|
-
unregisters.push(unregister);
|
|
3299
|
-
}
|
|
3300
|
-
return () => {
|
|
3301
|
-
for (const u of unregisters) u();
|
|
3302
|
-
};
|
|
3303
|
-
}
|
|
3304
|
-
function createSpawnTool(options = {}) {
|
|
3305
|
-
const localChildren = /* @__PURE__ */ new Map();
|
|
3306
|
-
let localCounter = 0;
|
|
3307
|
-
let localActiveCount = 0;
|
|
3308
|
-
const maxConcurrent = options.maxConcurrent ?? 3;
|
|
3309
|
-
const maxDepth = options.maxDepth ?? 3;
|
|
3310
|
-
const forwardHooks = options.forwardHooks ?? true;
|
|
3311
|
-
const localStats = {
|
|
3312
|
-
totalIn: 0,
|
|
3313
|
-
totalOut: 0,
|
|
3314
|
-
totalCacheRead: 0,
|
|
3315
|
-
totalCacheCreation: 0,
|
|
3316
|
-
turns: 0,
|
|
3317
|
-
elapsed: 0
|
|
3318
|
-
};
|
|
3319
|
-
return {
|
|
3320
|
-
get children() {
|
|
3321
|
-
return localChildren;
|
|
3322
|
-
},
|
|
3323
|
-
get totalChildStats() {
|
|
3324
|
-
return { ...localStats };
|
|
3325
|
-
},
|
|
3326
|
-
spec: {
|
|
3327
|
-
name: "spawn",
|
|
3328
|
-
description: "Spawn a sub-agent for a self-contained task that benefits from isolation (separate context window, separate retries) \u2014 for example, a deep research dive or a long codegen pass on a specific file. The sub-agent runs independently with its own tool access and returns its final response. Do NOT spawn for sequential steps you could do yourself.",
|
|
3329
|
-
inputSchema: {
|
|
3330
|
-
type: "object",
|
|
3331
|
-
properties: {
|
|
3332
|
-
task: {
|
|
3333
|
-
type: "string",
|
|
3334
|
-
description: "The task prompt for the sub-agent. Be specific about what you want it to accomplish."
|
|
3335
|
-
},
|
|
3336
|
-
system: {
|
|
3337
|
-
type: "string",
|
|
3338
|
-
description: "Optional system prompt override for this specific sub-agent."
|
|
3339
|
-
}
|
|
3340
|
-
},
|
|
3341
|
-
required: ["task"]
|
|
3342
|
-
}
|
|
3343
|
-
},
|
|
3344
|
-
async execute(input, ctx) {
|
|
3345
|
-
const task = input.task;
|
|
3346
|
-
const systemOverride = input.system;
|
|
3347
|
-
const parentDepth = ctx.depth ?? 0;
|
|
3348
|
-
const childDepth = parentDepth + 1;
|
|
3349
|
-
if (childDepth > maxDepth) {
|
|
3350
|
-
return `Cannot spawn: maxDepth=${maxDepth} reached (parent depth=${parentDepth}). Deepen the cap with createSpawnTool({ maxDepth }).`;
|
|
3351
|
-
}
|
|
3352
|
-
if (localActiveCount >= maxConcurrent) {
|
|
3353
|
-
return `Cannot spawn: ${localActiveCount}/${maxConcurrent} sub-agents already running. Wait for one to complete.`;
|
|
3354
|
-
}
|
|
3355
|
-
if (ctx.signal.aborted) {
|
|
3356
|
-
return `[sub-agent pre-aborted] Parent signal was already aborted \u2014 skipped "${task.slice(0, 80)}"`;
|
|
3357
|
-
}
|
|
3358
|
-
const id = `child-${++localCounter}`;
|
|
3359
|
-
localActiveCount++;
|
|
3360
|
-
const child = { id, task, startedAt: Date.now(), depth: childDepth };
|
|
3361
|
-
localChildren.set(id, child);
|
|
3362
|
-
let destroyError;
|
|
3363
|
-
let childRunStatus = "completed";
|
|
3364
|
-
let finalStats;
|
|
3365
|
-
let result = "";
|
|
3366
|
-
let unbubble;
|
|
3367
|
-
try {
|
|
3368
|
-
const parentPreset = {
|
|
3369
|
-
...ctx.name !== void 0 ? { name: ctx.name } : {},
|
|
3370
|
-
...ctx.system !== void 0 ? { system: ctx.system } : {},
|
|
3371
|
-
tools: ctx.tools,
|
|
3372
|
-
...ctx.toolAliases !== void 0 ? { toolAliases: ctx.toolAliases } : {},
|
|
3373
|
-
...ctx.mcpServers !== void 0 ? { mcpServers: ctx.mcpServers } : {},
|
|
3374
|
-
...ctx.skills !== void 0 ? { skills: ctx.skills } : {},
|
|
3375
|
-
...ctx.behavior !== void 0 ? { behavior: ctx.behavior } : {}
|
|
3376
|
-
};
|
|
3377
|
-
const agent = createAgent({
|
|
3378
|
-
...parentPreset,
|
|
3379
|
-
...options.preset,
|
|
3380
|
-
provider: ctx.provider,
|
|
3381
|
-
execution: ctx.execution,
|
|
3382
|
-
// Share the parent's session on opt-in. Child turns get appended to
|
|
3383
|
-
// the same session.turns stream with the child's runId; the child
|
|
3384
|
-
// run itself is tagged with parentRunId below, via AgentRunOptions.
|
|
3385
|
-
...options.persist && ctx.session ? { session: ctx.session } : {}
|
|
3386
|
-
});
|
|
3387
|
-
if (forwardHooks)
|
|
3388
|
-
unbubble = bubbleHooks(agent.hooks, ctx.hooks, id, childDepth);
|
|
3389
|
-
options.onSpawn?.(child);
|
|
3390
|
-
await ctx.hooks.callHook("spawn:before", { id, task, depth: childDepth });
|
|
3391
|
-
const runPromise = agent.run({
|
|
3392
|
-
prompt: task,
|
|
3393
|
-
model: options.model,
|
|
3394
|
-
system: systemOverride ?? options.system,
|
|
3395
|
-
thinking: options.thinking,
|
|
3396
|
-
signal: ctx.signal,
|
|
3397
|
-
depth: childDepth,
|
|
3398
|
-
...options.persist && ctx.runId ? { parentRunId: ctx.runId } : {}
|
|
3399
|
-
});
|
|
3400
|
-
try {
|
|
3401
|
-
finalStats = await raceWithTimeout(runPromise, options.timeoutMs);
|
|
3402
|
-
const treeTurns = flattenTurns(finalStats).length;
|
|
3403
|
-
if (ctx.signal.aborted) {
|
|
3404
|
-
childRunStatus = "aborted";
|
|
3405
|
-
result = [
|
|
3406
|
-
`[sub-agent ${id}] Aborted after ${treeTurns} turns (${finalStats.elapsed}ms)`,
|
|
3407
|
-
`Tokens: ${finalStats.totalIn} in / ${finalStats.totalOut} out`
|
|
3408
|
-
].join("\n");
|
|
3409
|
-
} else {
|
|
3410
|
-
const response = extractText(agent.turns.at(-1));
|
|
3411
|
-
result = [
|
|
3412
|
-
`[sub-agent ${id}] Completed in ${treeTurns} turns (${finalStats.elapsed}ms)`,
|
|
3413
|
-
`Tokens: ${finalStats.totalIn} in / ${finalStats.totalOut} out`,
|
|
3414
|
-
"",
|
|
3415
|
-
response || "(no text response)"
|
|
3416
|
-
].join("\n");
|
|
3417
|
-
}
|
|
3418
|
-
} catch (err) {
|
|
3419
|
-
if (err instanceof SpawnTimeoutError) {
|
|
3420
|
-
childRunStatus = "timeout";
|
|
3421
|
-
agent.abort();
|
|
3422
|
-
try {
|
|
3423
|
-
finalStats = await runPromise;
|
|
3424
|
-
} catch {
|
|
3425
|
-
finalStats = {
|
|
3426
|
-
totalIn: 0,
|
|
3427
|
-
totalOut: 0,
|
|
3428
|
-
totalCacheRead: 0,
|
|
3429
|
-
totalCacheCreation: 0,
|
|
3430
|
-
turns: 0,
|
|
3431
|
-
elapsed: err.timeoutMs
|
|
3432
|
-
};
|
|
3433
|
-
}
|
|
3434
|
-
result = `[sub-agent ${id}] Timed out after ${err.timeoutMs}ms`;
|
|
3435
|
-
} else {
|
|
3436
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
3437
|
-
childRunStatus = "error";
|
|
3438
|
-
finalStats = {
|
|
3439
|
-
totalIn: 0,
|
|
3440
|
-
totalOut: 0,
|
|
3441
|
-
totalCacheRead: 0,
|
|
3442
|
-
totalCacheCreation: 0,
|
|
3443
|
-
turns: 0,
|
|
3444
|
-
elapsed: 0
|
|
3445
|
-
};
|
|
3446
|
-
result = `[sub-agent ${id}] Error: ${error.message}`;
|
|
3447
|
-
await ctx.hooks.callHook("spawn:error", { id, task, depth: childDepth, error });
|
|
3448
|
-
}
|
|
3449
|
-
} finally {
|
|
3450
|
-
try {
|
|
3451
|
-
await agent.destroy();
|
|
3452
|
-
} catch (err) {
|
|
3453
|
-
destroyError = err instanceof Error ? err : new Error(String(err));
|
|
3454
|
-
}
|
|
3455
|
-
}
|
|
3456
|
-
if (finalStats) {
|
|
3457
|
-
localStats.totalIn += finalStats.totalIn;
|
|
3458
|
-
localStats.totalOut += finalStats.totalOut;
|
|
3459
|
-
localStats.totalCacheRead += finalStats.totalCacheRead;
|
|
3460
|
-
localStats.totalCacheCreation += finalStats.totalCacheCreation;
|
|
3461
|
-
localStats.turns += finalStats.turns;
|
|
3462
|
-
localStats.elapsed += finalStats.elapsed;
|
|
3463
|
-
}
|
|
3464
|
-
const childRunStats = {
|
|
3465
|
-
id,
|
|
3466
|
-
task,
|
|
3467
|
-
stats: finalStats,
|
|
3468
|
-
depth: childDepth,
|
|
3469
|
-
status: childRunStatus,
|
|
3470
|
-
...finalStats.output ? { output: finalStats.output } : {}
|
|
3471
|
-
};
|
|
3472
|
-
options.onComplete?.(child, finalStats, childRunStatus);
|
|
3473
|
-
await ctx.hooks.callHook("spawn:complete", childRunStats);
|
|
3474
|
-
if (destroyError) {
|
|
3475
|
-
await ctx.hooks.callHook("spawn:error", {
|
|
3476
|
-
id,
|
|
3477
|
-
task,
|
|
3478
|
-
depth: childDepth,
|
|
3479
|
-
error: destroyError
|
|
3480
|
-
});
|
|
3481
|
-
}
|
|
3482
|
-
return result;
|
|
3483
|
-
} finally {
|
|
3484
|
-
unbubble?.();
|
|
3485
|
-
localActiveCount--;
|
|
3486
|
-
localChildren.delete(id);
|
|
3487
|
-
}
|
|
3488
|
-
}
|
|
3489
|
-
};
|
|
3490
|
-
}
|
|
3491
|
-
|
|
3492
|
-
// src/tools/write-file.ts
|
|
3493
|
-
import { Buffer as Buffer4 } from "buffer";
|
|
3494
|
-
var writeFile = {
|
|
3495
|
-
spec: {
|
|
3496
|
-
name: "write_file",
|
|
3497
|
-
description: 'Write content to a file (creates parent directories). Returns Created / Updated / "No change needed" so the model can detect no-ops without a separate read.',
|
|
3498
|
-
inputSchema: {
|
|
3499
|
-
type: "object",
|
|
3500
|
-
properties: {
|
|
3501
|
-
path: { type: "string", description: "Relative file path." },
|
|
3502
|
-
content: { type: "string", description: "File content." }
|
|
3503
|
-
},
|
|
3504
|
-
required: ["path", "content"]
|
|
3505
|
-
}
|
|
3506
|
-
},
|
|
3507
|
-
async execute({ path, content }, ctx) {
|
|
3508
|
-
const targetPath = path;
|
|
3509
|
-
const targetContent = content;
|
|
3510
|
-
let existing;
|
|
3511
|
-
try {
|
|
3512
|
-
existing = await ctx.execution.readFile(ctx.handle, targetPath);
|
|
3513
|
-
} catch {
|
|
3514
|
-
}
|
|
3515
|
-
const bytes = Buffer4.byteLength(targetContent);
|
|
3516
|
-
if (existing === targetContent)
|
|
3517
|
-
return `No change needed: ${targetPath} already at target state (${bytes} bytes).`;
|
|
3518
|
-
await ctx.execution.writeFile(ctx.handle, targetPath, targetContent);
|
|
3519
|
-
return existing === void 0 ? `Created ${targetPath} (${bytes} bytes).` : `Updated ${targetPath} (${bytes} bytes).`;
|
|
3520
|
-
}
|
|
3521
|
-
};
|
|
3522
|
-
|
|
3523
|
-
export {
|
|
3524
|
-
validateToolArgs,
|
|
3525
|
-
createSkillsReadTool,
|
|
3526
|
-
createSkillsRunScriptTool,
|
|
3527
|
-
createSkillsUseTool,
|
|
3528
|
-
createToolSearchTool,
|
|
3529
|
-
createAgent,
|
|
3530
|
-
edit,
|
|
3531
|
-
glob,
|
|
3532
|
-
grep,
|
|
3533
|
-
createInteractionTool,
|
|
3534
|
-
listFiles,
|
|
3535
|
-
multiEdit,
|
|
3536
|
-
readFile,
|
|
3537
|
-
shell,
|
|
3538
|
-
createSpawnTool,
|
|
3539
|
-
writeFile
|
|
3540
|
-
};
|