@livx.cc/agentx 0.99.44 → 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.
@@ -1,5 +1,5 @@
1
1
  import { IFilesystem } from '@livx.cc/wcli/core';
2
- import { M as Message, H as HostBridge, A as AgentTool, C as ChatLike, g as MessageContent, U as UserQuestion } from './tools-S-ArIR3o.js';
2
+ import { M as Message, H as HostBridge, A as AgentTool, C as ChatLike, g as MessageContent, U as UserQuestion } from './tools-BqL8Lk4J.js';
3
3
 
4
4
  /**
5
5
  * Hooks — deterministic interception points around tool execution, run by the
@@ -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,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
- import { H as Hooks, i as RunResult, R as ReasoningEffort, A as Agent } from './Agent-f4um2q8E.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
- import { M as Message, U as UserQuestion, H as HostBridge, c as ContentPart, g as MessageContent } from './tools-S-ArIR3o.js';
4
+ import { M as Message, U as UserQuestion, H as HostBridge, c as ContentPart, g as MessageContent } from './tools-BqL8Lk4J.js';
5
5
 
6
6
  /**
7
7
  * On-disk session store for the CLI: each conversation is one JSON file at
@@ -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
@@ -23,6 +23,9 @@ var init_logging = __esm({
23
23
  import { readFileSync, statSync as statSync2, realpathSync } from "fs";
24
24
  import { resolve, sep } from "path";
25
25
  import { randomBytes } from "crypto";
26
+ function passesBackReasoning(model) {
27
+ return !!model && /^(deepseek|moonshot)\//.test(model);
28
+ }
26
29
  function contentText(content) {
27
30
  if (content == null) return "";
28
31
  if (typeof content === "string") return content;
@@ -203,7 +206,7 @@ function sendBytes(messages, keepRecent = 1, stub = IMAGE_ELIDE_STUB) {
203
206
  }
204
207
  function sendBytesPer(messages, keepRecent = 1, stub = IMAGE_ELIDE_STUB) {
205
208
  const keep = messages.some(bearsImage) ? imageWindows(messages, keepRecent) : null;
206
- return messages.map((m, i) => wireBytes(m.content, keep == null || keep.has(i), stub) + (m.tool_calls ? JSON.stringify(m.tool_calls).length : 0));
209
+ return messages.map((m, i) => wireBytes(m.content, keep == null || keep.has(i), stub) + (m.tool_calls ? JSON.stringify(m.tool_calls).length : 0) + (m.reasoning_content?.length ?? 0));
207
210
  }
208
211
  function wireBytes(content, kept, stub) {
209
212
  const tool = toolImageRef(content);
@@ -3958,6 +3961,10 @@ var AgentOptions = class {
3958
3961
  * not prose: a named tool with zero invocations is provable. On detection the loop nudges ONCE for a real
3959
3962
  * call, and records the names in `RunResult.fabricatedToolClaims` either way. */
3960
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;
3961
3968
  /** Fold the dropped middle of an over-long transcript into a synthetic summary (edge-safe, no LLM). Off => drop-oldest. */
3962
3969
  compaction;
3963
3970
  /** Add `Checkpoint`/`Rollback` tools (requires the fs to be an OverlayFilesystem). */
