agent.libx.js 0.93.17 → 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/cli.js +198 -27
- 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/cli.js
CHANGED
|
@@ -3575,6 +3575,17 @@ var DuplexAgent = class {
|
|
|
3575
3575
|
seq = 0;
|
|
3576
3576
|
pendingEvents = [];
|
|
3577
3577
|
flushQueued = false;
|
|
3578
|
+
/** Per-voice-turn guards (reset by resetTurn at each turn's start). The reflex is a weak model:
|
|
3579
|
+
* left unguarded it polls TaskStatus after a dispatch and/or dispatches silently (dead air).
|
|
3580
|
+
* Like CC's Task tool, a dispatch is "said my piece, now wait for the push" — these enforce that. */
|
|
3581
|
+
turnDispatched = false;
|
|
3582
|
+
// an Act/Think fired this turn
|
|
3583
|
+
turnBriefs = /* @__PURE__ */ new Set();
|
|
3584
|
+
// briefs dispatched this turn (detect identical re-dispatch)
|
|
3585
|
+
spokeThisTurn = false;
|
|
3586
|
+
// any non-empty text_delta streamed this turn
|
|
3587
|
+
nudging = false;
|
|
3588
|
+
// re-ack pass in flight: block ALL tools, prevent recursion
|
|
3578
3589
|
/** Parked worker questions awaiting a (voice-relayed) user answer, keyed by ask id. */
|
|
3579
3590
|
pendingAsks = /* @__PURE__ */ new Map();
|
|
3580
3591
|
/** Lazily resolved memory tools (async loadMemory runs in initMemory). */
|
|
@@ -3599,12 +3610,21 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
|
|
|
3599
3610
|
this.answerTaskTool(),
|
|
3600
3611
|
this.holdTool()
|
|
3601
3612
|
];
|
|
3613
|
+
const host = o.host;
|
|
3614
|
+
const voiceHost = host && {
|
|
3615
|
+
ask: host.ask ? (q2) => host.ask(q2) : void 0,
|
|
3616
|
+
confirm: host.confirm ? (p, m) => host.confirm(p, m) : void 0,
|
|
3617
|
+
notify: (ev) => {
|
|
3618
|
+
if (ev?.kind === "text_delta" && typeof ev.message === "string" && ev.message.trim()) this.spokeThisTurn = true;
|
|
3619
|
+
host.notify?.(ev);
|
|
3620
|
+
}
|
|
3621
|
+
};
|
|
3602
3622
|
this.voice = new Agent({
|
|
3603
3623
|
ai: o.ai,
|
|
3604
3624
|
fs: new MemFilesystem2(),
|
|
3605
3625
|
model: o.reflexModel,
|
|
3606
3626
|
stream: true,
|
|
3607
|
-
host:
|
|
3627
|
+
host: voiceHost,
|
|
3608
3628
|
// The reflex IS the conversational channel — it confirms ambiguity inline ("did you mean…?"),
|
|
3609
3629
|
// never via the blocking AskUserQuestion tool (Agent auto-adds it whenever a host is set). Left in,
|
|
3610
3630
|
// it stalls a voice turn until the kill-switch. Worker questions still reach the user via parkQuestion.
|
|
@@ -3614,7 +3634,9 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
|
|
|
3614
3634
|
maxSteps: 8,
|
|
3615
3635
|
timeoutMs: 3e4,
|
|
3616
3636
|
...o.reflexOptions,
|
|
3617
|
-
tools
|
|
3637
|
+
tools,
|
|
3638
|
+
// Composed AFTER the spread so the dispatch guard can't be dropped by reflexOptions.
|
|
3639
|
+
hooks: composeHooks(this.dispatchGuard(), o.reflexOptions?.hooks)
|
|
3618
3640
|
});
|
|
3619
3641
|
}
|
|
3620
3642
|
/** Resolve memory tools + inject index into voice system prompt (once). */
|
|
@@ -3625,11 +3647,54 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
|
|
|
3625
3647
|
this.voice.options.tools.push(...mem.tools);
|
|
3626
3648
|
if (mem.index) this.voice.options.systemPrompt += "\n\n" + mem.index;
|
|
3627
3649
|
}
|
|
3650
|
+
/** Clear the per-turn guards. Called at the head of every voice turn (user send + re-voice flush). */
|
|
3651
|
+
resetTurn() {
|
|
3652
|
+
this.turnDispatched = false;
|
|
3653
|
+
this.turnBriefs.clear();
|
|
3654
|
+
this.spokeThisTurn = false;
|
|
3655
|
+
}
|
|
3656
|
+
/** preToolUse guard on the reflex: once it has dispatched this turn, a dispatch is "said my piece,
|
|
3657
|
+
* now wait for the push" (CC's Task model). Block the temptations — TaskStatus polling and identical
|
|
3658
|
+
* re-dispatch — so the only remaining move is to voice a short ack and end. A genuinely NEW Act is
|
|
3659
|
+
* still allowed (parallel independent work). During a re-ack pass, block every tool. */
|
|
3660
|
+
dispatchGuard() {
|
|
3661
|
+
return {
|
|
3662
|
+
preToolUse: (call) => {
|
|
3663
|
+
if (this.nudging) return { block: true, reason: "Just say one short spoken acknowledgement \u2014 no tools this turn." };
|
|
3664
|
+
if (!this.turnDispatched) return;
|
|
3665
|
+
if (call.name === "TaskStatus")
|
|
3666
|
+
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." };
|
|
3667
|
+
if ((call.name === "Act" || call.name === "Think") && this.turnBriefs.has(String(call.args?.brief ?? "")))
|
|
3668
|
+
return { block: true, reason: "You already dispatched this exact task \u2014 acknowledge briefly and end your turn." };
|
|
3669
|
+
}
|
|
3670
|
+
};
|
|
3671
|
+
}
|
|
3672
|
+
/** True when the just-finished turn dispatched a task but voiced nothing — dead air to repair.
|
|
3673
|
+
* Requires a host: without one there's no stream to detect speech on (and no one to speak to). */
|
|
3674
|
+
get silentDispatch() {
|
|
3675
|
+
return !!this.options.host && this.turnDispatched && !this.spokeThisTurn;
|
|
3676
|
+
}
|
|
3677
|
+
/** A dispatch with no spoken text is dead air. Re-prompt the reflex ONCE so the LLM itself voices a
|
|
3678
|
+
* short ack (no template). If it STILL says nothing, fall back to a minimal line so silence never ships. */
|
|
3679
|
+
async ackIfSilent() {
|
|
3680
|
+
this.nudging = true;
|
|
3681
|
+
try {
|
|
3682
|
+
await this.voice.send("[reminder] You dispatched a task but said nothing to the user. Say ONE short spoken acknowledgement now \u2014 no tools.");
|
|
3683
|
+
} catch (e) {
|
|
3684
|
+
log6.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
|
|
3685
|
+
} finally {
|
|
3686
|
+
this.nudging = false;
|
|
3687
|
+
}
|
|
3688
|
+
if (!this.spokeThisTurn) this.options.host?.notify?.({ kind: "text_delta", message: "Okay, on it." });
|
|
3689
|
+
}
|
|
3628
3690
|
/** One user turn: the voice agent streams the reply (and may Act/Think). Serialized with re-voice turns. */
|
|
3629
3691
|
send(content) {
|
|
3630
3692
|
return this.enqueue(async () => {
|
|
3631
3693
|
await this.initMemory();
|
|
3632
|
-
|
|
3694
|
+
this.resetTurn();
|
|
3695
|
+
const res = await this.voice.send(content);
|
|
3696
|
+
if (this.silentDispatch) await this.ackIfSilent();
|
|
3697
|
+
return res;
|
|
3633
3698
|
});
|
|
3634
3699
|
}
|
|
3635
3700
|
/** Resolve when all queued voice turns AND all in-flight worker tasks have settled (tests, graceful shutdown). */
|
|
@@ -3662,7 +3727,9 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
|
|
|
3662
3727
|
this.flushQueued = false;
|
|
3663
3728
|
const events = this.pendingEvents.splice(0);
|
|
3664
3729
|
if (!events.length) return;
|
|
3730
|
+
this.resetTurn();
|
|
3665
3731
|
await this.voice.send(events.join("\n"));
|
|
3732
|
+
if (this.silentDispatch) await this.ackIfSilent();
|
|
3666
3733
|
this.notify("revoice_done", "");
|
|
3667
3734
|
});
|
|
3668
3735
|
}
|
|
@@ -3895,6 +3962,8 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
3895
3962
|
}
|
|
3896
3963
|
},
|
|
3897
3964
|
run: async ({ brief, label }) => {
|
|
3965
|
+
this.turnDispatched = true;
|
|
3966
|
+
this.turnBriefs.add(String(brief ?? ""));
|
|
3898
3967
|
const id = await this.dispatch(String(brief ?? ""), "act", label ? String(label) : void 0);
|
|
3899
3968
|
return `Acting on task ${id}. Acknowledge briefly; the result will arrive as a [task ${id} completed] event.`;
|
|
3900
3969
|
}
|
|
@@ -3913,6 +3982,8 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
3913
3982
|
}
|
|
3914
3983
|
},
|
|
3915
3984
|
run: async ({ brief, label }) => {
|
|
3985
|
+
this.turnDispatched = true;
|
|
3986
|
+
this.turnBriefs.add(String(brief ?? ""));
|
|
3916
3987
|
const id = await this.dispatch(String(brief ?? ""), "think", label ? String(label) : void 0);
|
|
3917
3988
|
return `Thinking on task ${id}. Acknowledge briefly; the result will arrive as a [task ${id} completed] event.`;
|
|
3918
3989
|
}
|
|
@@ -4781,6 +4852,7 @@ import { MemFilesystem as MemFilesystem3, IndexedDbFilesystem, CommandExecutor a
|
|
|
4781
4852
|
// src/mcp.client.ts
|
|
4782
4853
|
init_logging();
|
|
4783
4854
|
import { spawn } from "child_process";
|
|
4855
|
+
import { createHash } from "crypto";
|
|
4784
4856
|
var log10 = forComponent("mcp");
|
|
4785
4857
|
var PROTOCOL_VERSION = "2025-06-18";
|
|
4786
4858
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
@@ -4973,37 +5045,136 @@ var McpClient = class {
|
|
|
4973
5045
|
await this.transport.close();
|
|
4974
5046
|
}
|
|
4975
5047
|
};
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
5048
|
+
function buildTransport(cfg) {
|
|
5049
|
+
return cfg.url ? new HttpTransport({ url: cfg.url, headers: cfg.headers, bearerToken: cfg.bearerToken, timeoutMs: cfg.timeoutMs }) : new StdioTransport({ command: cfg.command, args: cfg.args, env: cfg.env, cwd: cfg.cwd, timeoutMs: cfg.timeoutMs });
|
|
5050
|
+
}
|
|
5051
|
+
function withTimeout(p, ms, label) {
|
|
5052
|
+
if (!ms || ms <= 0) return p;
|
|
5053
|
+
return new Promise((resolve4, reject) => {
|
|
5054
|
+
const timer = setTimeout(() => reject(new Error(`MCP "${label}" mount exceeded ${ms}ms`)), ms);
|
|
5055
|
+
timer.unref?.();
|
|
5056
|
+
p.then((v) => {
|
|
5057
|
+
clearTimeout(timer);
|
|
5058
|
+
resolve4(v);
|
|
5059
|
+
}, (e) => {
|
|
5060
|
+
clearTimeout(timer);
|
|
5061
|
+
reject(e);
|
|
5062
|
+
});
|
|
5063
|
+
});
|
|
5064
|
+
}
|
|
5065
|
+
async function mountWithDeadline(name, cfg, mountTimeoutMs) {
|
|
5066
|
+
const client = new McpClient(buildTransport(cfg));
|
|
5067
|
+
try {
|
|
5068
|
+
return await withTimeout((async () => {
|
|
5069
|
+
const init = await client.connect();
|
|
5070
|
+
const specs = await client.listTools();
|
|
5071
|
+
const tools = mcpToolsToAgentTools(specs, (tool, a) => client.callTool(tool, a), `mcp__${name}__`);
|
|
5072
|
+
return { name, client, tools, specs, serverInfo: init?.serverInfo };
|
|
5073
|
+
})(), mountTimeoutMs, name);
|
|
5074
|
+
} catch (e) {
|
|
5075
|
+
await client.close().catch((err2) => log10.debug(`close after failed mount of "${name}": ${err2}`));
|
|
5076
|
+
throw e;
|
|
5077
|
+
}
|
|
5078
|
+
}
|
|
5079
|
+
function mountMcpServer(name, cfg) {
|
|
5080
|
+
return mountWithDeadline(name, cfg);
|
|
5081
|
+
}
|
|
5082
|
+
function validEntries(servers) {
|
|
5083
|
+
return Object.entries(servers).filter(([name, cfg]) => {
|
|
5084
|
+
if (!cfg || cfg.disabled) return false;
|
|
4988
5085
|
if (!cfg.command && !cfg.url) {
|
|
4989
5086
|
log10.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
|
|
4990
|
-
|
|
4991
|
-
}
|
|
4992
|
-
try {
|
|
4993
|
-
const m = await mountMcpServer(name, cfg);
|
|
4994
|
-
out.push(m);
|
|
4995
|
-
log10.info(`MCP "${name}" mounted \u2014 ${m.tools.length} tool(s)${m.serverInfo?.name ? ` from ${m.serverInfo.name}` : ""}`);
|
|
4996
|
-
} catch (e) {
|
|
4997
|
-
if (e instanceof McpAuthError) log10.warn(`MCP "${name}" needs-auth: HTTP ${e.status} \u2014 set bearerToken or headers in its config; skipping`);
|
|
4998
|
-
else log10.error(`MCP server "${name}" failed to mount: ${e?.message ?? e}`);
|
|
5087
|
+
return false;
|
|
4999
5088
|
}
|
|
5000
|
-
|
|
5089
|
+
return true;
|
|
5090
|
+
});
|
|
5091
|
+
}
|
|
5092
|
+
function logMountFailure(name, e) {
|
|
5093
|
+
if (e instanceof McpAuthError) log10.warn(`MCP "${name}" needs-auth: HTTP ${e.status} \u2014 set bearerToken or headers in its config; skipping`);
|
|
5094
|
+
else log10.error(`MCP server "${name}" failed to mount: ${e?.message ?? e}`);
|
|
5095
|
+
}
|
|
5096
|
+
async function mountMcpServers(servers = {}, opts = {}) {
|
|
5097
|
+
const entries = validEntries(servers);
|
|
5098
|
+
const settled = await Promise.allSettled(entries.map(([name, cfg]) => mountWithDeadline(name, cfg, opts.mountTimeoutMs)));
|
|
5099
|
+
const out = [];
|
|
5100
|
+
settled.forEach((r, i) => {
|
|
5101
|
+
const name = entries[i][0];
|
|
5102
|
+
if (r.status === "fulfilled") {
|
|
5103
|
+
out.push(r.value);
|
|
5104
|
+
log10.info(`MCP "${name}" mounted \u2014 ${r.value.tools.length} tool(s)${r.value.serverInfo?.name ? ` from ${r.value.serverInfo.name}` : ""}`);
|
|
5105
|
+
} else logMountFailure(name, r.reason);
|
|
5106
|
+
});
|
|
5001
5107
|
return out;
|
|
5002
5108
|
}
|
|
5109
|
+
var MemMcpCatalog = class {
|
|
5110
|
+
constructor(ttlMs = 5 * 6e4) {
|
|
5111
|
+
this.ttlMs = ttlMs;
|
|
5112
|
+
}
|
|
5113
|
+
ttlMs;
|
|
5114
|
+
m = /* @__PURE__ */ new Map();
|
|
5115
|
+
get(key) {
|
|
5116
|
+
const e = this.m.get(key);
|
|
5117
|
+
if (!e) return null;
|
|
5118
|
+
if (Date.now() > e.exp) {
|
|
5119
|
+
this.m.delete(key);
|
|
5120
|
+
return null;
|
|
5121
|
+
}
|
|
5122
|
+
return e.specs;
|
|
5123
|
+
}
|
|
5124
|
+
set(key, specs) {
|
|
5125
|
+
this.m.set(key, { specs, exp: Date.now() + this.ttlMs });
|
|
5126
|
+
}
|
|
5127
|
+
};
|
|
5128
|
+
var McpPool = class {
|
|
5129
|
+
constructor(ttlMs = 5 * 6e4) {
|
|
5130
|
+
this.ttlMs = ttlMs;
|
|
5131
|
+
}
|
|
5132
|
+
ttlMs;
|
|
5133
|
+
warm = /* @__PURE__ */ new Map();
|
|
5134
|
+
get(key) {
|
|
5135
|
+
const e = this.warm.get(key);
|
|
5136
|
+
if (!e) return null;
|
|
5137
|
+
this.arm(key, e);
|
|
5138
|
+
return e.client;
|
|
5139
|
+
}
|
|
5140
|
+
put(key, client) {
|
|
5141
|
+
const prev = this.warm.get(key);
|
|
5142
|
+
if (prev) {
|
|
5143
|
+
clearTimeout(prev.timer);
|
|
5144
|
+
if (prev.client !== client) void prev.client.close().catch((err2) => log10.debug(`warm-pool replace close failed: ${err2}`));
|
|
5145
|
+
}
|
|
5146
|
+
const e = { client, timer: void 0 };
|
|
5147
|
+
this.warm.set(key, e);
|
|
5148
|
+
this.arm(key, e);
|
|
5149
|
+
}
|
|
5150
|
+
arm(key, e) {
|
|
5151
|
+
clearTimeout(e.timer);
|
|
5152
|
+
e.timer = setTimeout(() => {
|
|
5153
|
+
void this.evict(key);
|
|
5154
|
+
}, this.ttlMs);
|
|
5155
|
+
e.timer.unref?.();
|
|
5156
|
+
}
|
|
5157
|
+
async evict(key) {
|
|
5158
|
+
const e = this.warm.get(key);
|
|
5159
|
+
if (!e) return;
|
|
5160
|
+
this.warm.delete(key);
|
|
5161
|
+
await e.client.close().catch((err2) => log10.debug(`warm-pool evict close failed: ${err2}`));
|
|
5162
|
+
}
|
|
5163
|
+
async closeAll() {
|
|
5164
|
+
for (const e of this.warm.values()) {
|
|
5165
|
+
clearTimeout(e.timer);
|
|
5166
|
+
await e.client.close().catch(() => {
|
|
5167
|
+
});
|
|
5168
|
+
}
|
|
5169
|
+
this.warm.clear();
|
|
5170
|
+
}
|
|
5171
|
+
};
|
|
5172
|
+
var defaultCatalog = new MemMcpCatalog();
|
|
5173
|
+
var defaultPool = new McpPool();
|
|
5003
5174
|
|
|
5004
5175
|
// cli/mcpOAuth.ts
|
|
5005
5176
|
import { createServer } from "http";
|
|
5006
|
-
import { randomBytes, createHash } from "crypto";
|
|
5177
|
+
import { randomBytes, createHash as createHash2 } from "crypto";
|
|
5007
5178
|
import { readFileSync, writeFileSync as writeFileSync2, mkdirSync, existsSync } from "fs";
|
|
5008
5179
|
import { dirname as dirname2 } from "path";
|
|
5009
5180
|
var McpOAuthOptions = class {
|
|
@@ -5047,7 +5218,7 @@ var McpOAuth = class {
|
|
|
5047
5218
|
const meta = await this.discover(serverUrl);
|
|
5048
5219
|
const clientId = this.options.clientId ?? await this.registerClient(meta, serverUrl);
|
|
5049
5220
|
const verifier = b64url(randomBytes(32));
|
|
5050
|
-
const challenge = b64url(
|
|
5221
|
+
const challenge = b64url(createHash2("sha256").update(verifier).digest());
|
|
5051
5222
|
const state = b64url(randomBytes(16));
|
|
5052
5223
|
const { code, redirectUri } = await this.captureCode(meta.authorization_endpoint, clientId, challenge, state);
|
|
5053
5224
|
const tok = await this.exchange(meta.token_endpoint, {
|