@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
|
@@ -82,6 +82,11 @@ const router_1 = require("../external-events/router");
|
|
|
82
82
|
const habit_policy_1 = require("../../rsvp/habit-policy");
|
|
83
83
|
const container_runtime_1 = require("./container-runtime");
|
|
84
84
|
const habit_session_1 = require("../habits/habit-session");
|
|
85
|
+
const turn_execution_lease_1 = require("../turn-execution-lease");
|
|
86
|
+
const frontend_session_service_1 = require("../frontend-session-service");
|
|
87
|
+
const frontend_socket_1 = require("../frontend-socket");
|
|
88
|
+
const frontend_journal_1 = require("../frontend-journal");
|
|
89
|
+
const frontend_approval_runtime_1 = require("../frontend-approval-runtime");
|
|
85
90
|
const PIDFILE_PATH = path.join(os.homedir(), ".ouro-cli", "daemon.pids");
|
|
86
91
|
/**
|
|
87
92
|
* Defense-in-depth: detect if we're running under vitest. The pidfile lives
|
|
@@ -556,33 +561,35 @@ function taskIdFromTaskPokePrivateWakeCommand(command) {
|
|
|
556
561
|
* restarts the daemon process (fresh module cache).
|
|
557
562
|
*/
|
|
558
563
|
async function handleAgentSenseTurn(command, runtime) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
564
|
+
return (0, turn_execution_lease_1.withTurnExecutionLease)(async () => {
|
|
565
|
+
try {
|
|
566
|
+
const { setAgentName } = await Promise.resolve().then(() => __importStar(require("../identity")));
|
|
567
|
+
setAgentName(command.agent);
|
|
568
|
+
const { runSenseTurn } = await Promise.resolve().then(() => __importStar(require("../../senses/shared-turn")));
|
|
569
|
+
const result = await runSenseTurn({
|
|
570
|
+
agentName: command.agent,
|
|
571
|
+
channel: command.channel,
|
|
572
|
+
sessionKey: command.sessionKey,
|
|
573
|
+
friendId: command.friendId,
|
|
574
|
+
userMessage: command.message,
|
|
575
|
+
...(runtime?.socketPath ? { toolContext: { daemonSocketPath: runtime.socketPath } } : {}),
|
|
576
|
+
// Per-turn, per-agent runtime MCP injection (e.g. Workbench's ouro_workbench).
|
|
577
|
+
// Scoped to THIS turn only — never stored as module state, so a concurrent
|
|
578
|
+
// turn for a different agent cannot inherit these servers.
|
|
579
|
+
...(command.runtimeMcp ? { runtimeMcpServers: command.runtimeMcp } : {}),
|
|
580
|
+
});
|
|
581
|
+
return {
|
|
582
|
+
ok: true,
|
|
583
|
+
message: result.response,
|
|
584
|
+
data: { ponderDeferred: result.ponderDeferred },
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
/* v8 ignore next -- branch: String(error) fallback only for non-Error throws @preserve */
|
|
589
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
590
|
+
return { ok: false, error: `sense turn failed: ${errorMessage}` };
|
|
591
|
+
}
|
|
592
|
+
});
|
|
586
593
|
}
|
|
587
594
|
async function handleAgentAskTurn(command, runtime) {
|
|
588
595
|
/* v8 ignore start -- ask command parameter defaults are legacy MCP compatibility; send_message shares the primary path @preserve */
|
|
@@ -614,12 +621,16 @@ class OuroDaemon {
|
|
|
614
621
|
mode;
|
|
615
622
|
server = null;
|
|
616
623
|
mailboxServer = null;
|
|
624
|
+
frontendSocketServer = null;
|
|
617
625
|
socketIdentity = null;
|
|
618
626
|
senseAutostartTimer = null;
|
|
619
627
|
externalEventReconcileTimer = null;
|
|
620
628
|
externalEventReconcileRunning = false;
|
|
621
629
|
constructedAtMs = Date.now();
|
|
622
630
|
mailboxServerFactory;
|
|
631
|
+
frontendSocketPath;
|
|
632
|
+
frontendSessionService;
|
|
633
|
+
frontendSocketServerFactory;
|
|
623
634
|
privateRuntimePolicyDeps;
|
|
624
635
|
onStopCommandComplete;
|
|
625
636
|
externalEventRoot;
|
|
@@ -644,6 +655,17 @@ class OuroDaemon {
|
|
|
644
655
|
this.bundlesRoot = options.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
|
|
645
656
|
this.mode = options.mode ?? "production";
|
|
646
657
|
this.mailboxServerFactory = options.mailboxServerFactory ?? this.createDefaultMailboxServer.bind(this);
|
|
658
|
+
this.frontendSocketPath = options.frontendSocketPath ?? (0, frontend_socket_1.frontendSocketPathForDaemon)(this.socketPath);
|
|
659
|
+
/* v8 ignore next -- path-only default adapter; frontend service tests inject and verify the same root contract @preserve */
|
|
660
|
+
const frontendAgentRoot = (agent) => path.join(this.bundlesRoot, `${agent}.ouro`);
|
|
661
|
+
this.frontendSessionService = options.frontendSessionService ?? new frontend_session_service_1.FrontendSessionService({
|
|
662
|
+
journal: new frontend_journal_1.FrontendJournalStore({ agentRoot: frontendAgentRoot }),
|
|
663
|
+
authority: (0, frontend_approval_runtime_1.createFrontendApprovalRuntime)({
|
|
664
|
+
agentRoot: frontendAgentRoot,
|
|
665
|
+
settleApproval: frontend_approval_runtime_1.settleFrontendApproval,
|
|
666
|
+
}),
|
|
667
|
+
});
|
|
668
|
+
this.frontendSocketServerFactory = options.frontendSocketServerFactory ?? frontend_socket_1.startFrontendSocketServer;
|
|
647
669
|
this.privateRuntimePolicyDeps = options.privateRuntimePolicyDeps ?? {};
|
|
648
670
|
this.onStopCommandComplete = options.onStopCommandComplete ?? null;
|
|
649
671
|
this.externalEventRoot = options.externalEventRoot ?? null;
|
|
@@ -848,6 +870,21 @@ class OuroDaemon {
|
|
|
848
870
|
meta: { port: mailbox_types_1.MAILBOX_DEFAULT_PORT },
|
|
849
871
|
});
|
|
850
872
|
}
|
|
873
|
+
try {
|
|
874
|
+
this.frontendSocketServer = await this.frontendSocketServerFactory({
|
|
875
|
+
socketPath: this.frontendSocketPath,
|
|
876
|
+
service: this.frontendSessionService,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
catch (error) {
|
|
880
|
+
(0, runtime_1.emitNervesEvent)({
|
|
881
|
+
level: "warn",
|
|
882
|
+
component: "daemon",
|
|
883
|
+
event: "daemon.frontend_socket_start_failed",
|
|
884
|
+
message: `Frontend socket failed to start: ${String(error)}`,
|
|
885
|
+
meta: { socketPath: this.frontendSocketPath },
|
|
886
|
+
});
|
|
887
|
+
}
|
|
851
888
|
}
|
|
852
889
|
triggerAutoStartAgents() {
|
|
853
890
|
if (this.processManager.triggerAutoStartAgents) {
|
|
@@ -1117,6 +1154,12 @@ class OuroDaemon {
|
|
|
1117
1154
|
message: "stopping daemon server",
|
|
1118
1155
|
meta: { socketPath: this.socketPath },
|
|
1119
1156
|
});
|
|
1157
|
+
this.frontendSessionService.cancelAllTurns();
|
|
1158
|
+
if (this.frontendSocketServer) {
|
|
1159
|
+
await this.frontendSocketServer.stop();
|
|
1160
|
+
this.frontendSocketServer = null;
|
|
1161
|
+
}
|
|
1162
|
+
this.frontendSessionService.close?.();
|
|
1120
1163
|
(0, update_checker_1.stopUpdateChecker)();
|
|
1121
1164
|
(0, mcp_manager_1.shutdownSharedMcpManager)();
|
|
1122
1165
|
this.scheduler.stop?.();
|
|
@@ -2402,28 +2445,32 @@ class OuroDaemon {
|
|
|
2402
2445
|
return this.handlePrivateRuntimeWake(this.buildAwaitPrivateWakeCommand(command));
|
|
2403
2446
|
}
|
|
2404
2447
|
case "mcp.list": {
|
|
2405
|
-
(0,
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2448
|
+
return (0, turn_execution_lease_1.withTurnExecutionLease)(async () => {
|
|
2449
|
+
(0, identity_1.setAgentName)(command.agent ?? "default");
|
|
2450
|
+
const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)();
|
|
2451
|
+
if (!mcpManager) {
|
|
2452
|
+
return { ok: true, data: [], message: "no MCP servers configured" };
|
|
2453
|
+
}
|
|
2454
|
+
return { ok: true, data: mcpManager.listAllTools() };
|
|
2455
|
+
});
|
|
2411
2456
|
}
|
|
2412
2457
|
case "mcp.call": {
|
|
2413
|
-
(0,
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2458
|
+
return (0, turn_execution_lease_1.withTurnExecutionLease)(async () => {
|
|
2459
|
+
(0, identity_1.setAgentName)(command.agent ?? "default");
|
|
2460
|
+
const mcpCallManager = await (0, mcp_manager_1.getSharedMcpManager)();
|
|
2461
|
+
if (!mcpCallManager) {
|
|
2462
|
+
return { ok: false, error: "no MCP servers configured" };
|
|
2463
|
+
}
|
|
2464
|
+
try {
|
|
2465
|
+
const parsedArgs = command.args ? JSON.parse(command.args) : {};
|
|
2466
|
+
const result = await mcpCallManager.callTool(command.server, command.tool, parsedArgs);
|
|
2467
|
+
return { ok: true, data: result };
|
|
2468
|
+
}
|
|
2469
|
+
catch (error) {
|
|
2470
|
+
/* v8 ignore next -- defensive: callTool errors are always Error instances @preserve */
|
|
2471
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
2472
|
+
}
|
|
2473
|
+
});
|
|
2427
2474
|
}
|
|
2428
2475
|
case "hatch.start":
|
|
2429
2476
|
return {
|
|
@@ -359,7 +359,7 @@ function senseRepairHint(agent, sense) {
|
|
|
359
359
|
}
|
|
360
360
|
/* v8 ignore next -- Workbench is deliberately not daemon-managed, so getSenseInventory never asks the daemon manager for a repair hint @preserve */
|
|
361
361
|
if (sense === "workbench") {
|
|
362
|
-
return `Agent-runnable: install Ouro Workbench.app, then launch the boss through Workbench or run 'ouro
|
|
362
|
+
return `Agent-runnable: install Ouro Workbench.app, then launch the boss through Workbench or run 'ouro acp-serve --agent ${agent} --workbench-mcp'; 'ouro connect workbench --agent ${agent}' only cleans stale bundle entries.`;
|
|
363
363
|
}
|
|
364
364
|
return `Run 'ouro connect bluebubbles --agent ${agent}' to attach BlueBubbles on this machine; then run 'ouro up' again.`;
|
|
365
365
|
}
|
|
@@ -0,0 +1,432 @@
|
|
|
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.settleFrontendApproval = settleFrontendApproval;
|
|
37
|
+
exports.createFrontendApprovalRuntime = createFrontendApprovalRuntime;
|
|
38
|
+
const node_crypto_1 = require("node:crypto");
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
const approval_files_1 = require("./approval-files");
|
|
41
|
+
const approval_store_1 = require("./approval-store");
|
|
42
|
+
const tool_approval_1 = require("./tool-approval");
|
|
43
|
+
const core_1 = require("./core");
|
|
44
|
+
const identity_1 = require("./identity");
|
|
45
|
+
const turn_execution_lease_1 = require("./turn-execution-lease");
|
|
46
|
+
const context_1 = require("../mind/context");
|
|
47
|
+
const session_transaction_1 = require("../mind/session-transaction");
|
|
48
|
+
const tools_1 = require("../repertoire/tools");
|
|
49
|
+
const mcp_manager_1 = require("../repertoire/mcp-manager");
|
|
50
|
+
const runtime_1 = require("../nerves/runtime");
|
|
51
|
+
const PERMISSION_OPTIONS = [
|
|
52
|
+
{ optionId: "allow-once", name: "Allow once", kind: "allow_once" },
|
|
53
|
+
{ optionId: "reject-once", name: "Reject", kind: "reject_once" },
|
|
54
|
+
];
|
|
55
|
+
/* v8 ignore start -- lazy default adapter avoids widening every daemon test mock; settlement behavior is covered through the injected dependency @preserve */
|
|
56
|
+
async function defaultReleaseRuntimeMcpServers() {
|
|
57
|
+
const manager = await Promise.resolve().then(() => __importStar(require("../repertoire/mcp-manager")));
|
|
58
|
+
await manager.releaseRuntimeMcpServers();
|
|
59
|
+
}
|
|
60
|
+
/* v8 ignore stop */
|
|
61
|
+
const DEFAULT_SETTLEMENT_DEPENDENCIES = {
|
|
62
|
+
withTurnExecutionLease: turn_execution_lease_1.withTurnExecutionLease,
|
|
63
|
+
setAgentName: identity_1.setAgentName,
|
|
64
|
+
withSessionTurnLease: session_transaction_1.withSessionTurnLease,
|
|
65
|
+
readSessionTransaction: session_transaction_1.readSessionTransaction,
|
|
66
|
+
executeApprovalDecision: tool_approval_1.executeApprovalDecision,
|
|
67
|
+
resolveToolDefinition: tools_1.resolveToolDefinition,
|
|
68
|
+
approvalPolicyForInvocation: tools_1.approvalPolicyForInvocation,
|
|
69
|
+
execTool: tools_1.execTool,
|
|
70
|
+
getSharedMcpManager: mcp_manager_1.getSharedMcpManager,
|
|
71
|
+
releaseRuntimeMcpServers: defaultReleaseRuntimeMcpServers,
|
|
72
|
+
loadSession: context_1.loadSession,
|
|
73
|
+
postTurnTrim: context_1.postTurnTrim,
|
|
74
|
+
postTurnPersist: context_1.postTurnPersist,
|
|
75
|
+
resumeApprovalContinuation: core_1.resumeApprovalContinuation,
|
|
76
|
+
runAgent: core_1.runAgent,
|
|
77
|
+
randomUUID: node_crypto_1.randomUUID,
|
|
78
|
+
};
|
|
79
|
+
async function settleFrontendApproval(input, dependencies = DEFAULT_SETTLEMENT_DEPENDENCIES) {
|
|
80
|
+
const token = input.tokenStore.get(input.suspension.approvalId);
|
|
81
|
+
if (!token)
|
|
82
|
+
throw new Error(`frontend approval decision token is unavailable: ${input.suspension.approvalId}`);
|
|
83
|
+
const checkpoint = input.checkpointStore.read(input.suspension.approvalId);
|
|
84
|
+
if (!checkpoint)
|
|
85
|
+
throw new Error(`frontend approval checkpoint is unavailable: ${input.suspension.approvalId}`);
|
|
86
|
+
const record = input.approvalStore.read(input.suspension.approvalId);
|
|
87
|
+
if (!record)
|
|
88
|
+
throw new Error(`frontend approval record is unavailable: ${input.suspension.approvalId}`);
|
|
89
|
+
return dependencies.withTurnExecutionLease(async () => {
|
|
90
|
+
dependencies.setAgentName(input.request.agent);
|
|
91
|
+
try {
|
|
92
|
+
const mcpManager = await dependencies.getSharedMcpManager(input.request.runtimeMcpServers ? { runtimeServers: input.request.runtimeMcpServers } : undefined) ?? undefined;
|
|
93
|
+
return await dependencies.withSessionTurnLease(record.sessionPath, async (lease) => {
|
|
94
|
+
const currentRevision = dependencies.readSessionTransaction(record.sessionPath, lease).revision;
|
|
95
|
+
const continuationSignal = input.optionId === "cancelled" ? AbortSignal.abort() : input.signal;
|
|
96
|
+
const liveToolContext = input.approvalRequest.liveToolContext ?? {
|
|
97
|
+
signin: async () => undefined,
|
|
98
|
+
agentRoot: input.agentRoot,
|
|
99
|
+
currentSession: {
|
|
100
|
+
friendId: input.request.friendId,
|
|
101
|
+
channel: input.request.channel,
|
|
102
|
+
key: input.request.sessionKey,
|
|
103
|
+
sessionPath: record.sessionPath,
|
|
104
|
+
},
|
|
105
|
+
currentUserMessage: input.request.message,
|
|
106
|
+
};
|
|
107
|
+
const terminal = await dependencies.executeApprovalDecision({
|
|
108
|
+
approvalStore: input.approvalStore,
|
|
109
|
+
checkpointStore: input.checkpointStore,
|
|
110
|
+
decision: {
|
|
111
|
+
approvalId: record.approvalId,
|
|
112
|
+
decisionToken: token,
|
|
113
|
+
decision: input.optionId === "allow-once" ? "approve" : "deny",
|
|
114
|
+
requesterId: input.request.friendId,
|
|
115
|
+
transport: "frontend",
|
|
116
|
+
transportUserId: input.request.friendId,
|
|
117
|
+
transportChatId: input.request.sessionKey,
|
|
118
|
+
transportMessageId: record.approvalId,
|
|
119
|
+
sessionKey: input.request.sessionKey,
|
|
120
|
+
...(input.optionId === "expired" ? { decisionAt: Date.parse(record.expiresAt) } : {}),
|
|
121
|
+
},
|
|
122
|
+
ownerId: `frontend-decision-${dependencies.randomUUID()}`,
|
|
123
|
+
currentSessionRevision: currentRevision,
|
|
124
|
+
resolveTool: dependencies.resolveToolDefinition,
|
|
125
|
+
resolveApprovalPolicy: (name, args) => dependencies.approvalPolicyForInvocation(name, args, liveToolContext),
|
|
126
|
+
liveGuard: async () => ({ ok: true }),
|
|
127
|
+
liveRisk: async () => ({ ok: true }),
|
|
128
|
+
execute: (name, args) => dependencies.execTool(name, args, liveToolContext),
|
|
129
|
+
});
|
|
130
|
+
input.tokenStore.remove(record.approvalId);
|
|
131
|
+
const existing = dependencies.loadSession(record.sessionPath);
|
|
132
|
+
const existingStructuredOutputIds = new Set(existing?.structuredOutputs.map((output) => output.id) ?? []);
|
|
133
|
+
const deliveries = [];
|
|
134
|
+
let response = "";
|
|
135
|
+
const emit = input.frontendEventSink.onEvent;
|
|
136
|
+
const callbacks = {
|
|
137
|
+
settleOutputMode: "retractable_buffer",
|
|
138
|
+
onModelStart: () => emit({ type: "model_started", data: {} }),
|
|
139
|
+
onModelStreamStart: () => emit({ type: "model_stream_started", data: {} }),
|
|
140
|
+
onTextChunk: (text) => {
|
|
141
|
+
response += text;
|
|
142
|
+
emit({ type: "text_delta", data: { text } });
|
|
143
|
+
},
|
|
144
|
+
onReasoningChunk: (text) => emit({ type: "reasoning_delta", data: { text } }),
|
|
145
|
+
onToolStart: (name, args) => emit({ type: "tool_started", data: { name, args } }),
|
|
146
|
+
onToolEnd: (name, summary, success) => emit({ type: "tool_completed", data: { name, summary, success } }),
|
|
147
|
+
onError: (error, severity) => emit({ type: "error", data: { message: error.message, severity } }),
|
|
148
|
+
onClearText: () => {
|
|
149
|
+
response = "";
|
|
150
|
+
emit({ type: "text_cleared", data: {} });
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
let continuationEpoch = 0;
|
|
154
|
+
const continuationOwnerId = `frontend-continuation-${dependencies.randomUUID()}`;
|
|
155
|
+
const nestedCoordinator = {
|
|
156
|
+
propose: (request) => input.approvalCoordinatorFactory({
|
|
157
|
+
request: input.request,
|
|
158
|
+
publish: input.publish,
|
|
159
|
+
})({
|
|
160
|
+
sessionPath: record.sessionPath,
|
|
161
|
+
baseSessionRevision: dependencies.readSessionTransaction(record.sessionPath, lease).revision,
|
|
162
|
+
}).propose(request),
|
|
163
|
+
};
|
|
164
|
+
const persist = (messages, result) => {
|
|
165
|
+
const expectedRevision = dependencies.readSessionTransaction(record.sessionPath, lease).revision;
|
|
166
|
+
const prepared = dependencies.postTurnTrim(messages, result?.usage);
|
|
167
|
+
dependencies.postTurnPersist(record.sessionPath, prepared, result?.usage, existing?.state, lease, expectedRevision);
|
|
168
|
+
};
|
|
169
|
+
const continuation = await dependencies.resumeApprovalContinuation({
|
|
170
|
+
record: terminal,
|
|
171
|
+
checkpoint,
|
|
172
|
+
currentSessionRevision: dependencies.readSessionTransaction(record.sessionPath, lease).revision,
|
|
173
|
+
sessionMessages: checkpoint.preCallMessages,
|
|
174
|
+
callbacks,
|
|
175
|
+
channel: input.request.channel,
|
|
176
|
+
claimContinuation: () => {
|
|
177
|
+
const claim = input.approvalStore.claimContinuation({
|
|
178
|
+
approvalId: record.approvalId,
|
|
179
|
+
ownerId: continuationOwnerId,
|
|
180
|
+
});
|
|
181
|
+
continuationEpoch = claim.record.continuationEpoch;
|
|
182
|
+
return claim;
|
|
183
|
+
},
|
|
184
|
+
markContinuationMaterialized: () => {
|
|
185
|
+
input.approvalStore.markContinuationMaterialized({
|
|
186
|
+
approvalId: record.approvalId,
|
|
187
|
+
ownerId: continuationOwnerId,
|
|
188
|
+
epoch: continuationEpoch,
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
markContinuationAttempted: () => {
|
|
192
|
+
input.approvalStore.markContinuationAttempted({
|
|
193
|
+
approvalId: record.approvalId,
|
|
194
|
+
ownerId: continuationOwnerId,
|
|
195
|
+
epoch: continuationEpoch,
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
completeContinuation: () => {
|
|
199
|
+
input.approvalStore.completeContinuation({
|
|
200
|
+
approvalId: record.approvalId,
|
|
201
|
+
ownerId: continuationOwnerId,
|
|
202
|
+
epoch: continuationEpoch,
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
runAgent: dependencies.runAgent,
|
|
206
|
+
runAgentOptions: {
|
|
207
|
+
mcpManager,
|
|
208
|
+
toolContext: liveToolContext,
|
|
209
|
+
approvalCoordinator: nestedCoordinator,
|
|
210
|
+
},
|
|
211
|
+
persist,
|
|
212
|
+
deliver: async (text) => {
|
|
213
|
+
response = text;
|
|
214
|
+
deliveries.push({ kind: "text", text });
|
|
215
|
+
emit({ type: "assistant_delivery", data: { kind: "text", text } });
|
|
216
|
+
},
|
|
217
|
+
signal: continuationSignal,
|
|
218
|
+
});
|
|
219
|
+
const persisted = dependencies.loadSession(record.sessionPath);
|
|
220
|
+
for (const output of persisted?.structuredOutputs ?? []) {
|
|
221
|
+
if (!existingStructuredOutputIds.has(output.id)) {
|
|
222
|
+
emit({ type: "structured_output", data: { output } });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const turnOutcome = continuation.outcome === "terminal_notice"
|
|
226
|
+
? "settled"
|
|
227
|
+
: continuation.outcome === "already_continued"
|
|
228
|
+
? "superseded"
|
|
229
|
+
: continuation.outcome;
|
|
230
|
+
if (turnOutcome === "suspended" && !continuation.suspension) {
|
|
231
|
+
throw new Error("frontend approval continuation omitted its nested suspension");
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
response,
|
|
235
|
+
ponderDeferred: false,
|
|
236
|
+
deliveries,
|
|
237
|
+
deliveryFailures: [],
|
|
238
|
+
sessionPath: record.sessionPath,
|
|
239
|
+
turnOutcome,
|
|
240
|
+
...(continuation.suspension ? { suspension: continuation.suspension } : {}),
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
if (input.request.runtimeMcpServers) {
|
|
246
|
+
await dependencies.releaseRuntimeMcpServers();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
function createFrontendApprovalRuntime(options) {
|
|
252
|
+
const agentRoot = options.agentRoot ?? identity_1.getAgentRoot;
|
|
253
|
+
const now = options.now ?? Date.now;
|
|
254
|
+
const approvalTimeoutMs = options.approvalTimeoutMs ?? 300_000;
|
|
255
|
+
const settleApproval = options.settleApproval ?? settleFrontendApproval;
|
|
256
|
+
if (!Number.isSafeInteger(approvalTimeoutMs) || approvalTimeoutMs < 1) {
|
|
257
|
+
throw new Error("approvalTimeoutMs must be a positive integer");
|
|
258
|
+
}
|
|
259
|
+
(0, runtime_1.emitNervesEvent)({
|
|
260
|
+
component: "heart",
|
|
261
|
+
event: "heart.frontend_approval_runtime_ready",
|
|
262
|
+
message: "frontend approval runtime ready",
|
|
263
|
+
});
|
|
264
|
+
const states = new Map();
|
|
265
|
+
const pending = new Map();
|
|
266
|
+
let closed = false;
|
|
267
|
+
function closeStatesWhenIdle() {
|
|
268
|
+
if (!closed || pending.size > 0)
|
|
269
|
+
return;
|
|
270
|
+
for (const state of states.values())
|
|
271
|
+
state.approvalStore.close();
|
|
272
|
+
states.clear();
|
|
273
|
+
}
|
|
274
|
+
function stateFor(agent) {
|
|
275
|
+
let state = states.get(agent);
|
|
276
|
+
if (state)
|
|
277
|
+
return state;
|
|
278
|
+
const stateRoot = path.join(agentRoot(agent), "state", "approvals");
|
|
279
|
+
state = {
|
|
280
|
+
approvalStore: (0, approval_store_1.openApprovalStore)({ databasePath: path.join(stateRoot, "approvals.sqlite") }),
|
|
281
|
+
checkpointStore: new approval_files_1.FileApprovalCheckpointStore(path.join(stateRoot, "checkpoints.json")),
|
|
282
|
+
tokenStore: new approval_files_1.FileApprovalTokenStore(path.join(stateRoot, "tokens.json")),
|
|
283
|
+
};
|
|
284
|
+
states.set(agent, state);
|
|
285
|
+
return state;
|
|
286
|
+
}
|
|
287
|
+
function settlePending(item, optionId) {
|
|
288
|
+
if (item.settled)
|
|
289
|
+
return false;
|
|
290
|
+
item.settled = true;
|
|
291
|
+
item.resolve(optionId);
|
|
292
|
+
item.publish("permission_resolved", { requestId: item.requestId, optionId }, "permission_resolved");
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
const runtime = {
|
|
296
|
+
approvalCoordinatorFactory(input) {
|
|
297
|
+
return (context) => ({
|
|
298
|
+
propose: async (request) => {
|
|
299
|
+
if (closed)
|
|
300
|
+
throw new Error("frontend approval runtime is closed");
|
|
301
|
+
if (request.toolCall.type !== "function")
|
|
302
|
+
throw new Error("frontend approval requires a function tool call");
|
|
303
|
+
const state = stateFor(input.request.agent);
|
|
304
|
+
const committed = (0, tool_approval_1.commitApprovalProposal)({
|
|
305
|
+
approvalStore: state.approvalStore,
|
|
306
|
+
checkpointStore: state.checkpointStore,
|
|
307
|
+
tokenStore: state.tokenStore,
|
|
308
|
+
proposal: {
|
|
309
|
+
toolCallId: request.toolCall.id,
|
|
310
|
+
toolName: request.toolCall.function.name,
|
|
311
|
+
arguments: request.arguments,
|
|
312
|
+
schemaDigest: request.schemaDigest,
|
|
313
|
+
toolDigest: request.toolDigest,
|
|
314
|
+
policyDigest: request.policyDigest,
|
|
315
|
+
policyId: request.policyId,
|
|
316
|
+
sessionKey: input.request.sessionKey,
|
|
317
|
+
sessionPath: context.sessionPath,
|
|
318
|
+
baseSessionRevision: context.baseSessionRevision,
|
|
319
|
+
checkpointDigest: "0".repeat(64),
|
|
320
|
+
requesterId: input.request.friendId,
|
|
321
|
+
transport: "frontend",
|
|
322
|
+
transportUserId: input.request.friendId,
|
|
323
|
+
transportChatId: input.request.sessionKey,
|
|
324
|
+
expiresAt: new Date(now() + approvalTimeoutMs).toISOString(),
|
|
325
|
+
frozenAssistantMessage: request.frozenAssistantMessage,
|
|
326
|
+
},
|
|
327
|
+
preCallMessages: request.preCallMessages,
|
|
328
|
+
});
|
|
329
|
+
const decision = Promise.withResolvers();
|
|
330
|
+
state.approvalStore.bindPrompt({
|
|
331
|
+
approvalId: committed.record.approvalId,
|
|
332
|
+
transport: "frontend",
|
|
333
|
+
transportChatId: input.request.sessionKey,
|
|
334
|
+
transportMessageId: committed.record.approvalId,
|
|
335
|
+
});
|
|
336
|
+
const item = {
|
|
337
|
+
requestId: committed.record.approvalId,
|
|
338
|
+
toolCallId: request.toolCall.id,
|
|
339
|
+
title: `Approve ${request.toolCall.function.name}`,
|
|
340
|
+
request: input.request,
|
|
341
|
+
approvalRequest: request,
|
|
342
|
+
expiresAt: Date.parse(committed.record.expiresAt),
|
|
343
|
+
publish: input.publish,
|
|
344
|
+
decision: decision.promise,
|
|
345
|
+
resolve: decision.resolve,
|
|
346
|
+
settled: false,
|
|
347
|
+
};
|
|
348
|
+
pending.set(committed.record.approvalId, item);
|
|
349
|
+
input.publish("permission_requested", {
|
|
350
|
+
requestId: committed.record.approvalId,
|
|
351
|
+
toolCallId: request.toolCall.id,
|
|
352
|
+
title: `Approve ${request.toolCall.function.name}`,
|
|
353
|
+
options: PERMISSION_OPTIONS,
|
|
354
|
+
}, "permission_requested");
|
|
355
|
+
return {
|
|
356
|
+
approvalId: committed.record.approvalId,
|
|
357
|
+
checkpointDigest: committed.record.checkpointDigest,
|
|
358
|
+
suspendedSessionRevision: committed.record.suspendedSessionRevision,
|
|
359
|
+
};
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
},
|
|
363
|
+
async resumeApproval(input) {
|
|
364
|
+
const item = pending.get(input.suspension.approvalId);
|
|
365
|
+
if (!item || item.request.turnId !== input.request.turnId) {
|
|
366
|
+
throw new Error(`frontend approval ${input.suspension.approvalId} does not own turn ${input.request.turnId}`);
|
|
367
|
+
}
|
|
368
|
+
const abort = () => settlePending(item, "cancelled");
|
|
369
|
+
if (input.signal.aborted)
|
|
370
|
+
abort();
|
|
371
|
+
else
|
|
372
|
+
input.signal.addEventListener("abort", abort, { once: true });
|
|
373
|
+
const timeout = setTimeout(() => settlePending(item, "expired"), Math.max(0, item.expiresAt - now()));
|
|
374
|
+
try {
|
|
375
|
+
const optionId = await item.decision;
|
|
376
|
+
const state = stateFor(input.request.agent);
|
|
377
|
+
return await settleApproval({
|
|
378
|
+
...input,
|
|
379
|
+
approvalRequest: item.approvalRequest,
|
|
380
|
+
optionId,
|
|
381
|
+
publish: item.publish,
|
|
382
|
+
agentRoot: agentRoot(input.request.agent),
|
|
383
|
+
...state,
|
|
384
|
+
approvalCoordinatorFactory: runtime.approvalCoordinatorFactory,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
finally {
|
|
388
|
+
clearTimeout(timeout);
|
|
389
|
+
input.signal.removeEventListener("abort", abort);
|
|
390
|
+
pending.delete(input.suspension.approvalId);
|
|
391
|
+
closeStatesWhenIdle();
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
resolvePermission(requestId, optionId) {
|
|
395
|
+
if (closed || (optionId !== "allow-once" && optionId !== "reject-once" && optionId !== "cancelled"))
|
|
396
|
+
return false;
|
|
397
|
+
const item = pending.get(requestId);
|
|
398
|
+
return item ? settlePending(item, optionId) : false;
|
|
399
|
+
},
|
|
400
|
+
cancelTurn(turnId) {
|
|
401
|
+
for (const item of pending.values()) {
|
|
402
|
+
if (item.request.turnId === turnId)
|
|
403
|
+
settlePending(item, "cancelled");
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
pendingPermissions(ref) {
|
|
407
|
+
return [...pending.values()].flatMap((item) => {
|
|
408
|
+
if (item.settled
|
|
409
|
+
|| item.request.agent !== ref.agent
|
|
410
|
+
|| item.request.friendId !== ref.friendId
|
|
411
|
+
|| item.request.sessionKey !== ref.sessionKey)
|
|
412
|
+
return [];
|
|
413
|
+
return [{
|
|
414
|
+
requestId: item.requestId,
|
|
415
|
+
turnId: item.request.turnId,
|
|
416
|
+
toolCallId: item.toolCallId,
|
|
417
|
+
title: item.title,
|
|
418
|
+
options: [...PERMISSION_OPTIONS],
|
|
419
|
+
}];
|
|
420
|
+
});
|
|
421
|
+
},
|
|
422
|
+
close() {
|
|
423
|
+
if (closed)
|
|
424
|
+
return;
|
|
425
|
+
for (const item of pending.values())
|
|
426
|
+
settlePending(item, "cancelled");
|
|
427
|
+
closed = true;
|
|
428
|
+
closeStatesWhenIdle();
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
return runtime;
|
|
432
|
+
}
|