@livx.cc/agentx 0.99.39 → 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/index.d.ts CHANGED
@@ -1,14 +1,15 @@
1
- import { a as AgentOptions, H as Hooks, i as RunResult, A as Agent } from './Agent-DJwg1g8V.js';
2
- export { C as ChatFragment, b as CompactResult, D as DEFAULT_MUTATING, c as Decision, P as PermissionOptions, d as PermissionPolicy, e as PermissionRule, f as PreToolUseDecision, R as ReasoningEffort, g as RecordingHooks, h as RecordingLifecycle, T as ToolUse, j as ToolUseMeta, k as composeHooks, l as estimateTokens, p as planMode, r as reasoningToChatFragment } from './Agent-DJwg1g8V.js';
1
+ import { a as AgentOptions, H as Hooks, i as RunResult, A as Agent } from './Agent-D4-CLnxC.js';
2
+ export { C as ChatFragment, b as CompactResult, D as DEFAULT_MUTATING, c as Decision, P as PermissionOptions, d as PermissionPolicy, e as PermissionRule, f as PreToolUseDecision, R as ReasoningEffort, g as RecordingHooks, h as RecordingLifecycle, T as ToolUse, j as ToolUseMeta, k as composeHooks, l as estimateTokens, p as planMode, r as reasoningToChatFragment } from './Agent-D4-CLnxC.js';
3
3
  export { MODEL_ALIASES, ModelSwitch, ResolveModelSwitchOpts, modelShortLabel, resolveModelAlias, resolveModelSwitch } from './models.js';
4
4
  import { IFilesystem, FileMetadata } from '@livx.cc/wcli/core';
5
5
  export { CommandExecutor, FileMetadata, IFilesystem, IndexedDbFilesystem, MemFilesystem, registerHeadlessCommands } from '@livx.cc/wcli/core';
6
6
  import { BodDB } from '@bod.ee/db';
7
- import { A as AgentTool, C as ChatLike, a as ChatOptions, b as ChatResponse, j as ToolCall, H as HostBridge, U as UserQuestion, g as MessageContent } from './tools-Byyh_bae.js';
8
- export { c as ContentPart, d as HostEvent, I as IMAGE_ELIDE_STUB, e as IMAGE_REF_SCHEME, f as ImageRef, M as Message, R as Role, S as SandboxJobRegistry, h as StreamChunk, T as TodoItem, i as Tool, k as ToolContext, l as bashTool, m as clearImageRefs, n as contentBytes, o as contentText, p as defaultTools, q as editTool, r as elideImages, s as elideInlineImages, t as elideStaleImages, u as exitSessionTool, v as expandImagesForSend, w as imagePart, x as imageRefPart, y as imageRefResult, z as makeContext, B as makeJobTools, D as messageHasImage, E as messageHasImageRef, F as messageHasInlineImage, G as mintImageRef, J as parseImageRef, K as readTool, L as remintImageRefs, N as sendBytes, O as sendBytesPer, P as toWireTools, Q as todoWriteTool, V as toolImageRef, W as toolRegistry, X as toolsByName } from './tools-Byyh_bae.js';
7
+ import { A as AgentTool, C as ChatLike, a as ChatOptions, b as ChatResponse, j as ToolCall, H as HostBridge, U as UserQuestion, g as MessageContent } from './tools-BWzPQVy0.js';
8
+ export { c as ContentPart, d as HostEvent, I as IMAGE_ELIDE_STUB, e as IMAGE_REF_SCHEME, f as ImageRef, M as Message, R as Role, S as SandboxJobRegistry, h as StreamChunk, T as TodoItem, i as Tool, k as ToolContext, l as bashTool, m as clearImageRefs, n as contentBytes, o as contentText, p as defaultTools, q as editTool, r as elideImages, s as elideInlineImages, t as elideStaleImages, u as exitSessionTool, v as expandImagesForSend, w as imagePart, x as imageRefPart, y as imageRefResult, z as makeContext, B as makeJobTools, D as messageHasImage, E as messageHasImageRef, F as messageHasInlineImage, G as mintImageRef, J as parseImageRef, K as readTool, L as remintImageRefs, N as sendBytes, O as sendBytesPer, P as toWireTools, Q as todoWriteTool, V as toolImageRef, W as toolRegistry, X as toolsByName } from './tools-BWzPQVy0.js';
9
+ import { ShellJobRegistry } from './tools.shell.js';
9
10
  import { SpokenSplitter } from '@bod.ee/voice';
