@livx.cc/agentx 0.99.45 → 0.99.47

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.
@@ -186,6 +186,8 @@ interface RunResult {
186
186
  * Answer it and resume the session to continue. */
187
187
  finishReason: 'stop' | 'max_steps' | 'budget' | 'timeout' | 'loop' | 'max_tool_calls' | 'aborted' | 'error' | 'needs_input' | 'empty';
188
188
  messages: Message[];
189
+ /** The provider's own finish reason for the final model call (e.g. 'stop', 'end_turn', 'length'), when it gave one. */
190
+ providerFinishReason?: string;
189
191
  /** The parked question — present iff `finishReason === 'needs_input'`. */
190
192
  question?: UserQuestion;
191
193
  /** Accumulated token usage across all turns (non-stream path). With prompt caching,
@@ -352,6 +354,10 @@ declare class AgentOptions {
352
354
  * not prose: a named tool with zero invocations is provable. On detection the loop nudges ONCE for a real
353
355
  * call, and records the names in `RunResult.fabricatedToolClaims` either way. */
354
356
  catchFabricatedToolUse: boolean;
357
+ /** An empty model turn (no text, no tools) that ends the loop after work is re-prompted ONCE, then finishes
358
+ * as `finishReason:'empty'` (a failure) instead of a phantom 'stop'. Off for layers that deliberately
359
+ * allow a silent turn and repair it themselves (the duplex voice reflex). */
360
+ failEmptyTurns: boolean;
355
361
  /** Fold the dropped middle of an over-long transcript into a synthetic summary (edge-safe, no LLM). Off => drop-oldest. */
356
362
  compaction?: {
357
363
  maxMessages: number;
@@ -415,6 +421,22 @@ declare class Agent {
415
421
  /** Per-run ground truth: every tool name actually INVOKED this run (native dispatch + delegated
416
422
  * runtime activity). The ledger that makes a fabricated tool claim provable. Reset per run. */
417
423
  private invokedTools;
424
+ /** Delegated-runtime HOST tools currently executing (cursor toolExecutor). While > 0 the provider is
425
+ * not silent — it is waiting on US — so the idle-stall watchdog must not fire (see armStallWatchdog). */
426
+ private hostToolsInFlight;
427
+ /** The hold is bounded by the transport tool deadline: a host tool that never settles (the helper already gave
428
+ * up on it) must not disable stall protection for the rest of the request — or this instance. */
429
+ private hostToolHoldUntil;
430
+ /** Delegated-runtime NATIVE tools (cursor's own shell/mcp/task) seen `running` but not yet `completed`/`error`,
431
+ * id → hold-until. The runtime streams nothing while one runs, so it holds the watchdog like a host tool —
432
+ * bounded by the same tool deadline (`toolHoldMs`) so a lost `completed` event can't disable it. */
433
+ private nativeToolsInFlight;
434
+ private toolHoldMs;
435
+ /** A tool (host or native) already executed in the current step attempt: a stall retry replays the step
436
+ * from the pre-step transcript, which would re-run it — so such a stall is not retried. */
437
+ private toolRanThisAttempt;
438
+ /** Resets the active request's stall timer; a host tool's completion restarts the model's idle window. */
439
+ private stallPoke?;
418
440
  /** Advertised tools this run's text claimed to have used while the ledger showed zero calls. */
419
441
  private fabricatedClaims;
420
442
  private activeHooks?;
@@ -519,6 +541,8 @@ declare class Agent {
519
541
  * `stallMs = 0` disables the timer but still links parent-abort → child so cancellation propagates.
520
542
  */
521
543
  private armStallWatchdog;
544
+ /** A host or native delegated tool is running within its deadline — the provider is quiet, not stalled. */
545
+ private toolHoldActive;
522
546
  /** A clean-stop turn that carries no assistant text AND no tool calls (native or delegated) — a
523
547
  * non-result the model produced by going silent. Callers retry it rather than report "done". */
524
548
  private isEmptyStop;
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-Bzs57-iD.js';
2
+ import { H as Hooks, i as RunResult, R as ReasoningEffort, A as Agent } from './Agent-BWbQ4Sqp.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-BqL8Lk4J.js';
5
5
 
@@ -277,8 +277,6 @@ declare function expandMentions(fs: IFilesystem, line: string): Promise<{
277
277
  declare function jsonResult(res: RunResult, session: SessionData): {
278
278
  error?: any;
279
279
  question?: UserQuestion | undefined;
280
- ok: boolean;
281
- finishReason: "error" | "budget" | "stop" | "max_steps" | "timeout" | "loop" | "max_tool_calls" | "aborted" | "needs_input" | "empty";
282
280
  text: string;
283
281
  steps: number;
284
282
  tools: number;
@@ -290,6 +288,9 @@ declare function jsonResult(res: RunResult, session: SessionData): {
290
288
  cacheReadTokens?: number;
291
289
  } | undefined;
292
290
  sessionId: string;
291
+ providerFinishReason?: string | undefined;
292
+ ok: boolean;
293
+ finishReason: "error" | "budget" | "stop" | "max_steps" | "timeout" | "loop" | "max_tool_calls" | "aborted" | "needs_input" | "empty";
293
294
  };
294
295
  /**
295
296
  * Read one logical input, supporting `\`-continuation for multi-line prompts: a line ending in
package/dist/cli.js CHANGED
@@ -3961,6 +3961,10 @@ var AgentOptions = class {
3961
3961
  * not prose: a named tool with zero invocations is provable. On detection the loop nudges ONCE for a real
3962
3962
  * call, and records the names in `RunResult.fabricatedToolClaims` either way. */
3963
3963
  catchFabricatedToolUse = true;
3964
+ /** An empty model turn (no text, no tools) that ends the loop after work is re-prompted ONCE, then finishes
3965
+ * as `finishReason:'empty'` (a failure) instead of a phantom 'stop'. Off for layers that deliberately
3966
+ * allow a silent turn and repair it themselves (the duplex voice reflex). */
3967
+ failEmptyTurns = true;
3964
3968
  /** Fold the dropped middle of an over-long transcript into a synthetic summary (edge-safe, no LLM). Off => drop-oldest. */
3965
3969
  compaction;
3966
3970
  /** Add `Checkpoint`/`Rollback` tools (requires the fs to be an OverlayFilesystem). */
@@ -4034,6 +4038,22 @@ var Agent = class _Agent {
4034
4038
  /** Per-run ground truth: every tool name actually INVOKED this run (native dispatch + delegated
4035
4039
  * runtime activity). The ledger that makes a fabricated tool claim provable. Reset per run. */
4036
4040
  invokedTools = /* @__PURE__ */ new Set();
4041
+ /** Delegated-runtime HOST tools currently executing (cursor toolExecutor). While > 0 the provider is
4042
+ * not silent — it is waiting on US — so the idle-stall watchdog must not fire (see armStallWatchdog). */
4043
+ hostToolsInFlight = 0;
4044
+ /** The hold is bounded by the transport tool deadline: a host tool that never settles (the helper already gave
4045
+ * up on it) must not disable stall protection for the rest of the request — or this instance. */
4046
+ hostToolHoldUntil = 0;
4047
+ /** Delegated-runtime NATIVE tools (cursor's own shell/mcp/task) seen `running` but not yet `completed`/`error`,
4048
+ * id → hold-until. The runtime streams nothing while one runs, so it holds the watchdog like a host tool —
4049
+ * bounded by the same tool deadline (`toolHoldMs`) so a lost `completed` event can't disable it. */
4050
+ nativeToolsInFlight = /* @__PURE__ */ new Map();
4051
+ toolHoldMs = 0;
4052
+ /** A tool (host or native) already executed in the current step attempt: a stall retry replays the step
4053
+ * from the pre-step transcript, which would re-run it — so such a stall is not retried. */
4054
+ toolRanThisAttempt = false;
4055
+ /** Resets the active request's stall timer; a host tool's completion restarts the model's idle window. */
4056
+ stallPoke;
4037
4057
  /** Advertised tools this run's text claimed to have used while the ledger showed zero calls. */
4038
4058
  fabricatedClaims = /* @__PURE__ */ new Set();
4039
4059
  activeHooks;
@@ -4336,6 +4356,7 @@ var Agent = class _Agent {
4336
4356
  let toolCallsTotal = 0;
4337
4357
  let lastFp = "";
4338
4358
  let repeats = 0;
4359
+ let emptyNudged = false;
4339
4360
  let closureNudged = false;
4340
4361
  let lastProgressAt = start;
4341
4362
  const kill = (finishReason) => {
@@ -4344,12 +4365,13 @@ var Agent = class _Agent {
4344
4365
  const claims = this.survivingClaims();
4345
4366
  return { text: lastAssistantText(this.transcript), steps, finishReason, messages: this.transcript, usage, usageEstimated, ...claims.length ? { fabricatedToolClaims: claims } : {} };
4346
4367
  };
4368
+ const overBudget = () => !!o.maxTokens && usage.totalTokens - 0.9 * usage.cacheReadTokens >= o.maxTokens;
4369
+ const canNudge = () => steps < o.maxSteps && !overBudget();
4347
4370
  while (true) {
4348
4371
  if (o.signal?.aborted) return kill("aborted");
4349
4372
  if (steps >= o.maxSteps) return kill("max_steps");
4350
4373
  if (o.timeoutMs && Date.now() - lastProgressAt - this.parkedMs >= o.timeoutMs) return kill("timeout");
4351
- const budgetTokens = usage.totalTokens - 0.9 * usage.cacheReadTokens;
4352
- if (o.maxTokens && budgetTokens >= o.maxTokens) return kill("budget");
4374
+ if (overBudget()) return kill("budget");
4353
4375
  steps++;
4354
4376
  this.options.host?.notify?.({ kind: "turn_start", message: `step ${steps}` });
4355
4377
  let res;
@@ -4360,6 +4382,7 @@ var Agent = class _Agent {
4360
4382
  const hostToolTimeoutMs = o.providerOptions?.toolTimeoutMs;
4361
4383
  const derivedToolTimeoutMs = Math.max(TRANSPORT_TOOL_TIMEOUT_FLOOR_MS, ...this.activeTools.map(transportHeadroomMs));
4362
4384
  const effectiveToolTimeoutMs = typeof hostToolTimeoutMs === "number" ? Math.max(hostToolTimeoutMs, derivedToolTimeoutMs) : derivedToolTimeoutMs;
4385
+ this.toolHoldMs = effectiveToolTimeoutMs;
4363
4386
  if (isCursorWithTools) {
4364
4387
  if (typeof hostToolTimeoutMs === "number" && effectiveToolTimeoutMs !== hostToolTimeoutMs) {
4365
4388
  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.`);
@@ -4370,7 +4393,15 @@ var Agent = class _Agent {
4370
4393
  toolTimeoutMs: effectiveToolTimeoutMs,
4371
4394
  toolExecutor: async (name, args) => {
4372
4395
  const tc = { id: `cursor-${Date.now()}`, type: "function", function: { name, arguments: JSON.stringify(args) } };
4373
- return await this.dispatch(tc);
4396
+ this.hostToolsInFlight++;
4397
+ this.hostToolHoldUntil = Math.max(this.hostToolHoldUntil, Date.now() + effectiveToolTimeoutMs);
4398
+ try {
4399
+ return await this.dispatch(tc);
4400
+ } finally {
4401
+ this.hostToolsInFlight--;
4402
+ this.toolRanThisAttempt = true;
4403
+ this.stallPoke?.();
4404
+ }
4374
4405
  }
4375
4406
  } : void 0;
4376
4407
  const claudeSteerPo = o.model.startsWith("claude-code/") ? { steer: () => this.drainPendingSteers() } : void 0;
@@ -4381,6 +4412,8 @@ var Agent = class _Agent {
4381
4412
  };
4382
4413
  try {
4383
4414
  for (let attempt = 0; ; attempt++) {
4415
+ this.nativeToolsInFlight.clear();
4416
+ this.toolRanThisAttempt = false;
4384
4417
  try {
4385
4418
  if (useStream) {
4386
4419
  const stall = this.armStallWatchdog(o.signal, o.stallMs);
@@ -4410,7 +4443,9 @@ var Agent = class _Agent {
4410
4443
  const stalled = err2?.code === "stall";
4411
4444
  const empty = err2?.code === "empty" || err2?.code === "cursor_silent";
4412
4445
  const maxAttempts = empty ? Math.max(o.emptyRetries, 0) : 2;
4413
- const transient = !o.signal?.aborted && !isAbortError(err2) && attempt < maxAttempts && (network || serverSide || stalled || empty);
4446
+ const replaysTool = stalled && this.toolRanThisAttempt;
4447
+ if (replaysTool) log6.warn("model stall after tool(s) already ran this step \u2014 not retrying (a retry would replay them)");
4448
+ const transient = !o.signal?.aborted && !isAbortError(err2) && !replaysTool && attempt < maxAttempts && (network || serverSide || stalled || empty);
4414
4449
  if (!transient) throw err2;
4415
4450
  const waitMs = (empty ? o.emptyRetryBaseMs : 1e3) * (attempt + 1);
4416
4451
  log6.warn(`${empty ? "empty model turn" : stalled ? "model stall" : "network drop"} mid-step (${err2?.message ?? err2}) \u2014 retrying in ${waitMs}ms`);
@@ -4491,9 +4526,22 @@ var Agent = class _Agent {
4491
4526
  }
4492
4527
  if (toolCalls.length === 0) {
4493
4528
  if (this.drainInjections()) continue;
4529
+ if (emptyTurn && o.failEmptyTurns) {
4530
+ if (!emptyNudged && canNudge()) {
4531
+ emptyNudged = true;
4532
+ log6.warn(`empty model turn after tool work (step ${steps}, provider finishReason=${res.finishReason ?? "n/a"}) \u2014 nudging once`);
4533
+ this.transcript.push({ role: "user", content: "Continue \u2014 you returned nothing. Answer the user now using the tool results above, or keep working if you are not done." });
4534
+ continue;
4535
+ }
4536
+ log6.warn(`model returned empty turns after tool work \u2014 surfacing as failed`);
4537
+ const e = new Error("empty_response: model returned an empty turn (no text, no tools) after a re-prompt");
4538
+ e.code = "empty";
4539
+ return { text: "", steps, finishReason: "empty", ...res.finishReason ? { providerFinishReason: res.finishReason } : {}, messages: this.transcript, usage, usageEstimated, error: e };
4540
+ }
4494
4541
  const fabricated = o.catchFabricatedToolUse && !closureNudged ? this.fabricatedToolNames(contentText(res.content ?? "")) : [];
4495
4542
  for (const n of fabricated) this.fabricatedClaims.add(n);
4496
- if (fabricated.length && steps < o.maxSteps) {
4543
+ if (fabricated.length && !canNudge()) log6.warn(`fabricated tool use: text names ${fabricated.join(", ")} but no step/budget left to demand the call \u2014 finishing with the existing answer`);
4544
+ if (fabricated.length && canNudge()) {
4497
4545
  closureNudged = true;
4498
4546
  log6.warn(`fabricated tool use: text names ${fabricated.join(", ")} but the run invoked ${this.invokedTools.size ? [...this.invokedTools].join(", ") : "nothing"} \u2014 demanding the real call`);
4499
4547
  this.transcript.push({
@@ -4502,7 +4550,7 @@ var Agent = class _Agent {
4502
4550
  });
4503
4551
  continue;
4504
4552
  }
4505
- if (o.closeDelegatedTurns && res.endedWithoutClosing && !closureNudged && steps < o.maxSteps) {
4553
+ if (o.closeDelegatedTurns && res.endedWithoutClosing && !closureNudged && canNudge()) {
4506
4554
  closureNudged = true;
4507
4555
  log6.verbose("delegated turn went silent after a tool \u2014 nudging a closing answer");
4508
4556
  this.transcript.push({
@@ -4516,7 +4564,7 @@ var Agent = class _Agent {
4516
4564
  this.releaseStaleImages();
4517
4565
  await this.activeHooks?.onStop?.(res.content ?? "");
4518
4566
  const claims = this.survivingClaims();
4519
- return { text: res.content ?? "", steps, finishReason: "stop", messages: this.transcript, usage, usageEstimated, ...claims.length ? { fabricatedToolClaims: claims } : {} };
4567
+ return { text: res.content ?? "", steps, finishReason: "stop", ...res.finishReason ? { providerFinishReason: res.finishReason } : {}, messages: this.transcript, usage, usageEstimated, ...claims.length ? { fabricatedToolClaims: claims } : {} };
4520
4568
  }
4521
4569
  const fp = toolCalls.map((tc) => tc.function.name + ":" + (tc.function.arguments ?? "")).join("|");
4522
4570
  repeats = fp === lastFp ? repeats + 1 : 1;
@@ -4580,10 +4628,12 @@ var Agent = class _Agent {
4580
4628
  if (!stallMs) return;
4581
4629
  if (timer) clearTimeout(timer);
4582
4630
  timer = setTimeout(() => {
4631
+ if (this.toolHoldActive()) return poke();
4583
4632
  fired = true;
4584
4633
  ac.abort(stallError());
4585
4634
  }, stallMs);
4586
4635
  };
4636
+ this.stallPoke = poke;
4587
4637
  poke();
4588
4638
  const stalled = () => fired && !parent?.aborted;
4589
4639
  return {
@@ -4592,11 +4642,19 @@ var Agent = class _Agent {
4592
4642
  stalled,
4593
4643
  clear: () => {
4594
4644
  if (timer) clearTimeout(timer);
4645
+ if (this.stallPoke === poke) this.stallPoke = void 0;
4595
4646
  parent?.removeEventListener("abort", onParentAbort);
4596
4647
  },
4597
4648
  reclassify: (err2) => stalled() ? stallError() : err2
4598
4649
  };
4599
4650
  }
4651
+ /** A host or native delegated tool is running within its deadline — the provider is quiet, not stalled. */
4652
+ toolHoldActive() {
4653
+ const now4 = Date.now();
4654
+ if (this.hostToolsInFlight > 0 && now4 < this.hostToolHoldUntil) return true;
4655
+ for (const until of this.nativeToolsInFlight.values()) if (now4 < until) return true;
4656
+ return false;
4657
+ }
4600
4658
  /** A clean-stop turn that carries no assistant text AND no tool calls (native or delegated) — a
4601
4659
  * non-result the model produced by going silent. Callers retry it rather than report "done". */
4602
4660
  isEmptyStop(res) {
@@ -4635,6 +4693,12 @@ var Agent = class _Agent {
4635
4693
  if (a.output !== void 0) prev.output = a.output;
4636
4694
  prev.status = a.status;
4637
4695
  }
4696
+ if (a.status === "running") {
4697
+ if (a.id) this.nativeToolsInFlight.set(a.id, Date.now() + this.toolHoldMs);
4698
+ } else {
4699
+ if (a.id) this.nativeToolsInFlight.delete(a.id);
4700
+ this.toolRanThisAttempt = true;
4701
+ }
4638
4702
  sawTool = true;
4639
4703
  this.invokedTools.add(a.name);
4640
4704
  trailing = "";
@@ -4687,8 +4751,9 @@ var Agent = class _Agent {
4687
4751
  */
4688
4752
  fabricatedToolNames(text) {
4689
4753
  const out = [];
4754
+ const invoked = new Set([...this.invokedTools].map(toolKey));
4690
4755
  for (const t of this.activeTools) {
4691
- if (this.invokedTools.has(t.name)) continue;
4756
+ if (invoked.has(toolKey(t.name))) continue;
4692
4757
  if (t.name.length < 4 || !/[_\-A-Z]/.test(t.name)) continue;
4693
4758
  const re = new RegExp(`(?:^|[^\\w])${t.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^\\w]|$)`);
4694
4759
  if (re.test(text)) out.push(t.name);
@@ -4701,7 +4766,8 @@ var Agent = class _Agent {
4701
4766
  * call that demonstrably ran. See `RunResult.fabricatedToolClaims`. */
4702
4767
  survivingClaims() {
4703
4768
  if (!this.fabricatedClaims.size) return [];
4704
- return [...this.fabricatedClaims].filter((n) => !this.invokedTools.has(n));
4769
+ const invoked = new Set([...this.invokedTools].map(toolKey));
4770
+ return [...this.fabricatedClaims].filter((n) => !invoked.has(toolKey(n)));
4705
4771
  }
4706
4772
  async dispatch(tc) {
4707
4773
  this.invokedTools.add(tc.function.name);
@@ -5089,6 +5155,11 @@ function lastAssistantText(messages) {
5089
5155
  }
5090
5156
  return "";
5091
5157
  }
5158
+ var TOOL_ALIASES = { bash: "shell" };
5159
+ function toolKey(name) {
5160
+ const k = name.toLowerCase();
5161
+ return TOOL_ALIASES[k] ?? k;
5162
+ }
5092
5163
 
5093
5164
  // src/index.ts
5094
5165
  init_NodeDiskFilesystem();
@@ -7772,6 +7843,8 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
7772
7843
  instructionFiles: false,
7773
7844
  maxSteps: 8,
7774
7845
  timeoutMs: 3e4,
7846
+ failEmptyTurns: false,
7847
+ // a silent reflex turn after a dispatch is legitimate — the voice layer repairs it
7775
7848
  ...o.reflexOptions,
7776
7849
  tools,
7777
7850
  // Composed AFTER the spread so the dispatch guard can't be dropped by reflexOptions.
@@ -14071,6 +14144,7 @@ function jsonResult(res, session) {
14071
14144
  return {
14072
14145
  ok: res.finishReason === "stop",
14073
14146
  finishReason: res.finishReason,
14147
+ ...res.providerFinishReason ? { providerFinishReason: res.providerFinishReason } : {},
14074
14148
  text: res.text,
14075
14149
  steps: res.steps,
14076
14150
  tools: res.messages.slice(lastUser).filter((m) => m.role === "tool").length,
@@ -14078,7 +14152,7 @@ function jsonResult(res, session) {
14078
14152
  sessionId: session.meta.id,
14079
14153
  // The parked question, machine-readable — the caller answers it and resumes `sessionId`.
14080
14154
  ...res.finishReason === "needs_input" && res.question ? { question: res.question } : {},
14081
- ...res.finishReason === "error" && res.error ? { error: res.error?.message ?? String(res.error) } : {}
14155
+ ...(res.finishReason === "error" || res.finishReason === "empty") && res.error ? { error: res.error?.message ?? String(res.error) } : {}
14082
14156
  };
14083
14157
  }
14084
14158
  async function readMultiline(readLine) {