agent.libx.js 0.93.16 → 0.93.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.
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export { CommandExecutor, FileMetadata, IFilesystem, IndexedDbFilesystem, MemFil
5
5
  import { BodDB } from '@bod.ee/db';
6
6
  import { A as AgentTool, C as ChatLike, a as ChatOptions, b as ChatResponse, h as ToolCall, H as HostBridge, U as UserQuestion, e as MessageContent } from './tools-GPWp7oXq.js';
7
7
  export { c as ContentPart, d as HostEvent, M as Message, R as Role, S as SandboxJobRegistry, f as StreamChunk, T as TodoItem, g as Tool, i as ToolContext, j as bashTool, k as contentText, l as defaultTools, m as editTool, n as exitSessionTool, o as imagePart, p as makeContext, q as makeJobTools, r as readTool, t as toWireTools, s as todoWriteTool, u as toolRegistry, v as toolsByName } from './tools-GPWp7oXq.js';
8
- export { M as McpCall, a as McpImage, b as McpToolResult, c as McpToolSearchOptions, d as McpToolSpec, m as makeMcpToolSearch, e as mcpToolToAgentTool, f as mcpToolsToAgentTools } from './mcp-wwgXyhbi.js';
8
+ export { M as McpCall, a as McpImage, b as McpRoute, c as McpRouteResolver, d as McpToolResult, e as McpToolSearchOptions, f as McpToolSpec, g as MountedMcpLike, h as buildMcpCatalog, m as makeLazyMcpToolSearch, i as makeMcpToolSearch, j as makeMcpToolSearchFromMounted, k as mcpToolToAgentTool, l as mcpToolsToAgentTools } from './mcp-C5GuDinb.js';
9
9
  import * as libx_js_src_modules_log from 'libx.js/src/modules/log';
10
10
  export { log } from 'libx.js/src/modules/log';
11
11
 
@@ -660,6 +660,13 @@ declare class DuplexAgent {
660
660
  private seq;
661
661
  private pendingEvents;
662
662
  private flushQueued;
663
+ /** Per-voice-turn guards (reset by resetTurn at each turn's start). The reflex is a weak model:
664
+ * left unguarded it polls TaskStatus after a dispatch and/or dispatches silently (dead air).
665
+ * Like CC's Task tool, a dispatch is "said my piece, now wait for the push" — these enforce that. */
666
+ private turnDispatched;
667
+ private turnBriefs;
668
+ private spokeThisTurn;
669
+ private nudging;
663
670
  /** Parked worker questions awaiting a (voice-relayed) user answer, keyed by ask id. */
664
671
  readonly pendingAsks: Map<string, {
665
672
  question: string;
@@ -670,6 +677,19 @@ declare class DuplexAgent {
670
677
  constructor(options?: Partial<DuplexAgentOptions>);
671
678
  /** Resolve memory tools + inject index into voice system prompt (once). */
672
679
  private initMemory;
680
+ /** Clear the per-turn guards. Called at the head of every voice turn (user send + re-voice flush). */
681
+ private resetTurn;
682
+ /** preToolUse guard on the reflex: once it has dispatched this turn, a dispatch is "said my piece,
683
+ * now wait for the push" (CC's Task model). Block the temptations — TaskStatus polling and identical
684
+ * re-dispatch — so the only remaining move is to voice a short ack and end. A genuinely NEW Act is
685
+ * still allowed (parallel independent work). During a re-ack pass, block every tool. */
686
+ private dispatchGuard;
687
+ /** True when the just-finished turn dispatched a task but voiced nothing — dead air to repair.
688
+ * Requires a host: without one there's no stream to detect speech on (and no one to speak to). */
689
+ private get silentDispatch();
690
+ /** A dispatch with no spoken text is dead air. Re-prompt the reflex ONCE so the LLM itself voices a
691
+ * short ack (no template). If it STILL says nothing, fall back to a minimal line so silence never ships. */
692
+ private ackIfSilent;
673
693
  /** One user turn: the voice agent streams the reply (and may Act/Think). Serialized with re-voice turns. */
674
694
  send(content: MessageContent): Promise<RunResult>;
675
695
  /** Resolve when all queued voice turns AND all in-flight worker tasks have settled (tests, graceful shutdown). */
package/dist/index.js CHANGED
@@ -3675,6 +3675,17 @@ var DuplexAgent = class {
3675
3675
  seq = 0;
3676
3676
  pendingEvents = [];
3677
3677
  flushQueued = false;
3678
+ /** Per-voice-turn guards (reset by resetTurn at each turn's start). The reflex is a weak model:
3679
+ * left unguarded it polls TaskStatus after a dispatch and/or dispatches silently (dead air).
3680
+ * Like CC's Task tool, a dispatch is "said my piece, now wait for the push" — these enforce that. */
3681
+ turnDispatched = false;
3682
+ // an Act/Think fired this turn
3683
+ turnBriefs = /* @__PURE__ */ new Set();
3684
+ // briefs dispatched this turn (detect identical re-dispatch)
3685
+ spokeThisTurn = false;
3686
+ // any non-empty text_delta streamed this turn
3687
+ nudging = false;
3688
+ // re-ack pass in flight: block ALL tools, prevent recursion
3678
3689
  /** Parked worker questions awaiting a (voice-relayed) user answer, keyed by ask id. */
3679
3690
  pendingAsks = /* @__PURE__ */ new Map();
3680
3691
  /** Lazily resolved memory tools (async loadMemory runs in initMemory). */
@@ -3699,12 +3710,21 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
3699
3710
  this.answerTaskTool(),
3700
3711
  this.holdTool()
3701
3712
  ];
3713
+ const host = o.host;
3714
+ const voiceHost = host && {
3715
+ ask: host.ask ? (q) => host.ask(q) : void 0,
3716
+ confirm: host.confirm ? (p, m) => host.confirm(p, m) : void 0,
3717
+ notify: (ev) => {
3718
+ if (ev?.kind === "text_delta" && typeof ev.message === "string" && ev.message.trim()) this.spokeThisTurn = true;
3719
+ host.notify?.(ev);
3720
+ }
3721
+ };
3702
3722
  this.voice = new Agent({
3703
3723
  ai: o.ai,
3704
3724
  fs: new MemFilesystem2(),
3705
3725
  model: o.reflexModel,
3706
3726
  stream: true,
3707
- host: o.host,
3727
+ host: voiceHost,
3708
3728
  // The reflex IS the conversational channel — it confirms ambiguity inline ("did you mean…?"),
3709
3729
  // never via the blocking AskUserQuestion tool (Agent auto-adds it whenever a host is set). Left in,
3710
3730
  // it stalls a voice turn until the kill-switch. Worker questions still reach the user via parkQuestion.
@@ -3714,7 +3734,9 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
3714
3734
  maxSteps: 8,
3715
3735
  timeoutMs: 3e4,
3716
3736
  ...o.reflexOptions,
3717
- tools
3737
+ tools,
3738
+ // Composed AFTER the spread so the dispatch guard can't be dropped by reflexOptions.
3739
+ hooks: composeHooks(this.dispatchGuard(), o.reflexOptions?.hooks)
3718
3740
  });
3719
3741
  }
3720
3742
  /** Resolve memory tools + inject index into voice system prompt (once). */
@@ -3725,11 +3747,54 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
3725
3747
  this.voice.options.tools.push(...mem.tools);
3726
3748
  if (mem.index) this.voice.options.systemPrompt += "\n\n" + mem.index;
3727
3749
  }
3750
+ /** Clear the per-turn guards. Called at the head of every voice turn (user send + re-voice flush). */
3751
+ resetTurn() {
3752
+ this.turnDispatched = false;
3753
+ this.turnBriefs.clear();
3754
+ this.spokeThisTurn = false;
3755
+ }
3756
+ /** preToolUse guard on the reflex: once it has dispatched this turn, a dispatch is "said my piece,
3757
+ * now wait for the push" (CC's Task model). Block the temptations — TaskStatus polling and identical
3758
+ * re-dispatch — so the only remaining move is to voice a short ack and end. A genuinely NEW Act is
3759
+ * still allowed (parallel independent work). During a re-ack pass, block every tool. */
3760
+ dispatchGuard() {
3761
+ return {
3762
+ preToolUse: (call) => {
3763
+ if (this.nudging) return { block: true, reason: "Just say one short spoken acknowledgement \u2014 no tools this turn." };
3764
+ if (!this.turnDispatched) return;
3765
+ if (call.name === "TaskStatus")
3766
+ return { block: true, reason: "You just dispatched a task this turn \u2014 do NOT poll. Give one short spoken acknowledgement and end your turn; the result arrives later as a [task \u2026] event." };
3767
+ if ((call.name === "Act" || call.name === "Think") && this.turnBriefs.has(String(call.args?.brief ?? "")))
3768
+ return { block: true, reason: "You already dispatched this exact task \u2014 acknowledge briefly and end your turn." };
3769
+ }
3770
+ };
3771
+ }
3772
+ /** True when the just-finished turn dispatched a task but voiced nothing — dead air to repair.
3773
+ * Requires a host: without one there's no stream to detect speech on (and no one to speak to). */
3774
+ get silentDispatch() {
3775
+ return !!this.options.host && this.turnDispatched && !this.spokeThisTurn;
3776
+ }
3777
+ /** A dispatch with no spoken text is dead air. Re-prompt the reflex ONCE so the LLM itself voices a
3778
+ * short ack (no template). If it STILL says nothing, fall back to a minimal line so silence never ships. */
3779
+ async ackIfSilent() {
3780
+ this.nudging = true;
3781
+ try {
3782
+ await this.voice.send("[reminder] You dispatched a task but said nothing to the user. Say ONE short spoken acknowledgement now \u2014 no tools.");
3783
+ } catch (e) {
3784
+ log7.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
3785
+ } finally {
3786
+ this.nudging = false;
3787
+ }
3788
+ if (!this.spokeThisTurn) this.options.host?.notify?.({ kind: "text_delta", message: "Okay, on it." });
3789
+ }
3728
3790
  /** One user turn: the voice agent streams the reply (and may Act/Think). Serialized with re-voice turns. */
3729
3791
  send(content) {
3730
3792
  return this.enqueue(async () => {
3731
3793
  await this.initMemory();
3732
- return this.voice.send(content);
3794
+ this.resetTurn();
3795
+ const res = await this.voice.send(content);
3796
+ if (this.silentDispatch) await this.ackIfSilent();
3797
+ return res;
3733
3798
  });
3734
3799
  }
3735
3800
  /** Resolve when all queued voice turns AND all in-flight worker tasks have settled (tests, graceful shutdown). */
@@ -3762,7 +3827,9 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
3762
3827
  this.flushQueued = false;
3763
3828
  const events = this.pendingEvents.splice(0);
3764
3829
  if (!events.length) return;
3830
+ this.resetTurn();
3765
3831
  await this.voice.send(events.join("\n"));
3832
+ if (this.silentDispatch) await this.ackIfSilent();
3766
3833
  this.notify("revoice_done", "");
3767
3834
  });
