@livx.cc/agentx 0.99.39 → 0.99.41

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/index.js CHANGED
@@ -260,13 +260,17 @@ function stripContent(content, stub) {
260
260
  });
261
261
  return changed ? out : content;
262
262
  }
263
+ function clauses(sentence) {
264
+ return sentence.split(/[,;:]\s*|\s+(?:so|but|and|then|because|however|therefore|thus|although|though)\s+/i).map((c) => c.trim()).filter(Boolean);
265
+ }
263
266
  function endsOnAnnouncedAction(text) {
264
267
  const sentences = text.split(/(?<=[.!?])\s+|\n+/).map((s) => s.trim()).filter(Boolean);
265
268
  const last = sentences[sentences.length - 1];
266
269
  if (!last || NEXT_STEP.test(last) || SIGN_OFF.test(last)) return false;
267
- return ANNOUNCED_ACTION.test(last);
270
+ if (PROGRESSIVE.test(last)) return true;
271
+ return clauses(last).some((c) => !NEXT_STEP.test(c) && !SIGN_OFF.test(c) && COMMITMENT.test(c));
268
272
  }
269
- var log2, p_sep, IMAGE_ELIDE_STUB, IMAGE_REF_SCHEME, REF_REGISTRY_MAX, refRegistry, isImageRefUrl, bearsImage, ANNOUNCED_ACTION, NEXT_STEP, SIGN_OFF;
273
+ var log2, p_sep, IMAGE_ELIDE_STUB, IMAGE_REF_SCHEME, REF_REGISTRY_MAX, refRegistry, isImageRefUrl, bearsImage, COMMITMENT, PROGRESSIVE, NEXT_STEP, SIGN_OFF;
270
274
  var init_llm = __esm({
271
275
  "src/llm.ts"() {
272
276
  "use strict";
@@ -279,7 +283,8 @@ var init_llm = __esm({
279
283
  refRegistry = /* @__PURE__ */ new Map();
280
284
  isImageRefUrl = (url) => !!url && url.startsWith(IMAGE_REF_SCHEME);
281
285
  bearsImage = (m) => messageHasImage(m.content) || toolImageRef(m.content) != null;
282
- ANNOUNCED_ACTION = /^(?:also\s+|now\s+|then\s+)*(?:let me\b|i'?ll\b|i will\b|i'?m going to\b|(?: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;
286
+ 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;
287
+ 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;
283
288
  NEXT_STEP = /^next (?:step|run|up)\b/i;
284
289
  SIGN_OFF = /^let me know\b/i;
285
290
  }
@@ -904,6 +909,8 @@ function makeWebFetchTool(options = {}) {
904
909
  return {
905
910
  name: "WebFetch",
906
911
  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.",
912
+ // Declared so a transport above the loop sizes its deadline around this one (see AgentTool.maxDurationMs).
913
+ maxDurationMs: timeoutMs,
907
914
  parameters: { type: "object", required: ["url"], properties: { url: { type: "string", description: "absolute http(s) URL" } } },
908
915
  async run({ url }) {
909
916
  const doFetch = options.fetch ?? globalThis.fetch;
@@ -1356,11 +1363,12 @@ function toolRegistry() {
1356
1363
  const all = [bashTool, readTool, editTool, grepTool, globTool, writeTool, multiEditTool, applyEditsTool, repoMapTool, reviewTool(), todoWriteTool, webFetchTool, webSearchTool, webSearchAnthropicTool];
1357
1364
  return Object.fromEntries(all.map((t) => [t.name, t]));
1358
1365
  }
1359
- function toolsByName(names) {
1366
+ function toolsByName(names, available) {
1360
1367
  const reg = toolRegistry();
1368
+ const extra = Object.fromEntries((available ?? []).map((t) => [t.name, t]));
1361
1369
  return names.map((n) => {
1362
- const t = reg[n];
1363
- if (!t) throw new Error(`unknown tool '${n}'. Known: ${Object.keys(reg).join(", ")}`);
1370
+ const t = extra[n] ?? reg[n];
1371
+ if (!t) throw new Error(`unknown tool '${n}'. Known: ${[.../* @__PURE__ */ new Set([...Object.keys(extra), ...Object.keys(reg)])].join(", ")}`);
1364
1372
  return t;
1365
1373
  });
1366
1374
  }
@@ -1941,6 +1949,7 @@ var init_shell_sandbox = __esm({
1941
1949
  var tools_shell_exports = {};
1942
1950
  __export(tools_shell_exports, {
1943
1951
  ShellJobRegistry: () => ShellJobRegistry,
1952
+ formatJobExit: () => formatJobExit,
1944
1953
  makeRealShellTool: () => makeRealShellTool,
1945
1954
  makeShellJobTools: () => makeShellJobTools
1946
1955
  });
@@ -1971,12 +1980,45 @@ async function nodeSpawn() {
1971
1980
  if (!_spawn) _spawn = (await import("child_process")).spawn;
1972
1981
  return _spawn;
1973
1982
  }
1983
+ function formatJobExit(n) {
1984
+ return `[background job ${n.id} ${n.status}${n.exitCode != null ? ` exit ${n.exitCode}` : ""}] \`${n.command}\`
1985
+ ` + (n.tail ? `${n.tail}
1986
+ ` : "(no output)\n") + `Read the full output with ShellOutput({id:"${n.id}"}).`;
1987
+ }
1974
1988
  function makeRealShellTool(options) {
1975
1989
  const defaultTimeoutMs = options.timeoutMs ?? 12e4;
1976
1990
  const maxTimeoutMs = Math.max(options.maxTimeoutMs ?? 6e5, defaultTimeoutMs);
1991
+ const backgroundDoc = () => {
1992
+ 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. ";
1993
+ 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.";
1994
+ 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.";
1995
+ return `${base}${notify} ${survival}`;
1996
+ };
1977
1997
  return {
1978
1998
  name: "Shell",
1979
- description: `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\`). Set \`background:true\` for long-running processes (servers, watchers) \u2014 returns a job id immediately; poll with ShellOutput, stop with ShellKill.`,
1999
+ // Rebind for an isolated child agent (git worktree): same policy/env/timeouts, new cwd. The
2000
+ // `registry` is deliberately dropped — it is bound to the PARENT's cwd (and, in hosts like
2001
+ // shraga-ee, to the parent's session), so a background job started from the child would run
2002
+ // outside the child's isolation and report into the parent. Background stays a parent capability
2003
+ // (`makeShellJobTools`' companions drop out of an isolated child entirely — see their `withCwd`).
2004
+ // Only `run` is rebuilt: everything else is carried over from THIS instance, because a host may
2005
+ // have mutated the tool AFTER construction (shraga-ee renames `Shell` -> `Bash` and its system
2006
+ // prompt says `Bash` everywhere). Rebuilding from `options` alone silently dropped that rename,
2007
+ // so a worktree child advertised `Shell` while being told to call `Bash` — an unknown-tool error
2008
+ // on its first call, and a hard failure for an agentType def whose allowlist names `Bash`.
2009
+ // `description` is the ONE thing taken from the rebound tool instead: it is derived from the
2010
+ // options and states whether `background:true` works — the rebind drops the registry, so carrying
2011
+ // the parent's text over would advertise a background capability the child does not have. (The
2012
+ // spread also collapses the getter below to a plain value, which is why this override is explicit.)
2013
+ withCwd(cwd) {
2014
+ const rebound = makeRealShellTool({ ...options, cwd, registry: void 0 });
2015
+ return { ...this, description: rebound.description, run: rebound.run, withCwd: rebound.withCwd };
2016
+ },
2017
+ get description() {
2018
+ 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();
2019
+ },
2020
+ // Declared so transports above the agent loop can size their own deadline with headroom (see AgentTool.maxDurationMs).
2021
+ maxDurationMs: maxTimeoutMs,
1980
2022
  parameters: {
1981
2023
  type: "object",
1982
2024
  required: ["command"],
@@ -2078,14 +2120,17 @@ function makeRealShellTool(options) {
2078
2120
  };
2079
2121
  }
2080
2122
  function reasonFor(timedOut, timeoutMs, body) {
2081
- const head = timedOut ? `[exit 124] timed out after ${timeoutMs}ms (killed)` : "[exit 130] cancelled (killed)";
2123
+ 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)";
2082
2124
  return body ? `${head}
2125
+ Partial output before the kill:
2083
2126
  ${body}` : head;
2084
2127
  }
2085
2128
  function makeShellJobTools(registry) {
2086
2129
  const idParam = { type: "object", properties: { id: { type: "string", description: "the job id from Shell({background:true})" } } };
2130
+ const dropOnRebind = { withCwd: () => void 0 };
2087
2131
  return [
2088
2132
  {
2133
+ ...dropOnRebind,
2089
2134
  name: "ShellOutput",
2090
2135
  description: "Read the accumulated output (tail) of a background Shell job by id.",
2091
2136
  parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
@@ -2098,6 +2143,7 @@ ${clean(out) || "(no output yet)"}`;
2098
2143
  }
2099
2144
  },
2100
2145
  {
2146
+ ...dropOnRebind,
2101
2147
  name: "ShellStatus",
2102
2148
  description: "Status of a background Shell job (running/exited/killed + exit code). Omit `id` to list all jobs.",
2103
2149
  parameters: idParam,
@@ -2111,6 +2157,7 @@ ${clean(out) || "(no output yet)"}`;
2111
2157
  }
2112
2158
  },
2113
2159
  {
2160
+ ...dropOnRebind,
2114
2161
  name: "ShellKill",
2115
2162
  description: "Stop a running background Shell job by id (SIGTERM).",
2116
2163
  parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
@@ -2160,12 +2207,14 @@ var init_tools_shell = __esm({
2160
2207
  job.status = "error";
2161
2208
  append(`
2162
2209
  [error] ${err?.message ?? err}`);
2210
+ this.notifyExit(id, job);
2163
2211
  }
2164
2212
  });
2165
2213
  proc.on("close", (code) => {
2166
2214
  if (job.status === "running") {
2167
2215
  job.status = "exited";
2168
2216
  job.exitCode = code ?? void 0;
2217
+ this.notifyExit(id, job);
2169
2218
  }
2170
2219
  });
2171
2220
  } catch (e) {
@@ -2175,6 +2224,29 @@ var init_tools_shell = __esm({
2175
2224
  this.jobs.set(id, job);
2176
2225
  return id;
2177
2226
  }
2227
+ /** Fire `onExit` at most once per job, with the tail so the model can act without a second round-trip. */
2228
+ notified = /* @__PURE__ */ new Set();
2229
+ notifyExit(id, job) {
2230
+ if (this.notified.has(id) || !this.cfg.onExit) return;
2231
+ this.notified.add(id);
2232
+ try {
2233
+ this.cfg.onExit({ id, command: job.command, status: job.status, exitCode: job.exitCode, tail: clean(job.buf).slice(-4e3) });
2234
+ } catch {
2235
+ }
2236
+ }
2237
+ /**
2238
+ * Wire (or rewire) the completion callback AFTER construction. A host that only gets its agent handle
2239
+ * once the Agent is constructed (the library's own `fullAgentOptions` preset, any embedder) could not
2240
+ * pass `onExit` up front, so background completions were silently REPL-only. Set it here instead.
2241
+ */
2242
+ setOnExit(fn) {
2243
+ this.cfg.onExit = fn;
2244
+ }
2245
+ /** Whether a finished job will actually be reported to the model. The Shell tool's description reads
2246
+ * this so it can't promise a completion notice on a host that discards it. */
2247
+ get notifiesOnExit() {
2248
+ return !!this.cfg.onExit;
2249
+ }
2178
2250
  /** Current tail output for a job (null = no such job). */
2179
2251
  output(id) {
2180
2252
  return this.jobs.get(id)?.buf ?? (this.jobs.has(id) ? "" : null);
@@ -2897,6 +2969,53 @@ ${sections.join("\n\n---\n\n")}`;
2897
2969
  init_tools();
2898
2970
  import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
2899
2971
  import { join as join3 } from "path";
2972
+
2973
+ // src/models.ts
2974
+ var MODEL_ALIASES = {
2975
+ fable: "claude-fable-5",
2976
+ "fable-5": "claude-fable-5",
2977
+ opus: "claude-opus-4-8",
2978
+ "opus-4-8": "claude-opus-4-8",
2979
+ "opus-4-7": "claude-opus-4-7",
2980
+ "opus-4-6": "claude-opus-4-6",
2981
+ sonnet: "claude-sonnet-4-6",
2982
+ haiku: "claude-haiku-4-5-20251001"
2983
+ };
2984
+ function resolveModelAlias(input) {
2985
+ const slash = input.indexOf("/");
2986
+ const prefix = slash === -1 ? "" : input.slice(0, slash + 1);
2987
+ const rest = slash === -1 ? input : input.slice(slash + 1);
2988
+ return prefix + (MODEL_ALIASES[rest.toLowerCase()] ?? rest);
2989
+ }
2990
+ function modelProvider(model) {
2991
+ const i = model.indexOf("/");
2992
+ if (i !== -1) return model.slice(0, i);
2993
+ if (/^claude-/i.test(model)) return "anthropic";
2994
+ return `?${model}`;
2995
+ }
2996
+ function modelShortLabel(model) {
2997
+ if (!model) return "default";
2998
+ const id = model.split("/").pop();
2999
+ return id.replace(/^claude-/, "").replace(/-\d.*$/, "");
3000
+ }
3001
+ function resolveModelSwitch(opts) {
3002
+ const { requested, current, prior, allowed } = opts;
3003
+ if (!requested) return { model: current, denied: false };
3004
+ if (allowed && !allowed.some((a) => modelShortLabel(a) === modelShortLabel(requested))) {
3005
+ return { model: current, denied: true };
3006
+ }
3007
+ const fmt = opts.format ?? ((from, to) => `_[switching from ${from} to ${to}]_
3008
+
3009
+ `);
3010
+ const changed = prior !== void 0 && modelShortLabel(prior) !== modelShortLabel(requested);
3011
+ return {
3012
+ model: requested,
3013
+ denied: false,
3014
+ notice: changed ? fmt(modelShortLabel(prior), modelShortLabel(requested)) : void 0
3015
+ };
3016
+ }
3017
+
3018
+ // src/subagent.ts
2900
3019
  init_OverlayFilesystem();
2901
3020
 
2902
3021
  // src/worktree.ts
@@ -3064,6 +3183,30 @@ async function boundedPool(items, limit, fn) {
3064
3183
  await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, worker));
3065
3184
  return out;
3066
3185
  }
3186
+ var SUBAGENT_MAX_DURATION_MS = 18e5;
3187
+ function wallClock(parent, ms) {
3188
+ const ac = new AbortController();
3189
+ let fired = false;
3190
+ const onParent = () => ac.abort();
3191
+ const timer = setTimeout(() => {
3192
+ fired = true;
3193
+ ac.abort();
3194
+ }, ms);
3195
+ if (parent) {
3196
+ if (parent.aborted) ac.abort();
3197
+ else parent.addEventListener("abort", onParent, { once: true });
3198
+ }
3199
+ return {
3200
+ signal: ac.signal,
3201
+ timedOut: () => fired,
3202
+ dispose: () => {
3203
+ clearTimeout(timer);
3204
+ parent?.removeEventListener("abort", onParent);
3205
+ }
3206
+ };
3207
+ }
3208
+ var deadlineNote = (ms) => `
3209
+ [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.]`;
3067
3210
  function sanitizeSlug(raw) {
3068
3211
  return raw.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/-+/g, "-").slice(0, 40);
3069
3212
  }
@@ -3081,6 +3224,14 @@ function createWorktreeFs(cwd, label, index) {
3081
3224
  function releaseWorktree(handle) {
3082
3225
  return cleanupWorktree(handle.repoRoot, handle.slug, { force: true });
3083
3226
  }
3227
+ function rebindTools(tools, root) {
3228
+ if (!tools || !root) return tools;
3229
+ return tools.flatMap((t) => {
3230
+ if (!t.withCwd) return [t];
3231
+ const rebound = t.withCwd(root);
3232
+ return rebound ? [rebound] : [];
3233
+ });
3234
+ }
3084
3235
  function worktreePromptPrefix(branch) {
3085
3236
  return `You are running in an isolated git worktree on branch "${branch}". When you finish making changes, commit them with a clear message before replying with your summary. Your worktree will be removed after you finish \u2014 only committed work survives (on the branch).
3086
3237
 
@@ -3126,33 +3277,52 @@ function abortHint(label, res, file) {
3126
3277
  }
3127
3278
  async function runChildTraced(args) {
3128
3279
  const { opts, label, agentType, prompt } = args;
3129
- if (args.signal) args.childOpts.signal = args.signal;
3280
+ const deadlineMs = opts.maxDurationMs ?? SUBAGENT_MAX_DURATION_MS;
3281
+ const wc = wallClock(args.signal, deadlineMs);
3282
+ args.childOpts.signal = wc.signal;
3130
3283
  const file = opts.traceDir ? traceFileFor(opts.traceDir, label, args.childDepth) : void 0;
3131
3284
  const meta = { label, agentType, prompt };
3132
3285
  const child = new Agent(args.childOpts);
3133
3286
  if (file) installTrace(child, file, meta);
3134
- const res = await child.run(prompt);
3135
- if (file) flushTrace(file, meta, child.transcript, res.finishReason);
3136
- const result = res.finishReason === "stop" ? res.text || `(child '${label}' finished with no summary; finishReason=${res.finishReason})` : abortHint(label, res, file);
3287
+ let res;
3288
+ try {
3289
+ res = await child.run(prompt);
3290
+ } finally {
3291
+ wc.dispose();
3292
+ }
3293
+ if (file) flushTrace(file, meta, child.transcript, wc.timedOut() ? "deadline" : res.finishReason);
3294
+ let result = res.finishReason === "stop" ? res.text || `(child '${label}' finished with no summary; finishReason=${res.finishReason})` : abortHint(label, res, file);
3295
+ if (wc.timedOut()) {
3296
+ log5.warn(`sub-task '${label}' hit its ${deadlineMs}ms wall-clock deadline (steps=${res.steps})`);
3297
+ result += deadlineNote(deadlineMs);
3298
+ }
3137
3299
  await opts.hooks?.onSubagentStop?.(result, { label, agentType });
3138
3300
  return { res, result };
3139
3301
  }
3140
- function childOptionsFor(opts, fs, depth, maxDepth, agentType) {
3302
+ function childOptionsFor(opts, fs, depth, maxDepth, agentType, model, root) {
3141
3303
  let def;
3142
3304
  if (agentType) {
3143
3305
  def = (opts.agents ?? []).find((a) => a.name === agentType);
3144
3306
  if (!def) return `no subagent type '${agentType}'. Available: ${(opts.agents ?? []).map((a) => a.name).join(", ") || "(none defined)"}`;
3145
3307
  }
3146
- const childOpts = { ai: opts.ai, fs, model: def?.model ?? opts.model, subagents: true, depth: depth + 1, maxDepth, traceDir: opts.traceDir };
3308
+ const childModel = resolveModelAlias(model || def?.model || opts.model);
3309
+ const childOpts = { ai: opts.ai, fs, model: childModel, subagents: true, depth: depth + 1, maxDepth, traceDir: opts.traceDir };
3147
3310
  if (opts.maxSteps != null) childOpts.maxSteps = opts.maxSteps;
3148
- if (def?.systemPrompt) childOpts.systemPrompt = def.systemPrompt;
3311
+ const childPrompt = def?.systemPrompt ?? opts.systemPrompt;
3312
+ if (childPrompt) childOpts.systemPrompt = childPrompt;
3313
+ const inherited = rebindTools(opts.tools, root);
3149
3314
  if (def?.tools?.length) {
3150
3315
  try {
3151
- childOpts.tools = toolsByName(def.tools);
3316
+ childOpts.tools = toolsByName(def.tools, inherited);
3152
3317
  } catch (e) {
3153
3318
  return `subagent '${agentType}' declares an unknown tool \u2014 ${e.message}`;
3154
3319
  }
3320
+ } else if (inherited?.length) {
3321
+ childOpts.tools = inherited;
3155
3322
  }
3323
+ const po = opts.providerOptionsFor?.(childModel);
3324
+ if (po) childOpts.providerOptions = po;
3325
+ if (opts.hooks?.preToolUse) childOpts.hooks = { preToolUse: opts.hooks.preToolUse.bind(opts.hooks) };
3156
3326
  return childOpts;
3157
3327
  }
3158
3328
  function makeTaskTool(opts) {
@@ -3160,7 +3330,7 @@ function makeTaskTool(opts) {
3160
3330
  const maxDepth = opts.maxDepth ?? 2;
3161
3331
  return {
3162
3332
  name: "Task",
3163
- 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.',
3333
+ 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 VFS file edits are overlay-isolated and commit when it finishes \u2014 but a child that uses the real `Shell` writes straight to the working tree (an overlay cannot isolate a real process), so pair shell-heavy work with `isolation: "worktree"`. 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 its shell is re-bound to the worktree \u2014 keeping any name your host gave it \u2014 and background shell jobs, including inspecting or killing MINE, are unavailable there) \u2014 slower (~200ms setup), auto-cleaned if no changes. The child inherits YOUR tools and system prompt by default (and is subject to the same tool-use gate); pass `model` to run this one sub-task on a different model (e.g. a cheap one for bulk search, a stronger one for hard reasoning) \u2014 it overrides the agentType default.',
3164
3334
  parameters: {
3165
3335
  type: "object",
3166
3336
  required: ["description", "prompt"],
@@ -3168,11 +3338,18 @@ function makeTaskTool(opts) {
3168
3338
  description: { type: "string", description: "a short (3-5 word) label for the sub-task" },
3169
3339
  prompt: { type: "string", description: "the full instructions the child agent should carry out" },
3170
3340
  agentType: { type: "string", description: "optional named subagent type (its persona, model, and scoped tools) \u2014 see the catalog" },
3341
+ model: { type: "string", description: 'optional model for THIS sub-task (alias like "haiku"/"opus"/"sonnet", or a full id). Overrides the agentType default; otherwise the child runs on your model.' },
3171
3342
  background: { type: "boolean", description: "run detached (overlay-isolated, commits on finish); returns a job id to poll with JobOutput instead of blocking" },
3172
3343
  isolation: { type: "string", enum: ["overlay", "worktree"], description: 'isolation mode: "overlay" (default, VFS layer) or "worktree" (real git worktree for shell-heavy work)' }
3173
3344
  }
3174
3345
  },
3175
- async run({ description, prompt, agentType, background, isolation }, ctx) {
3346
+ // Declared because it is ENFORCED (see runChildTraced) — a transport above the loop sizes its own
3347
+ // deadline around this one (AgentTool.maxDurationMs). Declaring an unenforced number would be a lie;
3348
+ // declaring nothing left `Task` invisible to toolTimeoutViolations, which is the same hole.
3349
+ maxDurationMs: opts.maxDurationMs ?? SUBAGENT_MAX_DURATION_MS,
3350
+ maxDurationEnforced: true,
3351
+ // the wall clock below ABORTS the child at this bound — it cannot be overrun, so the transport needs slack, not a doubling
3352
+ async run({ description, prompt, agentType, model, background, isolation }, ctx) {
3176
3353
  if (depth >= maxDepth) {
3177
3354
  return `Error: Task depth limit reached (maxDepth ${maxDepth}). Cannot spawn another child agent \u2014 do this work directly instead.`;
3178
3355
  }
@@ -3195,7 +3372,7 @@ function makeTaskTool(opts) {
3195
3372
  fs2 = overlay;
3196
3373
  commit = () => overlay.commit();
3197
3374
  }
3198
- const childOpts2 = childOptionsFor(opts, fs2, depth, maxDepth, agentType);
3375
+ const childOpts2 = childOptionsFor(opts, fs2, depth, maxDepth, agentType, model, wtHandle2?.worktreePath);
3199
3376
  if (typeof childOpts2 === "string") throw new Error(childOpts2);
3200
3377
  const childPrompt = wtHandle2 ? worktreePromptPrefix(wtHandle2.branch) + String(prompt ?? "") : String(prompt ?? "");
3201
3378
  const { res, result } = await runChildTraced({ opts, childOpts: childOpts2, prompt: childPrompt, label, agentType, childDepth: depth + 1, signal });
@@ -3225,7 +3402,7 @@ function makeTaskTool(opts) {
3225
3402
  } else {
3226
3403
  fs = opts.fs;
3227
3404
  }
3228
- const childOpts = childOptionsFor(opts, fs, depth, maxDepth, agentType);
3405
+ const childOpts = childOptionsFor(opts, fs, depth, maxDepth, agentType, model, wtHandle?.worktreePath);
3229
3406
  if (typeof childOpts === "string") {
3230
3407
  wtHandle && releaseWorktree(wtHandle);
3231
3408
  return `Error: ${childOpts}`;
@@ -3254,7 +3431,7 @@ function makeTaskBatchTool(opts) {
3254
3431
  const maxParallel = opts.maxParallel ?? 4;
3255
3432
  return {
3256
3433
  name: "TaskBatch",
3257
- description: 'Delegate SEVERAL independent sub-tasks to child agents that run concurrently; returns all their summaries. Each child is write-isolated (its file edits are merged back in array order). Use for parallel fan-out (review/search/scoped refactors across files). Provide `tasks: [{ description, prompt, agentType? }]`. Set `isolation: "worktree"` to run each child in its own git worktree (needed for real shell commands) \u2014 slower, auto-cleaned if no changes.',
3434
+ description: 'Delegate SEVERAL independent sub-tasks to child agents that run concurrently; returns all their summaries. Each child is write-isolated at the VFS level (its file edits are merged back in array order); real `Shell` commands are NOT \u2014 use `isolation: "worktree"` for those. Use for parallel fan-out (review/search/scoped refactors across files). Provide `tasks: [{ description, prompt, agentType?, model? }]` \u2014 each child inherits your tools and system prompt, and `model` runs that one child on a different model. Set `isolation: "worktree"` to run each child in its own git worktree (needed for real shell commands) \u2014 slower, auto-cleaned if no changes.',
3258
3435
  parameters: {
3259
3436
  type: "object",
3260
3437
  required: ["tasks"],
@@ -3268,18 +3445,26 @@ function makeTaskBatchTool(opts) {
3268
3445
  properties: {
3269
3446
  description: { type: "string", description: "a short (3-5 word) label" },
3270
3447
  prompt: { type: "string", description: "the full instructions for this child" },
3271
- agentType: { type: "string", description: "optional named subagent type" }
3448
+ agentType: { type: "string", description: "optional named subagent type" },
3449
+ model: { type: "string", description: "optional model for this child (alias or full id); overrides the agentType default" }
3272
3450
  }
3273
3451
  }
3274
3452
  },
3275
3453
  isolation: { type: "string", enum: ["overlay", "worktree"], description: 'isolation mode for ALL children: "overlay" (default) or "worktree" (real git worktree each)' }
3276
3454
  }
3277
3455
  },
3456
+ // Same enforced bound as `Task`, applied to the WHOLE batch (below), not per child — otherwise the
3457
+ // honest ceiling would be ceil(n/maxParallel) x the per-child deadline, i.e. unbounded in `tasks`.
3458
+ maxDurationMs: opts.maxDurationMs ?? SUBAGENT_MAX_DURATION_MS,
3459
+ maxDurationEnforced: true,
3460
+ // the wall clock below ABORTS the child at this bound — it cannot be overrun, so the transport needs slack, not a doubling
3278
3461
  async run({ tasks, isolation }, ctx) {
3279
3462
  if (depth >= maxDepth) return `Error: Task depth limit reached (maxDepth ${maxDepth}). Cannot spawn child agents \u2014 do this work directly instead.`;
3280
3463
  const list = Array.isArray(tasks) ? tasks : [];
3281
3464
  if (!list.length) return "Error: TaskBatch needs a non-empty `tasks` array.";
3282
3465
  const useWorktree = isolation === "worktree";
3466
+ const batchDeadlineMs = opts.maxDurationMs ?? SUBAGENT_MAX_DURATION_MS;
3467
+ const batchClock = wallClock(ctx.signal, batchDeadlineMs);
3283
3468
  const results = await boundedPool(list, maxParallel, async (t, i) => {
3284
3469
  const label = String(t?.description ?? t?.agentType ?? `task ${i + 1}`);
3285
3470
  let fs;
@@ -3294,16 +3479,16 @@ function makeTaskBatchTool(opts) {
3294
3479
  overlay = new OverlayFilesystem(opts.fs);
3295
3480
  fs = overlay;
3296
3481
  }
3297
- const childOpts = childOptionsFor(opts, fs, depth, maxDepth, t?.agentType);
3482
+ const childOpts = childOptionsFor(opts, fs, depth, maxDepth, t?.agentType, t?.model, wtHandle?.worktreePath);
3298
3483
  if (typeof childOpts === "string") return { label, error: childOpts, ok: false };
3299
3484
  try {
3300
3485
  const childPrompt = wtHandle ? worktreePromptPrefix(wtHandle.branch) + String(t?.prompt ?? "") : String(t?.prompt ?? "");
3301
- const { res, result } = await runChildTraced({ opts, childOpts, prompt: childPrompt, label, agentType: t?.agentType, childDepth: depth + 1, signal: ctx.signal });
3486
+ const { res, result } = await runChildTraced({ opts, childOpts, prompt: childPrompt, label, agentType: t?.agentType, childDepth: depth + 1, signal: batchClock.signal });
3302
3487
  return { label, text: result, overlay, wtHandle, ok: res.finishReason !== "error" && res.finishReason !== "aborted" };
3303
3488
  } catch (e) {
3304
3489
  return { label, error: e instanceof Error ? e.message : String(e), wtHandle, ok: false };
3305
3490
  }
3306
- });
3491
+ }).finally(() => batchClock.dispose());
3307
3492
  const lines = [];
3308
3493
  for (const r of results) {
3309
3494
  if (r.ok && r.overlay) await r.overlay.commit().catch(() => {
@@ -3317,7 +3502,7 @@ function makeTaskBatchTool(opts) {
3317
3502
  lines.push(`### ${lines.length + 1}. ${r.label}
3318
3503
  ${r.error ? `ERROR: ${r.error}` : r.text || "(no summary)"}${extra}`);
3319
3504
  }
3320
- return lines.join("\n\n");
3505
+ return lines.join("\n\n") + (batchClock.timedOut() ? deadlineNote(batchDeadlineMs) : "");
3321
3506
  }
3322
3507
  };
3323
3508
  }
@@ -3721,6 +3906,10 @@ var AgentOptions = class {
3721
3906
  autoTest;
3722
3907
  /** Provider-specific options forwarded to ai.chat() (e.g. cursor mcpServers, cwd). */
3723
3908
  providerOptions;
3909
+ /** Derive `providerOptions` for a DELEGATED child whose model may differ from this agent's
3910
+ * (`Task({ model })` / an agentType def). Default: inherit `providerOptions` only when the child's
3911
+ * model is on the SAME provider — cursor's `cwd`/`cursorSession` on an anthropic child is a 400. */
3912
+ providerOptionsFor;
3724
3913
  /** Prompt caching (providers that support it, e.g. Anthropic): cache tools/system/conversation
3725
3914
  * prefix across the loop's steps — reads cost 0.1x, writes 1.25x. A multi-step agent loop
3726
3915
  * re-sends its whole prefix every step, so this is a large net cost cut. Default on. */
@@ -3944,7 +4133,19 @@ var Agent = class _Agent {
3944
4133
  agents = loaded.agents;
3945
4134
  if (loaded.catalog) systemPrompt += "\n\n" + loaded.catalog;
3946
4135
  }
3947
- const taskOpts = { ai: o.ai, model: o.model, fs, depth: o.depth, maxDepth: o.maxDepth, agents, hooks: o.hooks, traceDir: o.traceDir };
4136
+ const taskOpts = {
4137
+ ai: o.ai,
4138
+ model: o.model,
4139
+ fs,
4140
+ depth: o.depth,
4141
+ maxDepth: o.maxDepth,
4142
+ agents,
4143
+ hooks: o.hooks,
4144
+ traceDir: o.traceDir,
4145
+ tools: [...o.tools, ...this.injectedTools],
4146
+ systemPrompt: o.systemPrompt,
4147
+ providerOptionsFor: o.providerOptionsFor ?? ((m) => modelProvider(m) === modelProvider(o.model) ? o.providerOptions : void 0)
4148
+ };
3948
4149
  tools = [...tools, makeTaskTool(taskOpts), makeTaskBatchTool(taskOpts)];
3949
4150
  }
3950
4151
  if (o.checkpoints) tools = [...tools, ...checkpointTools()];
@@ -4081,7 +4282,17 @@ var Agent = class _Agent {
4081
4282
  const frag = reasoningToChatFragment(o.model, o.reasoning);
4082
4283
  const sentTools = o.model.startsWith("claude-code/") ? [] : wireTools;
4083
4284
  const isCursorWithTools = o.model.startsWith("cursor/") && wireTools.length > 0;
4285
+ const hostToolTimeoutMs = o.providerOptions?.toolTimeoutMs;
4286
+ const derivedToolTimeoutMs = Math.max(TRANSPORT_TOOL_TIMEOUT_FLOOR_MS, ...this.activeTools.map(transportHeadroomMs));
4287
+ const effectiveToolTimeoutMs = typeof hostToolTimeoutMs === "number" ? Math.max(hostToolTimeoutMs, derivedToolTimeoutMs) : derivedToolTimeoutMs;
4288
+ if (isCursorWithTools) {
4289
+ if (typeof hostToolTimeoutMs === "number" && effectiveToolTimeoutMs !== hostToolTimeoutMs) {
4290
+ 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.`);
4291
+ }
4292
+ for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs)) log6.warn(v);
4293
+ }
4084
4294
  const cursorPo = isCursorWithTools ? {
4295
+ toolTimeoutMs: effectiveToolTimeoutMs,
4085
4296
  toolExecutor: async (name, args) => {
4086
4297
  const tc = { id: `cursor-${Date.now()}`, type: "function", function: { name, arguments: JSON.stringify(args) } };
4087
4298
  return await this.dispatch(tc);
@@ -4183,14 +4394,12 @@ var Agent = class _Agent {
4183
4394
  })() } }))
4184
4395
  });
4185
4396
  for (const a of delegatedTools) {
4186
- const out = typeof a.output === "string" ? a.output : a.output == null ? "" : (() => {
4187
- try {
4188
- return JSON.stringify(a.output);
4189
- } catch {
4190
- return String(a.output);
4191
- }
4192
- })();
4193
- this.transcript.push({ role: "tool", tool_call_id: a.id, content: cap > 0 && out.length > cap ? cropResult(out, cap) : out });
4397
+ const name = a.name || "tool";
4398
+ const r = delegatedResult(a.output);
4399
+ const text = r.images.length ? r.text : nonEmptyResult(isVacuous(a.output) ? "" : r.text, name, deliveryOf(a.output, a.status));
4400
+ const body = cap > 0 && text.length > cap ? cropResult(text, cap) : text;
4401
+ const content = r.images.length ? [{ type: "text", text: body }, ...r.images.map((i) => imagePart(`data:${i.mimeType};base64,${i.data}`))] : body;
4402
+ this.transcript.push({ role: "tool", tool_call_id: a.id, content });
4194
4403
  }
4195
4404
  }
4196
4405
  const toolCalls = res.toolCalls ?? [];
@@ -4243,9 +4452,10 @@ var Agent = class _Agent {
4243
4452
  const raw = await this.dispatch(tc);
4244
4453
  let content;
4245
4454
  if (typeof raw === "string") {
4246
- content = raw;
4455
+ content = nonEmptyResult(raw, tc.function.name);
4247
4456
  } else {
4248
- const parts = [{ type: "text", text: raw.text }];
4457
+ const hasImages = !!raw.images?.length;
4458
+ const parts = [{ type: "text", text: hasImages ? raw.text : nonEmptyResult(raw.text ?? "", tc.function.name) }];
4249
4459
  for (const img of raw.images ?? []) parts.push(imagePart(`data:${img.mimeType};base64,${img.data}`));
4250
4460
  content = parts;
4251
4461
  }
@@ -4554,6 +4764,76 @@ var isAnthropicModel = (m) => !!m && (m.startsWith("anthropic/") || /(^|\/)claud
4554
4764
  function estimateTokens(m, keepRecentImages = 1) {
4555
4765
  return Math.ceil(sendBytes(m, keepRecentImages) / 4);
4556
4766
  }
4767
+ var TRANSPORT_TOOL_TIMEOUT_FLOOR_MS = 18e5;
4768
+ function transportHeadroomMs(t) {
4769
+ const d = Number(t.maxDurationMs) || 0;
4770
+ if (!d) return 0;
4771
+ return t.maxDurationEnforced ? d + 6e4 : d * 2 + 6e4;
4772
+ }
4773
+ function toolTimeoutViolations(tools, transportMs) {
4774
+ const out = [];
4775
+ for (const t of tools) {
4776
+ const d = Number(t.maxDurationMs) || 0;
4777
+ if (d && d >= transportMs) {
4778
+ 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.`);
4779
+ }
4780
+ }
4781
+ return out;
4782
+ }
4783
+ function nonEmptyResult(out, name = "tool", delivery = "ran") {
4784
+ if (out.trim()) return out;
4785
+ 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.`;
4786
+ if (delivery === "missing") {
4787
+ 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.`;
4788
+ 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.`;
4789
+ }
4790
+ return `[${name}] ran and returned no output. The call completed \u2014 this is an empty answer, not a failure.`;
4791
+ }
4792
+ function isVacuous(v, seen = /* @__PURE__ */ new Set(), depth = 0) {
4793
+ if (v == null) return true;
4794
+ if (typeof v === "string") return v.trim() === "";
4795
+ if (typeof v !== "object") return false;
4796
+ if (seen.has(v) || depth >= 64) return false;
4797
+ seen.add(v);
4798
+ try {
4799
+ if (Array.isArray(v)) return v.every((x) => isVacuous(x, seen, depth + 1));
4800
+ const proto = Object.getPrototypeOf(v);
4801
+ if (proto !== Object.prototype && proto !== null) return false;
4802
+ const vals = Object.values(v);
4803
+ return vals.length === 0 || vals.every((x) => isVacuous(x, seen, depth + 1));
4804
+ } finally {
4805
+ seen.delete(v);
4806
+ }
4807
+ }
4808
+ function delegatedResult(output) {
4809
+ if (typeof output === "string") return { text: output, images: [] };
4810
+ if (output == null) return { text: "", images: [] };
4811
+ const isImg = (i) => i && typeof i.data === "string" && typeof i.mimeType === "string";
4812
+ const o = output;
4813
+ if (Array.isArray(o.images)) {
4814
+ const images = o.images.filter(isImg);
4815
+ if (images.length) return { text: typeof o.text === "string" ? o.text : "", images };
4816
+ }
4817
+ const blocks = Array.isArray(output) ? output : Array.isArray(o.content) ? o.content : void 0;
4818
+ if (blocks) {
4819
+ const images = blocks.filter((b) => b?.type === "image" && isImg(b)).map((b) => ({ mimeType: b.mimeType, data: b.data }));
4820
+ if (images.length) {
4821
+ const text = blocks.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n");
4822
+ return { text, images };
4823
+ }
4824
+ }
4825
+ try {
4826
+ return { text: JSON.stringify(output), images: [] };
4827
+ } catch {
4828
+ return { text: String(output), images: [] };
4829
+ }
4830
+ }
4831
+ function deliveryOf(output, status) {
4832
+ if (status === "error") return "error";
4833
+ if (output == null) return "missing";
4834
+ if (status != null && status !== "completed") return "missing";
4835
+ return "ran";
4836
+ }
4557
4837
  function cropResult(result, cap) {
4558
4838
  const head = result.slice(0, cap);
4559
4839
  const nl = head.lastIndexOf("\n");
@@ -4730,45 +5010,6 @@ function lastAssistantText(messages) {
4730
5010
  return "";
4731
5011
  }
4732
5012
 
4733
- // src/models.ts
4734
- var MODEL_ALIASES = {
4735
- fable: "claude-fable-5",
4736
- "fable-5": "claude-fable-5",
4737
- opus: "claude-opus-4-8",
4738
- "opus-4-8": "claude-opus-4-8",
4739
- "opus-4-7": "claude-opus-4-7",
4740
- "opus-4-6": "claude-opus-4-6",
4741
- sonnet: "claude-sonnet-4-6",
4742
- haiku: "claude-haiku-4-5-20251001"
4743
- };
4744
- function resolveModelAlias(input) {
4745
- const slash = input.indexOf("/");
4746
- const prefix = slash === -1 ? "" : input.slice(0, slash + 1);
4747
- const rest = slash === -1 ? input : input.slice(slash + 1);
4748
- return prefix + (MODEL_ALIASES[rest.toLowerCase()] ?? rest);
4749
- }
4750
- function modelShortLabel(model) {
4751
- if (!model) return "default";
4752
- const id = model.split("/").pop();
4753
- return id.replace(/^claude-/, "").replace(/-\d.*$/, "");
4754
- }
4755
- function resolveModelSwitch(opts) {
4756
- const { requested, current, prior, allowed } = opts;
4757
- if (!requested) return { model: current, denied: false };
4758
- if (allowed && !allowed.some((a) => modelShortLabel(a) === modelShortLabel(requested))) {
4759
- return { model: current, denied: true };
4760
- }
4761
- const fmt = opts.format ?? ((from, to) => `_[switching from ${from} to ${to}]_
4762
-
4763
- `);
4764
- const changed = prior !== void 0 && modelShortLabel(prior) !== modelShortLabel(requested);
4765
- return {
4766
- model: requested,
4767
- denied: false,
4768
- notice: changed ? fmt(modelShortLabel(prior), modelShortLabel(requested)) : void 0
4769
- };
4770
- }
4771
-
4772
5013
  // src/index.ts
4773
5014
  init_NodeDiskFilesystem();
4774
5015
 
@@ -5359,9 +5600,10 @@ function diskAgentOptions(opts = {}) {
5359
5600
  return { ...opts };
5360
5601
  }
5361
5602
  async function fullAgentOptions(opts = {}) {
5362
- const { makeRealShellTool: makeRealShellTool2, makeShellJobTools: makeShellJobTools2, ShellJobRegistry: ShellJobRegistry2 } = await Promise.resolve().then(() => (init_tools_shell(), tools_shell_exports));
5363
- const { cwd = process.cwd(), tools, ...rest } = opts;
5364
- const registry = new ShellJobRegistry2({ cwd, killOnExit: true });
5603
+ const { makeRealShellTool: makeRealShellTool2, makeShellJobTools: makeShellJobTools2, ShellJobRegistry: ShellJobRegistry2, formatJobExit: formatJobExit2 } = await Promise.resolve().then(() => (init_tools_shell(), tools_shell_exports));
5604
+ const { cwd = process.cwd(), tools, onJobExit, jobs, ...rest } = opts;
5605
+ const registry = jobs ?? new ShellJobRegistry2({ cwd, killOnExit: true });
5606
+ if (onJobExit) registry.setOnExit((n) => onJobExit(formatJobExit2(n)));
5365
5607
  const shell = [makeRealShellTool2({ cwd, registry }), ...makeShellJobTools2(registry)];
5366
5608
  return { ...rest, tools: [...tools ?? defaultTools(), ...shell] };
5367
5609
  }
@@ -8422,13 +8664,29 @@ function toResult(result) {
8422
8664
  }
8423
8665
  return { text: JSON.stringify(result) };
8424
8666
  }
8425
- function mcpToolToAgentTool(spec, callTool, prefix = "mcp__") {
8667
+ var MCP_CALL_TIMEOUT_MS = 3e5;
8668
+ var TIMED_OUT = /* @__PURE__ */ Symbol("mcp-timeout");
8669
+ async function withDeadline(p, ms) {
8670
+ let t;
8671
+ try {
8672
+ return await Promise.race([p, new Promise((res) => {
8673
+ t = setTimeout(() => res(TIMED_OUT), ms);
8674
+ })]);
8675
+ } finally {
8676
+ clearTimeout(t);
8677
+ }
8678
+ }
8679
+ 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.`;
8680
+ function mcpToolToAgentTool(spec, callTool, prefix = "mcp__", timeoutMs = MCP_CALL_TIMEOUT_MS) {
8426
8681
  return {
8427
8682
  name: `${prefix}${spec.name}`,
8428
8683
  description: spec.description ?? `MCP tool ${spec.name}`,
8429
8684
  parameters: spec.inputSchema ?? { type: "object", properties: {} },
8685
+ maxDurationMs: timeoutMs,
8430
8686
  async run(args, _ctx) {
8431
- const r = toResult(await callTool(spec.name, args ?? {}));
8687
+ const raw = await withDeadline(callTool(spec.name, args ?? {}), timeoutMs);
8688
+ if (raw === TIMED_OUT) return mcpTimedOut(spec.name, timeoutMs);
8689
+ const r = toResult(raw);
8432
8690
  return r.images?.length ? r : r.text;
8433
8691
  }
8434
8692
  };
@@ -8443,6 +8701,7 @@ function describeSpec(s) {
8443
8701
  }
8444
8702
  function makeMcpToolSearch(specs, callTool, options = {}) {
8445
8703
  const maxResults = options.maxResults ?? 10;
8704
+ const callTimeoutMs = options.timeoutMs ?? MCP_CALL_TIMEOUT_MS;
8446
8705
  const byName = new Map(specs.map((s) => [s.name, s]));
8447
8706
  const catalogLine = `${specs.length} MCP tool(s) available \u2014 search by keyword, then call by exact name.`;
8448
8707
  const searchTool = {
@@ -8468,10 +8727,13 @@ function makeMcpToolSearch(specs, callTool, options = {}) {
8468
8727
  args: { type: "object", description: "arguments object for the tool (per its schema)" }
8469
8728
  }
8470
8729
  },
8730
+ maxDurationMs: callTimeoutMs,
8471
8731
  async run({ name, args }) {
8472
8732
  const n = String(name ?? "");
8473
8733
  if (!byName.has(n)) return `Error: unknown MCP tool '${n}'. Use ToolSearch to find valid names.`;
8474
- const r = toResult(await callTool(n, args ?? {}));
8734
+ const raw = await withDeadline(callTool(n, args ?? {}), callTimeoutMs);
8735
+ if (raw === TIMED_OUT) return mcpTimedOut(n, callTimeoutMs);
8736
+ const r = toResult(raw);
8475
8737
  return r.images?.length ? r : r.text;
8476
8738
  }
8477
8739
  };