@livx.cc/agentx 0.99.17 → 0.99.18

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) {