3768
3835
  }
@@ -3995,6 +4062,8 @@ Another agent just implemented the above. Independently check the CURRENT state
3995
4062
  }
3996
4063
  },
3997
4064
  run: async ({ brief, label }) => {
4065
+ this.turnDispatched = true;
4066
+ this.turnBriefs.add(String(brief ?? ""));
3998
4067
  const id = await this.dispatch(String(brief ?? ""), "act", label ? String(label) : void 0);
3999
4068
  return `Acting on task ${id}. Acknowledge briefly; the result will arrive as a [task ${id} completed] event.`;
4000
4069
  }
@@ -4013,6 +4082,8 @@ Another agent just implemented the above. Independently check the CURRENT state
4013
4082
  }
4014
4083
  },
4015
4084
  run: async ({ brief, label }) => {
4085
+ this.turnDispatched = true;
4086
+ this.turnBriefs.add(String(brief ?? ""));
4016
4087
  const id = await this.dispatch(String(brief ?? ""), "think", label ? String(label) : void 0);
4017
4088
  return `Thinking on task ${id}. Acknowledge briefly; the result will arrive as a [task ${id} completed] event.`;
4018
4089
  }
@@ -4214,6 +4285,37 @@ function makeMcpToolSearch(specs, callTool, options = {}) {
4214
4285
  };
