@ouro.bot/cli 0.1.0-alpha.803 → 0.1.0-alpha.805
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 +14 -0
- package/deploy/unraid/README.txt +38 -18
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/core.js +14 -11
- package/dist/heart/daemon/cli-defaults.js +22 -4
- package/dist/heart/daemon/cli-exec.js +95 -6
- package/dist/heart/daemon/cli-help.js +6 -0
- package/dist/heart/daemon/cli-parse.js +52 -0
- package/dist/heart/daemon/daemon-bootstrap-startup.js +8 -0
- package/dist/heart/daemon/daemon-entry.js +2 -1
- package/dist/heart/daemon/daemon.js +94 -47
- package/dist/heart/daemon/sense-manager.js +1 -1
- package/dist/heart/frontend-approval-runtime.js +432 -0
- package/dist/heart/frontend-journal.js +215 -0
- package/dist/heart/frontend-session-service.js +237 -0
- package/dist/heart/frontend-socket-client.js +154 -0
- package/dist/heart/frontend-socket.js +400 -0
- package/dist/heart/mail-import-discovery.js +3 -0
- package/dist/heart/turn-context.js +1 -0
- package/dist/heart/turn-execution-lease.js +30 -0
- package/dist/mind/prompt.js +4 -1
- package/dist/repertoire/mcp-manager.js +82 -17
- package/dist/senses/acp-server.js +386 -0
- package/dist/senses/pipeline.js +5 -0
- package/dist/senses/shared-turn.js +369 -257
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.McpManager = void 0;
|
|
4
4
|
exports.getSharedMcpManager = getSharedMcpManager;
|
|
5
|
+
exports.releaseRuntimeMcpServers = releaseRuntimeMcpServers;
|
|
5
6
|
exports.shutdownSharedMcpManager = shutdownSharedMcpManager;
|
|
6
7
|
exports.resetSharedMcpManager = resetSharedMcpManager;
|
|
7
8
|
const mcp_client_1 = require("./mcp-client");
|
|
@@ -57,10 +58,20 @@ function buildMergedServerConfig(runtimeServers) {
|
|
|
57
58
|
}
|
|
58
59
|
return { mergedServers, pluginOrigins };
|
|
59
60
|
}
|
|
61
|
+
function sameMcpServerConfig(left, right) {
|
|
62
|
+
const leftEnv = Object.entries(left.env ?? {}).sort();
|
|
63
|
+
const rightEnv = Object.entries(right.env ?? {}).sort();
|
|
64
|
+
return left.command === right.command
|
|
65
|
+
&& JSON.stringify(left.args ?? []) === JSON.stringify(right.args ?? [])
|
|
66
|
+
&& JSON.stringify(leftEnv) === JSON.stringify(rightEnv)
|
|
67
|
+
&& (left.cwd ?? "") === (right.cwd ?? "");
|
|
68
|
+
}
|
|
60
69
|
class McpManager {
|
|
61
70
|
servers = new Map();
|
|
71
|
+
desiredGenerations = new Map();
|
|
72
|
+
nextGeneration = 0;
|
|
62
73
|
shuttingDown = false;
|
|
63
|
-
async start(servers, pluginOrigins = {}) {
|
|
74
|
+
async start(servers, pluginOrigins = {}, ownerAgent) {
|
|
64
75
|
(0, runtime_1.emitNervesEvent)({
|
|
65
76
|
event: "mcp.manager_start",
|
|
66
77
|
component: "repertoire",
|
|
@@ -72,7 +83,7 @@ class McpManager {
|
|
|
72
83
|
});
|
|
73
84
|
const entries = Object.entries(servers);
|
|
74
85
|
for (const [name, config] of entries) {
|
|
75
|
-
await this.connectServer(name, config, pluginOrigins[name]);
|
|
86
|
+
await this.connectServer(name, config, pluginOrigins[name], ownerAgent);
|
|
76
87
|
}
|
|
77
88
|
}
|
|
78
89
|
listAllTools() {
|
|
@@ -150,18 +161,34 @@ class McpManager {
|
|
|
150
161
|
async reconcile(runtimeServers) {
|
|
151
162
|
try {
|
|
152
163
|
const { mergedServers, pluginOrigins } = buildMergedServerConfig(runtimeServers);
|
|
164
|
+
const ownerAgent = (0, identity_1.getAgentName)();
|
|
153
165
|
const currentNames = new Set(this.servers.keys());
|
|
154
166
|
const desiredNames = new Set(Object.keys(mergedServers));
|
|
155
167
|
// Connect new servers
|
|
156
168
|
for (const [name, cfg] of Object.entries(mergedServers)) {
|
|
157
|
-
|
|
169
|
+
const current = this.servers.get(name);
|
|
170
|
+
const pluginId = pluginOrigins[name];
|
|
171
|
+
if (!current) {
|
|
158
172
|
(0, runtime_1.emitNervesEvent)({
|
|
159
173
|
event: "mcp.server_added",
|
|
160
174
|
component: "repertoire",
|
|
161
175
|
message: `connecting new MCP server: ${name}`,
|
|
162
176
|
meta: { server: name, command: cfg.command },
|
|
163
177
|
});
|
|
164
|
-
await this.connectServer(name, cfg,
|
|
178
|
+
await this.connectServer(name, cfg, pluginId, ownerAgent);
|
|
179
|
+
}
|
|
180
|
+
else if (!sameMcpServerConfig(current.config, cfg)
|
|
181
|
+
|| current.pluginId !== pluginId
|
|
182
|
+
|| current.ownerAgent !== ownerAgent) {
|
|
183
|
+
(0, runtime_1.emitNervesEvent)({
|
|
184
|
+
event: "mcp.server_changed",
|
|
185
|
+
component: "repertoire",
|
|
186
|
+
message: `reconnecting changed MCP server: ${name}`,
|
|
187
|
+
meta: { server: name, command: cfg.command },
|
|
188
|
+
});
|
|
189
|
+
current.client.shutdown();
|
|
190
|
+
this.servers.delete(name);
|
|
191
|
+
await this.connectServer(name, cfg, pluginId, ownerAgent);
|
|
165
192
|
}
|
|
166
193
|
}
|
|
167
194
|
// Disconnect removed servers
|
|
@@ -178,8 +205,10 @@ class McpManager {
|
|
|
178
205
|
if (entry)
|
|
179
206
|
entry.client.shutdown();
|
|
180
207
|
this.servers.delete(name);
|
|
208
|
+
this.desiredGenerations.delete(name);
|
|
181
209
|
}
|
|
182
210
|
}
|
|
211
|
+
return true;
|
|
183
212
|
}
|
|
184
213
|
catch (error) {
|
|
185
214
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -189,6 +218,8 @@ class McpManager {
|
|
|
189
218
|
message: "failed to reconcile MCP servers",
|
|
190
219
|
meta: { reason: error instanceof Error ? error.message : String(error) },
|
|
191
220
|
});
|
|
221
|
+
this.shutdown();
|
|
222
|
+
return false;
|
|
192
223
|
}
|
|
193
224
|
}
|
|
194
225
|
shutdown() {
|
|
@@ -205,12 +236,13 @@ class McpManager {
|
|
|
205
236
|
entry.client.shutdown();
|
|
206
237
|
}
|
|
207
238
|
this.servers.clear();
|
|
239
|
+
this.desiredGenerations.clear();
|
|
208
240
|
}
|
|
209
241
|
/**
|
|
210
242
|
* Resolve `vault:DOMAIN/FIELD` references in server env config.
|
|
211
243
|
* Returns resolved env or throws with a descriptive error.
|
|
212
244
|
*/
|
|
213
|
-
async resolveVaultEnv(_serverName, env) {
|
|
245
|
+
async resolveVaultEnv(_serverName, env, ownerAgent) {
|
|
214
246
|
const resolved = { ...env };
|
|
215
247
|
// Short-circuit: only spin up a credential store if at least one env value
|
|
216
248
|
// actually requests vault resolution. Plugin MCP servers commonly ship with
|
|
@@ -219,7 +251,7 @@ class McpManager {
|
|
|
219
251
|
const hasVaultRef = Object.values(resolved).some((v) => /^vault:/.test(v));
|
|
220
252
|
if (!hasVaultRef)
|
|
221
253
|
return resolved;
|
|
222
|
-
const store = (0, credential_access_1.getCredentialStore)();
|
|
254
|
+
const store = (0, credential_access_1.getCredentialStore)(ownerAgent);
|
|
223
255
|
for (const [key, value] of Object.entries(resolved)) {
|
|
224
256
|
const match = value.match(/^vault:([^/]+)\/(.+)$/);
|
|
225
257
|
if (!match)
|
|
@@ -244,12 +276,14 @@ class McpManager {
|
|
|
244
276
|
}
|
|
245
277
|
return resolved;
|
|
246
278
|
}
|
|
247
|
-
async connectServer(name, config, pluginId) {
|
|
279
|
+
async connectServer(name, config, pluginId, ownerAgent) {
|
|
280
|
+
const generation = ++this.nextGeneration;
|
|
281
|
+
this.desiredGenerations.set(name, generation);
|
|
248
282
|
// Resolve vault: references in env before spawning
|
|
249
283
|
let resolvedConfig = config;
|
|
250
284
|
if (config.env) {
|
|
251
285
|
try {
|
|
252
|
-
const resolvedEnv = await this.resolveVaultEnv(name, config.env);
|
|
286
|
+
const resolvedEnv = await this.resolveVaultEnv(name, config.env, ownerAgent);
|
|
253
287
|
resolvedConfig = { ...config, env: resolvedEnv };
|
|
254
288
|
}
|
|
255
289
|
catch (err) {
|
|
@@ -262,9 +296,14 @@ class McpManager {
|
|
|
262
296
|
message: `skipping MCP server "${name}": ${reason}`,
|
|
263
297
|
meta: { server: name, reason },
|
|
264
298
|
});
|
|
299
|
+
if (this.desiredGenerations.get(name) === generation) {
|
|
300
|
+
this.desiredGenerations.delete(name);
|
|
301
|
+
}
|
|
265
302
|
return; // Skip this server, continue to next
|
|
266
303
|
}
|
|
267
304
|
}
|
|
305
|
+
if (this.shuttingDown || this.desiredGenerations.get(name) !== generation)
|
|
306
|
+
return;
|
|
268
307
|
const client = new mcp_client_1.McpClient(resolvedConfig);
|
|
269
308
|
const entry = {
|
|
270
309
|
name,
|
|
@@ -273,16 +312,26 @@ class McpManager {
|
|
|
273
312
|
cachedTools: [],
|
|
274
313
|
consecutiveFailures: 0,
|
|
275
314
|
pluginId,
|
|
315
|
+
ownerAgent,
|
|
316
|
+
generation,
|
|
276
317
|
};
|
|
277
318
|
this.servers.set(name, entry);
|
|
278
319
|
client.onClose(() => {
|
|
279
320
|
if (this.shuttingDown)
|
|
280
321
|
return;
|
|
281
|
-
this.
|
|
322
|
+
if (this.servers.get(name)?.client !== client)
|
|
323
|
+
return;
|
|
324
|
+
this.handleServerCrash(name, client);
|
|
282
325
|
});
|
|
283
326
|
try {
|
|
284
327
|
await client.connect();
|
|
285
328
|
const tools = await client.listTools();
|
|
329
|
+
if (this.shuttingDown
|
|
330
|
+
|| this.desiredGenerations.get(name) !== generation
|
|
331
|
+
|| this.servers.get(name)?.client !== client) {
|
|
332
|
+
client.shutdown();
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
286
335
|
entry.cachedTools = tools;
|
|
287
336
|
entry.consecutiveFailures = 0;
|
|
288
337
|
}
|
|
@@ -302,10 +351,10 @@ class McpManager {
|
|
|
302
351
|
});
|
|
303
352
|
}
|
|
304
353
|
}
|
|
305
|
-
handleServerCrash(name) {
|
|
354
|
+
handleServerCrash(name, crashedClient) {
|
|
306
355
|
const entry = this.servers.get(name);
|
|
307
|
-
/* v8 ignore next -- defensive: entry removed between close event and handler @preserve */
|
|
308
|
-
if (!entry)
|
|
356
|
+
/* v8 ignore next -- defensive: entry removed or replaced between close event and handler @preserve */
|
|
357
|
+
if (!entry || entry.client !== crashedClient)
|
|
309
358
|
return;
|
|
310
359
|
entry.consecutiveFailures++;
|
|
311
360
|
if (entry.consecutiveFailures > MAX_RESTART_RETRIES) {
|
|
@@ -329,21 +378,26 @@ class McpManager {
|
|
|
329
378
|
setTimeout(() => {
|
|
330
379
|
if (this.shuttingDown)
|
|
331
380
|
return;
|
|
332
|
-
this.
|
|
381
|
+
if (this.servers.get(name)?.client !== crashedClient)
|
|
382
|
+
return;
|
|
383
|
+
this.restartServer(name, crashedClient).catch(() => {
|
|
333
384
|
// Error handling is inside restartServer
|
|
334
385
|
});
|
|
335
386
|
}, RESTART_DELAY_MS);
|
|
336
387
|
/* v8 ignore stop */
|
|
337
388
|
}
|
|
338
389
|
/* v8 ignore start -- called from timer callback: covered by mcp-manager.test.ts via fake timers but v8 can't trace @preserve */
|
|
339
|
-
async restartServer(name) {
|
|
390
|
+
async restartServer(name, expectedClient) {
|
|
340
391
|
const entry = this.servers.get(name);
|
|
341
392
|
if (!entry)
|
|
342
393
|
return;
|
|
394
|
+
if (expectedClient && entry.client !== expectedClient)
|
|
395
|
+
return;
|
|
343
396
|
// Remove old entry and reconnect
|
|
344
397
|
this.servers.delete(name);
|
|
398
|
+
this.desiredGenerations.delete(name);
|
|
345
399
|
entry.client.shutdown();
|
|
346
|
-
await this.connectServer(name, entry.config);
|
|
400
|
+
await this.connectServer(name, entry.config, entry.pluginId, entry.ownerAgent);
|
|
347
401
|
// Preserve failure count
|
|
348
402
|
const newEntry = this.servers.get(name);
|
|
349
403
|
if (newEntry) {
|
|
@@ -383,7 +437,11 @@ async function getSharedMcpManager(options) {
|
|
|
383
437
|
// AND this turn's runtime overrides. Passing runtimeServers per-call is what
|
|
384
438
|
// scopes the runtime MCP to the active agent's turn.
|
|
385
439
|
if (_sharedManager) {
|
|
386
|
-
await _sharedManager.reconcile(runtimeServers);
|
|
440
|
+
const reconciled = await _sharedManager.reconcile(runtimeServers);
|
|
441
|
+
if (!reconciled) {
|
|
442
|
+
_sharedManager = null;
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
387
445
|
return _sharedManager;
|
|
388
446
|
}
|
|
389
447
|
/* v8 ignore next -- race guard: deduplicates concurrent initialization calls @preserve */
|
|
@@ -396,7 +454,7 @@ async function getSharedMcpManager(options) {
|
|
|
396
454
|
if (Object.keys(mergedServers).length === 0)
|
|
397
455
|
return null;
|
|
398
456
|
const manager = new McpManager();
|
|
399
|
-
await manager.start(mergedServers, pluginOrigins);
|
|
457
|
+
await manager.start(mergedServers, pluginOrigins, (0, identity_1.getAgentName)());
|
|
400
458
|
_sharedManager = manager;
|
|
401
459
|
return manager;
|
|
402
460
|
}
|
|
@@ -417,6 +475,13 @@ async function getSharedMcpManager(options) {
|
|
|
417
475
|
})();
|
|
418
476
|
return _sharedManagerPromise;
|
|
419
477
|
}
|
|
478
|
+
async function releaseRuntimeMcpServers() {
|
|
479
|
+
if (!_sharedManager)
|
|
480
|
+
return;
|
|
481
|
+
if (!await _sharedManager.reconcile()) {
|
|
482
|
+
_sharedManager = null;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
420
485
|
/**
|
|
421
486
|
* Shut down the shared MCP manager and clear the singleton.
|
|
422
487
|
* Called during daemon/agent shutdown.
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.createAcpServer = createAcpServer;
|
|
37
|
+
const node_crypto_1 = require("node:crypto");
|
|
38
|
+
const readline = __importStar(require("node:readline"));
|
|
39
|
+
const frontend_socket_client_1 = require("../heart/frontend-socket-client");
|
|
40
|
+
const runtime_1 = require("../nerves/runtime");
|
|
41
|
+
const SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
42
|
+
class AcpProtocolError extends Error {
|
|
43
|
+
code;
|
|
44
|
+
constructor(code, message) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.code = code;
|
|
47
|
+
this.name = "AcpProtocolError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function createAcpServer(options) {
|
|
51
|
+
const sessions = new Set();
|
|
52
|
+
const activePrompts = new Map();
|
|
53
|
+
const pendingOutbound = new Map();
|
|
54
|
+
const createClient = options.createFrontendClient ?? (async () => new frontend_socket_client_1.SocketFrontendClient(options.frontendSocketPath));
|
|
55
|
+
let clientPromise = null;
|
|
56
|
+
let frontendClient = null;
|
|
57
|
+
let removeEventListener = null;
|
|
58
|
+
let removeCloseListener = null;
|
|
59
|
+
let lines = null;
|
|
60
|
+
let running = false;
|
|
61
|
+
function write(message) {
|
|
62
|
+
options.stdout.write(`${JSON.stringify(message)}\n`);
|
|
63
|
+
}
|
|
64
|
+
function result(id, value) {
|
|
65
|
+
write({ jsonrpc: "2.0", id, result: value });
|
|
66
|
+
}
|
|
67
|
+
function error(id, code, message) {
|
|
68
|
+
write({ jsonrpc: "2.0", id, error: { code, message } });
|
|
69
|
+
}
|
|
70
|
+
async function client() {
|
|
71
|
+
if (!clientPromise) {
|
|
72
|
+
clientPromise = createClient().then((value) => {
|
|
73
|
+
frontendClient = value;
|
|
74
|
+
removeEventListener = value.onEvent(handleFrontendEvent);
|
|
75
|
+
removeCloseListener = value.onClose((error) => {
|
|
76
|
+
const failure = error ?? new Error("frontend provider disconnected");
|
|
77
|
+
for (const prompt of activePrompts.values())
|
|
78
|
+
prompt.reject(failure);
|
|
79
|
+
pendingOutbound.clear();
|
|
80
|
+
});
|
|
81
|
+
return value;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return clientPromise;
|
|
85
|
+
}
|
|
86
|
+
async function subscribe(sessionId) {
|
|
87
|
+
await (await client()).request("session.subscribe", { sessionKey: sessionId });
|
|
88
|
+
sessions.add(sessionId);
|
|
89
|
+
}
|
|
90
|
+
function sessionUpdate(sessionId, update) {
|
|
91
|
+
write({ jsonrpc: "2.0", method: "session/update", params: { sessionId, update } });
|
|
92
|
+
}
|
|
93
|
+
function permissionRequest(sessionId, event) {
|
|
94
|
+
const requestId = typeof event.requestId === "string" ? event.requestId : "";
|
|
95
|
+
if (!requestId || [...pendingOutbound.values()].some((pending) => pending.requestId === requestId))
|
|
96
|
+
return;
|
|
97
|
+
const rpcId = `ouro:${(0, node_crypto_1.randomUUID)()}`;
|
|
98
|
+
pendingOutbound.set(rpcId, { sessionId, requestId });
|
|
99
|
+
write({
|
|
100
|
+
jsonrpc: "2.0",
|
|
101
|
+
id: rpcId,
|
|
102
|
+
method: "session/request_permission",
|
|
103
|
+
params: {
|
|
104
|
+
sessionId,
|
|
105
|
+
toolCall: {
|
|
106
|
+
toolCallId: String(event.toolCallId ?? requestId),
|
|
107
|
+
title: String(event.title ?? "Permission requested"),
|
|
108
|
+
},
|
|
109
|
+
options: Array.isArray(event.options) ? event.options : [],
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function replayEvent(sessionId, event) {
|
|
114
|
+
const data = event.data ?? {};
|
|
115
|
+
if (event.type === "user_message" && typeof data.text === "string") {
|
|
116
|
+
sessionUpdate(sessionId, {
|
|
117
|
+
sessionUpdate: "user_message_chunk",
|
|
118
|
+
messageId: `${event.turnId}:user`,
|
|
119
|
+
content: { type: "text", text: data.text },
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
else if (event.type === "assistant_delivery" && typeof data.text === "string") {
|
|
123
|
+
sessionUpdate(sessionId, {
|
|
124
|
+
sessionUpdate: "agent_message_chunk",
|
|
125
|
+
messageId: `${event.turnId}:agent`,
|
|
126
|
+
content: { type: "text", text: data.text },
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
else if (event.type === "tool_started") {
|
|
130
|
+
sessionUpdate(sessionId, {
|
|
131
|
+
sessionUpdate: "tool_call",
|
|
132
|
+
toolCallId: `${event.turnId}:${String(data.name ?? "tool")}`,
|
|
133
|
+
title: String(data.name ?? "tool"),
|
|
134
|
+
status: "pending",
|
|
135
|
+
rawInput: data.args ?? {},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
else if (event.type === "tool_completed") {
|
|
139
|
+
sessionUpdate(sessionId, {
|
|
140
|
+
sessionUpdate: "tool_call_update",
|
|
141
|
+
toolCallId: `${event.turnId}:${String(data.name ?? "tool")}`,
|
|
142
|
+
status: data.success === false ? "failed" : "completed",
|
|
143
|
+
content: [{ type: "content", content: { type: "text", text: String(data.summary ?? "") } }],
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
else if (event.type === "structured_output") {
|
|
147
|
+
const output = data.output;
|
|
148
|
+
sessionUpdate(sessionId, {
|
|
149
|
+
sessionUpdate: "plan",
|
|
150
|
+
entries: (output?.items ?? []).flatMap((item) => typeof item.text === "string" && item.text.trim()
|
|
151
|
+
? [{ content: item.text.trim(), status: "pending" }]
|
|
152
|
+
: []),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function handleFrontendEvent(event) {
|
|
157
|
+
const sessionId = typeof event.sessionKey === "string" ? event.sessionKey : "";
|
|
158
|
+
if (!sessions.has(sessionId))
|
|
159
|
+
return;
|
|
160
|
+
const turnId = typeof event.turnId === "string" ? event.turnId : "";
|
|
161
|
+
const active = activePrompts.get(sessionId);
|
|
162
|
+
if (event.event === "text.delta" && typeof event.text === "string") {
|
|
163
|
+
sessionUpdate(sessionId, {
|
|
164
|
+
sessionUpdate: "agent_message_chunk",
|
|
165
|
+
messageId: `${turnId}:agent`,
|
|
166
|
+
content: { type: "text", text: event.text },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
else if (event.event === "reasoning.delta" && typeof event.text === "string") {
|
|
170
|
+
sessionUpdate(sessionId, {
|
|
171
|
+
sessionUpdate: "agent_thought_chunk",
|
|
172
|
+
messageId: `${turnId}:thought`,
|
|
173
|
+
content: { type: "text", text: event.text },
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
else if (event.event === "tool.started") {
|
|
177
|
+
replayEvent(sessionId, { type: "tool_started", turnId, data: event });
|
|
178
|
+
}
|
|
179
|
+
else if (event.event === "tool.completed") {
|
|
180
|
+
replayEvent(sessionId, { type: "tool_completed", turnId, data: event });
|
|
181
|
+
}
|
|
182
|
+
else if (event.event === "structured.output") {
|
|
183
|
+
replayEvent(sessionId, { type: "structured_output", turnId, data: event });
|
|
184
|
+
}
|
|
185
|
+
else if (event.event === "permission.requested") {
|
|
186
|
+
const requestId = typeof event.requestId === "string" ? event.requestId : "";
|
|
187
|
+
if (!requestId || !active || active.turnId !== turnId)
|
|
188
|
+
return;
|
|
189
|
+
permissionRequest(sessionId, event);
|
|
190
|
+
}
|
|
191
|
+
else if (active && active.turnId === turnId && event.event === "turn.completed") {
|
|
192
|
+
active.resolve("end_turn");
|
|
193
|
+
}
|
|
194
|
+
else if (active && active.turnId === turnId && event.event === "turn.cancelled") {
|
|
195
|
+
active.resolve("cancelled");
|
|
196
|
+
}
|
|
197
|
+
else if (active && active.turnId === turnId && event.event === "turn.failed") {
|
|
198
|
+
active.reject(new Error(String(event.error ?? "frontend turn failed")));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function params(value) {
|
|
202
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
203
|
+
throw new AcpProtocolError(-32602, "params must be an object");
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
function requiredString(value, field) {
|
|
207
|
+
if (typeof value !== "string" || !value.trim())
|
|
208
|
+
throw new AcpProtocolError(-32602, `${field} must be a non-empty string`);
|
|
209
|
+
return value.trim();
|
|
210
|
+
}
|
|
211
|
+
function promptText(value) {
|
|
212
|
+
if (!Array.isArray(value))
|
|
213
|
+
throw new AcpProtocolError(-32602, "prompt must be an array");
|
|
214
|
+
const text = value
|
|
215
|
+
.flatMap((part) => part?.type === "text" && typeof part.text === "string" ? [part.text] : [])
|
|
216
|
+
.join("");
|
|
217
|
+
if (!text.trim())
|
|
218
|
+
throw new AcpProtocolError(-32602, "prompt must contain text");
|
|
219
|
+
return text;
|
|
220
|
+
}
|
|
221
|
+
async function handleResponse(response) {
|
|
222
|
+
if (response.jsonrpc !== "2.0")
|
|
223
|
+
throw new AcpProtocolError(-32600, "jsonrpc must be 2.0");
|
|
224
|
+
if (typeof response.id !== "string")
|
|
225
|
+
throw new AcpProtocolError(-32602, "response id must be a string");
|
|
226
|
+
const pending = pendingOutbound.get(response.id);
|
|
227
|
+
if (!pending)
|
|
228
|
+
throw new AcpProtocolError(-32602, `unknown response id: ${response.id}`);
|
|
229
|
+
pendingOutbound.delete(response.id);
|
|
230
|
+
const outcome = response.result?.outcome;
|
|
231
|
+
const optionId = outcome?.outcome === "selected" && typeof outcome.optionId === "string"
|
|
232
|
+
? outcome.optionId
|
|
233
|
+
: "cancelled";
|
|
234
|
+
await (await client()).request("permission.resolve", { requestId: pending.requestId, optionId });
|
|
235
|
+
}
|
|
236
|
+
async function handle(request) {
|
|
237
|
+
if (request.jsonrpc !== "2.0")
|
|
238
|
+
throw new AcpProtocolError(-32600, "jsonrpc must be 2.0");
|
|
239
|
+
const method = requiredString(request.method, "method");
|
|
240
|
+
const requestParams = params(request.params ?? {});
|
|
241
|
+
const id = request.id;
|
|
242
|
+
if (method === "initialize") {
|
|
243
|
+
if (id === undefined)
|
|
244
|
+
return;
|
|
245
|
+
if (requestParams.protocolVersion !== 1)
|
|
246
|
+
throw new AcpProtocolError(-32602, "protocolVersion must be 1");
|
|
247
|
+
result(id, { protocolVersion: 1 });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (method === "session/new") {
|
|
251
|
+
if (id === undefined)
|
|
252
|
+
return;
|
|
253
|
+
const sessionId = requestParams.sessionId === undefined
|
|
254
|
+
? (0, node_crypto_1.randomUUID)()
|
|
255
|
+
: requiredString(requestParams.sessionId, "sessionId");
|
|
256
|
+
if (!SESSION_ID.test(sessionId))
|
|
257
|
+
throw new AcpProtocolError(-32602, "sessionId must be a safe identifier");
|
|
258
|
+
if (sessions.has(sessionId))
|
|
259
|
+
throw new AcpProtocolError(-32002, `session already exists: ${sessionId}`);
|
|
260
|
+
await subscribe(sessionId);
|
|
261
|
+
result(id, { sessionId, friendId: options.friendId });
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (method === "session/load") {
|
|
265
|
+
if (id === undefined)
|
|
266
|
+
return;
|
|
267
|
+
if (options.disableTools) {
|
|
268
|
+
throw new AcpProtocolError(-32601, "session/load is unavailable in observe-only mode");
|
|
269
|
+
}
|
|
270
|
+
const sessionId = requiredString(requestParams.sessionId, "sessionId");
|
|
271
|
+
const frontend = await client();
|
|
272
|
+
const replay = await frontend.request("session.load", {
|
|
273
|
+
agent: options.agent,
|
|
274
|
+
friendId: options.friendId,
|
|
275
|
+
sessionKey: sessionId,
|
|
276
|
+
afterSequence: 0,
|
|
277
|
+
});
|
|
278
|
+
if (!Array.isArray(replay?.events) || replay.events.length === 0) {
|
|
279
|
+
throw new AcpProtocolError(-32001, "session not found");
|
|
280
|
+
}
|
|
281
|
+
for (const event of replay.events)
|
|
282
|
+
replayEvent(sessionId, event);
|
|
283
|
+
await subscribe(sessionId);
|
|
284
|
+
for (const pending of replay.pendingPermissions ?? [])
|
|
285
|
+
permissionRequest(sessionId, pending);
|
|
286
|
+
result(id, {});
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (method === "session/prompt") {
|
|
290
|
+
if (id === undefined)
|
|
291
|
+
return;
|
|
292
|
+
const sessionId = requiredString(requestParams.sessionId, "sessionId");
|
|
293
|
+
if (!sessions.has(sessionId))
|
|
294
|
+
throw new AcpProtocolError(-32001, `unknown session: ${sessionId}`);
|
|
295
|
+
if (activePrompts.has(sessionId))
|
|
296
|
+
throw new AcpProtocolError(-32002, "session already has an active prompt");
|
|
297
|
+
const message = promptText(requestParams.prompt);
|
|
298
|
+
const turnId = (0, node_crypto_1.randomUUID)();
|
|
299
|
+
const completion = Promise.withResolvers();
|
|
300
|
+
activePrompts.set(sessionId, { rpcId: id, turnId, resolve: completion.resolve, reject: completion.reject });
|
|
301
|
+
try {
|
|
302
|
+
await (await client()).request(options.disableTools ? "turn.start.observe-only" : "turn.start", {
|
|
303
|
+
turnId,
|
|
304
|
+
agent: options.agent,
|
|
305
|
+
friendId: options.friendId,
|
|
306
|
+
channel: "mcp",
|
|
307
|
+
sessionKey: sessionId,
|
|
308
|
+
message,
|
|
309
|
+
...(options.runtimeMcpServers ? { runtimeMcpServers: options.runtimeMcpServers } : {}),
|
|
310
|
+
});
|
|
311
|
+
result(id, { stopReason: await completion.promise });
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
activePrompts.delete(sessionId);
|
|
315
|
+
}
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (method === "session/cancel") {
|
|
319
|
+
const sessionId = requiredString(requestParams.sessionId, "sessionId");
|
|
320
|
+
const active = activePrompts.get(sessionId);
|
|
321
|
+
if (active)
|
|
322
|
+
await (await client()).request("turn.cancel", { turnId: active.turnId });
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
throw new AcpProtocolError(-32601, `method not found: ${method}`);
|
|
326
|
+
}
|
|
327
|
+
async function dispatch(line) {
|
|
328
|
+
let request;
|
|
329
|
+
try {
|
|
330
|
+
request = JSON.parse(line);
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
error(null, -32700, "parse error");
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
if (request.method === undefined && (request.result !== undefined || request.error !== undefined)) {
|
|
338
|
+
await handleResponse(request);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
await handle(request);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch (caught) {
|
|
345
|
+
const id = request.id ?? null;
|
|
346
|
+
const code = caught instanceof AcpProtocolError ? caught.code : -32000;
|
|
347
|
+
error(id, code, caught instanceof Error ? caught.message : String(caught));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
start() {
|
|
352
|
+
if (running)
|
|
353
|
+
return;
|
|
354
|
+
running = true;
|
|
355
|
+
(0, runtime_1.emitNervesEvent)({
|
|
356
|
+
component: "senses",
|
|
357
|
+
event: "senses.acp_server_started",
|
|
358
|
+
message: "ACP server started",
|
|
359
|
+
});
|
|
360
|
+
lines = readline.createInterface({ input: options.stdin, crlfDelay: Infinity });
|
|
361
|
+
lines.on("line", (line) => {
|
|
362
|
+
if (line.trim())
|
|
363
|
+
void dispatch(line);
|
|
364
|
+
});
|
|
365
|
+
},
|
|
366
|
+
stop() {
|
|
367
|
+
if (!running)
|
|
368
|
+
return;
|
|
369
|
+
running = false;
|
|
370
|
+
lines?.close();
|
|
371
|
+
lines = null;
|
|
372
|
+
removeEventListener?.();
|
|
373
|
+
removeEventListener = null;
|
|
374
|
+
removeCloseListener?.();
|
|
375
|
+
removeCloseListener = null;
|
|
376
|
+
frontendClient?.close();
|
|
377
|
+
frontendClient = null;
|
|
378
|
+
clientPromise = null;
|
|
379
|
+
for (const prompt of activePrompts.values())
|
|
380
|
+
prompt.reject(new Error("ACP server stopped"));
|
|
381
|
+
activePrompts.clear();
|
|
382
|
+
pendingOutbound.clear();
|
|
383
|
+
sessions.clear();
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
}
|
package/dist/senses/pipeline.js
CHANGED
|
@@ -73,6 +73,7 @@ const orientation_frame_1 = require("../heart/orientation-frame");
|
|
|
73
73
|
const flight_recorder_1 = require("../arc/flight-recorder");
|
|
74
74
|
const context_loss_sentinel_1 = require("../heart/context-loss-sentinel");
|
|
75
75
|
const cares_1 = require("../arc/cares");
|
|
76
|
+
const turn_execution_lease_1 = require("../heart/turn-execution-lease");
|
|
76
77
|
const VOICE_PENDING_MAX_AGE_MS = 15 * 60 * 1_000;
|
|
77
78
|
const TELEGRAM_GENERIC_OUTREACH_PENDING_MAX_AGE_MS = 15 * 60 * 1_000;
|
|
78
79
|
function pendingExpirationReason(channel, message, now, agentName) {
|
|
@@ -497,6 +498,9 @@ function prepareInboundRunLedger(input) {
|
|
|
497
498
|
}
|
|
498
499
|
}
|
|
499
500
|
async function handleInboundTurn(input) {
|
|
501
|
+
return (0, turn_execution_lease_1.withTurnExecutionLease)(() => handleInboundTurnExclusive(input));
|
|
502
|
+
}
|
|
503
|
+
async function handleInboundTurnExclusive(input) {
|
|
500
504
|
// Continuity resumption is structured caller intent. Never infer it from the
|
|
501
505
|
// current message or from similarity to prior obligations.
|
|
502
506
|
const resumePriorWork = input.runAgentOptions?.resumePriorWork === true;
|
|
@@ -769,6 +773,7 @@ async function handleInboundTurn(input) {
|
|
|
769
773
|
currentSession,
|
|
770
774
|
channel: input.channel,
|
|
771
775
|
friendStore: input.friendStore,
|
|
776
|
+
includeAmbientBackgroundDiscovery: !liveLatencyMode,
|
|
772
777
|
});
|
|
773
778
|
// Propagate sync failure from pre-turn pull
|
|
774
779
|
ctx.syncFailure = syncFailure;
|