10
11
  export { AudioSink, AudioSource, AuthProvider, CartesiaTTS, CartesiaTTSOptions, STT_SAMPLE_RATE, SonioxSTT, SonioxSTTOptions, SttLike, TTS_SAMPLE_RATE, TtsLike, VoiceEngine, VoiceEngineOptions, VoiceState, resolveAuth } from '@bod.ee/voice';
11
- export { M as McpCall, a as McpImage, b as McpRoute, c as McpRouteResolver, d as McpToolResult, e as McpToolSearchOptions, f as McpToolSpec, g as MountedMcpLike, h as buildMcpCatalog, m as makeLazyMcpToolSearch, i as makeMcpToolSearch, j as makeMcpToolSearchFromMounted, k as mcpToolToAgentTool, l as mcpToolsToAgentTools } from './mcp-Bd19NiCL.js';
12
+ export { M as McpCall, a as McpImage, b as McpRoute, c as McpRouteResolver, d as McpToolResult, e as McpToolSearchOptions, f as McpToolSpec, g as MountedMcpLike, h as buildMcpCatalog, m as makeLazyMcpToolSearch, i as makeMcpToolSearch, j as makeMcpToolSearchFromMounted, k as mcpToolToAgentTool, l as mcpToolsToAgentTools } from './mcp-aONOXRNk.js';
12
13
  import * as libx_js_src_modules_log from 'libx.js/src/modules/log';
13
14
  export { log } from 'libx.js/src/modules/log';
14
15
 
@@ -438,6 +439,26 @@ declare function diskAgentOptions(opts?: Partial<AgentOptions>): Partial<AgentOp
438
439
  * edge-safe to import. `cwd` defaults to `process.cwd()`; secret env is scrubbed from spawned children. */
439
440
  declare function fullAgentOptions(opts?: Partial<AgentOptions> & {
440
441
  cwd?: string;
442
+ /**
443
+ * Push a finished background job back to the MODEL (Claude-Code parity). Without it `background:true`
444
+ * is write-only: the job finishes and nothing ever tells the model — so the Shell tool's description
445
+ * drops its cross-turn promise too, rather than lying to the model about this host.
446
+ *
447
+ * Wire it to the agent you are about to build — the closure runs long after construction:
448
+ * const agent = new Agent(await fullAgentOptions({ ai, onJobExit: (n) => agent.inject(n) }));
449
+ */
450
+ onJobExit?: (notice: string) => void;
451
+ /**
452
+ * Bring your OWN registry instead of letting the preset build one. This is the handle a preset host
453
+ * otherwise never gets (the preset returns `AgentOptions`, not the registry), which is what left
454
+ * `ShellJobRegistry.setOnExit()` unreachable from this path — a host can now wire (or rewire) the
455
+ * completion callback after the Agent exists:
456
+ * const jobs = new ShellJobRegistry({ cwd, killOnExit: true });
457
+ * const agent = new Agent(await fullAgentOptions({ ai, jobs }));
458
+ * jobs.setOnExit((n) => agent.inject(formatJobExit(n)));
459
+ * `onJobExit` still works and, when both are given, is applied to the registry you passed.
460
+ */
461
+ jobs?: ShellJobRegistry;
441
462
  }): Promise<Partial<AgentOptions>>;
442
463
 
443
464
  /**
@@ -1258,6 +1279,13 @@ interface TaskToolOptions {
1258
1279
  maxParallel?: number;
1259
1280
  /** Parent hooks — `onSubagentStop` fires with each child's summary when it finishes. */
1260
1281
  hooks?: Hooks;
