@livx.cc/agentx 0.99.38 → 0.99.40
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/dist/{Agent-DJwg1g8V.d.ts → Agent-D4-CLnxC.d.ts} +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +234 -31
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +33 -5
- package/dist/index.js +229 -31
- package/dist/index.js.map +1 -1
- package/dist/{mcp-Bd19NiCL.d.ts → mcp-aONOXRNk.d.ts} +4 -2
- package/dist/mcp.client.d.ts +2 -2
- package/dist/mcp.client.js +23 -3
- package/dist/mcp.client.js.map +1 -1
- package/dist/{tools-Byyh_bae.d.ts → tools-BWzPQVy0.d.ts} +15 -0
- package/dist/tools.shell.d.ts +36 -2
- package/dist/tools.shell.js +49 -4
- package/dist/tools.shell.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { IFilesystem } from '@livx.cc/wcli/core';
|
|
2
|
-
import { M as Message, H as HostBridge, A as AgentTool, C as ChatLike, g as MessageContent, U as UserQuestion } from './tools-
|
|
2
|
+
import { M as Message, H as HostBridge, A as AgentTool, C as ChatLike, g as MessageContent, U as UserQuestion } from './tools-BWzPQVy0.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Hooks — deterministic interception points around tool execution, run by the
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { H as Hooks, i as RunResult, R as ReasoningEffort, A as Agent } from './Agent-
|
|
2
|
+
import { H as Hooks, i as RunResult, R as ReasoningEffort, A as Agent } from './Agent-D4-CLnxC.js';
|
|
3
3
|
import { IFilesystem } from '@livx.cc/wcli/core';
|
|
4
|
-
import { M as Message, U as UserQuestion, H as HostBridge, c as ContentPart, g as MessageContent } from './tools-
|
|
4
|
+
import { M as Message, U as UserQuestion, H as HostBridge, c as ContentPart, g as MessageContent } from './tools-BWzPQVy0.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* On-disk session store for the CLI: each conversation is one JSON file at
|
package/dist/cli.js
CHANGED
|
@@ -258,13 +258,17 @@ function stripContent(content, stub) {
|
|
|
258
258
|
});
|
|
259
259
|
return changed ? out : content;
|
|
260
260
|
}
|
|
261
|
+
function clauses(sentence) {
|
|
262
|
+
return sentence.split(/[,;:]\s*|\s+(?:so|but|and|then|because|however|therefore|thus|although|though)\s+/i).map((c) => c.trim()).filter(Boolean);
|
|
263
|
+
}
|
|
261
264
|
function endsOnAnnouncedAction(text) {
|
|
262
265
|
const sentences = text.split(/(?<=[.!?])\s+|\n+/).map((s) => s.trim()).filter(Boolean);
|
|
263
266
|
const last = sentences[sentences.length - 1];
|
|
264
267
|
if (!last || NEXT_STEP.test(last) || SIGN_OFF.test(last)) return false;
|
|
265
|
-
|
|
268
|
+
if (PROGRESSIVE.test(last)) return true;
|
|
269
|
+
return clauses(last).some((c) => !NEXT_STEP.test(c) && !SIGN_OFF.test(c) && COMMITMENT.test(c));
|
|
266
270
|
}
|
|
267
|
-
var log2, p_sep, IMAGE_ELIDE_STUB, IMAGE_REF_SCHEME, REF_REGISTRY_MAX, refRegistry, isImageRefUrl, bearsImage,
|
|
271
|
+
var log2, p_sep, IMAGE_ELIDE_STUB, IMAGE_REF_SCHEME, REF_REGISTRY_MAX, refRegistry, isImageRefUrl, bearsImage, COMMITMENT, PROGRESSIVE, NEXT_STEP, SIGN_OFF;
|
|
268
272
|
var init_llm = __esm({
|
|
269
273
|
"src/llm.ts"() {
|
|
270
274
|
"use strict";
|
|
@@ -277,7 +281,8 @@ var init_llm = __esm({
|
|
|
277
281
|
refRegistry = /* @__PURE__ */ new Map();
|
|
278
282
|
isImageRefUrl = (url) => !!url && url.startsWith(IMAGE_REF_SCHEME);
|
|
279
283
|
bearsImage = (m) => messageHasImage(m.content) || toolImageRef(m.content) != null;
|
|
280
|
-
|
|
284
|
+
COMMITMENT = /^(?:also\s+|now\s+|then\s+|so\s+)*(?:let me\b(?!\s+know\b)|i'?ll\b|i will\b|i'?m going to\b|i\s+(?:still\s+|now\s+|first\s+)*need to\b)/i;
|
|
285
|
+
PROGRESSIVE = /^(?:also\s+|now\s+|then\s+)*(?:re-?)?(?:check|search|scan|read|verify|run|inspect|expand|look|examin|continu|proceed|try|fetch|load|open|test|build|grep|find|review|trac|dig|explor)\w*ing\b/i;
|
|
281
286
|
NEXT_STEP = /^next (?:step|run|up)\b/i;
|
|
282
287
|
SIGN_OFF = /^let me know\b/i;
|
|
283
288
|
}
|
|
@@ -902,6 +907,8 @@ function makeWebFetchTool(options = {}) {
|
|
|
902
907
|
return {
|
|
903
908
|
name: "WebFetch",
|
|
904
909
|
description: "Fetch an http/https URL and return its readable text (HTML is stripped to text). Use to read docs or web pages. Returns the status line then up to ~100k chars of content.",
|
|
910
|
+
// Declared so a transport above the loop sizes its deadline around this one (see AgentTool.maxDurationMs).
|
|
911
|
+
maxDurationMs: timeoutMs,
|
|
905
912
|
parameters: { type: "object", required: ["url"], properties: { url: { type: "string", description: "absolute http(s) URL" } } },
|
|
906
913
|
async run({ url }) {
|
|
907
914
|
const doFetch = options.fetch ?? globalThis.fetch;
|
|
@@ -2105,6 +2112,7 @@ var init_shell_sandbox = __esm({
|
|
|
2105
2112
|
var tools_shell_exports = {};
|
|
2106
2113
|
__export(tools_shell_exports, {
|
|
2107
2114
|
ShellJobRegistry: () => ShellJobRegistry,
|
|
2115
|
+
formatJobExit: () => formatJobExit,
|
|
2108
2116
|
makeRealShellTool: () => makeRealShellTool,
|
|
2109
2117
|
makeShellJobTools: () => makeShellJobTools
|
|
2110
2118
|
});
|
|
@@ -2135,12 +2143,27 @@ async function nodeSpawn() {
|
|
|
2135
2143
|
if (!_spawn) _spawn = (await import("child_process")).spawn;
|
|
2136
2144
|
return _spawn;
|
|
2137
2145
|
}
|
|
2146
|
+
function formatJobExit(n) {
|
|
2147
|
+
return `[background job ${n.id} ${n.status}${n.exitCode != null ? ` exit ${n.exitCode}` : ""}] \`${n.command}\`
|
|
2148
|
+
` + (n.tail ? `${n.tail}
|
|
2149
|
+
` : "(no output)\n") + `Read the full output with ShellOutput({id:"${n.id}"}).`;
|
|
2150
|
+
}
|
|
2138
2151
|
function makeRealShellTool(options) {
|
|
2139
2152
|
const defaultTimeoutMs = options.timeoutMs ?? 12e4;
|
|
2140
2153
|
const maxTimeoutMs = Math.max(options.maxTimeoutMs ?? 6e5, defaultTimeoutMs);
|
|
2154
|
+
const backgroundDoc = () => {
|
|
2155
|
+
const base = "Set `background:true` for long-running processes (servers, watchers) and for anything that already timed out in the foreground \u2014 returns a job id immediately; poll with ShellOutput/ShellStatus, stop with ShellKill. ";
|
|
2156
|
+
const notify = options.registry?.notifiesOnExit ? "Its completion is reported back to you when it finishes." : "NOTE: nothing will tell you when it finishes \u2014 poll ShellOutput/ShellStatus yourself, or you will never see its result.";
|
|
2157
|
+
const survival = options.jobsSurviveRun === false ? "The job does NOT survive the end of this run, so do not stop while you still need its result." : "It keeps running across turns.";
|
|
2158
|
+
return `${base}${notify} ${survival}`;
|
|
2159
|
+
};
|
|
2141
2160
|
return {
|
|
2142
2161
|
name: "Shell",
|
|
2143
|
-
description
|
|
2162
|
+
get description() {
|
|
2163
|
+
return `Run a shell command via /bin/sh in the working directory. Executes any installed binary \u2014 ls, cat, grep, git, bun, node, curl, scripts, etc. Returns combined stdout+stderr; non-zero exits are prefixed \`[exit N]\`. Runs non-interactively with no terminal (stdin is /dev/null): commands that prompt for input fail fast rather than hang \u2014 for privileged actions use a non-interactive flag (e.g. \`sudo -n\`), or ask the user to run the command themselves. Each command is killed after ${defaultTimeoutMs}ms (result \`[exit 124]\` with whatever output it produced) \u2014 pass \`timeoutMs\` (max ${maxTimeoutMs}) for a legitimately slower command, and always bound network commands yourself (e.g. \`curl -m 10\`). ` + backgroundDoc();
|
|
2164
|
+
},
|
|
2165
|
+
// Declared so transports above the agent loop can size their own deadline with headroom (see AgentTool.maxDurationMs).
|
|
2166
|
+
maxDurationMs: maxTimeoutMs,
|
|
2144
2167
|
parameters: {
|
|
2145
2168
|
type: "object",
|
|
2146
2169
|
required: ["command"],
|
|
@@ -2150,9 +2173,10 @@ function makeRealShellTool(options) {
|
|
|
2150
2173
|
timeoutMs: { type: "number", description: "wall-clock cap for THIS command, overriding the default (clamped to the tool max)" }
|
|
2151
2174
|
}
|
|
2152
2175
|
},
|
|
2153
|
-
async run({ command, background, timeoutMs: askedTimeoutMs }, ctx) {
|
|
2176
|
+
async run({ command, background, timeoutMs: askedTimeoutMs, timeout: aliasTimeout }, ctx) {
|
|
2154
2177
|
const cmd = String(command ?? "");
|
|
2155
|
-
const
|
|
2178
|
+
const asked = Number.isFinite(askedTimeoutMs) ? askedTimeoutMs : aliasTimeout;
|
|
2179
|
+
const timeoutMs = Number.isFinite(asked) && Number(asked) > 0 ? Math.min(Number(asked), maxTimeoutMs) : defaultTimeoutMs;
|
|
2156
2180
|
if (!cmd.trim()) return "[exit 1] empty command";
|
|
2157
2181
|
if (background) {
|
|
2158
2182
|
if (!options.registry) return "Error: background execution is not enabled in this host (no job registry).";
|
|
@@ -2241,8 +2265,9 @@ function makeRealShellTool(options) {
|
|
|
2241
2265
|
};
|
|
2242
2266
|
}
|
|
2243
2267
|
function reasonFor(timedOut, timeoutMs, body) {
|
|
2244
|
-
const head = timedOut ? `[exit 124] timed out after ${timeoutMs}ms (killed)
|
|
2268
|
+
const head = timedOut ? `[exit 124] timed out after ${timeoutMs}ms (killed). Re-running this command unchanged will time out again. Either re-run it with background:true (returns a job id immediately; poll with ShellOutput/ShellStatus, stop with ShellKill) or narrow it so it can finish \u2014 bound it, scope it, or ask for less.` : "[exit 130] cancelled (killed)";
|
|
2245
2269
|
return body ? `${head}
|
|
2270
|
+
Partial output before the kill:
|
|
2246
2271
|
${body}` : head;
|
|
2247
2272
|
}
|
|
2248
2273
|
function makeShellJobTools(registry) {
|
|
@@ -2323,12 +2348,14 @@ var init_tools_shell = __esm({
|
|
|
2323
2348
|
job.status = "error";
|
|
2324
2349
|
append(`
|
|
2325
2350
|
[error] ${err2?.message ?? err2}`);
|
|
2351
|
+
this.notifyExit(id, job);
|
|
2326
2352
|
}
|
|
2327
2353
|
});
|
|
2328
2354
|
proc.on("close", (code) => {
|
|
2329
2355
|
if (job.status === "running") {
|
|
2330
2356
|
job.status = "exited";
|
|
2331
2357
|
job.exitCode = code ?? void 0;
|
|
2358
|
+
this.notifyExit(id, job);
|
|
2332
2359
|
}
|
|
2333
2360
|
});
|
|
2334
2361
|
} catch (e) {
|
|
@@ -2338,6 +2365,29 @@ var init_tools_shell = __esm({
|
|
|
2338
2365
|
this.jobs.set(id, job);
|
|
2339
2366
|
return id;
|
|
2340
2367
|
}
|
|
2368
|
+
/** Fire `onExit` at most once per job, with the tail so the model can act without a second round-trip. */
|
|
2369
|
+
notified = /* @__PURE__ */ new Set();
|
|
2370
|
+
notifyExit(id, job) {
|
|
2371
|
+
if (this.notified.has(id) || !this.cfg.onExit) return;
|
|
2372
|
+
this.notified.add(id);
|
|
2373
|
+
try {
|
|
2374
|
+
this.cfg.onExit({ id, command: job.command, status: job.status, exitCode: job.exitCode, tail: clean(job.buf).slice(-4e3) });
|
|
2375
|
+
} catch {
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
/**
|
|
2379
|
+
* Wire (or rewire) the completion callback AFTER construction. A host that only gets its agent handle
|
|
2380
|
+
* once the Agent is constructed (the library's own `fullAgentOptions` preset, any embedder) could not
|
|
2381
|
+
* pass `onExit` up front, so background completions were silently REPL-only. Set it here instead.
|
|
2382
|
+
*/
|
|
2383
|
+
setOnExit(fn) {
|
|
2384
|
+
this.cfg.onExit = fn;
|
|
2385
|
+
}
|
|
2386
|
+
/** Whether a finished job will actually be reported to the model. The Shell tool's description reads
|
|
2387
|
+
* this so it can't promise a completion notice on a host that discards it. */
|
|
2388
|
+
get notifiesOnExit() {
|
|
2389
|
+
return !!this.cfg.onExit;
|
|
2390
|
+
}
|
|
2341
2391
|
/** Current tail output for a job (null = no such job). */
|
|
2342
2392
|
output(id) {
|
|
2343
2393
|
return this.jobs.get(id)?.buf ?? (this.jobs.has(id) ? "" : null);
|
|
@@ -3098,6 +3148,30 @@ async function boundedPool(items, limit, fn) {
|
|
|
3098
3148
|
await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, worker));
|
|
3099
3149
|
return out;
|
|
3100
3150
|
}
|
|
3151
|
+
var SUBAGENT_MAX_DURATION_MS = 18e5;
|
|
3152
|
+
function wallClock(parent, ms) {
|
|
3153
|
+
const ac = new AbortController();
|
|
3154
|
+
let fired = false;
|
|
3155
|
+
const onParent = () => ac.abort();
|
|
3156
|
+
const timer = setTimeout(() => {
|
|
3157
|
+
fired = true;
|
|
3158
|
+
ac.abort();
|
|
3159
|
+
}, ms);
|
|
3160
|
+
if (parent) {
|
|
3161
|
+
if (parent.aborted) ac.abort();
|
|
3162
|
+
else parent.addEventListener("abort", onParent, { once: true });
|
|
3163
|
+
}
|
|
3164
|
+
return {
|
|
3165
|
+
signal: ac.signal,
|
|
3166
|
+
timedOut: () => fired,
|
|
3167
|
+
dispose: () => {
|
|
3168
|
+
clearTimeout(timer);
|
|
3169
|
+
parent?.removeEventListener("abort", onParent);
|
|
3170
|
+
}
|
|
3171
|
+
};
|
|
3172
|
+
}
|
|
3173
|
+
var deadlineNote = (ms) => `
|
|
3174
|
+
[It was stopped by the sub-task wall-clock deadline of ${Math.round(ms / 1e3)}s \u2014 not by the user. Re-delegate a NARROWER sub-task (or continue from the trace); the same prompt will hit the same deadline again.]`;
|
|
3101
3175
|
function sanitizeSlug(raw) {
|
|
3102
3176
|
return raw.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/-+/g, "-").slice(0, 40);
|
|
3103
3177
|
}
|
|
@@ -3160,14 +3234,25 @@ function abortHint(label, res, file) {
|
|
|
3160
3234
|
}
|
|
3161
3235
|
async function runChildTraced(args) {
|
|
3162
3236
|
const { opts, label, agentType, prompt } = args;
|
|
3163
|
-
|
|
3237
|
+
const deadlineMs = opts.maxDurationMs ?? SUBAGENT_MAX_DURATION_MS;
|
|
3238
|
+
const wc = wallClock(args.signal, deadlineMs);
|
|
3239
|
+
args.childOpts.signal = wc.signal;
|
|
3164
3240
|
const file = opts.traceDir ? traceFileFor(opts.traceDir, label, args.childDepth) : void 0;
|
|
3165
3241
|
const meta = { label, agentType, prompt };
|
|
3166
3242
|
const child = new Agent(args.childOpts);
|
|
3167
3243
|
if (file) installTrace(child, file, meta);
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3244
|
+
let res;
|
|
3245
|
+
try {
|
|
3246
|
+
res = await child.run(prompt);
|
|
3247
|
+
} finally {
|
|
3248
|
+
wc.dispose();
|
|
3249
|
+
}
|
|
3250
|
+
if (file) flushTrace(file, meta, child.transcript, wc.timedOut() ? "deadline" : res.finishReason);
|
|
3251
|
+
let result = res.finishReason === "stop" ? res.text || `(child '${label}' finished with no summary; finishReason=${res.finishReason})` : abortHint(label, res, file);
|
|
3252
|
+
if (wc.timedOut()) {
|
|
3253
|
+
log5.warn(`sub-task '${label}' hit its ${deadlineMs}ms wall-clock deadline (steps=${res.steps})`);
|
|
3254
|
+
result += deadlineNote(deadlineMs);
|
|
3255
|
+
}
|
|
3171
3256
|
await opts.hooks?.onSubagentStop?.(result, { label, agentType });
|
|
3172
3257
|
return { res, result };
|
|
3173
3258
|
}
|
|
@@ -3194,7 +3279,7 @@ function makeTaskTool(opts) {
|
|
|
3194
3279
|
const maxDepth = opts.maxDepth ?? 2;
|
|
3195
3280
|
return {
|
|
3196
3281
|
name: "Task",
|
|
3197
|
-
description: 'Delegate a self-contained sub-task to a child agent over the same filesystem. It runs autonomously with its own step budget and returns a concise summary \u2014 use to isolate context-heavy work (broad search, a scoped refactor). Provide a short `description` and a full `prompt`. If it is interrupted or fails, the result is a STATUS HINT with a pointer to its full transcript (Read it to recover what it did, then decide whether to continue or restart) \u2014 do not assume an incomplete sub-task succeeded. Set `background: true` to run it detached (returns a job id to poll with JobOutput while you keep working); its file edits are overlay-isolated and commit when it finishes. Set `isolation: "worktree"` to run in a real git worktree (needed when the child uses real shell commands that must see/modify actual files) \u2014 slower (~200ms setup), auto-cleaned if no changes.',
|
|
3282
|
+
description: 'Delegate a self-contained sub-task to a child agent over the same filesystem. It runs autonomously with its own step budget and a wall-clock deadline, and returns a concise summary \u2014 use to isolate context-heavy work (broad search, a scoped refactor). Provide a short `description` and a full `prompt`. If it is interrupted or fails, the result is a STATUS HINT with a pointer to its full transcript (Read it to recover what it did, then decide whether to continue or restart) \u2014 do not assume an incomplete sub-task succeeded. Set `background: true` to run it detached (returns a job id to poll with JobOutput while you keep working); its file edits are overlay-isolated and commit when it finishes. Set `isolation: "worktree"` to run in a real git worktree (needed when the child uses real shell commands that must see/modify actual files) \u2014 slower (~200ms setup), auto-cleaned if no changes.',
|
|
3198
3283
|
parameters: {
|
|
3199
3284
|
type: "object",
|
|
3200
3285
|
required: ["description", "prompt"],
|
|
@@ -3206,6 +3291,12 @@ function makeTaskTool(opts) {
|
|
|
3206
3291
|
isolation: { type: "string", enum: ["overlay", "worktree"], description: 'isolation mode: "overlay" (default, VFS layer) or "worktree" (real git worktree for shell-heavy work)' }
|
|
3207
3292
|
}
|
|
3208
3293
|
},
|
|
3294
|
+
// Declared because it is ENFORCED (see runChildTraced) — a transport above the loop sizes its own
|
|
3295
|
+
// deadline around this one (AgentTool.maxDurationMs). Declaring an unenforced number would be a lie;
|
|
3296
|
+
// declaring nothing left `Task` invisible to toolTimeoutViolations, which is the same hole.
|
|
3297
|
+
maxDurationMs: opts.maxDurationMs ?? SUBAGENT_MAX_DURATION_MS,
|
|
3298
|
+
maxDurationEnforced: true,
|
|
3299
|
+
// the wall clock below ABORTS the child at this bound — it cannot be overrun, so the transport needs slack, not a doubling
|
|
3209
3300
|
async run({ description, prompt, agentType, background, isolation }, ctx) {
|
|
3210
3301
|
if (depth >= maxDepth) {
|
|
3211
3302
|
return `Error: Task depth limit reached (maxDepth ${maxDepth}). Cannot spawn another child agent \u2014 do this work directly instead.`;
|
|
@@ -3309,11 +3400,18 @@ function makeTaskBatchTool(opts) {
|
|
|
3309
3400
|
isolation: { type: "string", enum: ["overlay", "worktree"], description: 'isolation mode for ALL children: "overlay" (default) or "worktree" (real git worktree each)' }
|
|
3310
3401
|
}
|
|
3311
3402
|
},
|
|
3403
|
+
// Same enforced bound as `Task`, applied to the WHOLE batch (below), not per child — otherwise the
|
|
3404
|
+
// honest ceiling would be ceil(n/maxParallel) x the per-child deadline, i.e. unbounded in `tasks`.
|
|
3405
|
+
maxDurationMs: opts.maxDurationMs ?? SUBAGENT_MAX_DURATION_MS,
|
|
3406
|
+
maxDurationEnforced: true,
|
|
3407
|
+
// the wall clock below ABORTS the child at this bound — it cannot be overrun, so the transport needs slack, not a doubling
|
|
3312
3408
|
async run({ tasks, isolation }, ctx) {
|
|
3313
3409
|
if (depth >= maxDepth) return `Error: Task depth limit reached (maxDepth ${maxDepth}). Cannot spawn child agents \u2014 do this work directly instead.`;
|
|
3314
3410
|
const list = Array.isArray(tasks) ? tasks : [];
|
|
3315
3411
|
if (!list.length) return "Error: TaskBatch needs a non-empty `tasks` array.";
|
|
3316
3412
|
const useWorktree = isolation === "worktree";
|
|
3413
|
+
const batchDeadlineMs = opts.maxDurationMs ?? SUBAGENT_MAX_DURATION_MS;
|
|
3414
|
+
const batchClock = wallClock(ctx.signal, batchDeadlineMs);
|
|
3317
3415
|
const results = await boundedPool(list, maxParallel, async (t, i) => {
|
|
3318
3416
|
const label = String(t?.description ?? t?.agentType ?? `task ${i + 1}`);
|
|
3319
3417
|
let fs;
|
|
@@ -3332,12 +3430,12 @@ function makeTaskBatchTool(opts) {
|
|
|
3332
3430
|
if (typeof childOpts === "string") return { label, error: childOpts, ok: false };
|
|
3333
3431
|
try {
|
|
3334
3432
|
const childPrompt = wtHandle ? worktreePromptPrefix(wtHandle.branch) + String(t?.prompt ?? "") : String(t?.prompt ?? "");
|
|
3335
|
-
const { res, result } = await runChildTraced({ opts, childOpts, prompt: childPrompt, label, agentType: t?.agentType, childDepth: depth + 1, signal:
|
|
3433
|
+
const { res, result } = await runChildTraced({ opts, childOpts, prompt: childPrompt, label, agentType: t?.agentType, childDepth: depth + 1, signal: batchClock.signal });
|
|
3336
3434
|
return { label, text: result, overlay, wtHandle, ok: res.finishReason !== "error" && res.finishReason !== "aborted" };
|
|
3337
3435
|
} catch (e) {
|
|
3338
3436
|
return { label, error: e instanceof Error ? e.message : String(e), wtHandle, ok: false };
|
|
3339
3437
|
}
|
|
3340
|
-
});
|
|
3438
|
+
}).finally(() => batchClock.dispose());
|
|
3341
3439
|
const lines = [];
|
|
3342
3440
|
for (const r of results) {
|
|
3343
3441
|
if (r.ok && r.overlay) await r.overlay.commit().catch(() => {
|
|
@@ -3351,7 +3449,7 @@ function makeTaskBatchTool(opts) {
|
|
|
3351
3449
|
lines.push(`### ${lines.length + 1}. ${r.label}
|
|
3352
3450
|
${r.error ? `ERROR: ${r.error}` : r.text || "(no summary)"}${extra}`);
|
|
3353
3451
|
}
|
|
3354
|
-
return lines.join("\n\n");
|
|
3452
|
+
return lines.join("\n\n") + (batchClock.timedOut() ? deadlineNote(batchDeadlineMs) : "");
|
|
3355
3453
|
}
|
|
3356
3454
|
};
|
|
3357
3455
|
}
|
|
@@ -4115,7 +4213,17 @@ var Agent = class _Agent {
|
|
|
4115
4213
|
const frag = reasoningToChatFragment(o.model, o.reasoning);
|
|
4116
4214
|
const sentTools = o.model.startsWith("claude-code/") ? [] : wireTools;
|
|
4117
4215
|
const isCursorWithTools = o.model.startsWith("cursor/") && wireTools.length > 0;
|
|
4216
|
+
const hostToolTimeoutMs = o.providerOptions?.toolTimeoutMs;
|
|
4217
|
+
const derivedToolTimeoutMs = Math.max(TRANSPORT_TOOL_TIMEOUT_FLOOR_MS, ...this.activeTools.map(transportHeadroomMs));
|
|
4218
|
+
const effectiveToolTimeoutMs = typeof hostToolTimeoutMs === "number" ? Math.max(hostToolTimeoutMs, derivedToolTimeoutMs) : derivedToolTimeoutMs;
|
|
4219
|
+
if (isCursorWithTools) {
|
|
4220
|
+
if (typeof hostToolTimeoutMs === "number" && effectiveToolTimeoutMs !== hostToolTimeoutMs) {
|
|
4221
|
+
log6.warn(`providerOptions.toolTimeoutMs=${hostToolTimeoutMs}ms is below what the registered tools need \u2014 RAISED to ${effectiveToolTimeoutMs}ms. ${toolTimeoutViolations(this.activeTools, hostToolTimeoutMs).length} tool(s) could outlive it, and the transport would have destroyed their results. To lower it, lower the tools' own maxDurationMs (e.g. the Shell tool's maxTimeoutMs) instead.`);
|
|
4222
|
+
}
|
|
4223
|
+
for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs)) log6.warn(v);
|
|
4224
|
+
}
|
|
4118
4225
|
const cursorPo = isCursorWithTools ? {
|
|
4226
|
+
toolTimeoutMs: effectiveToolTimeoutMs,
|
|
4119
4227
|
toolExecutor: async (name, args) => {
|
|
4120
4228
|
const tc = { id: `cursor-${Date.now()}`, type: "function", function: { name, arguments: JSON.stringify(args) } };
|
|
4121
4229
|
return await this.dispatch(tc);
|
|
@@ -4217,14 +4325,12 @@ var Agent = class _Agent {
|
|
|
4217
4325
|
})() } }))
|
|
4218
4326
|
});
|
|
4219
4327
|
for (const a of delegatedTools) {
|
|
4220
|
-
const
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
})();
|
|
4227
|
-
this.transcript.push({ role: "tool", tool_call_id: a.id, content: cap > 0 && out.length > cap ? cropResult(out, cap) : out });
|
|
4328
|
+
const name = a.name || "tool";
|
|
4329
|
+
const r = delegatedResult(a.output);
|
|
4330
|
+
const text = r.images.length ? r.text : nonEmptyResult(isVacuous(a.output) ? "" : r.text, name, deliveryOf(a.output, a.status));
|
|
4331
|
+
const body = cap > 0 && text.length > cap ? cropResult(text, cap) : text;
|
|
4332
|
+
const content = r.images.length ? [{ type: "text", text: body }, ...r.images.map((i) => imagePart(`data:${i.mimeType};base64,${i.data}`))] : body;
|
|
4333
|
+
this.transcript.push({ role: "tool", tool_call_id: a.id, content });
|
|
4228
4334
|
}
|
|
4229
4335
|
}
|
|
4230
4336
|
const toolCalls = res.toolCalls ?? [];
|
|
@@ -4277,9 +4383,10 @@ var Agent = class _Agent {
|
|
|
4277
4383
|
const raw = await this.dispatch(tc);
|
|
4278
4384
|
let content;
|
|
4279
4385
|
if (typeof raw === "string") {
|
|
4280
|
-
content = raw;
|
|
4386
|
+
content = nonEmptyResult(raw, tc.function.name);
|
|
4281
4387
|
} else {
|
|
4282
|
-
const
|
|
4388
|
+
const hasImages = !!raw.images?.length;
|
|
4389
|
+
const parts = [{ type: "text", text: hasImages ? raw.text : nonEmptyResult(raw.text ?? "", tc.function.name) }];
|
|
4283
4390
|
for (const img of raw.images ?? []) parts.push(imagePart(`data:${img.mimeType};base64,${img.data}`));
|
|
4284
4391
|
content = parts;
|
|
4285
4392
|
}
|
|
@@ -4588,6 +4695,76 @@ var isAnthropicModel = (m) => !!m && (m.startsWith("anthropic/") || /(^|\/)claud
|
|
|
4588
4695
|
function estimateTokens(m, keepRecentImages = 1) {
|
|
4589
4696
|
return Math.ceil(sendBytes(m, keepRecentImages) / 4);
|
|
4590
4697
|
}
|
|
4698
|
+
var TRANSPORT_TOOL_TIMEOUT_FLOOR_MS = 18e5;
|
|
4699
|
+
function transportHeadroomMs(t) {
|
|
4700
|
+
const d = Number(t.maxDurationMs) || 0;
|
|
4701
|
+
if (!d) return 0;
|
|
4702
|
+
return t.maxDurationEnforced ? d + 6e4 : d * 2 + 6e4;
|
|
4703
|
+
}
|
|
4704
|
+
function toolTimeoutViolations(tools, transportMs) {
|
|
4705
|
+
const out = [];
|
|
4706
|
+
for (const t of tools) {
|
|
4707
|
+
const d = Number(t.maxDurationMs) || 0;
|
|
4708
|
+
if (d && d >= transportMs) {
|
|
4709
|
+
out.push(`tool-timeout layering VIOLATED: '${t.name}' may run up to ${d}ms but the transport deadline is ${transportMs}ms \u2014 the transport wins the race and the tool's real result is destroyed. Raise providerOptions.toolTimeoutMs above ${d}ms.`);
|
|
4710
|
+
}
|
|
4711
|
+
}
|
|
4712
|
+
return out;
|
|
4713
|
+
}
|
|
4714
|
+
function nonEmptyResult(out, name = "tool", delivery = "ran") {
|
|
4715
|
+
if (out.trim()) return out;
|
|
4716
|
+
if (delivery === "error") return `[${name}] failed with no error message \u2014 the tool reported an error but produced no detail. Its effect is UNKNOWN: verify the state before assuming it did or did not happen.`;
|
|
4717
|
+
if (delivery === "missing") {
|
|
4718
|
+
const base = `[${name}] returned no result. This is a transport failure, not an empty answer \u2014 the call's outcome is UNKNOWN, so do not assume it succeeded or failed.`;
|
|
4719
|
+
return name === "Shell" ? `${base} Do NOT re-issue the same long-running command: run it with background:true and poll ShellOutput/ShellStatus, or narrow it so it can finish.` : `${base} Retry it, or verify the state another way before continuing.`;
|
|
4720
|
+
}
|
|
4721
|
+
return `[${name}] ran and returned no output. The call completed \u2014 this is an empty answer, not a failure.`;
|
|
4722
|
+
}
|
|
4723
|
+
function isVacuous(v, seen = /* @__PURE__ */ new Set(), depth = 0) {
|
|
4724
|
+
if (v == null) return true;
|
|
4725
|
+
if (typeof v === "string") return v.trim() === "";
|
|
4726
|
+
if (typeof v !== "object") return false;
|
|
4727
|
+
if (seen.has(v) || depth >= 64) return false;
|
|
4728
|
+
seen.add(v);
|
|
4729
|
+
try {
|
|
4730
|
+
if (Array.isArray(v)) return v.every((x) => isVacuous(x, seen, depth + 1));
|
|
4731
|
+
const proto = Object.getPrototypeOf(v);
|
|
4732
|
+
if (proto !== Object.prototype && proto !== null) return false;
|
|
4733
|
+
const vals = Object.values(v);
|
|
4734
|
+
return vals.length === 0 || vals.every((x) => isVacuous(x, seen, depth + 1));
|
|
4735
|
+
} finally {
|
|
4736
|
+
seen.delete(v);
|
|
4737
|
+
}
|
|
4738
|
+
}
|
|
4739
|
+
function delegatedResult(output) {
|
|
4740
|
+
if (typeof output === "string") return { text: output, images: [] };
|
|
4741
|
+
if (output == null) return { text: "", images: [] };
|
|
4742
|
+
const isImg = (i) => i && typeof i.data === "string" && typeof i.mimeType === "string";
|
|
4743
|
+
const o = output;
|
|
4744
|
+
if (Array.isArray(o.images)) {
|
|
4745
|
+
const images = o.images.filter(isImg);
|
|
4746
|
+
if (images.length) return { text: typeof o.text === "string" ? o.text : "", images };
|
|
4747
|
+
}
|
|
4748
|
+
const blocks = Array.isArray(output) ? output : Array.isArray(o.content) ? o.content : void 0;
|
|
4749
|
+
if (blocks) {
|
|
4750
|
+
const images = blocks.filter((b) => b?.type === "image" && isImg(b)).map((b) => ({ mimeType: b.mimeType, data: b.data }));
|
|
4751
|
+
if (images.length) {
|
|
4752
|
+
const text = blocks.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n");
|
|
4753
|
+
return { text, images };
|
|
4754
|
+
}
|
|
4755
|
+
}
|
|
4756
|
+
try {
|
|
4757
|
+
return { text: JSON.stringify(output), images: [] };
|
|
4758
|
+
} catch {
|
|
4759
|
+
return { text: String(output), images: [] };
|
|
4760
|
+
}
|
|
4761
|
+
}
|
|
4762
|
+
function deliveryOf(output, status) {
|
|
4763
|
+
if (status === "error") return "error";
|
|
4764
|
+
if (output == null) return "missing";
|
|
4765
|
+
if (status != null && status !== "completed") return "missing";
|
|
4766
|
+
return "ran";
|
|
4767
|
+
}
|
|
4591
4768
|
function cropResult(result, cap) {
|
|
4592
4769
|
const head = result.slice(0, cap);
|
|
4593
4770
|
const nl = head.lastIndexOf("\n");
|
|
@@ -8333,13 +8510,29 @@ function toResult(result) {
|
|
|
8333
8510
|
}
|
|
8334
8511
|
return { text: JSON.stringify(result) };
|
|
8335
8512
|
}
|
|
8336
|
-
|
|
8513
|
+
var MCP_CALL_TIMEOUT_MS = 3e5;
|
|
8514
|
+
var TIMED_OUT = /* @__PURE__ */ Symbol("mcp-timeout");
|
|
8515
|
+
async function withDeadline(p, ms) {
|
|
8516
|
+
let t;
|
|
8517
|
+
try {
|
|
8518
|
+
return await Promise.race([p, new Promise((res) => {
|
|
8519
|
+
t = setTimeout(() => res(TIMED_OUT), ms);
|
|
8520
|
+
})]);
|
|
8521
|
+
} finally {
|
|
8522
|
+
clearTimeout(t);
|
|
8523
|
+
}
|
|
8524
|
+
}
|
|
8525
|
+
var mcpTimedOut = (name, ms = MCP_CALL_TIMEOUT_MS) => `[${name}] no response after ${ms / 1e3}s \u2014 the MCP server never answered. The call's outcome is UNKNOWN: do not assume it ran or that it failed. Verify the state (or pick a bounded/narrower call) before retrying.`;
|
|
8526
|
+
function mcpToolToAgentTool(spec, callTool, prefix = "mcp__", timeoutMs = MCP_CALL_TIMEOUT_MS) {
|
|
8337
8527
|
return {
|
|
8338
8528
|
name: `${prefix}${spec.name}`,
|
|
8339
8529
|
description: spec.description ?? `MCP tool ${spec.name}`,
|
|
8340
8530
|
parameters: spec.inputSchema ?? { type: "object", properties: {} },
|
|
8531
|
+
maxDurationMs: timeoutMs,
|
|
8341
8532
|
async run(args, _ctx) {
|
|
8342
|
-
const
|
|
8533
|
+
const raw = await withDeadline(callTool(spec.name, args ?? {}), timeoutMs);
|
|
8534
|
+
if (raw === TIMED_OUT) return mcpTimedOut(spec.name, timeoutMs);
|
|
8535
|
+
const r = toResult(raw);
|
|
8343
8536
|
return r.images?.length ? r : r.text;
|
|
8344
8537
|
}
|
|
8345
8538
|
};
|
|
@@ -8354,6 +8547,7 @@ function describeSpec(s) {
|
|
|
8354
8547
|
}
|
|
8355
8548
|
function makeMcpToolSearch(specs, callTool, options = {}) {
|
|
8356
8549
|
const maxResults = options.maxResults ?? 10;
|
|
8550
|
+
const callTimeoutMs = options.timeoutMs ?? MCP_CALL_TIMEOUT_MS;
|
|
8357
8551
|
const byName = new Map(specs.map((s) => [s.name, s]));
|
|
8358
8552
|
const catalogLine = `${specs.length} MCP tool(s) available \u2014 search by keyword, then call by exact name.`;
|
|
8359
8553
|
const searchTool = {
|
|
@@ -8379,10 +8573,13 @@ function makeMcpToolSearch(specs, callTool, options = {}) {
|
|
|
8379
8573
|
args: { type: "object", description: "arguments object for the tool (per its schema)" }
|
|
8380
8574
|
}
|
|
8381
8575
|
},
|
|
8576
|
+
maxDurationMs: callTimeoutMs,
|
|
8382
8577
|
async run({ name, args }) {
|
|
8383
8578
|
const n = String(name ?? "");
|
|
8384
8579
|
if (!byName.has(n)) return `Error: unknown MCP tool '${n}'. Use ToolSearch to find valid names.`;
|
|
8385
|
-
const
|
|
8580
|
+
const raw = await withDeadline(callTool(n, args ?? {}), callTimeoutMs);
|
|
8581
|
+
if (raw === TIMED_OUT) return mcpTimedOut(n, callTimeoutMs);
|
|
8582
|
+
const r = toResult(raw);
|
|
8386
8583
|
return r.images?.length ? r : r.text;
|
|
8387
8584
|
}
|
|
8388
8585
|
};
|
|
@@ -9185,14 +9382,20 @@ Reference files in them by their mount path (the left side).`;
|
|
|
9185
9382
|
const memoryWriteDir = memoryDir[0];
|
|
9186
9383
|
const hooks = o.learnFromMistakes && memoryWriteDir ? composeHooks(o.hooks, lessonCapture({ fs, dir: memoryWriteDir, minRepeats: 2 })) : o.hooks;
|
|
9187
9384
|
let realShell = [];
|
|
9385
|
+
const agentRef = {};
|
|
9188
9386
|
const useRealShell = o.realShell ?? !virtual;
|
|
9189
9387
|
if (useRealShell && !virtual && !isClaudeCode) {
|
|
9190
|
-
const jobs = new ShellJobRegistry({
|
|
9191
|
-
|
|
9388
|
+
const jobs = new ShellJobRegistry({
|
|
9389
|
+
cwd,
|
|
9390
|
+
killOnExit: true,
|
|
9391
|
+
osSandbox: o.osSandbox,
|
|
9392
|
+
onExit: (n) => agentRef.agent?.inject(formatJobExit(n))
|
|
9393
|
+
});
|
|
9394
|
+
realShell = [makeRealShellTool({ cwd, registry: jobs, osSandbox: o.osSandbox, jobsSurviveRun: o.singleRun !== true }), ...makeShellJobTools(jobs)];
|
|
9192
9395
|
}
|
|
9193
9396
|
const scratchDir = o.scratch ? o.scratchDir ?? (virtual ? `${cwd}/.agent/scratch` : `${tmpdir()}/agentx-scratch-${process.pid}`) : void 0;
|
|
9194
9397
|
const scratch = scratchDir ? new Scratch(fs, { dir: scratchDir }) : void 0;
|
|
9195
|
-
return new Agent({
|
|
9398
|
+
return agentRef.agent = new Agent({
|
|
9196
9399
|
ai: o.ai,
|
|
9197
9400
|
fs,
|
|
9198
9401
|
model: o.model ?? "anthropic/claude-sonnet-4-6",
|