4215
4286
  return [searchTool, callMcpTool];
4216
4287
  }
4288
+ function buildMcpCatalog(servers) {
4289
+ const specs = [];
4290
+ const routes = /* @__PURE__ */ new Map();
4291
+ for (const m of servers) {
4292
+ for (const s of m.specs) {
4293
+ const base = `mcp__${m.name}__${s.name}`.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 128);
4294
+ let display = base;
4295
+ for (let i = 2; routes.has(display); i++) display = `${base.slice(0, 128 - String(i).length - 1)}_${i}`;
4296
+ specs.push({ name: display, description: s.description, inputSchema: s.inputSchema });
4297
+ routes.set(display, { server: m.name, rawName: s.name });
4298
+ }
4299
+ }
4300
+ return { specs, routes };
4301
+ }
4302
+ function searchOverCatalog(servers, specs, routes, resolve, options) {
4303
+ const tools = specs.length ? makeMcpToolSearch(specs, (name, args) => {
4304
+ const r = routes.get(name);
4305
+ if (!r) throw new Error(`unknown MCP tool '${name}' \u2014 use ToolSearch to find valid names`);
4306
+ return resolve(r.server, r.rawName, args ?? {});
4307
+ }, options) : [];
4308
+ return { tools, serverNames: servers, toolCount: specs.length };
4309
+ }
4310
+ function makeMcpToolSearchFromMounted(mounted, options) {
4311
+ const { specs, routes } = buildMcpCatalog(mounted);
4312
+ const byName = new Map(mounted.map((m) => [m.name, m]));
4313
+ return searchOverCatalog(mounted.map((m) => m.name), specs, routes, (server, rawName, args) => byName.get(server).client.callTool(rawName, args), options);
4314
+ }
4315
+ function makeLazyMcpToolSearch(servers, resolve, options) {
4316
+ const { specs, routes } = buildMcpCatalog(servers);
4317
+ return searchOverCatalog(servers.map((s) => s.name), specs, routes, resolve, options);
4318
+ }
4217
4319
 
4218
4320
  // src/hooks.ts
4219
4321
  var RecordingHooks = class {
@@ -5011,6 +5113,7 @@ export {
5011
5113
  applyEditsTool,
5012
5114
  askUserQuestionTool,
5013
5115
  bashTool,
5116
+ buildMcpCatalog,
5014
5117
  checkpointTool,
5015
5118
  checkpointTools,
5016
5119
  compileSynthesizedTool,
@@ -5038,7 +5141,9 @@ export {
5038
5141
  log,
5039
5142
  makeContext,
5040
5143
  makeJobTools,
5144
+ makeLazyMcpToolSearch,
5041
5145
  makeMcpToolSearch,
5146
+ makeMcpToolSearchFromMounted,
5042
5147
  makeTaskBatchTool,
5043
5148
  makeTaskTool,
5044
5149
  makeWebFetchTool,