agent.libx.js 0.93.17 → 0.93.19
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/cli/cli.ts +19 -2
- package/dist/cli.js +210 -28
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +97 -12
- package/dist/index.js.map +1 -1
- package/dist/{mcp-DGWuuWJm.d.ts → mcp-C5GuDinb.d.ts} +35 -6
- package/dist/mcp.client.d.ts +70 -7
- package/dist/mcp.client.js +212 -31
- package/dist/mcp.client.js.map +1 -1
- package/package.json +1 -1
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
|
|
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:
|
|
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
|
-
|
|
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,24 +4285,36 @@ function makeMcpToolSearch(specs, callTool, options = {}) {
|
|
|
4214
4285
|
};
|
|
4215
4286
|
return [searchTool, callMcpTool];
|
|
4216
4287
|
}
|
|
4217
|
-
function
|
|
4288
|
+
function buildMcpCatalog(servers) {
|
|
4218
4289
|
const specs = [];
|
|
4219
|
-
const
|
|
4220
|
-
for (const m of
|
|
4290
|
+
const routes = /* @__PURE__ */ new Map();
|
|
4291
|
+
for (const m of servers) {
|
|
4221
4292
|
for (const s of m.specs) {
|
|
4222
4293
|
const base = `mcp__${m.name}__${s.name}`.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 128);
|
|
4223
4294
|
let display = base;
|
|
4224
|
-
for (let i = 2;
|
|
4295
|
+
for (let i = 2; routes.has(display); i++) display = `${base.slice(0, 128 - String(i).length - 1)}_${i}`;
|
|
4225
4296
|
specs.push({ name: display, description: s.description, inputSchema: s.inputSchema });
|
|
4226
|
-
|
|
4297
|
+
routes.set(display, { server: m.name, rawName: s.name });
|
|
4227
4298
|
}
|
|
4228
4299
|
}
|
|
4300
|
+
return { specs, routes };
|
|
4301
|
+
}
|
|
4302
|
+
function searchOverCatalog(servers, specs, routes, resolve, options) {
|
|
4229
4303
|
const tools = specs.length ? makeMcpToolSearch(specs, (name, args) => {
|
|
4230
|
-
const
|
|
4231
|
-
if (!
|
|
4232
|
-
return
|
|
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 ?? {});
|
|
4233
4307
|
}, options) : [];
|
|
4234
|
-
return { tools, serverNames:
|
|
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);
|
|
4235
4318
|
}
|
|
4236
4319
|
|
|
4237
4320
|
// src/hooks.ts
|
|
@@ -5030,6 +5113,7 @@ export {
|
|
|
5030
5113
|
applyEditsTool,
|
|
5031
5114
|
askUserQuestionTool,
|
|
5032
5115
|
bashTool,
|
|
5116
|
+
buildMcpCatalog,
|
|
5033
5117
|
checkpointTool,
|
|
5034
5118
|
checkpointTools,
|
|
5035
5119
|
compileSynthesizedTool,
|
|
@@ -5057,6 +5141,7 @@ export {
|
|
|
5057
5141
|
log,
|
|
5058
5142
|
makeContext,
|
|
5059
5143
|
makeJobTools,
|
|
5144
|
+
makeLazyMcpToolSearch,
|
|
5060
5145
|
makeMcpToolSearch,
|
|
5061
5146
|
makeMcpToolSearchFromMounted,
|
|
5062
5147
|
makeTaskBatchTool,
|