@livx.cc/agentx 0.99.17 → 0.99.19

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.
@@ -214,6 +214,10 @@ declare class AgentOptions {
214
214
  maxTokens: number;
215
215
  /** Wall-clock ceiling in ms for the whole run. 0 = unbounded. */
216
216
  timeoutMs: number;
217
+ /** Idle-stall watchdog for a SINGLE streamed model request: abort it if no chunk (text OR reasoning)
218
+ * arrives for this long. The between-steps `timeoutMs` can't preempt an in-flight `chat()`, so a
219
+ * provider that goes silent mid-stream would otherwise park the loop forever. 0 = off. */
220
+ stallMs: number;
217
221
  /** Stop if the identical tool-call batch (name+args) repeats this many times in a row. 0 = off. */
218
222
  maxRepeats: number;
219
223
  /** Cumulative cap on tool calls dispatched across the run. 0 = unbounded. */
@@ -436,6 +440,14 @@ declare class Agent {
436
440
  * (`{kind:'text_delta'}`) and fold all chunks back into the single
437
441
  * ChatResponse the non-stream path would have returned.
438
442
  */
443
+ /**
444
+ * Idle-stall watchdog for one streamed model request. Returns a linked AbortController `signal` to
445
+ * hand to `chat()`, a `poke()` to reset the idle timer on each chunk, a `reclassify()` that turns a
446
+ * watchdog-fired abort into an Error{code:'stall'} (leaving a parent/user abort untouched), and a
447
+ * `clear()` to disarm. Aborts the request if no `poke()` (or completion) happens within `stallMs`.
448
+ * `stallMs = 0` disables the timer but still links parent-abort → child so cancellation propagates.
449
+ */
450
+ private armStallWatchdog;
439
451
  private consumeStream;
440
452
  private dispatch;
441
453
  private static readonly WRITE_CLASS;
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bun
2
- import { H as Hooks, h as RunResult, R as ReasoningEffort, A as Agent } from './Agent-BfQS-MfB.js';
2
+ import { H as Hooks, h as RunResult, R as ReasoningEffort, A as Agent } from './Agent-Cn9BxS8h.js';
3
3
  import { IFilesystem } from '@livx.cc/wcli/core';
4
4
  import { M as Message, H as HostBridge, c as ContentPart, e as MessageContent } from './tools-DKf3hN4M.js';
5
5
 
package/dist/cli.js CHANGED
@@ -3219,6 +3219,10 @@ var AgentOptions = class {
3219
3219
  maxTokens = 2e5;
3220
3220
  /** Wall-clock ceiling in ms for the whole run. 0 = unbounded. */
3221
3221
  timeoutMs = 12e4;
3222
+ /** Idle-stall watchdog for a SINGLE streamed model request: abort it if no chunk (text OR reasoning)
3223
+ * arrives for this long. The between-steps `timeoutMs` can't preempt an in-flight `chat()`, so a
3224
+ * provider that goes silent mid-stream would otherwise park the loop forever. 0 = off. */
3225
+ stallMs = 6e4;
3222
3226
  /** Stop if the identical tool-call batch (name+args) repeats this many times in a row. 0 = off. */
3223
3227
  maxRepeats = 3;
3224
3228
  /** Cumulative cap on tool calls dispatched across the run. 0 = unbounded. */
@@ -3651,8 +3655,15 @@ var Agent = class _Agent {
3651
3655
  for (let attempt = 0; ; attempt++) {
3652
3656
  try {
3653
3657
  if (useStream) {
3654
- const r = await o.ai.chat({ model: o.model, messages: sent, tools: sentTools, stream: true, signal: o.signal, ...o.toolChoice ? { toolChoice: o.toolChoice } : {}, ...reasonOpts });
3655
- res = await this.consumeStream(r);
3658
+ const stall = this.armStallWatchdog(o.signal, o.stallMs);
3659
+ try {
3660
+ const r = await o.ai.chat({ model: o.model, messages: sent, tools: sentTools, stream: true, signal: stall.signal, ...o.toolChoice ? { toolChoice: o.toolChoice } : {}, ...reasonOpts });
3661
+ res = await this.consumeStream(r, stall.poke);
3662
+ } catch (err2) {
3663
+ throw stall.reclassify(err2);
3664
+ } finally {
3665
+ stall.clear();
3666
+ }
3656
3667
  } else {
3657
3668
  const r = await o.ai.chat({ model: o.model, messages: sent, tools: sentTools, stream: false, signal: o.signal, ...o.toolChoice ? { toolChoice: o.toolChoice } : {}, ...reasonOpts });
3658
3669
  res = r;
@@ -3662,16 +3673,21 @@ var Agent = class _Agent {
3662
3673
  const sc = err2?.statusCode;
3663
3674
  const serverSide = sc >= 500 || sc === 408 || /Service Unavailable|overloaded|Internal server error|Bad gateway|Gateway time/i.test(String(err2?.message ?? ""));
3664
3675
  const network = !sc && /ECONNRESET|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|EPIPE|socket hang up|fetch failed|network|terminated|UND_ERR/i.test(String(err2?.message ?? err2?.code ?? err2));
3665
- const transient = !o.signal?.aborted && !isAbortError(err2) && attempt < 2 && (network || serverSide);
3676
+ const stalled = err2?.code === "stall";
3677
+ const transient = !o.signal?.aborted && !isAbortError(err2) && attempt < 2 && (network || serverSide || stalled);
3666
3678
  if (!transient) throw err2;
3667
3679
  const waitMs = 1e3 * (attempt + 1);
3668
- log4.warn(`network drop mid-step (${err2?.message ?? err2}) \u2014 retrying in ${waitMs}ms`);
3669
- o.host?.notify?.({ kind: "retry", message: `connection dropped \u2014 retrying step (#${attempt + 1})` });
3680
+ log4.warn(`${stalled ? "model stall" : "network drop"} mid-step (${err2?.message ?? err2}) \u2014 retrying in ${waitMs}ms`);
3681
+ o.host?.notify?.({ kind: "retry", message: `${stalled ? "model stalled" : "connection dropped"} \u2014 retrying step (#${attempt + 1})` });
3670
3682
  await new Promise((r) => setTimeout(r, waitMs));
3671
3683
  }
3672
3684
  }
3673
3685
  } catch (err2) {
3674
3686
  if (err2?.code === "budget") return kill("budget");
3687
+ if (err2?.code === "stall") {
3688
+ log4.warn(`model stalled out after retries: ${err2?.message ?? err2}`);
3689
+ return kill("timeout");
3690
+ }
3675
3691
  if (o.signal?.aborted || isAbortError(err2)) return kill("aborted");
3676
3692
  const body = err2?.body ?? err2?.response?.data ?? err2?.error;
3677
3693
  let bodyStr;
@@ -3742,12 +3758,54 @@ var Agent = class _Agent {
3742
3758
  * (`{kind:'text_delta'}`) and fold all chunks back into the single
3743
3759
  * ChatResponse the non-stream path would have returned.
3744
3760
  */
3745
- async consumeStream(stream) {
3761
+ /**
3762
+ * Idle-stall watchdog for one streamed model request. Returns a linked AbortController `signal` to
3763
+ * hand to `chat()`, a `poke()` to reset the idle timer on each chunk, a `reclassify()` that turns a
3764
+ * watchdog-fired abort into an Error{code:'stall'} (leaving a parent/user abort untouched), and a
3765
+ * `clear()` to disarm. Aborts the request if no `poke()` (or completion) happens within `stallMs`.
3766
+ * `stallMs = 0` disables the timer but still links parent-abort → child so cancellation propagates.
3767
+ */
3768
+ armStallWatchdog(parent, stallMs) {
3769
+ const ac = new AbortController();
3770
+ let fired = false;
3771
+ const onParentAbort = () => ac.abort();
3772
+ if (parent?.aborted) ac.abort();
3773
+ else parent?.addEventListener("abort", onParentAbort, { once: true });
3774
+ let timer;
3775
+ const poke = () => {
3776
+ if (!stallMs) return;
3777
+ if (timer) clearTimeout(timer);
3778
+ timer = setTimeout(() => {
3779
+ fired = true;
3780
+ ac.abort();
3781
+ }, stallMs);
3782
+ };
3783
+ poke();
3784
+ return {
3785
+ signal: ac.signal,
3786
+ poke,
3787
+ clear: () => {
3788
+ if (timer) clearTimeout(timer);
3789
+ parent?.removeEventListener("abort", onParentAbort);
3790
+ },
3791
+ // Distinguish OUR abort (silent provider) from a parent/user cancel, which must stay an abort.
3792
+ reclassify: (err2) => {
3793
+ if (fired && !parent?.aborted) {
3794
+ const e = new Error(`model stalled: no stream output for ${stallMs >= 1e3 ? `${Math.round(stallMs / 1e3)}s` : `${stallMs}ms`}`);
3795
+ e.code = "stall";
3796
+ return e;
3797
+ }
3798
+ return err2;
3799
+ }
3800
+ };
3801
+ }
3802
+ async consumeStream(stream, poke) {
3746
3803
  let content = "";
3747
3804
  let finishReason;
3748
3805
  let toolCalls;
3749
3806
  let usage;
3750
3807
  for await (const chunk of stream) {
3808
+ poke?.();
3751
3809
  if (this.options.signal?.aborted) break;
3752
3810
  if (chunk.reasoningContent) this.options.host.notify({ kind: "thinking_delta", message: chunk.reasoningContent });
3753
3811
  if (chunk.toolActivity) {
@@ -9836,11 +9894,15 @@ function lastToken(line) {
9836
9894
  const m = line.match(/(\S*)$/);
9837
9895
  return m ? m[1] : "";
9838
9896
  }
9897
+ function atLineStart(line, token) {
9898
+ return line.slice(0, line.length - token.length).trim() === "";
9899
+ }
9839
9900
  function completeLine(line, ctx) {
9840
9901
  const token = lastToken(line);
9841
- if (token.startsWith("/") && line.trimStart() === token) {
9902
+ if (token.startsWith("/")) {
9903
+ const pool = atLineStart(line, token) ? ctx.commands : ctx.agentCommands ?? ctx.commands;
9842
9904
  const want = token.slice(1);
9843
- const hits = rank(ctx.commands.filter((c) => fuzzy(want, c)), want).map((c) => "/" + c);
9905
+ const hits = rank(pool.filter((c) => fuzzy(want, c)), want).map((c) => "/" + c);
9844
9906
  return [hits, token];
9845
9907
  }
9846
9908
  if (line.startsWith("!") && line.length > 1 && ctx.bangHistory?.length) {
@@ -10426,6 +10488,14 @@ var EditorState = class _EditorState {
10426
10488
  for (const [ph, full] of this.pastes) if (out.includes(ph)) out = out.split(ph).join(full);
10427
10489
  return out;
10428
10490
  }
10491
+ /**
10492
+ * A `/cmd` the REPL will DISPATCH — i.e. the line's first word. A slash token elsewhere in the
10493
+ * prompt is prose the model reads, so it completes like an `@path`: no accept-and-run, no
10494
+ * exact-match row. Kept in sync with the REPL's whole-line `/` dispatch via `atLineStart`.
10495
+ */
10496
+ get runnable() {
10497
+ return this.token.startsWith("/") && atLineStart(this.buf.slice(0, this.cursor), this.token);
10498
+ }
10429
10499
  /** Recompute suggestions from the line up to the cursor. */
10430
10500
  refresh() {
10431
10501
  const r = this.suggest(this.buf.slice(0, this.cursor));
@@ -10433,7 +10503,7 @@ var EditorState = class _EditorState {
10433
10503
  this.token = r.token;
10434
10504
  this.describe = r.describe;
10435
10505
  const exact = r.hits.length === 1 && r.hits[0] === r.token;
10436
- this.menuOpen = r.hits.length > 0 && (!exact || r.token.startsWith("/"));
10506
+ this.menuOpen = r.hits.length > 0 && (!exact || this.runnable);
10437
10507
  if (this.sel >= r.hits.length) this.sel = 0;
10438
10508
  }
10439
10509
  // ── Editor undo (Ctrl-_): snapshot before every mutation, pop to restore ──
@@ -10629,13 +10699,22 @@ var EditorState = class _EditorState {
10629
10699
  this.hits = [];
10630
10700
  this.sel = 0;
10631
10701
  }
10632
- /** Replace the trailing token with the highlighted hit; re-suggest (e.g. `@src/` opens its children). */
10702
+ /**
10703
+ * Replace the trailing token with the highlighted hit; re-suggest (e.g. `@src/` opens its children).
10704
+ * A finished hit gets a trailing space so the next word is typed straight away (CC/shell parity).
10705
+ * Two exceptions: a DIRECTORY hit (`@src/`) is a waypoint, not a word — a space there would end the
10706
+ * mention mid-path; and an existing following space isn't doubled. A spaced dir rides the quoted
10707
+ * form (`@"my dir/"`), so the `/` test must look past a trailing quote.
10708
+ */
10633
10709
  accept() {
10634
10710
  if (!this.menuOpen || !this.hits.length) return false;
10635
10711
  const hit = this.hits[this.sel] ?? this.hits[0];
10636
10712
  const start = this.cursor - this.token.length;
10637
- this.buf = this.buf.slice(0, start) + hit + this.buf.slice(this.cursor);
10638
- this.cursor = start + hit.length;
10713
+ const rest = this.buf.slice(this.cursor);
10714
+ const isDir = hit.replace(/"$/, "").endsWith("/");
10715
+ const pad = isDir || rest.startsWith(" ") ? "" : " ";
10716
+ this.buf = this.buf.slice(0, start) + hit + pad + rest;
10717
+ this.cursor = start + hit.length + pad.length;
10639
10718
  this.refresh();
10640
10719
  return true;
10641
10720
  }
@@ -10849,9 +10928,9 @@ function applyKey(s, key, str) {
10849
10928
  case "return":
10850
10929
  case "enter":
10851
10930
  if (s.menuOpen) {
10852
- const isCmd = s.token.startsWith("/");
10931
+ const run = s.runnable;
10853
10932
  s.accept();
10854
- return isCmd ? "submit" : "none";
10933
+ return run ? "submit" : "none";
10855
10934
  }
10856
10935
  if (s.buf.slice(0, s.cursor).endsWith("\\")) {
10857
10936
  s.backspace();
@@ -14693,10 +14772,11 @@ ${task}`;
14693
14772
  return null;
14694
14773
  }
14695
14774
  };
14696
- const commandNames = () => [...Object.keys(builtins).filter((k) => k !== "quit"), ...cmds.map((c) => c.name), ...skills.map((s) => s.name)];
14775
+ const agentCommandNames = () => [...cmds.map((c) => c.name), ...skills.map((s) => s.name)];
14776
+ const commandNames = () => [...Object.keys(builtins).filter((k) => k !== "quit"), ...agentCommandNames()];
14697
14777
  const describe = (hit) => hit.startsWith("/") ? builtins[hit.slice(1)]?.desc ?? cmds.find((c) => c.name === hit.slice(1))?.description ?? skills.find((s) => s.name === hit.slice(1))?.description : void 0;
14698
14778
  const suggest = (lineToCursor) => {
14699
- const [hits, token] = completeLine(lineToCursor, { commands: commandNames(), listDir, bangHistory: history });
14779
+ const [hits, token] = completeLine(lineToCursor, { commands: commandNames(), agentCommands: agentCommandNames(), listDir, bangHistory: history });
14700
14780
  return { hits, token, describe };
14701
14781
  };
14702
14782
  const editor = createLineEditor(process.stderr);