@livx.cc/agentx 0.99.41 → 0.99.43

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.
@@ -251,7 +251,9 @@ declare class AgentOptions {
251
251
  /** Retry budget for an EMPTY/silent-turn (a delegated-runtime wedge: cursor's warm helper spins then
252
252
  * returns a content-less end, surfaced as `code:'empty'`/`code:'cursor_silent'`). Tuned WIDER than the
253
253
  * network-drop budget (fixed 2) because clearing a wedge needs several reap+respawn cycles — each retry
254
- * hits a FRESH cursor session (the adapter evicts the wedged warm helper on the silent-end). Still bounded:
254
+ * hits a FRESH cursor session (the adapter evicts the wedged warm helper on the silent-end; a stall-watchdog
255
+ * cancel is NOT this class — it is classified `stall`, and the watchdog's coded abort reason is what tells
256
+ * the adapter to evict on that path too). Still bounded:
255
257
  * after exhaustion the turn surfaces `finishReason:'empty'` (non-zero exit), never infinite, never silent.
256
258
  * Env: AGENTX_EMPTY_RETRIES. */
257
259
  emptyRetries: number;
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bun
2
- import { H as Hooks, i as RunResult, R as ReasoningEffort, A as Agent } from './Agent-DydgLoT-.js';
2
+ import { H as Hooks, i as RunResult, R as ReasoningEffort, A as Agent } from './Agent-f4um2q8E.js';
3
3
  import { IFilesystem } from '@livx.cc/wcli/core';
4
4
  import { M as Message, U as UserQuestion, H as HostBridge, c as ContentPart, g as MessageContent } from './tools-S-ArIR3o.js';
5
5
 
package/dist/cli.js CHANGED
@@ -2179,7 +2179,7 @@ function makeRealShellTool(options) {
2179
2179
  return { ...this, description: rebound.description, run: rebound.run, withCwd: rebound.withCwd };
2180
2180
  },
2181
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();
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. " + (options.registry?.adopt ? `A command still running after ${defaultTimeoutMs}ms is NOT killed \u2014 it is handed to a background job and you get its id, so you can keep checking on it (\`[still running]\`). Pass \`timeoutMs\` (max ${maxTimeoutMs}) to wait longer in the foreground, and always bound network commands yourself (e.g. \`curl -m 10\`). ` : `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
2183
  },
2184
2184
  // Declared so transports above the agent loop can size their own deadline with headroom (see AgentTool.maxDurationMs).
2185
2185
  maxDurationMs: maxTimeoutMs,
@@ -2218,9 +2218,12 @@ function makeRealShellTool(options) {
2218
2218
  else ctx.signal.addEventListener("abort", onAbort, { once: true });
2219
2219
  }
2220
2220
  let timedOut = false;
