@livx.cc/agentx 0.99.16 → 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. */
@@ -381,6 +385,11 @@ declare class Agent {
381
385
  /** Drop any queued-but-unconsumed injections. The REPL calls this after a LOCAL command (/cmd, !bash)
382
386
  * — which runs no send() to drain them — so a mid-command steer can't leak into the next real turn. */
383
387
  clearInjections(): void;
388
+ /** Pull queued injections that are plain TEXT, removing them, for a runtime that can accept mid-turn
389
+ * steers over its own channel (claude-code's warm InputQueue via `providerOptions.steer`). Non-text
390
+ * injections (images) stay queued for the step-boundary `drainInjections()` re-open — the text-only
391
+ * steer channel can't carry them. Consumed DURING chat(); no overlap with drainInjections() (post-call). */
392
+ drainPendingSteers(): string[];
384
393
  /** Fold any host-injected messages into the transcript as user turns. Returns true if any landed. */
385
394
  private drainInjections;
386
395
  constructor(options?: Partial<AgentOptions>);
@@ -431,6 +440,14 @@ declare class Agent {
431
440
  * (`{kind:'text_delta'}`) and fold all chunks back into the single
432
441
  * ChatResponse the non-stream path would have returned.
433
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;
434
451
  private consumeStream;
435
452
  private dispatch;
436
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-CvLBalJE.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. */
@@ -3421,6 +3425,22 @@ var Agent = class _Agent {
3421
3425
  clearInjections() {
3422
3426
  this.pendingInjections.length = 0;
3423
3427
  }
3428
+ /** Pull queued injections that are plain TEXT, removing them, for a runtime that can accept mid-turn
3429
+ * steers over its own channel (claude-code's warm InputQueue via `providerOptions.steer`). Non-text
3430
+ * injections (images) stay queued for the step-boundary `drainInjections()` re-open — the text-only
3431
+ * steer channel can't carry them. Consumed DURING chat(); no overlap with drainInjections() (post-call). */
3432
+ drainPendingSteers() {
3433
+ if (!this.pendingInjections.length) return [];
3434
+ const texts = [];
3435
+ this.pendingInjections = this.pendingInjections.filter((c) => {
3436
+ if (typeof c === "string") {
3437
+ texts.push(c);
3438
+ return false;
3439
+ }
3440
+ return true;
3441
+ });
3442
+ return texts;
3443
+ }
3424
3444
  /** Fold any host-injected messages into the transcript as user turns. Returns true if any landed. */
3425
3445
  drainInjections() {
3426
3446
  if (!this.pendingInjections.length) return false;
@@ -3625,17 +3645,25 @@ var Agent = class _Agent {
3625
3645
  return typeof raw === "string" ? raw : raw.text;
3626
3646
  }
3627
3647
  } : void 0;
3648
+ const claudeSteerPo = o.model.startsWith("claude-code/") ? { steer: () => this.drainPendingSteers() } : void 0;
3628
3649
  const reasonOpts = {
3629
3650
  ...frag,
3630
3651
  ...o.promptCache ? { promptCache: true } : {},
3631
- ...o.providerOptions || cursorPo ? { providerOptions: { ...frag.providerOptions, ...o.providerOptions, ...cursorPo } } : {}
3652
+ ...o.providerOptions || cursorPo || claudeSteerPo ? { providerOptions: { ...frag.providerOptions, ...o.providerOptions, ...cursorPo, ...claudeSteerPo } } : {}
3632
3653
  };
3633
3654
  try {
3634
3655
  for (let attempt = 0; ; attempt++) {
3635
3656
  try {
3636
3657
  if (useStream) {
3637
- const r = await o.ai.chat({ model: o.model, messages: sent, tools: sentTools, stream: true, signal: o.signal, ...o.toolChoice ? { toolChoice: o.toolChoice } : {}, ...reasonOpts });
3638
- 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
+ }
3639
3667
  } else {
3640
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 });
3641
3669
  res = r;
@@ -3645,16 +3673,21 @@ var Agent = class _Agent {
3645
3673
  const sc = err2?.statusCode;
3646
3674
  const serverSide = sc >= 500 || sc === 408 || /Service Unavailable|overloaded|Internal server error|Bad gateway|Gateway time/i.test(String(err2?.message ?? ""));
3647
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));
3648
- 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);
3649
3678
  if (!transient) throw err2;
3650
3679
  const waitMs = 1e3 * (attempt + 1);
3651
- log4.warn(`network drop mid-step (${err2?.message ?? err2}) \u2014 retrying in ${waitMs}ms`);
3652
- 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})` });
3653
3682
  await new Promise((r) => setTimeout(r, waitMs));
3654
3683
  }
3655
3684
  }
3656
3685
  } catch (err2) {
3657
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
+ }
3658
3691
  if (o.signal?.aborted || isAbortError(err2)) return kill("aborted");
3659
3692
  const body = err2?.body ?? err2?.response?.data ?? err2?.error;
3660
3693
  let bodyStr;
@@ -3725,12 +3758,54 @@ var Agent = class _Agent {
3725
3758
  * (`{kind:'text_delta'}`) and fold all chunks back into the single
3726
3759
  * ChatResponse the non-stream path would have returned.
3727
3760
  */
3728
- 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) {
3729
3803
  let content = "";
3730
3804
  let finishReason;
3731
3805
  let toolCalls;
3732
3806
  let usage;
3733
3807
  for await (const chunk of stream) {
3808
+ poke?.();
3734
3809
  if (this.options.signal?.aborted) break;
3735
3810
  if (chunk.reasoningContent) this.options.host.notify({ kind: "thinking_delta", message: chunk.reasoningContent });
3736
3811
  if (chunk.toolActivity) {