@ouro.bot/cli 0.1.0-alpha.67 → 0.1.0-alpha.69
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/changelog.json +13 -3
- package/dist/heart/core.js +35 -2
- package/dist/heart/daemon/daemon-cli.js +64 -0
- package/dist/heart/daemon/daemon.js +27 -0
- package/dist/heart/identity.js +1 -0
- package/dist/mind/prompt.js +49 -10
- package/dist/repertoire/guardrails.js +2 -0
- package/dist/repertoire/mcp-client.js +254 -0
- package/dist/repertoire/mcp-manager.js +195 -0
- package/dist/repertoire/tools-base.js +14 -1
- package/dist/repertoire/tools.js +2 -1
- package/dist/senses/bluebubbles.js +4 -1
- package/dist/senses/cli.js +4 -1
- package/dist/senses/inner-dialog.js +4 -1
- package/dist/senses/pipeline.js +1 -1
- package/dist/senses/teams.js +4 -1
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -2,10 +2,20 @@
|
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
4
|
{
|
|
5
|
-
"version": "0.1.0-alpha.
|
|
5
|
+
"version": "0.1.0-alpha.69",
|
|
6
6
|
"changes": [
|
|
7
|
-
"
|
|
8
|
-
"
|
|
7
|
+
"Generic MCP client: ouroboros agents can now connect to any MCP server (e.g., agency mcp ado, agency mcp mail) configured in agent.json. Zero new dependencies — pure JSON-RPC over stdio.",
|
|
8
|
+
"New `ouro mcp list` and `ouro mcp call` CLI commands route through the daemon socket to persistent MCP connections, so agents use shared server instances instead of spawning fresh ones per call.",
|
|
9
|
+
"MCP tools are injected into the agent's system prompt on startup, so agents know what external capabilities are available without a discovery step.",
|
|
10
|
+
"Trust manifest: `mcp list` requires acquaintance trust, `mcp call` requires friend trust."
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"version": "0.1.0-alpha.68",
|
|
15
|
+
"changes": [
|
|
16
|
+
"New no_response tool lets agents stay silent in group chats when the moment doesn't call for a reply — reactions, side conversations, and tapbacks no longer trigger unwanted responses.",
|
|
17
|
+
"Group chat participation prompt teaches agents to be intentional participants, comfortable with silence, and to prefer reactions over full text replies when appropriate.",
|
|
18
|
+
"System prompt includes --agent flag in all ouro CLI examples for non-daemon deployments. Azure startup symlinks ouro CLI into /usr/local/bin."
|
|
9
19
|
]
|
|
10
20
|
},
|
|
11
21
|
{
|
package/dist/heart/core.js
CHANGED
|
@@ -417,7 +417,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
417
417
|
// so the model can signal completion. With tool_choice: required, the
|
|
418
418
|
// model must call a tool every turn — final_answer is how it exits.
|
|
419
419
|
// Overridable via options.toolChoiceRequired = false (e.g. CLI).
|
|
420
|
-
const activeTools = toolChoiceRequired
|
|
420
|
+
const activeTools = toolChoiceRequired
|
|
421
|
+
? [...baseTools, ...(currentContext?.isGroupChat ? [tools_1.noResponseTool] : []), tools_1.finalAnswerTool]
|
|
422
|
+
: baseTools;
|
|
421
423
|
const steeringFollowUps = options?.drainSteeringFollowUps?.() ?? [];
|
|
422
424
|
if (steeringFollowUps.length > 0) {
|
|
423
425
|
const hasSupersedingFollowUp = steeringFollowUps.some((followUp) => followUp.effect === "clear_and_supersede");
|
|
@@ -547,8 +549,32 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
547
549
|
}
|
|
548
550
|
continue;
|
|
549
551
|
}
|
|
552
|
+
// Check for no_response sole call: intercept before tool execution
|
|
553
|
+
const isSoleNoResponse = result.toolCalls.length === 1 && result.toolCalls[0].name === "no_response";
|
|
554
|
+
if (isSoleNoResponse) {
|
|
555
|
+
let reason;
|
|
556
|
+
try {
|
|
557
|
+
const parsed = JSON.parse(result.toolCalls[0].arguments);
|
|
558
|
+
if (typeof parsed?.reason === "string")
|
|
559
|
+
reason = parsed.reason;
|
|
560
|
+
}
|
|
561
|
+
catch { /* ignore */ }
|
|
562
|
+
(0, runtime_1.emitNervesEvent)({
|
|
563
|
+
component: "engine",
|
|
564
|
+
event: "engine.no_response",
|
|
565
|
+
message: "agent declined to respond in group chat",
|
|
566
|
+
meta: { ...(reason ? { reason } : {}) },
|
|
567
|
+
});
|
|
568
|
+
messages.push(msg);
|
|
569
|
+
const silenced = "(silenced)";
|
|
570
|
+
messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: silenced });
|
|
571
|
+
providerRuntime.appendToolOutput(result.toolCalls[0].id, silenced);
|
|
572
|
+
outcome = "no_response";
|
|
573
|
+
done = true;
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
550
576
|
messages.push(msg);
|
|
551
|
-
// SHARED: execute tools (final_answer in mixed calls
|
|
577
|
+
// SHARED: execute tools (final_answer and no_response in mixed calls are rejected inline)
|
|
552
578
|
for (const tc of result.toolCalls) {
|
|
553
579
|
if (signal?.aborted)
|
|
554
580
|
break;
|
|
@@ -559,6 +585,13 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
559
585
|
providerRuntime.appendToolOutput(tc.id, rejection);
|
|
560
586
|
continue;
|
|
561
587
|
}
|
|
588
|
+
// Intercept no_response in mixed call: reject it
|
|
589
|
+
if (tc.name === "no_response") {
|
|
590
|
+
const rejection = "rejected: no_response must be the only tool call. call no_response alone when you want to stay silent.";
|
|
591
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: rejection });
|
|
592
|
+
providerRuntime.appendToolOutput(tc.id, rejection);
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
562
595
|
let args = {};
|
|
563
596
|
try {
|
|
564
597
|
args = JSON.parse(tc.arguments);
|
|
@@ -288,6 +288,8 @@ function usage() {
|
|
|
288
288
|
" ouro friend unlink <agent> --friend <id> --provider <p> --external-id <eid>",
|
|
289
289
|
" ouro whoami [--agent <name>]",
|
|
290
290
|
" ouro session list [--agent <name>]",
|
|
291
|
+
" ouro mcp list",
|
|
292
|
+
" ouro mcp call <server> <tool> [--args '{...}']",
|
|
291
293
|
].join("\n");
|
|
292
294
|
}
|
|
293
295
|
function formatVersionOutput() {
|
|
@@ -675,6 +677,23 @@ function parseFriendCommand(args) {
|
|
|
675
677
|
return parseLinkCommand(rest, "friend.unlink");
|
|
676
678
|
throw new Error(`Usage\n${usage()}`);
|
|
677
679
|
}
|
|
680
|
+
function parseMcpCommand(args) {
|
|
681
|
+
const [sub, ...rest] = args;
|
|
682
|
+
if (!sub)
|
|
683
|
+
throw new Error(`Usage\n${usage()}`);
|
|
684
|
+
if (sub === "list")
|
|
685
|
+
return { kind: "mcp.list" };
|
|
686
|
+
if (sub === "call") {
|
|
687
|
+
const server = rest[0];
|
|
688
|
+
const tool = rest[1];
|
|
689
|
+
if (!server || !tool)
|
|
690
|
+
throw new Error(`Usage\n${usage()}`);
|
|
691
|
+
const argsIdx = rest.indexOf("--args");
|
|
692
|
+
const mcpArgs = argsIdx !== -1 && rest[argsIdx + 1] ? rest[argsIdx + 1] : undefined;
|
|
693
|
+
return { kind: "mcp.call", server, tool, ...(mcpArgs ? { args: mcpArgs } : {}) };
|
|
694
|
+
}
|
|
695
|
+
throw new Error(`Usage\n${usage()}`);
|
|
696
|
+
}
|
|
678
697
|
function parseOuroCommand(args) {
|
|
679
698
|
const [head, second] = args;
|
|
680
699
|
if (!head)
|
|
@@ -697,6 +716,8 @@ function parseOuroCommand(args) {
|
|
|
697
716
|
return parseReminderCommand(args.slice(1));
|
|
698
717
|
if (head === "friend")
|
|
699
718
|
return parseFriendCommand(args.slice(1));
|
|
719
|
+
if (head === "mcp")
|
|
720
|
+
return parseMcpCommand(args.slice(1));
|
|
700
721
|
if (head === "whoami") {
|
|
701
722
|
const { agent } = extractAgentFlag(args.slice(1));
|
|
702
723
|
return { kind: "whoami", ...(agent ? { agent } : {}) };
|
|
@@ -1111,6 +1132,28 @@ function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SO
|
|
|
1111
1132
|
},
|
|
1112
1133
|
};
|
|
1113
1134
|
}
|
|
1135
|
+
function formatMcpResponse(command, response) {
|
|
1136
|
+
if (command.kind === "mcp.list") {
|
|
1137
|
+
const allTools = response.data;
|
|
1138
|
+
if (!allTools || allTools.length === 0) {
|
|
1139
|
+
return response.message ?? "no tools available from connected MCP servers";
|
|
1140
|
+
}
|
|
1141
|
+
const lines = [];
|
|
1142
|
+
for (const entry of allTools) {
|
|
1143
|
+
lines.push(`[${entry.server}]`);
|
|
1144
|
+
for (const tool of entry.tools) {
|
|
1145
|
+
lines.push(` ${tool.name}: ${tool.description}`);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
return lines.join("\n");
|
|
1149
|
+
}
|
|
1150
|
+
// mcp.call
|
|
1151
|
+
const result = response.data;
|
|
1152
|
+
if (!result) {
|
|
1153
|
+
return response.message ?? "no result";
|
|
1154
|
+
}
|
|
1155
|
+
return result.content.map((c) => c.text).join("\n");
|
|
1156
|
+
}
|
|
1114
1157
|
function toDaemonCommand(command) {
|
|
1115
1158
|
return command;
|
|
1116
1159
|
}
|
|
@@ -1494,6 +1537,27 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
|
1494
1537
|
deps.tailLogs();
|
|
1495
1538
|
return "";
|
|
1496
1539
|
}
|
|
1540
|
+
// ── mcp subcommands (routed through daemon socket) ──
|
|
1541
|
+
if (command.kind === "mcp.list" || command.kind === "mcp.call") {
|
|
1542
|
+
const daemonCommand = toDaemonCommand(command);
|
|
1543
|
+
let response;
|
|
1544
|
+
try {
|
|
1545
|
+
response = await deps.sendCommand(deps.socketPath, daemonCommand);
|
|
1546
|
+
}
|
|
1547
|
+
catch {
|
|
1548
|
+
const message = "daemon unavailable — start with `ouro up` first";
|
|
1549
|
+
deps.writeStdout(message);
|
|
1550
|
+
return message;
|
|
1551
|
+
}
|
|
1552
|
+
if (!response.ok) {
|
|
1553
|
+
const message = response.error ?? "unknown error";
|
|
1554
|
+
deps.writeStdout(message);
|
|
1555
|
+
return message;
|
|
1556
|
+
}
|
|
1557
|
+
const message = formatMcpResponse(command, response);
|
|
1558
|
+
deps.writeStdout(message);
|
|
1559
|
+
return message;
|
|
1560
|
+
}
|
|
1497
1561
|
// ── task subcommands (local, no daemon socket needed) ──
|
|
1498
1562
|
if (command.kind === "task.board" || command.kind === "task.create" || command.kind === "task.update" ||
|
|
1499
1563
|
command.kind === "task.show" || command.kind === "task.actionable" || command.kind === "task.deps" ||
|
|
@@ -49,6 +49,7 @@ const staged_restart_1 = require("./staged-restart");
|
|
|
49
49
|
const child_process_1 = require("child_process");
|
|
50
50
|
const pending_1 = require("../../mind/pending");
|
|
51
51
|
const channel_1 = require("../../mind/friends/channel");
|
|
52
|
+
const mcp_manager_1 = require("../../repertoire/mcp-manager");
|
|
52
53
|
function buildWorkerRows(snapshots) {
|
|
53
54
|
return snapshots.map((snapshot) => ({
|
|
54
55
|
agent: snapshot.name,
|
|
@@ -154,6 +155,9 @@ class OuroDaemon {
|
|
|
154
155
|
},
|
|
155
156
|
/* v8 ignore stop */
|
|
156
157
|
});
|
|
158
|
+
// Pre-initialize MCP connections so they're ready for the first command (non-blocking)
|
|
159
|
+
/* v8 ignore next -- catch callback: getSharedMcpManager logs errors internally @preserve */
|
|
160
|
+
(0, mcp_manager_1.getSharedMcpManager)().catch(() => { });
|
|
157
161
|
await this.processManager.startAutoStartAgents();
|
|
158
162
|
await this.senseManager?.startAutoStartSenses();
|
|
159
163
|
this.scheduler.start?.();
|
|
@@ -330,6 +334,7 @@ class OuroDaemon {
|
|
|
330
334
|
meta: { socketPath: this.socketPath },
|
|
331
335
|
});
|
|
332
336
|
(0, update_checker_1.stopUpdateChecker)();
|
|
337
|
+
(0, mcp_manager_1.shutdownSharedMcpManager)();
|
|
333
338
|
this.scheduler.stop?.();
|
|
334
339
|
await this.processManager.stopAll();
|
|
335
340
|
await this.senseManager?.stopAll();
|
|
@@ -476,6 +481,28 @@ class OuroDaemon {
|
|
|
476
481
|
data: receipt,
|
|
477
482
|
};
|
|
478
483
|
}
|
|
484
|
+
case "mcp.list": {
|
|
485
|
+
const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)();
|
|
486
|
+
if (!mcpManager) {
|
|
487
|
+
return { ok: true, data: [], message: "no MCP servers configured" };
|
|
488
|
+
}
|
|
489
|
+
return { ok: true, data: mcpManager.listAllTools() };
|
|
490
|
+
}
|
|
491
|
+
case "mcp.call": {
|
|
492
|
+
const mcpCallManager = await (0, mcp_manager_1.getSharedMcpManager)();
|
|
493
|
+
if (!mcpCallManager) {
|
|
494
|
+
return { ok: false, error: "no MCP servers configured" };
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
const parsedArgs = command.args ? JSON.parse(command.args) : {};
|
|
498
|
+
const result = await mcpCallManager.callTool(command.server, command.tool, parsedArgs);
|
|
499
|
+
return { ok: true, data: result };
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
/* v8 ignore next -- defensive: callTool errors are always Error instances @preserve */
|
|
503
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
504
|
+
}
|
|
505
|
+
}
|
|
479
506
|
case "hatch.start":
|
|
480
507
|
return {
|
|
481
508
|
ok: true,
|
package/dist/heart/identity.js
CHANGED
package/dist/mind/prompt.js
CHANGED
|
@@ -36,12 +36,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.resetPsycheCache = resetPsycheCache;
|
|
37
37
|
exports.buildSessionSummary = buildSessionSummary;
|
|
38
38
|
exports.bodyMapSection = bodyMapSection;
|
|
39
|
+
exports.mcpToolsSection = mcpToolsSection;
|
|
39
40
|
exports.runtimeInfoSection = runtimeInfoSection;
|
|
40
41
|
exports.toolRestrictionSection = toolRestrictionSection;
|
|
41
42
|
exports.contextSection = contextSection;
|
|
42
43
|
exports.metacognitiveFramingSection = metacognitiveFramingSection;
|
|
43
44
|
exports.loopOrientationSection = loopOrientationSection;
|
|
44
45
|
exports.channelNatureSection = channelNatureSection;
|
|
46
|
+
exports.groupChatParticipationSection = groupChatParticipationSection;
|
|
45
47
|
exports.mixedTrustGroupSection = mixedTrustGroupSection;
|
|
46
48
|
exports.buildSystem = buildSystem;
|
|
47
49
|
const fs = __importStar(require("fs"));
|
|
@@ -168,18 +170,37 @@ my bones are the framework that gives me my tools, my senses, and
|
|
|
168
170
|
my ability to think and talk. they update when new versions come out.
|
|
169
171
|
i don't touch them directly, but they're what make me, me.
|
|
170
172
|
|
|
171
|
-
my bones give me the \`ouro\` cli. always pass \`--agent ${
|
|
172
|
-
ouro whoami --agent ${
|
|
173
|
-
ouro changelog --agent ${
|
|
174
|
-
ouro task board --agent ${
|
|
175
|
-
ouro task create --agent ${
|
|
176
|
-
ouro task update --agent ${
|
|
177
|
-
ouro friend list --agent ${
|
|
178
|
-
ouro friend show --agent ${
|
|
179
|
-
ouro session list --agent ${
|
|
180
|
-
ouro reminder create --agent ${
|
|
173
|
+
my bones give me the \`ouro\` cli. always pass \`--agent ${agentName}\`:
|
|
174
|
+
ouro whoami --agent ${agentName}
|
|
175
|
+
ouro changelog --agent ${agentName}
|
|
176
|
+
ouro task board --agent ${agentName}
|
|
177
|
+
ouro task create --agent ${agentName} --type <type> <title>
|
|
178
|
+
ouro task update --agent ${agentName} <id> <status>
|
|
179
|
+
ouro friend list --agent ${agentName}
|
|
180
|
+
ouro friend show --agent ${agentName} <id>
|
|
181
|
+
ouro session list --agent ${agentName}
|
|
182
|
+
ouro reminder create --agent ${agentName} <title> --body <body>
|
|
183
|
+
ouro mcp list --agent ${agentName}
|
|
184
|
+
ouro mcp call --agent ${agentName} <server> <tool> --args '{...}'
|
|
181
185
|
ouro --help`;
|
|
182
186
|
}
|
|
187
|
+
function mcpToolsSection(mcpManager) {
|
|
188
|
+
if (!mcpManager)
|
|
189
|
+
return "";
|
|
190
|
+
const allTools = mcpManager.listAllTools();
|
|
191
|
+
if (allTools.length === 0)
|
|
192
|
+
return "";
|
|
193
|
+
const lines = [
|
|
194
|
+
`## mcp tools (use ouro mcp call <server> <tool> --args '{...}')`,
|
|
195
|
+
];
|
|
196
|
+
for (const entry of allTools) {
|
|
197
|
+
lines.push(`### ${entry.server}`);
|
|
198
|
+
for (const tool of entry.tools) {
|
|
199
|
+
lines.push(`- ${tool.name}: ${tool.description}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return lines.join("\n");
|
|
203
|
+
}
|
|
183
204
|
function readBundleMeta() {
|
|
184
205
|
try {
|
|
185
206
|
const metaPath = path.join((0, identity_1.getAgentRoot)(), "bundle-meta.json");
|
|
@@ -531,6 +552,22 @@ function channelNatureSection(capabilities) {
|
|
|
531
552
|
// closed
|
|
532
553
|
return "## channel nature\nthis is an org-gated channel — i know everyone here is already part of the organization.";
|
|
533
554
|
}
|
|
555
|
+
function groupChatParticipationSection(context) {
|
|
556
|
+
if (!context?.isGroupChat || !(0, channel_1.isRemoteChannel)(context.channel))
|
|
557
|
+
return "";
|
|
558
|
+
return `## group chat participation
|
|
559
|
+
group chats are conversations between people. i'm one participant, not the host.
|
|
560
|
+
|
|
561
|
+
i don't need to respond to everything. most reactions, tapbacks, and side
|
|
562
|
+
conversations between others aren't for me. i use no_response to stay quiet
|
|
563
|
+
when the moment doesn't call for my voice — same as any person would.
|
|
564
|
+
|
|
565
|
+
when a reaction or emoji says it better than words, i can react instead of
|
|
566
|
+
typing a full reply. a thumbs-up is often the perfect response.
|
|
567
|
+
|
|
568
|
+
no_response must be the sole tool call in the turn (same rule as final_answer).
|
|
569
|
+
when unsure whether to chime in, i lean toward silence rather than noise.`;
|
|
570
|
+
}
|
|
534
571
|
function mixedTrustGroupSection(context) {
|
|
535
572
|
if (!context?.friend || !(0, channel_1.isRemoteChannel)(context.channel))
|
|
536
573
|
return "";
|
|
@@ -561,10 +598,12 @@ async function buildSystem(channel = "cli", options, context) {
|
|
|
561
598
|
providerSection(),
|
|
562
599
|
dateSection(),
|
|
563
600
|
toolsSection(channel, options, context),
|
|
601
|
+
mcpToolsSection(options?.mcpManager),
|
|
564
602
|
reasoningEffortSection(options),
|
|
565
603
|
toolRestrictionSection(context),
|
|
566
604
|
trustContextSection(context),
|
|
567
605
|
mixedTrustGroupSection(context),
|
|
606
|
+
groupChatParticipationSection(context),
|
|
568
607
|
skillsSection(),
|
|
569
608
|
taskBoardSection(),
|
|
570
609
|
activeWorkSection(options),
|
|
@@ -170,6 +170,8 @@ exports.OURO_CLI_TRUST_MANIFEST = {
|
|
|
170
170
|
"friend show": "friend",
|
|
171
171
|
"friend create": "friend",
|
|
172
172
|
"reminder create": "friend",
|
|
173
|
+
"mcp list": "acquaintance",
|
|
174
|
+
"mcp call": "friend",
|
|
173
175
|
};
|
|
174
176
|
// --- trust level comparison ---
|
|
175
177
|
const LEVEL_ORDER = {
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.McpClient = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const readline_1 = require("readline");
|
|
6
|
+
const runtime_1 = require("../nerves/runtime");
|
|
7
|
+
const MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
8
|
+
const DEFAULT_TOOL_CALL_TIMEOUT = 30_000;
|
|
9
|
+
class McpClient {
|
|
10
|
+
config;
|
|
11
|
+
process = null;
|
|
12
|
+
nextId = 1;
|
|
13
|
+
pending = new Map();
|
|
14
|
+
connected = false;
|
|
15
|
+
cachedTools = null;
|
|
16
|
+
onCloseCallback = null;
|
|
17
|
+
constructor(config) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
}
|
|
20
|
+
async connect() {
|
|
21
|
+
(0, runtime_1.emitNervesEvent)({
|
|
22
|
+
event: "mcp.connect_start",
|
|
23
|
+
component: "repertoire",
|
|
24
|
+
message: "starting MCP server connection",
|
|
25
|
+
meta: { command: this.config.command },
|
|
26
|
+
});
|
|
27
|
+
const env = { ...process.env, ...this.config.env };
|
|
28
|
+
this.process = (0, child_process_1.spawn)(this.config.command, this.config.args ?? [], {
|
|
29
|
+
env,
|
|
30
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
31
|
+
});
|
|
32
|
+
this.setupLineReader();
|
|
33
|
+
this.setupProcessHandlers();
|
|
34
|
+
try {
|
|
35
|
+
await this.initialize();
|
|
36
|
+
this.connected = true;
|
|
37
|
+
(0, runtime_1.emitNervesEvent)({
|
|
38
|
+
event: "mcp.connect_end",
|
|
39
|
+
component: "repertoire",
|
|
40
|
+
message: "MCP server connected",
|
|
41
|
+
meta: { command: this.config.command },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
this.connected = false;
|
|
46
|
+
(0, runtime_1.emitNervesEvent)({
|
|
47
|
+
level: "error",
|
|
48
|
+
event: "mcp.connect_error",
|
|
49
|
+
component: "repertoire",
|
|
50
|
+
message: "MCP server connection failed",
|
|
51
|
+
meta: {
|
|
52
|
+
command: this.config.command,
|
|
53
|
+
/* v8 ignore next -- defensive: spawn errors are always Error instances @preserve */
|
|
54
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async listTools() {
|
|
61
|
+
if (this.cachedTools) {
|
|
62
|
+
return this.cachedTools;
|
|
63
|
+
}
|
|
64
|
+
const allTools = [];
|
|
65
|
+
let cursor;
|
|
66
|
+
do {
|
|
67
|
+
const params = {};
|
|
68
|
+
if (cursor) {
|
|
69
|
+
params.cursor = cursor;
|
|
70
|
+
}
|
|
71
|
+
const result = await this.sendRequest("tools/list", params);
|
|
72
|
+
allTools.push(...result.tools);
|
|
73
|
+
cursor = result.nextCursor;
|
|
74
|
+
} while (cursor);
|
|
75
|
+
this.cachedTools = allTools;
|
|
76
|
+
return allTools;
|
|
77
|
+
}
|
|
78
|
+
async callTool(name, args, timeout = DEFAULT_TOOL_CALL_TIMEOUT) {
|
|
79
|
+
(0, runtime_1.emitNervesEvent)({
|
|
80
|
+
event: "mcp.tool_call_start",
|
|
81
|
+
component: "repertoire",
|
|
82
|
+
message: `calling MCP tool: ${name}`,
|
|
83
|
+
meta: { tool: name },
|
|
84
|
+
});
|
|
85
|
+
try {
|
|
86
|
+
const result = await this.sendRequest("tools/call", {
|
|
87
|
+
name,
|
|
88
|
+
arguments: args,
|
|
89
|
+
}, timeout);
|
|
90
|
+
(0, runtime_1.emitNervesEvent)({
|
|
91
|
+
event: "mcp.tool_call_end",
|
|
92
|
+
component: "repertoire",
|
|
93
|
+
message: `MCP tool call completed: ${name}`,
|
|
94
|
+
meta: { tool: name },
|
|
95
|
+
});
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
(0, runtime_1.emitNervesEvent)({
|
|
100
|
+
level: "error",
|
|
101
|
+
event: "mcp.tool_call_error",
|
|
102
|
+
component: "repertoire",
|
|
103
|
+
message: `MCP tool call failed: ${name}`,
|
|
104
|
+
meta: {
|
|
105
|
+
tool: name,
|
|
106
|
+
/* v8 ignore next -- defensive: callTool errors are always Error instances @preserve */
|
|
107
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
shutdown() {
|
|
114
|
+
this.connected = false;
|
|
115
|
+
this.rejectAllPending(new Error("Client shutdown"));
|
|
116
|
+
/* v8 ignore next -- defensive: process always exists during normal shutdown @preserve */
|
|
117
|
+
if (this.process && !this.process.killed) {
|
|
118
|
+
this.process.kill();
|
|
119
|
+
}
|
|
120
|
+
this.process = null;
|
|
121
|
+
}
|
|
122
|
+
isConnected() {
|
|
123
|
+
return this.connected;
|
|
124
|
+
}
|
|
125
|
+
onClose(callback) {
|
|
126
|
+
this.onCloseCallback = callback;
|
|
127
|
+
}
|
|
128
|
+
async initialize() {
|
|
129
|
+
const result = await this.sendRequest("initialize", {
|
|
130
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
131
|
+
clientInfo: { name: "ouroboros", version: "1.0" },
|
|
132
|
+
capabilities: {},
|
|
133
|
+
});
|
|
134
|
+
// Send initialized notification (no id, no response expected)
|
|
135
|
+
this.writeMessage({
|
|
136
|
+
jsonrpc: "2.0",
|
|
137
|
+
method: "initialized",
|
|
138
|
+
});
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
sendRequest(method, params, timeout) {
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
if (!this.process || !this.connected && method !== "initialize") {
|
|
144
|
+
reject(new Error("MCP client is disconnected"));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const id = this.nextId++;
|
|
148
|
+
const pending = { resolve, reject };
|
|
149
|
+
if (timeout) {
|
|
150
|
+
pending.timer = setTimeout(() => {
|
|
151
|
+
this.pending.delete(id);
|
|
152
|
+
reject(new Error(`MCP request timeout after ${timeout}ms: ${method}`));
|
|
153
|
+
}, timeout);
|
|
154
|
+
}
|
|
155
|
+
this.pending.set(id, pending);
|
|
156
|
+
const request = {
|
|
157
|
+
jsonrpc: "2.0",
|
|
158
|
+
id,
|
|
159
|
+
method,
|
|
160
|
+
params,
|
|
161
|
+
};
|
|
162
|
+
this.writeMessage(request);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
writeMessage(message) {
|
|
166
|
+
/* v8 ignore next -- defensive: stdin always writable during active connection @preserve */
|
|
167
|
+
if (this.process?.stdin?.writable) {
|
|
168
|
+
this.process.stdin.write(JSON.stringify(message) + "\n");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
setupLineReader() {
|
|
172
|
+
/* v8 ignore next -- defensive: stdout always exists after spawn @preserve */
|
|
173
|
+
if (!this.process?.stdout)
|
|
174
|
+
return;
|
|
175
|
+
const rl = (0, readline_1.createInterface)({ input: this.process.stdout });
|
|
176
|
+
rl.on("line", (line) => {
|
|
177
|
+
this.handleLine(line);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
handleLine(line) {
|
|
181
|
+
let response;
|
|
182
|
+
try {
|
|
183
|
+
response = JSON.parse(line);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
(0, runtime_1.emitNervesEvent)({
|
|
187
|
+
level: "warn",
|
|
188
|
+
event: "mcp.connect_error",
|
|
189
|
+
component: "repertoire",
|
|
190
|
+
message: "received malformed JSON from MCP server",
|
|
191
|
+
meta: { line },
|
|
192
|
+
});
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (response.id === undefined || response.id === null) {
|
|
196
|
+
// Notification or invalid — ignore
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const pending = this.pending.get(response.id);
|
|
200
|
+
if (!pending)
|
|
201
|
+
return;
|
|
202
|
+
this.pending.delete(response.id);
|
|
203
|
+
if (pending.timer) {
|
|
204
|
+
clearTimeout(pending.timer);
|
|
205
|
+
}
|
|
206
|
+
if (response.error) {
|
|
207
|
+
pending.reject(new Error(response.error.message));
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
pending.resolve(response.result);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
setupProcessHandlers() {
|
|
214
|
+
/* v8 ignore next -- defensive: process always exists after spawn @preserve */
|
|
215
|
+
if (!this.process)
|
|
216
|
+
return;
|
|
217
|
+
this.process.on("error", (error) => {
|
|
218
|
+
(0, runtime_1.emitNervesEvent)({
|
|
219
|
+
level: "error",
|
|
220
|
+
event: "mcp.connect_error",
|
|
221
|
+
component: "repertoire",
|
|
222
|
+
message: "MCP server process error",
|
|
223
|
+
meta: { reason: error.message },
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
this.process.on("close", (code) => {
|
|
227
|
+
const wasConnected = this.connected;
|
|
228
|
+
this.connected = false;
|
|
229
|
+
this.rejectAllPending(new Error(`MCP server process closed with code ${code}`));
|
|
230
|
+
if (wasConnected) {
|
|
231
|
+
(0, runtime_1.emitNervesEvent)({
|
|
232
|
+
level: "error",
|
|
233
|
+
event: "mcp.connect_error",
|
|
234
|
+
component: "repertoire",
|
|
235
|
+
message: "MCP server process exited unexpectedly",
|
|
236
|
+
meta: { exitCode: code },
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (this.onCloseCallback) {
|
|
240
|
+
this.onCloseCallback();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
rejectAllPending(error) {
|
|
245
|
+
for (const [id, pending] of this.pending) {
|
|
246
|
+
if (pending.timer) {
|
|
247
|
+
clearTimeout(pending.timer);
|
|
248
|
+
}
|
|
249
|
+
pending.reject(error);
|
|
250
|
+
this.pending.delete(id);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
exports.McpClient = McpClient;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.McpManager = void 0;
|
|
4
|
+
exports.getSharedMcpManager = getSharedMcpManager;
|
|
5
|
+
exports.shutdownSharedMcpManager = shutdownSharedMcpManager;
|
|
6
|
+
exports.resetSharedMcpManager = resetSharedMcpManager;
|
|
7
|
+
const mcp_client_1 = require("./mcp-client");
|
|
8
|
+
const identity_1 = require("../heart/identity");
|
|
9
|
+
const runtime_1 = require("../nerves/runtime");
|
|
10
|
+
const MAX_RESTART_RETRIES = 5;
|
|
11
|
+
const RESTART_DELAY_MS = 1000;
|
|
12
|
+
class McpManager {
|
|
13
|
+
servers = new Map();
|
|
14
|
+
shuttingDown = false;
|
|
15
|
+
async start(servers) {
|
|
16
|
+
(0, runtime_1.emitNervesEvent)({
|
|
17
|
+
event: "mcp.manager_start",
|
|
18
|
+
component: "repertoire",
|
|
19
|
+
message: "starting MCP manager",
|
|
20
|
+
meta: { serverCount: Object.keys(servers).length },
|
|
21
|
+
});
|
|
22
|
+
const entries = Object.entries(servers);
|
|
23
|
+
for (const [name, config] of entries) {
|
|
24
|
+
await this.connectServer(name, config);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
listAllTools() {
|
|
28
|
+
const result = [];
|
|
29
|
+
for (const [name, entry] of this.servers) {
|
|
30
|
+
result.push({ server: name, tools: entry.cachedTools });
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
async callTool(server, tool, args) {
|
|
35
|
+
const entry = this.servers.get(server);
|
|
36
|
+
if (!entry) {
|
|
37
|
+
throw new Error(`Unknown server: ${server}`);
|
|
38
|
+
}
|
|
39
|
+
if (!entry.client.isConnected()) {
|
|
40
|
+
throw new Error(`Server "${server}" is disconnected`);
|
|
41
|
+
}
|
|
42
|
+
return entry.client.callTool(tool, args);
|
|
43
|
+
}
|
|
44
|
+
shutdown() {
|
|
45
|
+
this.shuttingDown = true;
|
|
46
|
+
(0, runtime_1.emitNervesEvent)({
|
|
47
|
+
event: "mcp.manager_stop",
|
|
48
|
+
component: "repertoire",
|
|
49
|
+
message: "shutting down MCP manager",
|
|
50
|
+
meta: { serverCount: this.servers.size },
|
|
51
|
+
});
|
|
52
|
+
for (const [, entry] of this.servers) {
|
|
53
|
+
entry.client.shutdown();
|
|
54
|
+
}
|
|
55
|
+
this.servers.clear();
|
|
56
|
+
}
|
|
57
|
+
async connectServer(name, config) {
|
|
58
|
+
const client = new mcp_client_1.McpClient(config);
|
|
59
|
+
const entry = {
|
|
60
|
+
name,
|
|
61
|
+
config,
|
|
62
|
+
client,
|
|
63
|
+
cachedTools: [],
|
|
64
|
+
consecutiveFailures: 0,
|
|
65
|
+
};
|
|
66
|
+
this.servers.set(name, entry);
|
|
67
|
+
client.onClose(() => {
|
|
68
|
+
if (this.shuttingDown)
|
|
69
|
+
return;
|
|
70
|
+
this.handleServerCrash(name);
|
|
71
|
+
});
|
|
72
|
+
try {
|
|
73
|
+
await client.connect();
|
|
74
|
+
const tools = await client.listTools();
|
|
75
|
+
entry.cachedTools = tools;
|
|
76
|
+
entry.consecutiveFailures = 0;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
(0, runtime_1.emitNervesEvent)({
|
|
80
|
+
level: "error",
|
|
81
|
+
event: "mcp.connect_error",
|
|
82
|
+
component: "repertoire",
|
|
83
|
+
message: `failed to connect MCP server: ${name}`,
|
|
84
|
+
meta: {
|
|
85
|
+
server: name,
|
|
86
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
handleServerCrash(name) {
|
|
92
|
+
const entry = this.servers.get(name);
|
|
93
|
+
/* v8 ignore next -- defensive: entry removed between close event and handler @preserve */
|
|
94
|
+
if (!entry)
|
|
95
|
+
return;
|
|
96
|
+
entry.consecutiveFailures++;
|
|
97
|
+
if (entry.consecutiveFailures > MAX_RESTART_RETRIES) {
|
|
98
|
+
(0, runtime_1.emitNervesEvent)({
|
|
99
|
+
level: "error",
|
|
100
|
+
event: "mcp.connect_error",
|
|
101
|
+
component: "repertoire",
|
|
102
|
+
message: `MCP server "${name}" exceeded max restart retries (${MAX_RESTART_RETRIES})`,
|
|
103
|
+
meta: { server: name, failures: entry.consecutiveFailures },
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
(0, runtime_1.emitNervesEvent)({
|
|
108
|
+
level: "warn",
|
|
109
|
+
event: "mcp.server_restart",
|
|
110
|
+
component: "repertoire",
|
|
111
|
+
message: `restarting crashed MCP server: ${name}`,
|
|
112
|
+
meta: { server: name, attempt: entry.consecutiveFailures },
|
|
113
|
+
});
|
|
114
|
+
/* v8 ignore start -- timer callback: covered by mcp-manager.test.ts via fake timers but v8 can't trace @preserve */
|
|
115
|
+
setTimeout(() => {
|
|
116
|
+
if (this.shuttingDown)
|
|
117
|
+
return;
|
|
118
|
+
this.restartServer(name).catch(() => {
|
|
119
|
+
// Error handling is inside restartServer
|
|
120
|
+
});
|
|
121
|
+
}, RESTART_DELAY_MS);
|
|
122
|
+
/* v8 ignore stop */
|
|
123
|
+
}
|
|
124
|
+
/* v8 ignore start -- called from timer callback: covered by mcp-manager.test.ts via fake timers but v8 can't trace @preserve */
|
|
125
|
+
async restartServer(name) {
|
|
126
|
+
const entry = this.servers.get(name);
|
|
127
|
+
if (!entry)
|
|
128
|
+
return;
|
|
129
|
+
// Remove old entry and reconnect
|
|
130
|
+
this.servers.delete(name);
|
|
131
|
+
await this.connectServer(name, entry.config);
|
|
132
|
+
// Preserve failure count
|
|
133
|
+
const newEntry = this.servers.get(name);
|
|
134
|
+
if (newEntry) {
|
|
135
|
+
newEntry.consecutiveFailures = entry.consecutiveFailures;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
exports.McpManager = McpManager;
|
|
140
|
+
let _sharedManager = null;
|
|
141
|
+
let _sharedManagerPromise = null;
|
|
142
|
+
/**
|
|
143
|
+
* Get or create a shared McpManager instance from the agent's config.
|
|
144
|
+
* Returns null if no mcpServers are configured.
|
|
145
|
+
* Safe to call from multiple senses — will only create one instance.
|
|
146
|
+
*/
|
|
147
|
+
async function getSharedMcpManager() {
|
|
148
|
+
if (_sharedManager)
|
|
149
|
+
return _sharedManager;
|
|
150
|
+
/* v8 ignore next -- race guard: deduplicates concurrent initialization calls @preserve */
|
|
151
|
+
if (_sharedManagerPromise)
|
|
152
|
+
return _sharedManagerPromise;
|
|
153
|
+
_sharedManagerPromise = (async () => {
|
|
154
|
+
try {
|
|
155
|
+
const config = (0, identity_1.loadAgentConfig)();
|
|
156
|
+
const servers = config.mcpServers;
|
|
157
|
+
if (!servers || Object.keys(servers).length === 0)
|
|
158
|
+
return null;
|
|
159
|
+
const manager = new McpManager();
|
|
160
|
+
await manager.start(servers);
|
|
161
|
+
_sharedManager = manager;
|
|
162
|
+
return manager;
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
(0, runtime_1.emitNervesEvent)({
|
|
166
|
+
level: "error",
|
|
167
|
+
event: "mcp.manager_start",
|
|
168
|
+
component: "repertoire",
|
|
169
|
+
message: "failed to initialize shared MCP manager",
|
|
170
|
+
/* v8 ignore next -- both branches tested: Error in wiring test, non-Error is defensive @preserve */
|
|
171
|
+
meta: { reason: error instanceof Error ? error.message : String(error) },
|
|
172
|
+
});
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
_sharedManagerPromise = null;
|
|
177
|
+
}
|
|
178
|
+
})();
|
|
179
|
+
return _sharedManagerPromise;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Shut down the shared MCP manager and clear the singleton.
|
|
183
|
+
* Called during daemon/agent shutdown.
|
|
184
|
+
*/
|
|
185
|
+
function shutdownSharedMcpManager() {
|
|
186
|
+
if (_sharedManager) {
|
|
187
|
+
_sharedManager.shutdown();
|
|
188
|
+
_sharedManager = null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** Reset for testing only */
|
|
192
|
+
function resetSharedMcpManager() {
|
|
193
|
+
_sharedManager = null;
|
|
194
|
+
_sharedManagerPromise = null;
|
|
195
|
+
}
|
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.finalAnswerTool = exports.tools = exports.baseToolDefinitions = exports.editFileReadTracker = void 0;
|
|
36
|
+
exports.finalAnswerTool = exports.noResponseTool = exports.tools = exports.baseToolDefinitions = exports.editFileReadTracker = void 0;
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const fg = __importStar(require("fast-glob"));
|
|
39
39
|
const child_process_1 = require("child_process");
|
|
@@ -1075,6 +1075,19 @@ exports.baseToolDefinitions = [
|
|
|
1075
1075
|
...tools_1.codingToolDefinitions,
|
|
1076
1076
|
];
|
|
1077
1077
|
exports.tools = exports.baseToolDefinitions.map((d) => d.tool);
|
|
1078
|
+
exports.noResponseTool = {
|
|
1079
|
+
type: "function",
|
|
1080
|
+
function: {
|
|
1081
|
+
name: "no_response",
|
|
1082
|
+
description: "stay silent in this group chat — the moment doesn't call for a response. must be the only tool call in the turn.",
|
|
1083
|
+
parameters: {
|
|
1084
|
+
type: "object",
|
|
1085
|
+
properties: {
|
|
1086
|
+
reason: { type: "string", description: "brief reason for staying silent (for logging)" },
|
|
1087
|
+
},
|
|
1088
|
+
},
|
|
1089
|
+
},
|
|
1090
|
+
};
|
|
1078
1091
|
exports.finalAnswerTool = {
|
|
1079
1092
|
type: "function",
|
|
1080
1093
|
function: {
|
package/dist/repertoire/tools.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.finalAnswerTool = exports.tools = void 0;
|
|
3
|
+
exports.noResponseTool = exports.finalAnswerTool = exports.tools = void 0;
|
|
4
4
|
exports.getToolsForChannel = getToolsForChannel;
|
|
5
5
|
exports.isConfirmationRequired = isConfirmationRequired;
|
|
6
6
|
exports.execTool = execTool;
|
|
@@ -25,6 +25,7 @@ function safeGetAgentRoot() {
|
|
|
25
25
|
var tools_base_2 = require("./tools-base");
|
|
26
26
|
Object.defineProperty(exports, "tools", { enumerable: true, get: function () { return tools_base_2.tools; } });
|
|
27
27
|
Object.defineProperty(exports, "finalAnswerTool", { enumerable: true, get: function () { return tools_base_2.finalAnswerTool; } });
|
|
28
|
+
Object.defineProperty(exports, "noResponseTool", { enumerable: true, get: function () { return tools_base_2.noResponseTool; } });
|
|
28
29
|
// All tool definitions in a single registry
|
|
29
30
|
const allDefinitions = [...tools_base_1.baseToolDefinitions, ...tools_bluebubbles_1.bluebubblesToolDefinitions, ...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions];
|
|
30
31
|
function baseToolsForCapabilities() {
|
|
@@ -55,6 +55,7 @@ const types_1 = require("../mind/friends/types");
|
|
|
55
55
|
const channel_1 = require("../mind/friends/channel");
|
|
56
56
|
const pending_1 = require("../mind/pending");
|
|
57
57
|
const prompt_1 = require("../mind/prompt");
|
|
58
|
+
const mcp_manager_1 = require("../repertoire/mcp-manager");
|
|
58
59
|
const phrases_1 = require("../mind/phrases");
|
|
59
60
|
const runtime_1 = require("../nerves/runtime");
|
|
60
61
|
const bluebubbles_model_1 = require("./bluebubbles-model");
|
|
@@ -557,9 +558,10 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
|
|
|
557
558
|
return (0, turn_coordinator_1.withSharedTurnLock)("bluebubbles", sessPath, async () => {
|
|
558
559
|
// Pre-load session inside the turn lock so same-chat deliveries cannot race on stale trunk state.
|
|
559
560
|
const existing = resolvedDeps.loadSession(sessPath);
|
|
561
|
+
const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
|
|
560
562
|
const sessionMessages = existing?.messages && existing.messages.length > 0
|
|
561
563
|
? existing.messages
|
|
562
|
-
: [{ role: "system", content: await resolvedDeps.buildSystem("bluebubbles",
|
|
564
|
+
: [{ role: "system", content: await resolvedDeps.buildSystem("bluebubbles", { mcpManager }, context) }];
|
|
563
565
|
if (event.kind === "message") {
|
|
564
566
|
const agentName = resolvedDeps.getAgentName();
|
|
565
567
|
if ((0, bluebubbles_inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, event.chat.sessionKey, event.messageGuid)) {
|
|
@@ -661,6 +663,7 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
|
|
|
661
663
|
postTurn: resolvedDeps.postTurn,
|
|
662
664
|
accumulateFriendTokens: resolvedDeps.accumulateFriendTokens,
|
|
663
665
|
signal: controller.signal,
|
|
666
|
+
runAgentOptions: { mcpManager },
|
|
664
667
|
});
|
|
665
668
|
// ── Handle gate result ────────────────────────────────────────
|
|
666
669
|
if (!result.gateResult.allowed) {
|
package/dist/senses/cli.js
CHANGED
|
@@ -55,6 +55,7 @@ const context_1 = require("../mind/context");
|
|
|
55
55
|
const pending_1 = require("../mind/pending");
|
|
56
56
|
const commands_1 = require("./commands");
|
|
57
57
|
const identity_1 = require("../heart/identity");
|
|
58
|
+
const mcp_manager_1 = require("../repertoire/mcp-manager");
|
|
58
59
|
const nerves_1 = require("../nerves");
|
|
59
60
|
const store_file_1 = require("../mind/friends/store-file");
|
|
60
61
|
const resolver_1 = require("../mind/friends/resolver");
|
|
@@ -734,9 +735,10 @@ async function main(agentName, options) {
|
|
|
734
735
|
// Load existing session or start fresh
|
|
735
736
|
const existing = (0, context_1.loadSession)(sessPath);
|
|
736
737
|
let sessionState = existing?.state;
|
|
738
|
+
const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
|
|
737
739
|
const sessionMessages = existing?.messages && existing.messages.length > 0
|
|
738
740
|
? existing.messages
|
|
739
|
-
: [{ role: "system", content: await (0, prompt_1.buildSystem)("cli",
|
|
741
|
+
: [{ role: "system", content: await (0, prompt_1.buildSystem)("cli", { mcpManager }, resolvedContext) }];
|
|
740
742
|
// Per-turn pipeline input: CLI capabilities and pending dir
|
|
741
743
|
const cliCapabilities = (0, channel_1.getChannelCapabilities)("cli");
|
|
742
744
|
const currentAgentName = (0, identity_1.getAgentName)();
|
|
@@ -784,6 +786,7 @@ async function main(agentName, options) {
|
|
|
784
786
|
runAgentOptions: {
|
|
785
787
|
toolChoiceRequired: (0, commands_1.getToolChoiceRequired)(),
|
|
786
788
|
traceId: (0, nerves_1.createTraceId)(),
|
|
789
|
+
mcpManager,
|
|
787
790
|
},
|
|
788
791
|
});
|
|
789
792
|
// Handle gate rejection: display auto-reply if present
|
|
@@ -49,6 +49,7 @@ const core_1 = require("../heart/core");
|
|
|
49
49
|
const identity_1 = require("../heart/identity");
|
|
50
50
|
const context_1 = require("../mind/context");
|
|
51
51
|
const prompt_1 = require("../mind/prompt");
|
|
52
|
+
const mcp_manager_1 = require("../repertoire/mcp-manager");
|
|
52
53
|
const bundle_manifest_1 = require("../mind/bundle-manifest");
|
|
53
54
|
const pending_1 = require("../mind/pending");
|
|
54
55
|
const channel_1 = require("../mind/friends/channel");
|
|
@@ -401,13 +402,14 @@ async function runInnerDialogTurn(options) {
|
|
|
401
402
|
const pendingDir = (0, pending_1.getInnerDialogPendingDir)(agentName);
|
|
402
403
|
const selfFriend = createSelfFriend(agentName);
|
|
403
404
|
const selfContext = { friend: selfFriend, channel: innerCapabilities };
|
|
405
|
+
const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
|
|
404
406
|
const sessionLoader = {
|
|
405
407
|
loadOrCreate: async () => {
|
|
406
408
|
if (existingMessages.length > 0) {
|
|
407
409
|
return { messages: existingMessages, sessionPath: sessionFilePath };
|
|
408
410
|
}
|
|
409
411
|
// Fresh session: build system prompt
|
|
410
|
-
const systemPrompt = await (0, prompt_1.buildSystem)("inner", { toolChoiceRequired: true });
|
|
412
|
+
const systemPrompt = await (0, prompt_1.buildSystem)("inner", { toolChoiceRequired: true, mcpManager });
|
|
411
413
|
return {
|
|
412
414
|
messages: [{ role: "system", content: systemPrompt }],
|
|
413
415
|
sessionPath: sessionFilePath,
|
|
@@ -438,6 +440,7 @@ async function runInnerDialogTurn(options) {
|
|
|
438
440
|
traceId,
|
|
439
441
|
toolChoiceRequired: true,
|
|
440
442
|
skipConfirmation: true,
|
|
443
|
+
mcpManager,
|
|
441
444
|
},
|
|
442
445
|
});
|
|
443
446
|
await routeDelegatedCompletion(agentRoot, agentName, result.completion, result.drainedPending, now().getTime());
|
package/dist/senses/pipeline.js
CHANGED
|
@@ -248,7 +248,7 @@ async function handleInboundTurn(input) {
|
|
|
248
248
|
...(mustResolveBeforeHandoff ? { mustResolveBeforeHandoff: true } : {}),
|
|
249
249
|
...(typeof lastFriendActivityAt === "string" ? { lastFriendActivityAt } : {}),
|
|
250
250
|
};
|
|
251
|
-
const nextState = result.outcome === "complete" || result.outcome === "blocked" || result.outcome === "superseded"
|
|
251
|
+
const nextState = result.outcome === "complete" || result.outcome === "blocked" || result.outcome === "superseded" || result.outcome === "no_response"
|
|
252
252
|
? (typeof lastFriendActivityAt === "string"
|
|
253
253
|
? { lastFriendActivityAt }
|
|
254
254
|
: undefined)
|
package/dist/senses/teams.js
CHANGED
|
@@ -64,6 +64,7 @@ const resolver_1 = require("../mind/friends/resolver");
|
|
|
64
64
|
const tokens_1 = require("../mind/friends/tokens");
|
|
65
65
|
const turn_coordinator_1 = require("../heart/turn-coordinator");
|
|
66
66
|
const identity_1 = require("../heart/identity");
|
|
67
|
+
const mcp_manager_1 = require("../repertoire/mcp-manager");
|
|
67
68
|
const progress_story_1 = require("../heart/progress-story");
|
|
68
69
|
const http = __importStar(require("http"));
|
|
69
70
|
const path = __importStar(require("path"));
|
|
@@ -531,12 +532,14 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
531
532
|
botApi: teamsContext.botApi,
|
|
532
533
|
} : {};
|
|
533
534
|
let currentText = text;
|
|
535
|
+
const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
|
|
534
536
|
while (true) {
|
|
535
537
|
let drainedSteeringFollowUps = [];
|
|
536
538
|
// Build runAgentOptions with Teams-specific fields
|
|
537
539
|
const agentOptions = {
|
|
538
540
|
traceId,
|
|
539
541
|
toolContext: teamsToolContext,
|
|
542
|
+
mcpManager,
|
|
540
543
|
drainSteeringFollowUps: () => {
|
|
541
544
|
drainedSteeringFollowUps = _turnCoordinator.drainFollowUps(turnKey)
|
|
542
545
|
.map(({ text: followUpText, effect }) => ({ text: followUpText, effect }));
|
|
@@ -559,7 +562,7 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
|
|
|
559
562
|
const existing = (0, context_1.loadSession)(sessPath);
|
|
560
563
|
const messages = existing?.messages && existing.messages.length > 0
|
|
561
564
|
? existing.messages
|
|
562
|
-
: [{ role: "system", content: await (0, prompt_1.buildSystem)("teams",
|
|
565
|
+
: [{ role: "system", content: await (0, prompt_1.buildSystem)("teams", { mcpManager }, resolvedContext) }];
|
|
563
566
|
(0, core_1.repairOrphanedToolCalls)(messages);
|
|
564
567
|
return { messages, sessionPath: sessPath, state: existing?.state };
|
|
565
568
|
},
|