2221
- const timer = setTimeout(() => {
2221
+ let onDeadline = () => {
2222
2222
  timedOut = true;
2223
2223
  ctl.abort();
2224
+ };
2225
+ const timer = setTimeout(() => {
2226
+ onDeadline();
2224
2227
  }, timeoutMs);
2225
2228
  let pend = "";
2226
2229
  let flushTimer = null;
@@ -2251,6 +2254,18 @@ function makeRealShellTool(options) {
2251
2254
  }
2252
2255
  if (ctl.signal.aborted) killGroup(proc, "SIGKILL");
2253
2256
  else ctl.signal.addEventListener("abort", () => killGroup(proc, "SIGKILL"), { once: true });
2257
+ const registry = options.registry;
2258
+ if (registry?.adopt) {
2259
+ onDeadline = () => {
2260
+ timedOut = true;
2261
+ if (settled) return;
2262
+ flushEmit(ctx);
2263
+ proc.stdout?.off?.("data", collect);
2264
+ proc.stderr?.off?.("data", collect);
2265
+ const id = registry.adopt(cmd, proc, out);
2266
+ finish(handoffFor(timeoutMs, id, clean(out), !!registry.notifiesOnExit));
2267
+ };
2268
+ }
2254
2269
  const collect = (chunk) => {
2255
2270
  const s = typeof chunk === "string" ? chunk : chunk?.toString?.("utf8") ?? "";
2256
2271
  out += s;
@@ -2283,6 +2298,12 @@ function makeRealShellTool(options) {
2283
2298
  }
2284
2299
  };
2285
2300
  }
2301
+ function handoffFor(timeoutMs, id, body, notifies) {
2302
+ const head = `[still running] exceeded ${timeoutMs}ms, so it was handed to background job ${id} \u2014 NOT killed, it is still going. Check on it with ShellOutput({id:"${id}"}) / ShellStatus({id:"${id}"}), stop it with ShellKill({id:"${id}"}). ` + (notifies ? "Its completion will be reported to you, so you may continue with other work meanwhile." : "Nothing will tell you when it finishes \u2014 poll it yourself before you rely on its result.");
2303
+ return body ? `${head}
2304
+ Output so far:
2305
+ ${body}` : head;
2306
+ }
2286
2307
  function reasonFor(timedOut, timeoutMs, body) {
2287
2308
  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)";
2288
2309
  return body ? `${head}
@@ -2422,6 +2443,41 @@ var init_tools_shell = __esm({
2422
2443
  list() {
2423
2444
  return [...this.jobs].map(([id, j]) => ({ id, command: j.command, status: j.status }));
2424
2445
  }
2446
+ /**
2447
+ * Take over an ALREADY-RUNNING child as a background job. A foreground command that outruns its
2448
+ * timeout is not necessarily a hung command — killing it throws away work that was nearly done and,
2449
+ * worse, a launcher that spawned its own detached worker leaves that worker running with nothing
2450
+ * tracking it. Adopting hands the model a handle instead: the command keeps going, its completion
2451
+ * is reported like any other job, and `seed` carries the output produced before the handoff.
2452
+ */
2453
+ adopt(command, proc, seed = "") {
2454
+ const id = `job-${++this.seq}`;
2455
+ const max = this.cfg.maxBuffer ?? 256 * 1024;
2456
+ const job = { command, buf: seed.slice(-max), status: "running", proc };
2457
+ const append = (chunk) => {
2458
+ const s = typeof chunk === "string" ? chunk : chunk?.toString?.("utf8") ?? "";
2459
+ job.buf = (job.buf + s).slice(-max);
2460
+ };
2461
+ proc.stdout?.on("data", append);
2462
+ proc.stderr?.on("data", append);
2463
+ proc.on("error", (err2) => {
2464
+ if (job.status === "running") {
2465
+ job.status = "error";
2466
+ append(`
2467
+ [error] ${err2?.message ?? err2}`);
2468
+ this.notifyExit(id, job);
2469
+ }
2470
+ });
2471
+ proc.on("close", (code) => {
2472
+ if (job.status === "running") {
2473
+ job.status = "exited";
2474
+ job.exitCode = code ?? void 0;
2475
+ this.notifyExit(id, job);
2476
+ }
2477
+ });
2478
+ this.jobs.set(id, job);
2479
+ return id;
2480
+ }
2425
2481
  kill(id) {
2426
2482
  const j = this.jobs.get(id);
2427
2483
  if (!j) return false;
@@ -3807,7 +3863,9 @@ var AgentOptions = class {
3807
3863
  /** Retry budget for an EMPTY/silent-turn (a delegated-runtime wedge: cursor's warm helper spins then
3808
3864
  * returns a content-less end, surfaced as `code:'empty'`/`code:'cursor_silent'`). Tuned WIDER than the
3809
3865
  * network-drop budget (fixed 2) because clearing a wedge needs several reap+respawn cycles — each retry
3810
- * hits a FRESH cursor session (the adapter evicts the wedged warm helper on the silent-end). Still bounded:
3866
+ * hits a FRESH cursor session (the adapter evicts the wedged warm helper on the silent-end; a stall-watchdog
3867
+ * cancel is NOT this class — it is classified `stall`, and the watchdog's coded abort reason is what tells
3868
+ * the adapter to evict on that path too). Still bounded:
3811
3869
  * after exhaustion the turn surfaces `finishReason:'empty'` (non-zero exit), never infinite, never silent.
3812
3870
  * Env: AGENTX_EMPTY_RETRIES. */
3813
3871
  emptyRetries = envInt("AGENTX_EMPTY_RETRIES", 4);
@@ -4326,6 +4384,7 @@ var Agent = class _Agent {
4326
4384
  try {
4327
4385
  const r = await o.ai.chat({ model: o.model, messages: sent, tools: sentTools, stream: true, signal: stall.signal, ...o.toolChoice ? { toolChoice: o.toolChoice } : {}, ...reasonOpts });
4328
4386
  res = await this.consumeStream(r, stall.poke);
4387
+ if (stall.stalled()) throw stall.reclassify(void 0);
4329
4388
  } catch (err2) {
4330
4389
  throw stall.reclassify(err2);
4331
4390
  } finally {
@@ -4360,7 +4419,7 @@ var Agent = class _Agent {
4360
4419
  if (err2?.code === "budget") return kill("budget");
4361
4420
  if (err2?.code === "stall") {
4362
4421
  log6.warn(`model stalled out after retries: ${err2?.message ?? err2}`);
4363
- return kill("timeout");
4422
+ return { ...kill("timeout"), error: err2 };
4364
4423
  }
4365
4424
  if (o.signal?.aborted || isAbortError(err2)) return kill("aborted");
4366
4425
  if (err2?.code === "empty" || err2?.code === "cursor_silent") {
@@ -4508,31 +4567,30 @@ var Agent = class _Agent {
4508
4567
  if (parent?.aborted) ac.abort();
4509
4568
  else parent?.addEventListener("abort", onParentAbort, { once: true });
4510
4569
  let timer;
4570
+ const stallError = () => {
4571
+ const e = new Error(`model stalled: no stream output for ${stallMs >= 1e3 ? `${Math.round(stallMs / 1e3)}s` : `${stallMs}ms`}`);
4572
+ e.code = "stall";
4573
+ return e;
4574
+ };
4511
4575
  const poke = () => {
4512
4576
  if (!stallMs) return;
4513
4577
  if (timer) clearTimeout(timer);
4514
4578
  timer = setTimeout(() => {
4515
4579
  fired = true;
4516
- ac.abort();
4580
+ ac.abort(stallError());
4517
4581
  }, stallMs);
4518
4582
  };
4519
4583
  poke();
4584
+ const stalled = () => fired && !parent?.aborted;
4520
4585
  return {
4521
4586
  signal: ac.signal,
4522
4587
  poke,
4588
+ stalled,
4523
4589
  clear: () => {
4524
4590
  if (timer) clearTimeout(timer);
4525
4591
  parent?.removeEventListener("abort", onParentAbort);
4526
4592
  },
4527
- // Distinguish OUR abort (silent provider) from a parent/user cancel, which must stay an abort.
4528
- reclassify: (err2) => {
4529
- if (fired && !parent?.aborted) {
4530
- const e = new Error(`model stalled: no stream output for ${stallMs >= 1e3 ? `${Math.round(stallMs / 1e3)}s` : `${stallMs}ms`}`);
4531
- e.code = "stall";
4532
- return e;
4533
- }
4534
- return err2;
4535
- }
4593
+ reclassify: (err2) => stalled() ? stallError() : err2
4536
4594
  };
4537
4595
  }
4538
4596
  /** A clean-stop turn that carries no assistant text AND no tool calls (native or delegated) — a