@@ -4031,6 +4038,14 @@ var Agent = class _Agent {
4031
4038
  /** Per-run ground truth: every tool name actually INVOKED this run (native dispatch + delegated
4032
4039
  * runtime activity). The ledger that makes a fabricated tool claim provable. Reset per run. */
4033
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;
4034
4049
  /** Advertised tools this run's text claimed to have used while the ledger showed zero calls. */
4035
4050
  fabricatedClaims = /* @__PURE__ */ new Set();
4036
4051
  activeHooks;
@@ -4333,6 +4348,7 @@ var Agent = class _Agent {
4333
4348
  let toolCallsTotal = 0;
4334
4349
  let lastFp = "";
4335
4350
  let repeats = 0;
4351
+ let emptyNudged = false;
4336
4352
  let closureNudged = false;
4337
4353
  let lastProgressAt = start;
4338
4354
  const kill = (finishReason) => {
@@ -4341,12 +4357,13 @@ var Agent = class _Agent {
4341
4357
  const claims = this.survivingClaims();
4342
4358
  return { text: lastAssistantText(this.transcript), steps, finishReason, messages: this.transcript, usage, usageEstimated, ...claims.length ? { fabricatedToolClaims: claims } : {} };
4343
4359
  };
4360
+ const overBudget = () => !!o.maxTokens && usage.totalTokens - 0.9 * usage.cacheReadTokens >= o.maxTokens;
4361
+ const canNudge = () => steps < o.maxSteps && !overBudget();
4344
4362
  while (true) {
4345
4363
  if (o.signal?.aborted) return kill("aborted");
4346
4364
  if (steps >= o.maxSteps) return kill("max_steps");
4347
4365
  if (o.timeoutMs && Date.now() - lastProgressAt - this.parkedMs >= o.timeoutMs) return kill("timeout");
4348
- const budgetTokens = usage.totalTokens - 0.9 * usage.cacheReadTokens;
4349
- if (o.maxTokens && budgetTokens >= o.maxTokens) return kill("budget");
4366
+ if (overBudget()) return kill("budget");
4350
4367
  steps++;
4351
4368
  this.options.host?.notify?.({ kind: "turn_start", message: `step ${steps}` });
4352
4369
  let res;
@@ -4367,7 +4384,14 @@ var Agent = class _Agent {
4367
4384
  toolTimeoutMs: effectiveToolTimeoutMs,
4368
4385
  toolExecutor: async (name, args) => {
4369
4386
  const tc = { id: `cursor-${Date.now()}`, type: "function", function: { name, arguments: JSON.stringify(args) } };
4370
- 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
+ }
4371
4395
  }
4372
4396
  } : void 0;
4373
4397
  const claudeSteerPo = o.model.startsWith("claude-code/") ? { steer: () => this.drainPendingSteers() } : void 0;
@@ -4482,14 +4506,28 @@ var Agent = class _Agent {
4482
4506
  this.transcript.push({
4483
4507
  role: "assistant",
4484
4508
  content: res.content ?? "",
4485
- ...toolCalls.length ? { tool_calls: toolCalls } : {}
4509
+ ...toolCalls.length ? { tool_calls: toolCalls } : {},
4510
+ ...res.reasoningContent && passesBackReasoning(o.model) ? { reasoning_content: res.reasoningContent } : {}
4486
4511
  });
4487
4512
  }
4488
4513
  if (toolCalls.length === 0) {
4489
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
+ }
4490
4527
  const fabricated = o.catchFabricatedToolUse && !closureNudged ? this.fabricatedToolNames(contentText(res.content ?? "")) : [];
4491
4528
  for (const n of fabricated) this.fabricatedClaims.add(n);
4492
- 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()) {
4493
4531
  closureNudged = true;
4494
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`);
4495
4533
  this.transcript.push({
@@ -4498,7 +4536,7 @@ var Agent = class _Agent {
4498
4536
  });
4499
4537
  continue;
4500
4538
  }
4501
- if (o.closeDelegatedTurns && res.endedWithoutClosing && !closureNudged && steps < o.maxSteps) {
4539
+ if (o.closeDelegatedTurns && res.endedWithoutClosing && !closureNudged && canNudge()) {
4502
4540
  closureNudged = true;
4503
4541
  log6.verbose("delegated turn went silent after a tool \u2014 nudging a closing answer");
4504
4542
  this.transcript.push({
@@ -4512,7 +4550,7 @@ var Agent = class _Agent {
4512
4550
  this.releaseStaleImages();
4513
4551
  await this.activeHooks?.onStop?.(res.content ?? "");
4514
4552
  const claims = this.survivingClaims();
4515
- 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 } : {} };
4516
4554
  }
4517
4555
  const fp = toolCalls.map((tc) => tc.function.name + ":" + (tc.function.arguments ?? "")).join("|");
4518
4556
  repeats = fp === lastFp ? repeats + 1 : 1;
@@ -4576,10 +4614,12 @@ var Agent = class _Agent {
4576
4614
  if (!stallMs) return;
4577
4615
  if (timer) clearTimeout(timer);
4578
4616
  timer = setTimeout(() => {
4617
+ if (this.hostToolsInFlight > 0 && Date.now() < this.hostToolHoldUntil) return poke();
4579
4618
  fired = true;
4580
4619
  ac.abort(stallError());
4581
4620
  }, stallMs);
4582
4621
  };
4622
+ this.stallPoke = poke;
4583
4623
  poke();
4584
4624
  const stalled = () => fired && !parent?.aborted;
4585
4625
  return {
@@ -4588,6 +4628,7 @@ var Agent = class _Agent {
4588
4628
  stalled,
4589
4629
  clear: () => {
4590
4630
  if (timer) clearTimeout(timer);
4631
+ if (this.stallPoke === poke) this.stallPoke = void 0;
4591
4632
  parent?.removeEventListener("abort", onParentAbort);
4592
4633
  },
4593
4634
  reclassify: (err2) => stalled() ? stallError() : err2
@@ -4608,10 +4649,14 @@ var Agent = class _Agent {
4608
4649
  const delegatedOrder = [];
4609
4650
  let sawTool = false;
4610
4651
  let trailing = "";
4652
+ let reasoning = "";
4611
4653
  for await (const chunk of stream) {
4612
4654
  poke?.();
4613
4655
  if (this.options.signal?.aborted) break;
4614
- if (chunk.reasoningContent) this.options.host.notify({ kind: "thinking_delta", message: chunk.reasoningContent });
4656
+ if (chunk.reasoningContent) {
4657
+ reasoning += chunk.reasoningContent;
4658
+ this.options.host.notify({ kind: "thinking_delta", message: chunk.reasoningContent });
4659
+ }
4615
4660
  if (chunk.toolActivity) {
4616
4661
  const a = chunk.toolActivity;
4617
4662
  const verb = a.status === "error" ? "\u2717" : a.status === "completed" ? "\u2713" : "\u2192";
@@ -4642,7 +4687,7 @@ var Agent = class _Agent {
4642
4687
  }
4643
4688
  const delegatedTools = delegatedOrder.map((k) => delegated.get(k));
4644
4689
  const endedWithoutClosing = sawTool && (!trailing.trim() || endsOnAnnouncedAction(trailing));
4645
- return { content, ...finishReason ? { finishReason } : {}, ...toolCalls ? { toolCalls } : {}, ...usage ? { usage } : {}, ...delegatedTools.length ? { delegatedTools } : {}, ...endedWithoutClosing ? { endedWithoutClosing: true } : {} };
4690
+ return { content, ...reasoning ? { reasoningContent: reasoning } : {}, ...finishReason ? { finishReason } : {}, ...toolCalls ? { toolCalls } : {}, ...usage ? { usage } : {}, ...delegatedTools.length ? { delegatedTools } : {}, ...endedWithoutClosing ? { endedWithoutClosing: true } : {} };
4646
4691
  }
4647
4692
  /**
4648
4693
  * FABRICATED TOOL USE — the failure no call-reactive layer can see, because there IS no call.
@@ -4679,8 +4724,9 @@ var Agent = class _Agent {
4679
4724
  */
4680
4725
  fabricatedToolNames(text) {
4681
4726
  const out = [];
4727
+ const invoked = new Set([...this.invokedTools].map(toolKey));
4682
4728
  for (const t of this.activeTools) {
4683
- if (this.invokedTools.has(t.name)) continue;
4729
+ if (invoked.has(toolKey(t.name))) continue;
4684
4730
  if (t.name.length < 4 || !/[_\-A-Z]/.test(t.name)) continue;
4685
4731
  const re = new RegExp(`(?:^|[^\\w])${t.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^\\w]|$)`);
4686
4732
  if (re.test(text)) out.push(t.name);
@@ -4693,7 +4739,8 @@ var Agent = class _Agent {
4693
4739
  * call that demonstrably ran. See `RunResult.fabricatedToolClaims`. */
4694
4740
  survivingClaims() {
4695
4741
  if (!this.fabricatedClaims.size) return [];
4696
- 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)));
4697
4744
  }
4698
4745
  async dispatch(tc) {
4699
4746
  this.invokedTools.add(tc.function.name);
@@ -5081,6 +5128,11 @@ function lastAssistantText(messages) {
5081
5128
  }
5082
5129
  return "";
5083
5130
  }
5131
+ var TOOL_ALIASES = { bash: "shell" };
5132
+ function toolKey(name) {
5133
+ const k = name.toLowerCase();
5134
+ return TOOL_ALIASES[k] ?? k;
5135
+ }
5084
5136
 
5085
5137
  // src/index.ts
5086
5138
  init_NodeDiskFilesystem();
@@ -7764,6 +7816,8 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
7764
7816
  instructionFiles: false,
7765
7817
  maxSteps: 8,
7766
7818
  timeoutMs: 3e4,
7819
+ failEmptyTurns: false,
7820
+ // a silent reflex turn after a dispatch is legitimate — the voice layer repairs it
7767
7821
  ...o.reflexOptions,
7768
7822
  tools,
7769
7823
  // Composed AFTER the spread so the dispatch guard can't be dropped by reflexOptions.
@@ -14063,6 +14117,7 @@ function jsonResult(res, session) {
14063
14117
  return {
14064
14118
  ok: res.finishReason === "stop",
14065
14119
  finishReason: res.finishReason,
14120
+ ...res.providerFinishReason ? { providerFinishReason: res.providerFinishReason } : {},
14066
14121
  text: res.text,
14067
14122
  steps: res.steps,
14068
14123
  tools: res.messages.slice(lastUser).filter((m) => m.role === "tool").length,
@@ -14070,7 +14125,7 @@ function jsonResult(res, session) {
14070
14125
  sessionId: session.meta.id,
14071
14126
  // The parked question, machine-readable — the caller answers it and resumes `sessionId`.
14072
14127
  ...res.finishReason === "needs_input" && res.question ? { question: res.question } : {},
14073
- ...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) } : {}
14074
14129
  };
14075
14130
  }
14076
14131
  async function readMultiline(readLine) {