@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/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
- return ANNOUNCED_ACTION.test(last);
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, ANNOUNCED_ACTION, NEXT_STEP, SIGN_OFF;
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
- 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;
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;
@@ -1354,11 +1361,12 @@ function toolRegistry() {
1354
1361
  const all = [bashTool, readTool, editTool, grepTool, globTool, writeTool, multiEditTool, applyEditsTool, repoMapTool, reviewTool(), todoWriteTool, webFetchTool, webSearchTool, webSearchAnthropicTool];
1355
1362
  return Object.fromEntries(all.map((t) => [t.name, t]));
1356
1363
  }
1357
- function toolsByName(names) {
1364
+ function toolsByName(names, available) {
1358
1365
  const reg = toolRegistry();
1366
+ const extra = Object.fromEntries((available ?? []).map((t) => [t.name, t]));
1359
1367
  return names.map((n) => {
1360
- const t = reg[n];
1361
- if (!t) throw new Error(`unknown tool '${n}'. Known: ${Object.keys(reg).join(", ")}`);
1368
+ const t = extra[n] ?? reg[n];
1369
+ if (!t) throw new Error(`unknown tool '${n}'. Known: ${[.../* @__PURE__ */ new Set([...Object.keys(extra), ...Object.keys(reg)])].join(", ")}`);
1362
1370
  return t;
1363
1371
  });
1364
1372
  }
@@ -2105,6 +2113,7 @@ var init_shell_sandbox = __esm({
2105
2113
  var tools_shell_exports = {};
2106
2114
  __export(tools_shell_exports, {
2107
2115
  ShellJobRegistry: () => ShellJobRegistry,
2116
+ formatJobExit: () => formatJobExit,
2108
2117
  makeRealShellTool: () => makeRealShellTool,
2109
2118
  makeShellJobTools: () => makeShellJobTools
2110
2119
  });
@@ -2135,12 +2144,45 @@ async function nodeSpawn() {
2135
2144
  if (!_spawn) _spawn = (await import("child_process")).spawn;
2136
2145
  return _spawn;
2137
2146
  }
2147
+ function formatJobExit(n) {
2148
+ return `[background job ${n.id} ${n.status}${n.exitCode != null ? ` exit ${n.exitCode}` : ""}] \`${n.command}\`
2149
+ ` + (n.tail ? `${n.tail}
2150
+ ` : "(no output)\n") + `Read the full output with ShellOutput({id:"${n.id}"}).`;
2151
+ }
2138
2152
  function makeRealShellTool(options) {
2139
2153
  const defaultTimeoutMs = options.timeoutMs ?? 12e4;
2140
2154
  const maxTimeoutMs = Math.max(options.maxTimeoutMs ?? 6e5, defaultTimeoutMs);
2155
+ const backgroundDoc = () => {
2156
+ 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. ";
2157
+ 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.";
2158
+ 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.";
2159
+ return `${base}${notify} ${survival}`;
2160
+ };
2141
2161
  return {
2142
2162
  name: "Shell",
2143
- 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.`,
2163
+ // Rebind for an isolated child agent (git worktree): same policy/env/timeouts, new cwd. The
2164
+ // `registry` is deliberately dropped — it is bound to the PARENT's cwd (and, in hosts like
2165
+ // shraga-ee, to the parent's session), so a background job started from the child would run
2166
+ // outside the child's isolation and report into the parent. Background stays a parent capability
2167
+ // (`makeShellJobTools`' companions drop out of an isolated child entirely — see their `withCwd`).
2168
+ // Only `run` is rebuilt: everything else is carried over from THIS instance, because a host may
2169
+ // have mutated the tool AFTER construction (shraga-ee renames `Shell` -> `Bash` and its system
2170
+ // prompt says `Bash` everywhere). Rebuilding from `options` alone silently dropped that rename,
2171
+ // so a worktree child advertised `Shell` while being told to call `Bash` — an unknown-tool error
2172
+ // on its first call, and a hard failure for an agentType def whose allowlist names `Bash`.
2173
+ // `description` is the ONE thing taken from the rebound tool instead: it is derived from the
2174
+ // options and states whether `background:true` works — the rebind drops the registry, so carrying
2175
+ // the parent's text over would advertise a background capability the child does not have. (The
2176
+ // spread also collapses the getter below to a plain value, which is why this override is explicit.)
2177
+ withCwd(cwd) {
2178
+ const rebound = makeRealShellTool({ ...options, cwd, registry: void 0 });
2179
+ return { ...this, description: rebound.description, run: rebound.run, withCwd: rebound.withCwd };
2180
+ },
2181
+ get description() {
2182
+ 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();
2183
+ },
2184
+ // Declared so transports above the agent loop can size their own deadline with headroom (see AgentTool.maxDurationMs).
2185
+ maxDurationMs: maxTimeoutMs,
2144
2186
  parameters: {
2145
2187
  type: "object",
2146
2188
  required: ["command"],
@@ -2242,14 +2284,17 @@ function makeRealShellTool(options) {
2242
2284
  };
2243
2285
  }
2244
2286
  function reasonFor(timedOut, timeoutMs, body) {
2245
- const head = timedOut ? `[exit 124] timed out after ${timeoutMs}ms (killed)` : "[exit 130] cancelled (killed)";
2287
+ 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)";
2246
2288
  return body ? `${head}
2289
+ Partial output before the kill:
2247
2290
  ${body}` : head;
2248
2291
  }
2249
2292
  function makeShellJobTools(registry) {
2250
2293
  const idParam = { type: "object", properties: { id: { type: "string", description: "the job id from Shell({background:true})" } } };
2294
+ const dropOnRebind = { withCwd: () => void 0 };
2251
2295
  return [
2252
2296
  {
2297
+ ...dropOnRebind,
2253
2298
  name: "ShellOutput",
2254
2299
  description: "Read the accumulated output (tail) of a background Shell job by id.",
2255
2300
  parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
@@ -2262,6 +2307,7 @@ ${clean(out) || "(no output yet)"}`;
2262
2307
  }
2263
2308
  },
2264
2309
  {
2310
+ ...dropOnRebind,
2265
2311
  name: "ShellStatus",
2266
2312
  description: "Status of a background Shell job (running/exited/killed + exit code). Omit `id` to list all jobs.",
2267
2313
  parameters: idParam,
@@ -2275,6 +2321,7 @@ ${clean(out) || "(no output yet)"}`;
2275
2321
  }
2276
2322
  },
2277
2323
  {
2324
+ ...dropOnRebind,
2278
2325
  name: "ShellKill",
2279
2326
  description: "Stop a running background Shell job by id (SIGTERM).",
2280
2327
  parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
@@ -2324,12 +2371,14 @@ var init_tools_shell = __esm({
2324
2371
  job.status = "error";
2325
2372
  append(`
2326
2373
  [error] ${err2?.message ?? err2}`);
2374
+ this.notifyExit(id, job);
2327
2375
  }
2328
2376
  });
2329
2377
  proc.on("close", (code) => {
2330
2378
  if (job.status === "running") {
2331
2379
  job.status = "exited";
2332
2380
  job.exitCode = code ?? void 0;
2381
+ this.notifyExit(id, job);
2333
2382
  }
2334
2383
  });
2335
2384
  } catch (e) {
@@ -2339,6 +2388,29 @@ var init_tools_shell = __esm({
2339
2388
  this.jobs.set(id, job);
2340
2389
  return id;
2341
2390
  }
2391
+ /** Fire `onExit` at most once per job, with the tail so the model can act without a second round-trip. */
2392
+ notified = /* @__PURE__ */ new Set();
2393
+ notifyExit(id, job) {
2394
+ if (this.notified.has(id) || !this.cfg.onExit) return;
2395
+ this.notified.add(id);
2396
+ try {
2397
+ this.cfg.onExit({ id, command: job.command, status: job.status, exitCode: job.exitCode, tail: clean(job.buf).slice(-4e3) });
2398
+ } catch {
2399
+ }
2400
+ }
2401
+ /**
2402
+ * Wire (or rewire) the completion callback AFTER construction. A host that only gets its agent handle
2403
+ * once the Agent is constructed (the library's own `fullAgentOptions` preset, any embedder) could not
2404
+ * pass `onExit` up front, so background completions were silently REPL-only. Set it here instead.
2405
+ */
2406
+ setOnExit(fn) {
2407
+ this.cfg.onExit = fn;
2408
+ }
2409
+ /** Whether a finished job will actually be reported to the model. The Shell tool's description reads
2410
+ * this so it can't promise a completion notice on a host that discards it. */
2411
+ get notifiesOnExit() {
2412
+ return !!this.cfg.onExit;
2413
+ }
2342
2414
  /** Current tail output for a job (null = no such job). */
2343
2415
  output(id) {
2344
2416
  return this.jobs.get(id)?.buf ?? (this.jobs.has(id) ? "" : null);
@@ -3082,6 +3154,32 @@ ${sections.join("\n\n---\n\n")}`;
3082
3154
  init_tools();
3083
3155
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
3084
3156
  import { join as join4 } from "path";
3157
+
3158
+ // src/models.ts
3159
+ var MODEL_ALIASES = {
3160
+ fable: "claude-fable-5",
3161
+ "fable-5": "claude-fable-5",
3162
+ opus: "claude-opus-4-8",
3163
+ "opus-4-8": "claude-opus-4-8",
3164
+ "opus-4-7": "claude-opus-4-7",
3165
+ "opus-4-6": "claude-opus-4-6",
3166
+ sonnet: "claude-sonnet-4-6",
3167
+ haiku: "claude-haiku-4-5-20251001"
3168
+ };
3169
+ function resolveModelAlias(input) {
3170
+ const slash = input.indexOf("/");
3171
+ const prefix = slash === -1 ? "" : input.slice(0, slash + 1);
3172
+ const rest = slash === -1 ? input : input.slice(slash + 1);
3173
+ return prefix + (MODEL_ALIASES[rest.toLowerCase()] ?? rest);
3174
+ }
3175
+ function modelProvider(model) {
3176
+ const i = model.indexOf("/");
3177
+ if (i !== -1) return model.slice(0, i);
3178
+ if (/^claude-/i.test(model)) return "anthropic";
3179
+ return `?${model}`;
3180
+ }
3181
+
3182
+ // src/subagent.ts
3085
3183
  init_OverlayFilesystem();
3086
3184
  init_worktree();
3087
3185
  init_NodeDiskFilesystem();
@@ -3099,6 +3197,30 @@ async function boundedPool(items, limit, fn) {
3099
3197
  await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, worker));
3100
3198
  return out;
3101
3199
  }
3200
+ var SUBAGENT_MAX_DURATION_MS = 18e5;
3201
+ function wallClock(parent, ms) {
3202
+ const ac = new AbortController();
3203
+ let fired = false;
3204
+ const onParent = () => ac.abort();
3205
+ const timer = setTimeout(() => {
3206
+ fired = true;
3207
+ ac.abort();
3208
+ }, ms);
3209
+ if (parent) {
3210
+ if (parent.aborted) ac.abort();
3211
+ else parent.addEventListener("abort", onParent, { once: true });
3212
+ }
3213
+ return {
3214
+ signal: ac.signal,
3215
+ timedOut: () => fired,
3216
+ dispose: () => {
3217
+ clearTimeout(timer);
3218
+ parent?.removeEventListener("abort", onParent);
3219
+ }
3220
+ };
3221
+ }
3222
+ var deadlineNote = (ms) => `
3223
+ [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.]`;
3102
3224
  function sanitizeSlug(raw) {
3103
3225
  return raw.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/-+/g, "-").slice(0, 40);
3104
3226
  }
@@ -3116,6 +3238,14 @@ function createWorktreeFs(cwd, label, index) {
3116
3238
  function releaseWorktree(handle) {
3117
3239
  return cleanupWorktree(handle.repoRoot, handle.slug, { force: true });
3118
3240
  }
3241
+ function rebindTools(tools, root) {
3242
+ if (!tools || !root) return tools;
3243
+ return tools.flatMap((t) => {
3244
+ if (!t.withCwd) return [t];
3245
+ const rebound = t.withCwd(root);
3246
+ return rebound ? [rebound] : [];
3247
+ });
3248
+ }
3119
3249
  function worktreePromptPrefix(branch) {
3120
3250
  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).
3121
3251
 
@@ -3161,33 +3291,52 @@ function abortHint(label, res, file) {
3161
3291
  }
3162
3292
  async function runChildTraced(args) {
3163
3293
  const { opts, label, agentType, prompt } = args;
3164
- if (args.signal) args.childOpts.signal = args.signal;
3294
+ const deadlineMs = opts.maxDurationMs ?? SUBAGENT_MAX_DURATION_MS;
3295
+ const wc = wallClock(args.signal, deadlineMs);
3296
+ args.childOpts.signal = wc.signal;
3165
3297
  const file = opts.traceDir ? traceFileFor(opts.traceDir, label, args.childDepth) : void 0;
3166
3298
  const meta = { label, agentType, prompt };
3167
3299
  const child = new Agent(args.childOpts);
3168
3300
  if (file) installTrace(child, file, meta);
3169
- const res = await child.run(prompt);
3170
- if (file) flushTrace(file, meta, child.transcript, res.finishReason);
3171
- const result = res.finishReason === "stop" ? res.text || `(child '${label}' finished with no summary; finishReason=${res.finishReason})` : abortHint(label, res, file);
3301
+ let res;
3302
+ try {
3303
+ res = await child.run(prompt);
3304
+ } finally {
3305
+ wc.dispose();
3306
+ }
3307
+ if (file) flushTrace(file, meta, child.transcript, wc.timedOut() ? "deadline" : res.finishReason);
3308
+ let result = res.finishReason === "stop" ? res.text || `(child '${label}' finished with no summary; finishReason=${res.finishReason})` : abortHint(label, res, file);
3309
+ if (wc.timedOut()) {
3310
+ log5.warn(`sub-task '${label}' hit its ${deadlineMs}ms wall-clock deadline (steps=${res.steps})`);
3311
+ result += deadlineNote(deadlineMs);
3312
+ }
3172
3313
  await opts.hooks?.onSubagentStop?.(result, { label, agentType });
3173
3314
  return { res, result };
3174
3315
  }
3175
- function childOptionsFor(opts, fs, depth, maxDepth, agentType) {
3316
+ function childOptionsFor(opts, fs, depth, maxDepth, agentType, model, root) {
3176
3317
  let def;
3177
3318
  if (agentType) {
3178
3319
  def = (opts.agents ?? []).find((a) => a.name === agentType);
3179
3320
  if (!def) return `no subagent type '${agentType}'. Available: ${(opts.agents ?? []).map((a) => a.name).join(", ") || "(none defined)"}`;
3180
3321
  }
3181
- const childOpts = { ai: opts.ai, fs, model: def?.model ?? opts.model, subagents: true, depth: depth + 1, maxDepth, traceDir: opts.traceDir };
3322
+ const childModel = resolveModelAlias(model || def?.model || opts.model);
3323
+ const childOpts = { ai: opts.ai, fs, model: childModel, subagents: true, depth: depth + 1, maxDepth, traceDir: opts.traceDir };
3182
3324
  if (opts.maxSteps != null) childOpts.maxSteps = opts.maxSteps;
3183
- if (def?.systemPrompt) childOpts.systemPrompt = def.systemPrompt;
3325
+ const childPrompt = def?.systemPrompt ?? opts.systemPrompt;
3326
+ if (childPrompt) childOpts.systemPrompt = childPrompt;
3327
+ const inherited = rebindTools(opts.tools, root);
3184
3328
  if (def?.tools?.length) {
3185
3329
  try {
3186
- childOpts.tools = toolsByName(def.tools);
3330
+ childOpts.tools = toolsByName(def.tools, inherited);
3187
3331
  } catch (e) {
3188
3332
  return `subagent '${agentType}' declares an unknown tool \u2014 ${e.message}`;
3189
3333
  }
3334
+ } else if (inherited?.length) {
3335
+ childOpts.tools = inherited;
3190
3336
  }
3337
+ const po = opts.providerOptionsFor?.(childModel);
3338
+ if (po) childOpts.providerOptions = po;
3339
+ if (opts.hooks?.preToolUse) childOpts.hooks = { preToolUse: opts.hooks.preToolUse.bind(opts.hooks) };
3191
3340
  return childOpts;
3192
3341
  }
3193
3342
  function makeTaskTool(opts) {
@@ -3195,7 +3344,7 @@ function makeTaskTool(opts) {
3195
3344
  const maxDepth = opts.maxDepth ?? 2;
3196
3345
  return {
3197
3346
  name: "Task",
3198
- 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.',
3347
+ 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.',
3199
3348
  parameters: {
3200
3349
  type: "object",
3201
3350
  required: ["description", "prompt"],
@@ -3203,11 +3352,18 @@ function makeTaskTool(opts) {
3203
3352
  description: { type: "string", description: "a short (3-5 word) label for the sub-task" },
3204
3353
  prompt: { type: "string", description: "the full instructions the child agent should carry out" },
3205
3354
  agentType: { type: "string", description: "optional named subagent type (its persona, model, and scoped tools) \u2014 see the catalog" },
3355
+ 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.' },
3206
3356
  background: { type: "boolean", description: "run detached (overlay-isolated, commits on finish); returns a job id to poll with JobOutput instead of blocking" },
3207
3357
  isolation: { type: "string", enum: ["overlay", "worktree"], description: 'isolation mode: "overlay" (default, VFS layer) or "worktree" (real git worktree for shell-heavy work)' }
3208
3358
  }
3209
3359
  },
3210
- async run({ description, prompt, agentType, background, isolation }, ctx) {
3360
+ // Declared because it is ENFORCED (see runChildTraced) — a transport above the loop sizes its own
3361
+ // deadline around this one (AgentTool.maxDurationMs). Declaring an unenforced number would be a lie;
3362
+ // declaring nothing left `Task` invisible to toolTimeoutViolations, which is the same hole.
3363
+ maxDurationMs: opts.maxDurationMs ?? SUBAGENT_MAX_DURATION_MS,
3364
+ maxDurationEnforced: true,
3365
+ // the wall clock below ABORTS the child at this bound — it cannot be overrun, so the transport needs slack, not a doubling
3366
+ async run({ description, prompt, agentType, model, background, isolation }, ctx) {
3211
3367
  if (depth >= maxDepth) {
3212
3368
  return `Error: Task depth limit reached (maxDepth ${maxDepth}). Cannot spawn another child agent \u2014 do this work directly instead.`;
3213
3369
  }
@@ -3230,7 +3386,7 @@ function makeTaskTool(opts) {
3230
3386
  fs2 = overlay;
3231
3387
  commit = () => overlay.commit();
3232
3388
  }
3233
- const childOpts2 = childOptionsFor(opts, fs2, depth, maxDepth, agentType);
3389
+ const childOpts2 = childOptionsFor(opts, fs2, depth, maxDepth, agentType, model, wtHandle2?.worktreePath);
3234
3390
  if (typeof childOpts2 === "string") throw new Error(childOpts2);
3235
3391
  const childPrompt = wtHandle2 ? worktreePromptPrefix(wtHandle2.branch) + String(prompt ?? "") : String(prompt ?? "");
3236
3392
  const { res, result } = await runChildTraced({ opts, childOpts: childOpts2, prompt: childPrompt, label, agentType, childDepth: depth + 1, signal });
@@ -3260,7 +3416,7 @@ function makeTaskTool(opts) {
3260
3416
  } else {
3261
3417
  fs = opts.fs;
3262
3418
  }
3263
- const childOpts = childOptionsFor(opts, fs, depth, maxDepth, agentType);
3419
+ const childOpts = childOptionsFor(opts, fs, depth, maxDepth, agentType, model, wtHandle?.worktreePath);
3264
3420
  if (typeof childOpts === "string") {
3265
3421
  wtHandle && releaseWorktree(wtHandle);
3266
3422
  return `Error: ${childOpts}`;
@@ -3289,7 +3445,7 @@ function makeTaskBatchTool(opts) {
3289
3445
  const maxParallel = opts.maxParallel ?? 4;
3290
3446
  return {
3291
3447
  name: "TaskBatch",
3292
- 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.',
3448
+ 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.',
3293
3449
  parameters: {
3294
3450
  type: "object",
3295
3451
  required: ["tasks"],
@@ -3303,18 +3459,26 @@ function makeTaskBatchTool(opts) {
3303
3459
  properties: {
3304
3460
  description: { type: "string", description: "a short (3-5 word) label" },
3305
3461
  prompt: { type: "string", description: "the full instructions for this child" },
3306
- agentType: { type: "string", description: "optional named subagent type" }
3462
+ agentType: { type: "string", description: "optional named subagent type" },
3463
+ model: { type: "string", description: "optional model for this child (alias or full id); overrides the agentType default" }
3307
3464
  }
3308
3465
  }
3309
3466
  },
3310
3467
  isolation: { type: "string", enum: ["overlay", "worktree"], description: 'isolation mode for ALL children: "overlay" (default) or "worktree" (real git worktree each)' }
3311
3468
  }
3312
3469
  },
3470
+ // Same enforced bound as `Task`, applied to the WHOLE batch (below), not per child — otherwise the
3471
+ // honest ceiling would be ceil(n/maxParallel) x the per-child deadline, i.e. unbounded in `tasks`.
3472
+ maxDurationMs: opts.maxDurationMs ?? SUBAGENT_MAX_DURATION_MS,
3473
+ maxDurationEnforced: true,
3474
+ // the wall clock below ABORTS the child at this bound — it cannot be overrun, so the transport needs slack, not a doubling
3313
3475
  async run({ tasks, isolation }, ctx) {
3314
3476
  if (depth >= maxDepth) return `Error: Task depth limit reached (maxDepth ${maxDepth}). Cannot spawn child agents \u2014 do this work directly instead.`;
3315
3477
  const list = Array.isArray(tasks) ? tasks : [];
3316
3478
  if (!list.length) return "Error: TaskBatch needs a non-empty `tasks` array.";
3317
3479
  const useWorktree = isolation === "worktree";
3480
+ const batchDeadlineMs = opts.maxDurationMs ?? SUBAGENT_MAX_DURATION_MS;
3481
+ const batchClock = wallClock(ctx.signal, batchDeadlineMs);
3318
3482
  const results = await boundedPool(list, maxParallel, async (t, i) => {
3319
3483
  const label = String(t?.description ?? t?.agentType ?? `task ${i + 1}`);
3320
3484
  let fs;
@@ -3329,16 +3493,16 @@ function makeTaskBatchTool(opts) {
3329
3493
  overlay = new OverlayFilesystem(opts.fs);
3330
3494
  fs = overlay;
3331
3495
  }
3332
- const childOpts = childOptionsFor(opts, fs, depth, maxDepth, t?.agentType);
3496
+ const childOpts = childOptionsFor(opts, fs, depth, maxDepth, t?.agentType, t?.model, wtHandle?.worktreePath);
3333
3497
  if (typeof childOpts === "string") return { label, error: childOpts, ok: false };
3334
3498
  try {
3335
3499
  const childPrompt = wtHandle ? worktreePromptPrefix(wtHandle.branch) + String(t?.prompt ?? "") : String(t?.prompt ?? "");
3336
- const { res, result } = await runChildTraced({ opts, childOpts, prompt: childPrompt, label, agentType: t?.agentType, childDepth: depth + 1, signal: ctx.signal });
3500
+ const { res, result } = await runChildTraced({ opts, childOpts, prompt: childPrompt, label, agentType: t?.agentType, childDepth: depth + 1, signal: batchClock.signal });
3337
3501
  return { label, text: result, overlay, wtHandle, ok: res.finishReason !== "error" && res.finishReason !== "aborted" };
3338
3502
  } catch (e) {
3339
3503
  return { label, error: e instanceof Error ? e.message : String(e), wtHandle, ok: false };
3340
3504
  }
3341
- });
3505
+ }).finally(() => batchClock.dispose());
3342
3506
  const lines = [];
3343
3507
  for (const r of results) {
3344
3508
  if (r.ok && r.overlay) await r.overlay.commit().catch(() => {
@@ -3352,7 +3516,7 @@ function makeTaskBatchTool(opts) {
3352
3516
  lines.push(`### ${lines.length + 1}. ${r.label}
3353
3517
  ${r.error ? `ERROR: ${r.error}` : r.text || "(no summary)"}${extra}`);
3354
3518
  }
3355
- return lines.join("\n\n");
3519
+ return lines.join("\n\n") + (batchClock.timedOut() ? deadlineNote(batchDeadlineMs) : "");
3356
3520
  }
3357
3521
  };
3358
3522
  }
@@ -3756,6 +3920,10 @@ var AgentOptions = class {
3756
3920
  autoTest;
3757
3921
  /** Provider-specific options forwarded to ai.chat() (e.g. cursor mcpServers, cwd). */
3758
3922
  providerOptions;
3923
+ /** Derive `providerOptions` for a DELEGATED child whose model may differ from this agent's
3924
+ * (`Task({ model })` / an agentType def). Default: inherit `providerOptions` only when the child's
3925
+ * model is on the SAME provider — cursor's `cwd`/`cursorSession` on an anthropic child is a 400. */
3926
+ providerOptionsFor;
3759
3927
  /** Prompt caching (providers that support it, e.g. Anthropic): cache tools/system/conversation
3760
3928
  * prefix across the loop's steps — reads cost 0.1x, writes 1.25x. A multi-step agent loop
3761
3929
  * re-sends its whole prefix every step, so this is a large net cost cut. Default on. */
@@ -3979,7 +4147,19 @@ var Agent = class _Agent {
3979
4147
  agents = loaded.agents;
3980
4148
  if (loaded.catalog) systemPrompt += "\n\n" + loaded.catalog;
3981
4149
  }
3982
- const taskOpts = { ai: o.ai, model: o.model, fs, depth: o.depth, maxDepth: o.maxDepth, agents, hooks: o.hooks, traceDir: o.traceDir };
4150
+ const taskOpts = {
4151
+ ai: o.ai,
4152
+ model: o.model,
4153
+ fs,
4154
+ depth: o.depth,
4155
+ maxDepth: o.maxDepth,
4156
+ agents,
4157
+ hooks: o.hooks,
4158
+ traceDir: o.traceDir,
4159
+ tools: [...o.tools, ...this.injectedTools],
4160
+ systemPrompt: o.systemPrompt,
4161
+ providerOptionsFor: o.providerOptionsFor ?? ((m) => modelProvider(m) === modelProvider(o.model) ? o.providerOptions : void 0)
4162
+ };
3983
4163
  tools = [...tools, makeTaskTool(taskOpts), makeTaskBatchTool(taskOpts)];
3984
4164
  }
3985
4165
  if (o.checkpoints) tools = [...tools, ...checkpointTools()];
@@ -4116,7 +4296,17 @@ var Agent = class _Agent {
4116
4296
  const frag = reasoningToChatFragment(o.model, o.reasoning);
4117
4297
  const sentTools = o.model.startsWith("claude-code/") ? [] : wireTools;
4118
4298
  const isCursorWithTools = o.model.startsWith("cursor/") && wireTools.length > 0;
4299
+ const hostToolTimeoutMs = o.providerOptions?.toolTimeoutMs;
4300
+ const derivedToolTimeoutMs = Math.max(TRANSPORT_TOOL_TIMEOUT_FLOOR_MS, ...this.activeTools.map(transportHeadroomMs));
4301
+ const effectiveToolTimeoutMs = typeof hostToolTimeoutMs === "number" ? Math.max(hostToolTimeoutMs, derivedToolTimeoutMs) : derivedToolTimeoutMs;
4302
+ if (isCursorWithTools) {
4303
+ if (typeof hostToolTimeoutMs === "number" && effectiveToolTimeoutMs !== hostToolTimeoutMs) {
4304
+ 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.`);
4305
+ }
4306
+ for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs)) log6.warn(v);
4307
+ }
4119
4308
  const cursorPo = isCursorWithTools ? {
4309
+ toolTimeoutMs: effectiveToolTimeoutMs,
4120
4310
  toolExecutor: async (name, args) => {
4121
4311
  const tc = { id: `cursor-${Date.now()}`, type: "function", function: { name, arguments: JSON.stringify(args) } };
4122
4312
  return await this.dispatch(tc);
@@ -4218,14 +4408,12 @@ var Agent = class _Agent {
4218
4408
  })() } }))
4219
4409
  });
4220
4410
  for (const a of delegatedTools) {
4221
- const out = typeof a.output === "string" ? a.output : a.output == null ? "" : (() => {
4222
- try {
4223
- return JSON.stringify(a.output);
4224
- } catch {
4225
- return String(a.output);
4226
- }
4227
- })();
4228
- this.transcript.push({ role: "tool", tool_call_id: a.id, content: cap > 0 && out.length > cap ? cropResult(out, cap) : out });
4411
+ const name = a.name || "tool";
4412
+ const r = delegatedResult(a.output);
4413
+ const text = r.images.length ? r.text : nonEmptyResult(isVacuous(a.output) ? "" : r.text, name, deliveryOf(a.output, a.status));
4414
+ const body = cap > 0 && text.length > cap ? cropResult(text, cap) : text;
4415
+ const content = r.images.length ? [{ type: "text", text: body }, ...r.images.map((i) => imagePart(`data:${i.mimeType};base64,${i.data}`))] : body;
4416
+ this.transcript.push({ role: "tool", tool_call_id: a.id, content });
4229
4417
  }
4230
4418
  }
4231
4419
  const toolCalls = res.toolCalls ?? [];
@@ -4278,9 +4466,10 @@ var Agent = class _Agent {
4278
4466
  const raw = await this.dispatch(tc);
4279
4467
  let content;
4280
4468
  if (typeof raw === "string") {
4281
- content = raw;
4469
+ content = nonEmptyResult(raw, tc.function.name);
4282
4470
  } else {
4283
- const parts = [{ type: "text", text: raw.text }];
4471
+ const hasImages = !!raw.images?.length;
4472
+ const parts = [{ type: "text", text: hasImages ? raw.text : nonEmptyResult(raw.text ?? "", tc.function.name) }];
4284
4473
  for (const img of raw.images ?? []) parts.push(imagePart(`data:${img.mimeType};base64,${img.data}`));
4285
4474
  content = parts;
4286
4475
  }
@@ -4589,6 +4778,76 @@ var isAnthropicModel = (m) => !!m && (m.startsWith("anthropic/") || /(^|\/)claud
4589
4778
  function estimateTokens(m, keepRecentImages = 1) {
4590
4779
  return Math.ceil(sendBytes(m, keepRecentImages) / 4);
4591
4780
  }
4781
+ var TRANSPORT_TOOL_TIMEOUT_FLOOR_MS = 18e5;
4782
+ function transportHeadroomMs(t) {
4783
+ const d = Number(t.maxDurationMs) || 0;
4784
+ if (!d) return 0;
4785
+ return t.maxDurationEnforced ? d + 6e4 : d * 2 + 6e4;
4786
+ }
4787
+ function toolTimeoutViolations(tools, transportMs) {
4788
+ const out = [];
4789
+ for (const t of tools) {
4790
+ const d = Number(t.maxDurationMs) || 0;
4791
+ if (d && d >= transportMs) {
4792
+ 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.`);
4793
+ }
4794
+ }
4795
+ return out;
4796
+ }
4797
+ function nonEmptyResult(out, name = "tool", delivery = "ran") {
4798
+ if (out.trim()) return out;
4799
+ 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.`;
4800
+ if (delivery === "missing") {
4801
+ 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.`;
4802
+ 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.`;
4803
+ }
4804
+ return `[${name}] ran and returned no output. The call completed \u2014 this is an empty answer, not a failure.`;
4805
+ }
4806
+ function isVacuous(v, seen = /* @__PURE__ */ new Set(), depth = 0) {
4807
+ if (v == null) return true;
4808
+ if (typeof v === "string") return v.trim() === "";
4809
+ if (typeof v !== "object") return false;
4810
+ if (seen.has(v) || depth >= 64) return false;
4811
+ seen.add(v);
4812
+ try {
4813
+ if (Array.isArray(v)) return v.every((x) => isVacuous(x, seen, depth + 1));
4814
+ const proto = Object.getPrototypeOf(v);
4815
+ if (proto !== Object.prototype && proto !== null) return false;
4816
+ const vals = Object.values(v);
4817
+ return vals.length === 0 || vals.every((x) => isVacuous(x, seen, depth + 1));
4818
+ } finally {
4819
+ seen.delete(v);
4820
+ }
4821
+ }
4822
+ function delegatedResult(output) {
4823
+ if (typeof output === "string") return { text: output, images: [] };
4824
+ if (output == null) return { text: "", images: [] };
4825
+ const isImg = (i) => i && typeof i.data === "string" && typeof i.mimeType === "string";
4826
+ const o = output;
4827
+ if (Array.isArray(o.images)) {
4828
+ const images = o.images.filter(isImg);
4829
+ if (images.length) return { text: typeof o.text === "string" ? o.text : "", images };
4830
+ }
4831
+ const blocks = Array.isArray(output) ? output : Array.isArray(o.content) ? o.content : void 0;
4832
+ if (blocks) {
4833
+ const images = blocks.filter((b) => b?.type === "image" && isImg(b)).map((b) => ({ mimeType: b.mimeType, data: b.data }));
4834
+ if (images.length) {
4835
+ const text = blocks.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n");
4836
+ return { text, images };
4837
+ }
4838
+ }
4839
+ try {
4840
+ return { text: JSON.stringify(output), images: [] };
4841
+ } catch {
4842
+ return { text: String(output), images: [] };
4843
+ }
4844
+ }
4845
+ function deliveryOf(output, status) {
4846
+ if (status === "error") return "error";
4847
+ if (output == null) return "missing";
4848
+ if (status != null && status !== "completed") return "missing";
4849
+ return "ran";
4850
+ }
4592
4851
  function cropResult(result, cap) {
4593
4852
  const head = result.slice(0, cap);
4594
4853
  const nl = head.lastIndexOf("\n");
@@ -8334,13 +8593,29 @@ function toResult(result) {
8334
8593
  }
8335
8594
  return { text: JSON.stringify(result) };
8336
8595
  }
8337
- function mcpToolToAgentTool(spec, callTool, prefix = "mcp__") {
8596
+ var MCP_CALL_TIMEOUT_MS = 3e5;
8597
+ var TIMED_OUT = /* @__PURE__ */ Symbol("mcp-timeout");
8598
+ async function withDeadline(p, ms) {
8599
+ let t;
8600
+ try {
8601
+ return await Promise.race([p, new Promise((res) => {
8602
+ t = setTimeout(() => res(TIMED_OUT), ms);
8603
+ })]);
8604
+ } finally {
8605
+ clearTimeout(t);
8606
+ }
8607
+ }
8608
+ 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.`;
8609
+ function mcpToolToAgentTool(spec, callTool, prefix = "mcp__", timeoutMs = MCP_CALL_TIMEOUT_MS) {
8338
8610
  return {
8339
8611
  name: `${prefix}${spec.name}`,
8340
8612
  description: spec.description ?? `MCP tool ${spec.name}`,
8341
8613
  parameters: spec.inputSchema ?? { type: "object", properties: {} },
8614
+ maxDurationMs: timeoutMs,
8342
8615
  async run(args, _ctx) {
8343
- const r = toResult(await callTool(spec.name, args ?? {}));
8616
+ const raw = await withDeadline(callTool(spec.name, args ?? {}), timeoutMs);
8617
+ if (raw === TIMED_OUT) return mcpTimedOut(spec.name, timeoutMs);
8618
+ const r = toResult(raw);
8344
8619
  return r.images?.length ? r : r.text;
8345
8620
  }
8346
8621
  };
@@ -8355,6 +8630,7 @@ function describeSpec(s) {
8355
8630
  }
8356
8631
  function makeMcpToolSearch(specs, callTool, options = {}) {
8357
8632
  const maxResults = options.maxResults ?? 10;
8633
+ const callTimeoutMs = options.timeoutMs ?? MCP_CALL_TIMEOUT_MS;
8358
8634
  const byName = new Map(specs.map((s) => [s.name, s]));
8359
8635
  const catalogLine = `${specs.length} MCP tool(s) available \u2014 search by keyword, then call by exact name.`;
8360
8636
  const searchTool = {
@@ -8380,10 +8656,13 @@ function makeMcpToolSearch(specs, callTool, options = {}) {
8380
8656
  args: { type: "object", description: "arguments object for the tool (per its schema)" }
8381
8657
  }
8382
8658
  },
8659
+ maxDurationMs: callTimeoutMs,
8383
8660
  async run({ name, args }) {
8384
8661
  const n = String(name ?? "");
8385
8662
  if (!byName.has(n)) return `Error: unknown MCP tool '${n}'. Use ToolSearch to find valid names.`;
8386
- const r = toResult(await callTool(n, args ?? {}));
8663
+ const raw = await withDeadline(callTool(n, args ?? {}), callTimeoutMs);
8664
+ if (raw === TIMED_OUT) return mcpTimedOut(n, callTimeoutMs);
8665
+ const r = toResult(raw);
8387
8666
  return r.images?.length ? r : r.text;
8388
8667
  }
8389
8668
  };
@@ -9186,14 +9465,20 @@ Reference files in them by their mount path (the left side).`;
9186
9465
  const memoryWriteDir = memoryDir[0];
9187
9466
  const hooks = o.learnFromMistakes && memoryWriteDir ? composeHooks(o.hooks, lessonCapture({ fs, dir: memoryWriteDir, minRepeats: 2 })) : o.hooks;
9188
9467
  let realShell = [];
9468
+ const agentRef = {};
9189
9469
  const useRealShell = o.realShell ?? !virtual;
9190
9470
  if (useRealShell && !virtual && !isClaudeCode) {
9191
- const jobs = new ShellJobRegistry({ cwd, killOnExit: true, osSandbox: o.osSandbox });
9192
- realShell = [makeRealShellTool({ cwd, registry: jobs, osSandbox: o.osSandbox }), ...makeShellJobTools(jobs)];
9471
+ const jobs = new ShellJobRegistry({
9472
+ cwd,
9473
+ killOnExit: true,
9474
+ osSandbox: o.osSandbox,
9475
+ onExit: (n) => agentRef.agent?.inject(formatJobExit(n))
9476
+ });
9477
+ realShell = [makeRealShellTool({ cwd, registry: jobs, osSandbox: o.osSandbox, jobsSurviveRun: o.singleRun !== true }), ...makeShellJobTools(jobs)];
9193
9478
  }
9194
9479
  const scratchDir = o.scratch ? o.scratchDir ?? (virtual ? `${cwd}/.agent/scratch` : `${tmpdir()}/agentx-scratch-${process.pid}`) : void 0;
9195
9480
  const scratch = scratchDir ? new Scratch(fs, { dir: scratchDir }) : void 0;
9196
- return new Agent({
9481
+ return agentRef.agent = new Agent({
9197
9482
  ai: o.ai,
9198
9483
  fs,
9199
9484
  model: o.model ?? "anthropic/claude-sonnet-4-6",