1282
+ /**
1283
+ * Wall-clock ceiling for ONE child run, in ms (default `SUBAGENT_MAX_DURATION_MS`). `maxSteps` alone is
1284
+ * not a bound: 25 steps x a 600s Shell is ~250 minutes, far past any transport deadline wrapping the
1285
+ * parent — and an undeclared tool cannot be named by `toolTimeoutViolations`. This deadline is ENFORCED
1286
+ * (the child's signal is aborted) and therefore honestly declarable as the tool's `maxDurationMs`.
1287
+ */
1288
+ maxDurationMs?: number;
1261
1289
  /** Origin-anchored real-disk dir for child transcript traces (see `AgentOptions.traceDir`). Passed
1262
1290
  * down UNCHANGED to nested children so every level writes to the same origin `.agent`. */
1263
1291
  traceDir?: string;
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;
@@ -1941,6 +1948,7 @@ var init_shell_sandbox = __esm({
1941
1948
  var tools_shell_exports = {};
1942
1949
  __export(tools_shell_exports, {
1943
1950
  ShellJobRegistry: () => ShellJobRegistry,
1951
+ formatJobExit: () => formatJobExit,
1944
1952
  makeRealShellTool: () => makeRealShellTool,
1945
1953
  makeShellJobTools: () => makeShellJobTools
1946
1954
  });
@@ -1971,12 +1979,27 @@ async function nodeSpawn() {
1971
1979
  if (!_spawn) _spawn = (await import("child_process")).spawn;
1972
1980
  return _spawn;
1973
1981
  }
1982
+ function formatJobExit(n) {
1983
+ return `[background job ${n.id} ${n.status}${n.exitCode != null ? ` exit ${n.exitCode}` : ""}] \`${n.command}\`
1984
+ ` + (n.tail ? `${n.tail}
1985
+ ` : "(no output)\n") + `Read the full output with ShellOutput({id:"${n.id}"}).`;
1986
+ }
1974
1987
  function makeRealShellTool(options) {
1975
1988
  const defaultTimeoutMs = options.timeoutMs ?? 12e4;
1976
1989
  const maxTimeoutMs = Math.max(options.maxTimeoutMs ?? 6e5, defaultTimeoutMs);
1990
+ const backgroundDoc = () => {
1991
+ 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. ";
1992
+ 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.";
1993
+ 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.";
1994
+ return `${base}${notify} ${survival}`;
1995
+ };
1977
1996
  return {
1978
1997
  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.`,
1998
+ get description() {
1999
+ 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();
2000
+ },
2001
+ // Declared so transports above the agent loop can size their own deadline with headroom (see AgentTool.maxDurationMs).
2002
+ maxDurationMs: maxTimeoutMs,
1980
2003
  parameters: {
1981
2004
  type: "object",
1982
2005
  required: ["command"],
@@ -2078,8 +2101,9 @@ function makeRealShellTool(options) {
2078
2101
  };
2079
2102
  }
2080
2103
  function reasonFor(timedOut, timeoutMs, body) {
2081
- const head = timedOut ? `[exit 124] timed out after ${timeoutMs}ms (killed)` : "[exit 130] cancelled (killed)";
2104
+ 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
2105
  return body ? `${head}
2106
+ Partial output before the kill:
2083
2107
  ${body}` : head;
2084
2108
  }
2085
2109
  function makeShellJobTools(registry) {
@@ -2160,12 +2184,14 @@ var init_tools_shell = __esm({
2160
2184
  job.status = "error";
2161
2185
  append(`
2162
2186
  [error] ${err?.message ?? err}`);
2187
+ this.notifyExit(id, job);
2163
2188
  }
2164
2189
  });
2165
2190
  proc.on("close", (code) => {
2166
2191
  if (job.status === "running") {
2167
2192
  job.status = "exited";
2168
2193
  job.exitCode = code ?? void 0;
2194
+ this.notifyExit(id, job);
2169
2195
  }
2170
2196
  });
2171
2197
  } catch (e) {
@@ -2175,6 +2201,29 @@ var init_tools_shell = __esm({
2175
2201
  this.jobs.set(id, job);
2176
2202
  return id;
2177
2203
  }
2204
+ /** Fire `onExit` at most once per job, with the tail so the model can act without a second round-trip. */
2205
+ notified = /* @__PURE__ */ new Set();
2206
+ notifyExit(id, job) {
2207
+ if (this.notified.has(id) || !this.cfg.onExit) return;
2208
+ this.notified.add(id);
2209
+ try {
2210
+ this.cfg.onExit({ id, command: job.command, status: job.status, exitCode: job.exitCode, tail: clean(job.buf).slice(-4e3) });
2211
+ } catch {
2212
+ }
2213
+ }
2214
+ /**
2215
+ * Wire (or rewire) the completion callback AFTER construction. A host that only gets its agent handle
2216
+ * once the Agent is constructed (the library's own `fullAgentOptions` preset, any embedder) could not
2217
+ * pass `onExit` up front, so background completions were silently REPL-only. Set it here instead.
2218
+ */
2219
+ setOnExit(fn) {
2220
+ this.cfg.onExit = fn;
2221
+ }
2222
+ /** Whether a finished job will actually be reported to the model. The Shell tool's description reads
2223
+ * this so it can't promise a completion notice on a host that discards it. */
2224
+ get notifiesOnExit() {
2225
+ return !!this.cfg.onExit;
2226
+ }
2178
2227
  /** Current tail output for a job (null = no such job). */
2179
2228
  output(id) {
2180
2229
  return this.jobs.get(id)?.buf ?? (this.jobs.has(id) ? "" : null);
@@ -3064,6 +3113,30 @@ async function boundedPool(items, limit, fn) {
3064
3113
  await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, worker));
3065
3114
  return out;
3066
3115
  }
3116
+ var SUBAGENT_MAX_DURATION_MS = 18e5;
3117
+ function wallClock(parent, ms) {
3118
+ const ac = new AbortController();
3119
+ let fired = false;
3120
+ const onParent = () => ac.abort();
3121
+ const timer = setTimeout(() => {
3122
+ fired = true;
3123
+ ac.abort();
3124
+ }, ms);
3125
+ if (parent) {
3126
+ if (parent.aborted) ac.abort();
3127
+ else parent.addEventListener("abort", onParent, { once: true });
3128
+ }
3129
+ return {
3130
+ signal: ac.signal,
3131
+ timedOut: () => fired,
3132
+ dispose: () => {
3133
+ clearTimeout(timer);
3134
+ parent?.removeEventListener("abort", onParent);
3135
+ }
3136
+ };
3137
+ }
3138
+ var deadlineNote = (ms) => `
3139
+ [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
3140
  function sanitizeSlug(raw) {
3068
3141
  return raw.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/-+/g, "-").slice(0, 40);
3069
3142
  }
@@ -3126,14 +3199,25 @@ function abortHint(label, res, file) {
3126
3199
  }
3127
3200
  async function runChildTraced(args) {
3128
3201
  const { opts, label, agentType, prompt } = args;
3129
- if (args.signal) args.childOpts.signal = args.signal;
3202
+ const deadlineMs = opts.maxDurationMs ?? SUBAGENT_MAX_DURATION_MS;
3203
+ const wc = wallClock(args.signal, deadlineMs);
3204
+ args.childOpts.signal = wc.signal;
3130
3205
  const file = opts.traceDir ? traceFileFor(opts.traceDir, label, args.childDepth) : void 0;
3131
3206
  const meta = { label, agentType, prompt };
3132
3207
  const child = new Agent(args.childOpts);
3133
3208
  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);
3209
+ let res;
3210
+ try {
3211
+ res = await child.run(prompt);
3212
+ } finally {
3213
+ wc.dispose();
3214
+ }
3215
+ if (file) flushTrace(file, meta, child.transcript, wc.timedOut() ? "deadline" : res.finishReason);
3216
+ let result = res.finishReason === "stop" ? res.text || `(child '${label}' finished with no summary; finishReason=${res.finishReason})` : abortHint(label, res, file);
3217
+ if (wc.timedOut()) {
3218
+ log5.warn(`sub-task '${label}' hit its ${deadlineMs}ms wall-clock deadline (steps=${res.steps})`);
3219
+ result += deadlineNote(deadlineMs);
3220
+ }
3137
3221
  await opts.hooks?.onSubagentStop?.(result, { label, agentType });
3138
3222
  return { res, result };
3139
3223
  }
@@ -3160,7 +3244,7 @@ function makeTaskTool(opts) {
3160
3244
  const maxDepth = opts.maxDepth ?? 2;
3161
3245
  return {
3162
3246
  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.',
3247
+ 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.',
3164
3248
  parameters: {
3165
3249
  type: "object",
3166
3250
  required: ["description", "prompt"],
@@ -3172,6 +3256,12 @@ function makeTaskTool(opts) {
3172
3256
  isolation: { type: "string", enum: ["overlay", "worktree"], description: 'isolation mode: "overlay" (default, VFS layer) or "worktree" (real git worktree for shell-heavy work)' }
3173
3257
  }
3174
3258
  },
3259
+ // Declared because it is ENFORCED (see runChildTraced) — a transport above the loop sizes its own
3260
+ // deadline around this one (AgentTool.maxDurationMs). Declaring an unenforced number would be a lie;
3261
+ // declaring nothing left `Task` invisible to toolTimeoutViolations, which is the same hole.
3262
+ maxDurationMs: opts.maxDurationMs ?? SUBAGENT_MAX_DURATION_MS,
3263
+ maxDurationEnforced: true,
3264
+ // the wall clock below ABORTS the child at this bound — it cannot be overrun, so the transport needs slack, not a doubling
3175
3265
  async run({ description, prompt, agentType, background, isolation }, ctx) {
3176
3266
  if (depth >= maxDepth) {
3177
3267
  return `Error: Task depth limit reached (maxDepth ${maxDepth}). Cannot spawn another child agent \u2014 do this work directly instead.`;
@@ -3275,11 +3365,18 @@ function makeTaskBatchTool(opts) {
3275
3365
  isolation: { type: "string", enum: ["overlay", "worktree"], description: 'isolation mode for ALL children: "overlay" (default) or "worktree" (real git worktree each)' }
3276
3366
  }
3277
3367
  },
3368
+ // Same enforced bound as `Task`, applied to the WHOLE batch (below), not per child — otherwise the
3369
+ // honest ceiling would be ceil(n/maxParallel) x the per-child deadline, i.e. unbounded in `tasks`.
3370
+ maxDurationMs: opts.maxDurationMs ?? SUBAGENT_MAX_DURATION_MS,
3371
+ maxDurationEnforced: true,
3372
+ // the wall clock below ABORTS the child at this bound — it cannot be overrun, so the transport needs slack, not a doubling
3278
3373
  async run({ tasks, isolation }, ctx) {
3279
3374
  if (depth >= maxDepth) return `Error: Task depth limit reached (maxDepth ${maxDepth}). Cannot spawn child agents \u2014 do this work directly instead.`;
3280
3375
  const list = Array.isArray(tasks) ? tasks : [];
3281
3376
  if (!list.length) return "Error: TaskBatch needs a non-empty `tasks` array.";
3282
3377
  const useWorktree = isolation === "worktree";
3378
+ const batchDeadlineMs = opts.maxDurationMs ?? SUBAGENT_MAX_DURATION_MS;
3379
+ const batchClock = wallClock(ctx.signal, batchDeadlineMs);
3283
3380
  const results = await boundedPool(list, maxParallel, async (t, i) => {
3284
3381
  const label = String(t?.description ?? t?.agentType ?? `task ${i + 1}`);
3285
3382
  let fs;
@@ -3298,12 +3395,12 @@ function makeTaskBatchTool(opts) {
3298
3395
  if (typeof childOpts === "string") return { label, error: childOpts, ok: false };
3299
3396
  try {
3300
3397
  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 });
3398
+ const { res, result } = await runChildTraced({ opts, childOpts, prompt: childPrompt, label, agentType: t?.agentType, childDepth: depth + 1, signal: batchClock.signal });
3302
3399
  return { label, text: result, overlay, wtHandle, ok: res.finishReason !== "error" && res.finishReason !== "aborted" };
3303
3400
  } catch (e) {
3304
3401
  return { label, error: e instanceof Error ? e.message : String(e), wtHandle, ok: false };
3305
3402
  }
3306
- });
3403
+ }).finally(() => batchClock.dispose());
3307
3404
  const lines = [];
3308
3405
  for (const r of results) {
3309
3406
  if (r.ok && r.overlay) await r.overlay.commit().catch(() => {
@@ -3317,7 +3414,7 @@ function makeTaskBatchTool(opts) {
3317
3414
  lines.push(`### ${lines.length + 1}. ${r.label}
3318
3415
  ${r.error ? `ERROR: ${r.error}` : r.text || "(no summary)"}${extra}`);
3319
3416
  }
3320
- return lines.join("\n\n");
3417
+ return lines.join("\n\n") + (batchClock.timedOut() ? deadlineNote(batchDeadlineMs) : "");
3321
3418
  }
3322
3419
  };
3323
3420
  }
@@ -4081,7 +4178,17 @@ var Agent = class _Agent {
4081
4178
  const frag = reasoningToChatFragment(o.model, o.reasoning);
4082
4179
  const sentTools = o.model.startsWith("claude-code/") ? [] : wireTools;
4083
4180
  const isCursorWithTools = o.model.startsWith("cursor/") && wireTools.length > 0;
4181
+ const hostToolTimeoutMs = o.providerOptions?.toolTimeoutMs;
4182
+ const derivedToolTimeoutMs = Math.max(TRANSPORT_TOOL_TIMEOUT_FLOOR_MS, ...this.activeTools.map(transportHeadroomMs));
4183
+ const effectiveToolTimeoutMs = typeof hostToolTimeoutMs === "number" ? Math.max(hostToolTimeoutMs, derivedToolTimeoutMs) : derivedToolTimeoutMs;
4184
+ if (isCursorWithTools) {
4185
+ if (typeof hostToolTimeoutMs === "number" && effectiveToolTimeoutMs !== hostToolTimeoutMs) {
4186
+ 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.`);
4187
+ }
4188
+ for (const v of toolTimeoutViolations(this.activeTools, effectiveToolTimeoutMs)) log6.warn(v);
4189
+ }
4084
4190
  const cursorPo = isCursorWithTools ? {
4191
+ toolTimeoutMs: effectiveToolTimeoutMs,
4085
4192
  toolExecutor: async (name, args) => {
4086
4193
  const tc = { id: `cursor-${Date.now()}`, type: "function", function: { name, arguments: JSON.stringify(args) } };
4087
4194
  return await this.dispatch(tc);
@@ -4183,14 +4290,12 @@ var Agent = class _Agent {
4183
4290
  })() } }))
4184
4291
  });
4185
4292
  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 });
4293
+ const name = a.name || "tool";
4294
+ const r = delegatedResult(a.output);
4295
+ const text = r.images.length ? r.text : nonEmptyResult(isVacuous(a.output) ? "" : r.text, name, deliveryOf(a.output, a.status));
4296
+ const body = cap > 0 && text.length > cap ? cropResult(text, cap) : text;
4297
+ const content = r.images.length ? [{ type: "text", text: body }, ...r.images.map((i) => imagePart(`data:${i.mimeType};base64,${i.data}`))] : body;
4298
+ this.transcript.push({ role: "tool", tool_call_id: a.id, content });
4194
4299
  }
4195
4300
  }
4196
4301
  const toolCalls = res.toolCalls ?? [];
@@ -4243,9 +4348,10 @@ var Agent = class _Agent {
4243
4348
  const raw = await this.dispatch(tc);
4244
4349
  let content;
4245
4350
  if (typeof raw === "string") {
4246
- content = raw;
4351
+ content = nonEmptyResult(raw, tc.function.name);
4247
4352
  } else {
4248
- const parts = [{ type: "text", text: raw.text }];
4353
+ const hasImages = !!raw.images?.length;
4354
+ const parts = [{ type: "text", text: hasImages ? raw.text : nonEmptyResult(raw.text ?? "", tc.function.name) }];
4249
4355
  for (const img of raw.images ?? []) parts.push(imagePart(`data:${img.mimeType};base64,${img.data}`));
4250
4356
  content = parts;
4251
4357
  }
@@ -4554,6 +4660,76 @@ var isAnthropicModel = (m) => !!m && (m.startsWith("anthropic/") || /(^|\/)claud
4554
4660
  function estimateTokens(m, keepRecentImages = 1) {
4555
4661
  return Math.ceil(sendBytes(m, keepRecentImages) / 4);
4556
4662
  }
4663
+ var TRANSPORT_TOOL_TIMEOUT_FLOOR_MS = 18e5;
4664
+ function transportHeadroomMs(t) {
4665
+ const d = Number(t.maxDurationMs) || 0;
4666
+ if (!d) return 0;
4667
+ return t.maxDurationEnforced ? d + 6e4 : d * 2 + 6e4;
4668
+ }
4669
+ function toolTimeoutViolations(tools, transportMs) {
4670
+ const out = [];
4671
+ for (const t of tools) {
4672
+ const d = Number(t.maxDurationMs) || 0;
4673
+ if (d && d >= transportMs) {
4674
+ 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.`);
4675
+ }
4676
+ }
4677
+ return out;
4678
+ }
4679
+ function nonEmptyResult(out, name = "tool", delivery = "ran") {
4680
+ if (out.trim()) return out;
4681
+ 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.`;
4682
+ if (delivery === "missing") {
4683
+ 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.`;
4684
+ 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.`;
4685
+ }
4686
+ return `[${name}] ran and returned no output. The call completed \u2014 this is an empty answer, not a failure.`;
4687
+ }
4688
+ function isVacuous(v, seen = /* @__PURE__ */ new Set(), depth = 0) {
4689
+ if (v == null) return true;
4690
+ if (typeof v === "string") return v.trim() === "";
4691
+ if (typeof v !== "object") return false;
4692
+ if (seen.has(v) || depth >= 64) return false;
4693
+ seen.add(v);
4694
+ try {
4695
+ if (Array.isArray(v)) return v.every((x) => isVacuous(x, seen, depth + 1));
4696
+ const proto = Object.getPrototypeOf(v);
4697
+ if (proto !== Object.prototype && proto !== null) return false;
4698
+ const vals = Object.values(v);
4699
+ return vals.length === 0 || vals.every((x) => isVacuous(x, seen, depth + 1));
4700
+ } finally {
4701
+ seen.delete(v);
4702
+ }
4703
+ }
4704
+ function delegatedResult(output) {
4705
+ if (typeof output === "string") return { text: output, images: [] };
4706
+ if (output == null) return { text: "", images: [] };
4707
+ const isImg = (i) => i && typeof i.data === "string" && typeof i.mimeType === "string";
4708
+ const o = output;
4709
+ if (Array.isArray(o.images)) {
4710
+ const images = o.images.filter(isImg);
4711
+ if (images.length) return { text: typeof o.text === "string" ? o.text : "", images };
4712
+ }
4713
+ const blocks = Array.isArray(output) ? output : Array.isArray(o.content) ? o.content : void 0;
4714
+ if (blocks) {
4715
+ const images = blocks.filter((b) => b?.type === "image" && isImg(b)).map((b) => ({ mimeType: b.mimeType, data: b.data }));
4716
+ if (images.length) {
4717
+ const text = blocks.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n");
4718
+ return { text, images };
4719
+ }
4720
+ }
4721
+ try {
4722
+ return { text: JSON.stringify(output), images: [] };
4723
+ } catch {
4724
+ return { text: String(output), images: [] };
4725
+ }
4726
+ }
4727
+ function deliveryOf(output, status) {
4728
+ if (status === "error") return "error";
4729
+ if (output == null) return "missing";
4730
+ if (status != null && status !== "completed") return "missing";
4731
+ return "ran";
4732
+ }
4557
4733
  function cropResult(result, cap) {
4558
4734
  const head = result.slice(0, cap);
4559
4735
  const nl = head.lastIndexOf("\n");
@@ -5359,9 +5535,10 @@ function diskAgentOptions(opts = {}) {
5359
5535
  return { ...opts };
5360
5536
  }
5361
5537
  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 });
5538
+ const { makeRealShellTool: makeRealShellTool2, makeShellJobTools: makeShellJobTools2, ShellJobRegistry: ShellJobRegistry2, formatJobExit: formatJobExit2 } = await Promise.resolve().then(() => (init_tools_shell(), tools_shell_exports));
5539
+ const { cwd = process.cwd(), tools, onJobExit, jobs, ...rest } = opts;
5540
+ const registry = jobs ?? new ShellJobRegistry2({ cwd, killOnExit: true });
5541
+ if (onJobExit) registry.setOnExit((n) => onJobExit(formatJobExit2(n)));
5365
5542
  const shell = [makeRealShellTool2({ cwd, registry }), ...makeShellJobTools2(registry)];
5366
5543
  return { ...rest, tools: [...tools ?? defaultTools(), ...shell] };
5367
5544
  }
@@ -8422,13 +8599,29 @@ function toResult(result) {
8422
8599
  }
8423
8600
  return { text: JSON.stringify(result) };
8424
8601
  }
8425
- function mcpToolToAgentTool(spec, callTool, prefix = "mcp__") {
8602
+ var MCP_CALL_TIMEOUT_MS = 3e5;
8603
+ var TIMED_OUT = /* @__PURE__ */ Symbol("mcp-timeout");
8604
+ async function withDeadline(p, ms) {
8605
+ let t;
8606
+ try {
8607
+ return await Promise.race([p, new Promise((res) => {
8608
+ t = setTimeout(() => res(TIMED_OUT), ms);
8609
+ })]);
8610
+ } finally {
8611
+ clearTimeout(t);
8612
+ }
8613
+ }
8614
+ 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.`;
8615
+ function mcpToolToAgentTool(spec, callTool, prefix = "mcp__", timeoutMs = MCP_CALL_TIMEOUT_MS) {
8426
8616
  return {
8427
8617
  name: `${prefix}${spec.name}`,
8428
8618
  description: spec.description ?? `MCP tool ${spec.name}`,
8429
8619
  parameters: spec.inputSchema ?? { type: "object", properties: {} },
8620
+ maxDurationMs: timeoutMs,
8430
8621
  async run(args, _ctx) {
8431
- const r = toResult(await callTool(spec.name, args ?? {}));
8622
+ const raw = await withDeadline(callTool(spec.name, args ?? {}), timeoutMs);
8623
+ if (raw === TIMED_OUT) return mcpTimedOut(spec.name, timeoutMs);
8624
+ const r = toResult(raw);
8432
8625
  return r.images?.length ? r : r.text;
8433
8626
  }
8434
8627
  };
@@ -8443,6 +8636,7 @@ function describeSpec(s) {
8443
8636
  }
8444
8637
  function makeMcpToolSearch(specs, callTool, options = {}) {
8445
8638
  const maxResults = options.maxResults ?? 10;
8639
+ const callTimeoutMs = options.timeoutMs ?? MCP_CALL_TIMEOUT_MS;
8446
8640
  const byName = new Map(specs.map((s) => [s.name, s]));
8447
8641
  const catalogLine = `${specs.length} MCP tool(s) available \u2014 search by keyword, then call by exact name.`;
8448
8642
  const searchTool = {
@@ -8468,10 +8662,13 @@ function makeMcpToolSearch(specs, callTool, options = {}) {
8468
8662
  args: { type: "object", description: "arguments object for the tool (per its schema)" }
8469
8663
  }
8470
8664
  },
8665
+ maxDurationMs: callTimeoutMs,
8471
8666
  async run({ name, args }) {
8472
8667
  const n = String(name ?? "");
8473
8668
  if (!byName.has(n)) return `Error: unknown MCP tool '${n}'. Use ToolSearch to find valid names.`;
8474
- const r = toResult(await callTool(n, args ?? {}));
8669
+ const raw = await withDeadline(callTool(n, args ?? {}), callTimeoutMs);
8670
+ if (raw === TIMED_OUT) return mcpTimedOut(n, callTimeoutMs);
8671
+ const r = toResult(raw);
8475
8672
  return r.images?.length ? r : r.text;
8476
8673
  }
8477
8674
  };