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