@livx.cc/agentx 0.99.45 → 0.99.46

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,14 @@ 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
+ /** Resets the active request's stall timer; a host tool's completion restarts the model's idle window. */
431
+ private stallPoke?;
418
432
  /** Advertised tools this run's text claimed to have used while the ledger showed zero calls. */
419
433
  private fabricatedClaims;
420
434
  private activeHooks?;
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-OcAg2yxA.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,14 @@ 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
+ /** Resets the active request's stall timer; a host tool's completion restarts the model's idle window. */
4048
+ stallPoke;
4037
4049
  /** Advertised tools this run's text claimed to have used while the ledger showed zero calls. */
4038
4050
  fabricatedClaims = /* @__PURE__ */ new Set();
4039
4051
  activeHooks;
@@ -4336,6 +4348,7 @@ var Agent = class _Agent {
4336
4348
  let toolCallsTotal = 0;
4337
4349
  let lastFp = "";
4338
4350
  let repeats = 0;
4351
+ let emptyNudged = false;
4339
4352
  let closureNudged = false;
4340
4353
  let lastProgressAt = start;
4341
4354
  const kill = (finishReason) => {
@@ -4344,12 +4357,13 @@ var Agent = class _Agent {
4344
4357
  const claims = this.survivingClaims();
4345
4358
  return { text: lastAssistantText(this.transcript), steps, finishReason, messages: this.transcript, usage, usageEstimated, ...claims.length ? { fabricatedToolClaims: claims } : {} };
4346
4359
  };
4360
+ const overBudget = () => !!o.maxTokens && usage.totalTokens - 0.9 * usage.cacheReadTokens >= o.maxTokens;
4361
+ const canNudge = () => steps < o.maxSteps && !overBudget();
4347
4362
  while (true) {
4348
4363
  if (o.signal?.aborted) return kill("aborted");
4349
4364
  if (steps >= o.maxSteps) return kill("max_steps");
4350
4365
  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");
4366
+ if (overBudget()) return kill("budget");
4353
4367
  steps++;
4354
4368
  this.options.host?.notify?.({ kind: "turn_start", message: `step ${steps}` });
4355
4369
  let res;
@@ -4370,7 +4384,14 @@ var Agent = class _Agent {
4370
4384
  toolTimeoutMs: effectiveToolTimeoutMs,
4371
4385
  toolExecutor: async (name, args) => {
4372
4386
  const tc = { id: `cursor-${Date.now()}`, type: "function", function: { name, arguments: JSON.stringify(args) } };
4373
- return await this.dispatch(tc);
4387
+ this.hostToolsInFlight++;
4388
+ this.hostToolHoldUntil = Math.max(this.hostToolHoldUntil, Date.now() + effectiveToolTimeoutMs);
4389
+ try {
4390
+ return await this.dispatch(tc);
4391
+ } finally {
4392
+ this.hostToolsInFlight--;
4393
+ this.stallPoke?.();
4394
+ }
4374
4395
  }
4375
4396
  } : void 0;
4376
4397
  const claudeSteerPo = o.model.startsWith("claude-code/") ? { steer: () => this.drainPendingSteers() } : void 0;
@@ -4491,9 +4512,22 @@ var Agent = class _Agent {
4491
4512
  }
4492
4513
  if (toolCalls.length === 0) {
4493
4514
  if (this.drainInjections()) continue;
4515
+ if (emptyTurn && o.failEmptyTurns) {
4516
+ if (!emptyNudged && canNudge()) {
4517
+ emptyNudged = true;
4518
+ log6.warn(`empty model turn after tool work (step ${steps}, provider finishReason=${res.finishReason ?? "n/a"}) \u2014 nudging once`);
4519
+ 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." });
4520
+ continue;
4521
+ }
4522
+ log6.warn(`model returned empty turns after tool work \u2014 surfacing as failed`);
4523
+ const e = new Error("empty_response: model returned an empty turn (no text, no tools) after a re-prompt");
4524
+ e.code = "empty";
4525
+ return { text: "", steps, finishReason: "empty", ...res.finishReason ? { providerFinishReason: res.finishReason } : {}, messages: this.transcript, usage, usageEstimated, error: e };
4526
+ }
4494
4527
  const fabricated = o.catchFabricatedToolUse && !closureNudged ? this.fabricatedToolNames(contentText(res.content ?? "")) : [];
4495
4528
  for (const n of fabricated) this.fabricatedClaims.add(n);
4496
- if (fabricated.length && steps < o.maxSteps) {
4529
+ 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`);
4530
+ if (fabricated.length && canNudge()) {
4497
4531
  closureNudged = true;
4498
4532
  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
4533
  this.transcript.push({
@@ -4502,7 +4536,7 @@ var Agent = class _Agent {
4502
4536
  });
4503
4537
  continue;
4504
4538
  }
4505
- if (o.closeDelegatedTurns && res.endedWithoutClosing && !closureNudged && steps < o.maxSteps) {
4539
+ if (o.closeDelegatedTurns && res.endedWithoutClosing && !closureNudged && canNudge()) {
4506
4540
  closureNudged = true;
4507
4541
  log6.verbose("delegated turn went silent after a tool \u2014 nudging a closing answer");
4508
4542
  this.transcript.push({
@@ -4516,7 +4550,7 @@ var Agent = class _Agent {
4516
4550
  this.releaseStaleImages();
4517
4551
  await this.activeHooks?.onStop?.(res.content ?? "");
4518
4552
  const claims = this.survivingClaims();
4519
- return { text: res.content ?? "", steps, finishReason: "stop", messages: this.transcript, usage, usageEstimated, ...claims.length ? { fabricatedToolClaims: claims } : {} };
4553
+ return { text: res.content ?? "", steps, finishReason: "stop", ...res.finishReason ? { providerFinishReason: res.finishReason } : {}, messages: this.transcript, usage, usageEstimated, ...claims.length ? { fabricatedToolClaims: claims } : {} };
4520
4554
  }
4521
4555
  const fp = toolCalls.map((tc) => tc.function.name + ":" + (tc.function.arguments ?? "")).join("|");
4522
4556
  repeats = fp === lastFp ? repeats + 1 : 1;
@@ -4580,10 +4614,12 @@ var Agent = class _Agent {
4580
4614
  if (!stallMs) return;
4581
4615
  if (timer) clearTimeout(timer);
4582
4616
  timer = setTimeout(() => {
4617
+ if (this.hostToolsInFlight > 0 && Date.now() < this.hostToolHoldUntil) return poke();
4583
4618
  fired = true;
4584
4619
  ac.abort(stallError());
4585
4620
  }, stallMs);
4586
4621
  };
4622
+ this.stallPoke = poke;
4587
4623
  poke();
4588
4624
  const stalled = () => fired && !parent?.aborted;
4589
4625
  return {
@@ -4592,6 +4628,7 @@ var Agent = class _Agent {
4592
4628
  stalled,
4593
4629
  clear: () => {
4594
4630
  if (timer) clearTimeout(timer);
4631
+ if (this.stallPoke === poke) this.stallPoke = void 0;
4595
4632
  parent?.removeEventListener("abort", onParentAbort);
4596
4633
  },
4597
4634
  reclassify: (err2) => stalled() ? stallError() : err2
@@ -4687,8 +4724,9 @@ var Agent = class _Agent {
4687
4724
  */
4688
4725
  fabricatedToolNames(text) {
4689
4726
  const out = [];
4727
+ const invoked = new Set([...this.invokedTools].map(toolKey));
4690
4728
  for (const t of this.activeTools) {
4691
- if (this.invokedTools.has(t.name)) continue;
4729
+ if (invoked.has(toolKey(t.name))) continue;
4692
4730
  if (t.name.length < 4 || !/[_\-A-Z]/.test(t.name)) continue;
4693
4731
  const re = new RegExp(`(?:^|[^\\w])${t.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^\\w]|$)`);
4694
4732
  if (re.test(text)) out.push(t.name);
@@ -4701,7 +4739,8 @@ var Agent = class _Agent {
4701
4739
  * call that demonstrably ran. See `RunResult.fabricatedToolClaims`. */
4702
4740
  survivingClaims() {
4703
4741
  if (!this.fabricatedClaims.size) return [];
4704
- return [...this.fabricatedClaims].filter((n) => !this.invokedTools.has(n));
4742
+ const invoked = new Set([...this.invokedTools].map(toolKey));
4743
+ return [...this.fabricatedClaims].filter((n) => !invoked.has(toolKey(n)));
4705
4744
  }
4706
4745
  async dispatch(tc) {
4707
4746
  this.invokedTools.add(tc.function.name);
@@ -5089,6 +5128,11 @@ function lastAssistantText(messages) {
5089
5128
  }
5090
5129
  return "";
5091
5130
  }
5131
+ var TOOL_ALIASES = { bash: "shell" };
5132
+ function toolKey(name) {
5133
+ const k = name.toLowerCase();
5134
+ return TOOL_ALIASES[k] ?? k;
5135
+ }
5092
5136
 
5093
5137
  // src/index.ts
5094
5138
  init_NodeDiskFilesystem();
@@ -7772,6 +7816,8 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
7772
7816
  instructionFiles: false,
7773
7817
  maxSteps: 8,
7774
7818
  timeoutMs: 3e4,
7819
+ failEmptyTurns: false,
7820
+ // a silent reflex turn after a dispatch is legitimate — the voice layer repairs it
7775
7821
  ...o.reflexOptions,
7776
7822
  tools,
7777
7823
  // Composed AFTER the spread so the dispatch guard can't be dropped by reflexOptions.
@@ -14071,6 +14117,7 @@ function jsonResult(res, session) {
14071
14117
  return {
14072
14118
  ok: res.finishReason === "stop",
14073
14119
  finishReason: res.finishReason,
14120
+ ...res.providerFinishReason ? { providerFinishReason: res.providerFinishReason } : {},
14074
14121
  text: res.text,
14075
14122
  steps: res.steps,
14076
14123
  tools: res.messages.slice(lastUser).filter((m) => m.role === "tool").length,
@@ -14078,7 +14125,7 @@ function jsonResult(res, session) {
14078
14125
  sessionId: session.meta.id,
14079
14126
  // The parked question, machine-readable — the caller answers it and resumes `sessionId`.
14080
14127
  ...res.finishReason === "needs_input" && res.question ? { question: res.question } : {},
14081
- ...res.finishReason === "error" && res.error ? { error: res.error?.message ?? String(res.error) } : {}
14128
+ ...(res.finishReason === "error" || res.finishReason === "empty") && res.error ? { error: res.error?.message ?? String(res.error) } : {}
14082
14129
  };
14083
14130
  }
14084
14131
  async function readMultiline(readLine) {