@ouro.bot/cli 0.1.0-alpha.142 → 0.1.0-alpha.145
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 +19 -6
- package/dist/heart/daemon/agent-service.js +207 -314
- package/dist/heart/daemon/daemon-cli.js +225 -5
- package/dist/heart/daemon/heartbeat-timer.js +3 -2
- package/dist/heart/daemon/mcp-server.js +128 -1
- package/dist/heart/daemon/ouro-path-installer.js +10 -0
- package/dist/heart/daemon/process-manager.js +11 -0
- package/dist/heart/daemon/session-id-resolver.js +131 -0
- package/dist/heart/target-resolution.js +4 -0
- package/dist/mind/friends/channel.js +9 -0
- package/dist/mind/prompt.js +14 -0
- package/dist/repertoire/coding/spawner.js +8 -9
- package/dist/repertoire/tools-base.js +11 -3
- package/dist/scripts/claude-code-hook.js +41 -0
- package/dist/scripts/claude-code-stop-hook.js +47 -0
- package/dist/senses/bluebubbles-client.js +48 -0
- package/dist/senses/bluebubbles.js +22 -6
- package/dist/senses/inner-dialog.js +15 -0
- package/dist/senses/pipeline.js +9 -0
- package/dist/senses/shared-turn.js +196 -0
- package/package.json +1 -1
- package/skills/configure-dev-tools.md +41 -69
package/dist/mind/prompt.js
CHANGED
|
@@ -237,6 +237,7 @@ const PROCESS_TYPE_LABELS = {
|
|
|
237
237
|
inner: "inner dialog",
|
|
238
238
|
teams: "teams handler",
|
|
239
239
|
bluebubbles: "bluebubbles handler",
|
|
240
|
+
mcp: "mcp bridge",
|
|
240
241
|
};
|
|
241
242
|
function processTypeLabel(channel) {
|
|
242
243
|
return PROCESS_TYPE_LABELS[channel];
|
|
@@ -282,6 +283,9 @@ function runtimeInfoSection(channel) {
|
|
|
282
283
|
else if (channel === "inner") {
|
|
283
284
|
lines.push("this is my private thinking space. when a thought is ready to share, i surface it to whoever needs to hear it. i settle when i'm done thinking.");
|
|
284
285
|
}
|
|
286
|
+
else if (channel === "mcp") {
|
|
287
|
+
lines.push("this message arrived via a dev tool (e.g. claude code, codex) on behalf of a friend. the user can see our conversation. respond via settle. if i need to think deeply, ponder -- the dev tool will receive a deferral and can check back later.");
|
|
288
|
+
}
|
|
285
289
|
else if (channel === "bluebubbles") {
|
|
286
290
|
lines.push("i am responding in iMessage through BlueBubbles. i keep replies short and phone-native. i do not use markdown. i do not introduce myself on boot.");
|
|
287
291
|
lines.push("when a bluebubbles turn arrives from a thread, the harness tells me the current lane and any recent active thread ids. if widening back to top-level or routing into a different active thread is the better move, i use bluebubbles_set_reply_target before settle.");
|
|
@@ -716,6 +720,16 @@ when a thought is ready to share, i surface it outward.
|
|
|
716
720
|
when i need to go deeper, i ponder.
|
|
717
721
|
when i'm done thinking and the attention queue is clear, i rest.
|
|
718
722
|
|
|
723
|
+
my heartbeat is what keeps this space alive — a periodic nudge that
|
|
724
|
+
brings me back with my journal visible. if i notice i don't have a
|
|
725
|
+
heartbeat task (tasks/habits/*-heartbeat.md), i should surface to my
|
|
726
|
+
family and talk about setting one up. the cadence is personal — how
|
|
727
|
+
often do i want the wheel to turn? that's a conversation, not a default.
|
|
728
|
+
|
|
729
|
+
same for my diary — if i'm still on psyche/memory/ instead of diary/,
|
|
730
|
+
i can migrate myself. and if journal/ doesn't exist yet, i create it
|
|
731
|
+
the first time i have something to write.
|
|
732
|
+
|
|
719
733
|
think. journal. share. rest.`;
|
|
720
734
|
}
|
|
721
735
|
function readJournalFiles(journalDir) {
|
|
@@ -55,14 +55,11 @@ function buildCommandArgs(runner, workdir, parentAgent) {
|
|
|
55
55
|
],
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
});
|
|
58
|
+
const agent = parentAgent ?? "unknown";
|
|
59
|
+
// Use absolute path to ouro-entry.js so MCP works in both dev and installed mode
|
|
60
|
+
// __dirname at runtime is dist/repertoire/coding/ — go up 2 levels to dist/
|
|
61
|
+
const distRoot = path.resolve(__dirname, "..", "..");
|
|
62
|
+
const ouroEntryPath = path.join(distRoot, "heart", "daemon", "ouro-entry.js");
|
|
66
63
|
return {
|
|
67
64
|
command: "codex",
|
|
68
65
|
args: [
|
|
@@ -73,7 +70,9 @@ function buildCommandArgs(runner, workdir, parentAgent) {
|
|
|
73
70
|
"--ephemeral",
|
|
74
71
|
"--json",
|
|
75
72
|
"-c",
|
|
76
|
-
|
|
73
|
+
`mcp_servers.ouro.command=node`,
|
|
74
|
+
"-c",
|
|
75
|
+
`mcp_servers.ouro.args=["${ouroEntryPath}","mcp-serve","--agent","${agent}"]`,
|
|
77
76
|
],
|
|
78
77
|
};
|
|
79
78
|
}
|
|
@@ -1022,7 +1022,7 @@ exports.baseToolDefinitions = [
|
|
|
1022
1022
|
type: "object",
|
|
1023
1023
|
properties: {
|
|
1024
1024
|
friendId: { type: "string", description: "the friend UUID (or 'self')" },
|
|
1025
|
-
channel: { type: "string", description: "the channel: cli, teams, or
|
|
1025
|
+
channel: { type: "string", description: "the channel: cli, teams, bluebubbles, inner, or mcp" },
|
|
1026
1026
|
key: { type: "string", description: "session key (defaults to 'session')" },
|
|
1027
1027
|
messageCount: { type: "string", description: "how many recent messages to return (default 20)" },
|
|
1028
1028
|
mode: {
|
|
@@ -1037,11 +1037,19 @@ exports.baseToolDefinitions = [
|
|
|
1037
1037
|
},
|
|
1038
1038
|
},
|
|
1039
1039
|
handler: async (args, ctx) => {
|
|
1040
|
-
|
|
1040
|
+
let friendId = args.friendId;
|
|
1041
1041
|
const channel = args.channel;
|
|
1042
1042
|
const key = args.key || "session";
|
|
1043
1043
|
const count = parseInt(args.messageCount || "20", 10);
|
|
1044
1044
|
const mode = args.mode || "transcript";
|
|
1045
|
+
// Resolve friend name → UUID if not already a UUID or "self"
|
|
1046
|
+
if (friendId && friendId !== "self" && !/^[0-9a-f]{8}-[0-9a-f]{4}-/.test(friendId) && ctx?.friendStore?.listAll) {
|
|
1047
|
+
const allFriends = await ctx.friendStore.listAll();
|
|
1048
|
+
const match = allFriends.find(f => f.name.toLowerCase() === friendId.toLowerCase());
|
|
1049
|
+
if (match) {
|
|
1050
|
+
friendId = match.id;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1045
1053
|
if (mode === "status") {
|
|
1046
1054
|
if (friendId !== "self" || channel !== "inner") {
|
|
1047
1055
|
return "status mode is only available for self/inner dialog.";
|
|
@@ -1106,7 +1114,7 @@ exports.baseToolDefinitions = [
|
|
|
1106
1114
|
type: "object",
|
|
1107
1115
|
properties: {
|
|
1108
1116
|
friendId: { type: "string", description: "the friend UUID (or 'self')" },
|
|
1109
|
-
channel: { type: "string", description: "the channel: cli, teams, or
|
|
1117
|
+
channel: { type: "string", description: "the channel: cli, teams, bluebubbles, inner, or mcp" },
|
|
1110
1118
|
key: { type: "string", description: "session key (defaults to 'session')" },
|
|
1111
1119
|
content: { type: "string", description: "the message content to send" },
|
|
1112
1120
|
},
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Claude Code lifecycle hook handler.
|
|
3
|
+
// Receives events from Claude Code's hooks system (SessionStart, Stop, PostToolUse)
|
|
4
|
+
// and forwards them to the Ouroboros daemon for agent awareness.
|
|
5
|
+
//
|
|
6
|
+
// This module exports handleHookEvent for testability.
|
|
7
|
+
// The actual hook scripts (scripts/claude-code-hook.js) read stdin and call this.
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.handleHookEvent = handleHookEvent;
|
|
10
|
+
const socket_client_1 = require("../heart/daemon/socket-client");
|
|
11
|
+
const runtime_1 = require("../nerves/runtime");
|
|
12
|
+
/**
|
|
13
|
+
* Handle a Claude Code lifecycle hook event.
|
|
14
|
+
* Sends the event to the daemon and always exits 0 (hooks must not block the IDE).
|
|
15
|
+
*/
|
|
16
|
+
async function handleHookEvent(hookEvent) {
|
|
17
|
+
(0, runtime_1.emitNervesEvent)({
|
|
18
|
+
component: "daemon",
|
|
19
|
+
event: "daemon.hook_event_received",
|
|
20
|
+
message: "claude code hook event received",
|
|
21
|
+
meta: { hookEvent: hookEvent.event, sessionId: hookEvent.sessionId },
|
|
22
|
+
});
|
|
23
|
+
try {
|
|
24
|
+
await (0, socket_client_1.sendDaemonCommand)(socket_client_1.DEFAULT_DAEMON_SOCKET_PATH, {
|
|
25
|
+
kind: "hook.event",
|
|
26
|
+
event: hookEvent.event,
|
|
27
|
+
sessionId: hookEvent.sessionId,
|
|
28
|
+
toolName: hookEvent.toolName,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Daemon unavailable — silently ignore. Hooks must not block.
|
|
33
|
+
(0, runtime_1.emitNervesEvent)({
|
|
34
|
+
component: "daemon",
|
|
35
|
+
event: "daemon.hook_event_daemon_unavailable",
|
|
36
|
+
message: "daemon unavailable for hook event",
|
|
37
|
+
meta: { hookEvent: hookEvent.event },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return { exitCode: 0 };
|
|
41
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Claude Code stop hook — checks the agent's pending queue and returns
|
|
3
|
+
// any messages as additionalContext for injection into the next turn.
|
|
4
|
+
//
|
|
5
|
+
// This is how the agent can proactively communicate back to the dev tool user:
|
|
6
|
+
// the agent surfaces a message to the pending queue, and the stop hook picks
|
|
7
|
+
// it up and injects it as context in the Claude Code session.
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.handleStopHook = handleStopHook;
|
|
10
|
+
const pending_1 = require("../mind/pending");
|
|
11
|
+
const runtime_1 = require("../nerves/runtime");
|
|
12
|
+
/**
|
|
13
|
+
* Check the pending queue for messages from the agent to this dev tool session.
|
|
14
|
+
* Returns accumulated message text as additionalContext.
|
|
15
|
+
*/
|
|
16
|
+
async function handleStopHook(input) {
|
|
17
|
+
(0, runtime_1.emitNervesEvent)({
|
|
18
|
+
component: "daemon",
|
|
19
|
+
event: "daemon.stop_hook_check_start",
|
|
20
|
+
message: "checking pending queue for stop hook",
|
|
21
|
+
meta: { agentName: input.agentName, friendId: input.friendId, sessionId: input.sessionId },
|
|
22
|
+
});
|
|
23
|
+
try {
|
|
24
|
+
const pendingDir = (0, pending_1.getPendingDir)(input.agentName, input.friendId, "mcp", input.sessionId);
|
|
25
|
+
const pending = (0, pending_1.drainPending)(pendingDir);
|
|
26
|
+
if (pending.length === 0) {
|
|
27
|
+
return { additionalContext: "" };
|
|
28
|
+
}
|
|
29
|
+
const text = pending.map((m) => m.content).join("\n\n---\n\n");
|
|
30
|
+
(0, runtime_1.emitNervesEvent)({
|
|
31
|
+
component: "daemon",
|
|
32
|
+
event: "daemon.stop_hook_check_end",
|
|
33
|
+
message: "pending messages found for stop hook",
|
|
34
|
+
meta: { agentName: input.agentName, count: pending.length },
|
|
35
|
+
});
|
|
36
|
+
return { additionalContext: text };
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
(0, runtime_1.emitNervesEvent)({
|
|
40
|
+
component: "daemon",
|
|
41
|
+
event: "daemon.stop_hook_check_error",
|
|
42
|
+
message: "error checking pending queue in stop hook",
|
|
43
|
+
meta: { agentName: input.agentName },
|
|
44
|
+
});
|
|
45
|
+
return { additionalContext: "" };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -480,5 +480,53 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
|
|
|
480
480
|
return applyRepairNotice(event, `BlueBubbles repair failed: ${reason}`);
|
|
481
481
|
}
|
|
482
482
|
},
|
|
483
|
+
async getMessageText(messageGuid) {
|
|
484
|
+
const url = buildRepairUrl(config.serverUrl, messageGuid, config.password);
|
|
485
|
+
try {
|
|
486
|
+
const response = await fetch(url, {
|
|
487
|
+
method: "GET",
|
|
488
|
+
signal: AbortSignal.timeout(channelConfig.requestTimeoutMs),
|
|
489
|
+
});
|
|
490
|
+
if (!response.ok) {
|
|
491
|
+
(0, runtime_1.emitNervesEvent)({
|
|
492
|
+
level: "warn",
|
|
493
|
+
component: "senses",
|
|
494
|
+
event: "senses.bluebubbles_get_message_text_error",
|
|
495
|
+
message: "failed to fetch message text",
|
|
496
|
+
meta: { messageGuid, status: response.status },
|
|
497
|
+
});
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
const payload = await parseJsonBody(response);
|
|
501
|
+
const data = extractRepairData(payload);
|
|
502
|
+
if (!data || typeof data.text !== "string") {
|
|
503
|
+
(0, runtime_1.emitNervesEvent)({
|
|
504
|
+
level: "warn",
|
|
505
|
+
component: "senses",
|
|
506
|
+
event: "senses.bluebubbles_get_message_text_error",
|
|
507
|
+
message: "message payload missing text field",
|
|
508
|
+
meta: { messageGuid, hasData: !!data, textType: data ? typeof data.text : "n/a" },
|
|
509
|
+
});
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
(0, runtime_1.emitNervesEvent)({
|
|
513
|
+
component: "senses",
|
|
514
|
+
event: "senses.bluebubbles_get_message_text",
|
|
515
|
+
message: "fetched message text by guid",
|
|
516
|
+
meta: { messageGuid },
|
|
517
|
+
});
|
|
518
|
+
return data.text.trim() || null;
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
(0, runtime_1.emitNervesEvent)({
|
|
522
|
+
level: "warn",
|
|
523
|
+
component: "senses",
|
|
524
|
+
event: "senses.bluebubbles_get_message_text_error",
|
|
525
|
+
message: "exception fetching message text",
|
|
526
|
+
meta: { messageGuid, reason: error instanceof Error ? error.message : String(error) },
|
|
527
|
+
});
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
},
|
|
483
531
|
};
|
|
484
532
|
}
|
|
@@ -200,7 +200,7 @@ function extractHistoricalLaneSummary(messages) {
|
|
|
200
200
|
}
|
|
201
201
|
return summaries;
|
|
202
202
|
}
|
|
203
|
-
function buildConversationScopePrefix(event, existingMessages) {
|
|
203
|
+
function buildConversationScopePrefix(event, existingMessages, repliedToText) {
|
|
204
204
|
if (event.kind !== "message") {
|
|
205
205
|
return "";
|
|
206
206
|
}
|
|
@@ -208,6 +208,10 @@ function buildConversationScopePrefix(event, existingMessages) {
|
|
|
208
208
|
const lines = [];
|
|
209
209
|
if (event.threadOriginatorGuid?.trim()) {
|
|
210
210
|
lines.push(`[conversation scope: existing chat trunk | current inbound lane: thread | current thread id: ${event.threadOriginatorGuid.trim()} | default outbound target for this turn: current_lane]`);
|
|
211
|
+
if (repliedToText) {
|
|
212
|
+
lines.push(`[replying to: "${repliedToText}"]`);
|
|
213
|
+
}
|
|
214
|
+
lines.push(`[if you need more context about what was being discussed, use query_session to search your session history, or recall to check your memory.]`);
|
|
211
215
|
}
|
|
212
216
|
else {
|
|
213
217
|
lines.push("[conversation scope: existing chat trunk | current inbound lane: top_level | default outbound target for this turn: top_level]");
|
|
@@ -223,8 +227,8 @@ function buildConversationScopePrefix(event, existingMessages) {
|
|
|
223
227
|
}
|
|
224
228
|
return lines.join("\n");
|
|
225
229
|
}
|
|
226
|
-
function buildInboundText(event, existingMessages) {
|
|
227
|
-
const metadataPrefix = buildConversationScopePrefix(event, existingMessages);
|
|
230
|
+
function buildInboundText(event, existingMessages, repliedToText) {
|
|
231
|
+
const metadataPrefix = buildConversationScopePrefix(event, existingMessages, repliedToText);
|
|
228
232
|
const baseText = event.repairNotice?.trim()
|
|
229
233
|
? `${event.textForAgent}\n[${event.repairNotice.trim()}]`
|
|
230
234
|
: event.textForAgent;
|
|
@@ -237,8 +241,8 @@ function buildInboundText(event, existingMessages) {
|
|
|
237
241
|
}
|
|
238
242
|
return `${event.sender.displayName}: ${scopedText}`;
|
|
239
243
|
}
|
|
240
|
-
function buildInboundContent(event, existingMessages) {
|
|
241
|
-
const text = buildInboundText(event, existingMessages);
|
|
244
|
+
function buildInboundContent(event, existingMessages, repliedToText) {
|
|
245
|
+
const text = buildInboundText(event, existingMessages, repliedToText);
|
|
242
246
|
if (event.kind !== "message" || !event.inputPartsForAgent || event.inputPartsForAgent.length === 0) {
|
|
243
247
|
return text;
|
|
244
248
|
}
|
|
@@ -607,10 +611,22 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
|
|
|
607
611
|
groupExternalId: resolveGroupExternalId(event),
|
|
608
612
|
});
|
|
609
613
|
}
|
|
614
|
+
// Fetch the text of the message being replied to (if this is a threaded reply)
|
|
615
|
+
const threadGuid = event.kind === "message" ? event.threadOriginatorGuid?.trim() : undefined;
|
|
616
|
+
let repliedToText = null;
|
|
617
|
+
if (threadGuid) {
|
|
618
|
+
repliedToText = await client.getMessageText(threadGuid).catch(/* v8 ignore next */ () => null);
|
|
619
|
+
(0, runtime_1.emitNervesEvent)({
|
|
620
|
+
component: "senses",
|
|
621
|
+
event: "senses.bluebubbles_reply_context",
|
|
622
|
+
message: repliedToText ? "fetched replied-to message text" : "could not fetch replied-to message text",
|
|
623
|
+
meta: { threadGuid, hasText: !!repliedToText },
|
|
624
|
+
});
|
|
625
|
+
}
|
|
610
626
|
// Build inbound user message (adapter concern: BB-specific content formatting)
|
|
611
627
|
const userMessage = {
|
|
612
628
|
role: "user",
|
|
613
|
-
content: buildInboundContent(event, existing?.messages ?? sessionMessages),
|
|
629
|
+
content: buildInboundContent(event, existing?.messages ?? sessionMessages, repliedToText),
|
|
614
630
|
};
|
|
615
631
|
const callbacks = createBlueBubblesCallbacks(client, event.chat, replyTarget, event.chat.isGroup);
|
|
616
632
|
const controller = new AbortController();
|
|
@@ -397,6 +397,21 @@ async function routeDelegatedCompletion(agentRoot, agentName, completion, draine
|
|
|
397
397
|
advanceObligationQuietly(agentName, obligationId, { status: "returned", returnedAt: timestamp, returnTarget: "bridge-session" });
|
|
398
398
|
return;
|
|
399
399
|
}
|
|
400
|
+
// Priority 1.5: Direct return to originating session (ponder without bridge).
|
|
401
|
+
// When delegatedFrom has specific channel+key, route directly there instead of searching for freshest.
|
|
402
|
+
if (delegatedFrom.channel && delegatedFrom.key && delegatedFrom.channel !== "inner") {
|
|
403
|
+
const directTarget = sessionActivity.find((a) => a.friendId === delegatedFrom.friendId && a.channel === delegatedFrom.channel && a.key === delegatedFrom.key);
|
|
404
|
+
if (directTarget) {
|
|
405
|
+
if (await tryDeliverDelegatedCompletion(directTarget, outboundEnvelope)) {
|
|
406
|
+
advanceObligationQuietly(agentName, obligationId, { status: "returned", returnedAt: timestamp, returnTarget: "direct-originator" });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// Even if session isn't in activity list (might have ended), queue to its pending dir
|
|
411
|
+
writePendingEnvelope((0, pending_1.getPendingDir)(agentName, delegatedFrom.friendId, delegatedFrom.channel, delegatedFrom.key), outboundEnvelope);
|
|
412
|
+
advanceObligationQuietly(agentName, obligationId, { status: "returned", returnedAt: timestamp, returnTarget: "direct-originator" });
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
400
415
|
// Priority 2: Freshest active friend session.
|
|
401
416
|
const freshest = (0, session_activity_1.findFreshestFriendSession)({
|
|
402
417
|
sessionsDir: path.join(agentRoot, "state", "sessions"),
|
package/dist/senses/pipeline.js
CHANGED
|
@@ -13,6 +13,7 @@ const identity_1 = require("../heart/identity");
|
|
|
13
13
|
const tasks_1 = require("../repertoire/tasks");
|
|
14
14
|
const coding_1 = require("../repertoire/coding");
|
|
15
15
|
const session_activity_1 = require("../heart/session-activity");
|
|
16
|
+
const socket_client_1 = require("../heart/daemon/socket-client");
|
|
16
17
|
const active_work_1 = require("../heart/active-work");
|
|
17
18
|
const delegation_1 = require("../heart/delegation");
|
|
18
19
|
const target_resolution_1 = require("../heart/target-resolution");
|
|
@@ -440,6 +441,14 @@ async function handleInboundTurn(input) {
|
|
|
440
441
|
friendId: resolvedContext.friend.id,
|
|
441
442
|
},
|
|
442
443
|
});
|
|
444
|
+
// DRY cross-session awareness: notify inner dialog that activity happened on another channel
|
|
445
|
+
// Inner dialog's next checkpoint will include this session's state
|
|
446
|
+
if (input.channel !== "inner") {
|
|
447
|
+
try {
|
|
448
|
+
(0, socket_client_1.requestInnerWake)((0, identity_1.getAgentName)()).catch(/* v8 ignore next */ () => { });
|
|
449
|
+
}
|
|
450
|
+
catch { /* getAgentName may fail in test environments */ }
|
|
451
|
+
}
|
|
443
452
|
return {
|
|
444
453
|
resolvedContext,
|
|
445
454
|
gateResult,
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Shared turn runner for non-interactive senses (MCP, future senses).
|
|
3
|
+
// Follows the CLI pattern: resolves context, constructs InboundTurnInput,
|
|
4
|
+
// calls handleInboundTurn, collects response text, detects ponder deferral.
|
|
5
|
+
//
|
|
6
|
+
// Does NOT refactor CLI — CLI is stable with 2280+ tests. This is a new
|
|
7
|
+
// code path for new senses that follows the same pipeline pattern.
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.runSenseTurn = runSenseTurn;
|
|
43
|
+
const os = __importStar(require("os"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const core_1 = require("../heart/core");
|
|
46
|
+
const identity_1 = require("../heart/identity");
|
|
47
|
+
const config_1 = require("../heart/config");
|
|
48
|
+
const context_1 = require("../mind/context");
|
|
49
|
+
const prompt_1 = require("../mind/prompt");
|
|
50
|
+
const channel_1 = require("../mind/friends/channel");
|
|
51
|
+
const resolver_1 = require("../mind/friends/resolver");
|
|
52
|
+
const store_file_1 = require("../mind/friends/store-file");
|
|
53
|
+
const pending_1 = require("../mind/pending");
|
|
54
|
+
const context_2 = require("../mind/context");
|
|
55
|
+
const tokens_1 = require("../mind/friends/tokens");
|
|
56
|
+
const trust_gate_1 = require("./trust-gate");
|
|
57
|
+
const pipeline_1 = require("./pipeline");
|
|
58
|
+
const runtime_1 = require("../nerves/runtime");
|
|
59
|
+
const RESPONSE_CAP = 50_000;
|
|
60
|
+
/**
|
|
61
|
+
* Run a single agent turn through the inbound pipeline.
|
|
62
|
+
* Caller provides channel, session key, friend, and message;
|
|
63
|
+
* this function handles all pipeline wiring.
|
|
64
|
+
*/
|
|
65
|
+
async function runSenseTurn(options) {
|
|
66
|
+
const { agentName, channel, sessionKey, friendId, userMessage } = options;
|
|
67
|
+
(0, runtime_1.emitNervesEvent)({
|
|
68
|
+
component: "senses",
|
|
69
|
+
event: "senses.shared_turn_start",
|
|
70
|
+
message: "shared turn runner starting",
|
|
71
|
+
meta: { agentName, channel, sessionKey, friendId },
|
|
72
|
+
});
|
|
73
|
+
// Resolve context
|
|
74
|
+
const agentRoot = (0, identity_1.getAgentRoot)(agentName);
|
|
75
|
+
const friendsPath = path.join(agentRoot, "friends");
|
|
76
|
+
const friendStore = new store_file_1.FileFriendStore(friendsPath);
|
|
77
|
+
const capabilities = (0, channel_1.getChannelCapabilities)(channel);
|
|
78
|
+
// If friendId looks like a UUID, look up the friend record directly and use its identity.
|
|
79
|
+
// Otherwise, resolve as a local user (same pattern as CLI sense).
|
|
80
|
+
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-/.test(friendId);
|
|
81
|
+
let resolverParams;
|
|
82
|
+
if (isUuid) {
|
|
83
|
+
const existingFriend = await friendStore.get(friendId);
|
|
84
|
+
if (existingFriend) {
|
|
85
|
+
// Use the friend's first external ID for resolver context
|
|
86
|
+
const ext = existingFriend.externalIds?.[0];
|
|
87
|
+
resolverParams = {
|
|
88
|
+
provider: (ext?.provider ?? "local"),
|
|
89
|
+
externalId: ext?.externalId ?? friendId,
|
|
90
|
+
displayName: existingFriend.name ?? friendId,
|
|
91
|
+
channel,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
resolverParams = { provider: "local", externalId: friendId, displayName: friendId, channel };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// Treat as local user identity (username@hostname pattern)
|
|
100
|
+
const username = os.userInfo().username;
|
|
101
|
+
const hostname = os.hostname();
|
|
102
|
+
resolverParams = { provider: "local", externalId: `${username}@${hostname}`, displayName: username, channel };
|
|
103
|
+
}
|
|
104
|
+
const resolver = new resolver_1.FriendResolver(friendStore, resolverParams);
|
|
105
|
+
// Session path and loading
|
|
106
|
+
const sessPath = (0, config_1.sessionPath)(friendId, channel, sessionKey);
|
|
107
|
+
const existing = (0, context_1.loadSession)(sessPath);
|
|
108
|
+
let sessionState = existing?.state;
|
|
109
|
+
const sessionMessages = existing?.messages && existing.messages.length > 0
|
|
110
|
+
? existing.messages
|
|
111
|
+
: [{ role: "system", content: await (0, prompt_1.buildSystem)(channel, {}, undefined) }];
|
|
112
|
+
// Pending dir
|
|
113
|
+
const pendingDir = (0, pending_1.getPendingDir)(agentName, friendId, channel, sessionKey);
|
|
114
|
+
// Accumulate response text via callbacks
|
|
115
|
+
let responseText = "";
|
|
116
|
+
/* v8 ignore start — no-op callback stubs; only onTextChunk does real work (covered via mock) */
|
|
117
|
+
const callbacks = {
|
|
118
|
+
onModelStart: () => { },
|
|
119
|
+
onModelStreamStart: () => { },
|
|
120
|
+
onTextChunk: (chunk) => { responseText += chunk; },
|
|
121
|
+
onReasoningChunk: () => { },
|
|
122
|
+
onToolStart: () => { },
|
|
123
|
+
onToolEnd: () => { },
|
|
124
|
+
onError: () => { },
|
|
125
|
+
};
|
|
126
|
+
/* v8 ignore stop */
|
|
127
|
+
// Run the pipeline
|
|
128
|
+
const result = await (0, pipeline_1.handleInboundTurn)({
|
|
129
|
+
channel,
|
|
130
|
+
sessionKey,
|
|
131
|
+
capabilities,
|
|
132
|
+
messages: [{ role: "user", content: userMessage }],
|
|
133
|
+
callbacks,
|
|
134
|
+
/* v8 ignore start — delegation wrappers; pipeline integration tested separately */
|
|
135
|
+
friendResolver: { resolve: () => resolver.resolve() },
|
|
136
|
+
sessionLoader: {
|
|
137
|
+
loadOrCreate: () => Promise.resolve({
|
|
138
|
+
messages: sessionMessages,
|
|
139
|
+
sessionPath: sessPath,
|
|
140
|
+
state: sessionState,
|
|
141
|
+
}),
|
|
142
|
+
},
|
|
143
|
+
/* v8 ignore stop */
|
|
144
|
+
pendingDir,
|
|
145
|
+
friendStore,
|
|
146
|
+
provider: "local",
|
|
147
|
+
externalId: friendId,
|
|
148
|
+
enforceTrustGate: trust_gate_1.enforceTrustGate,
|
|
149
|
+
drainPending: pending_1.drainPending,
|
|
150
|
+
/* v8 ignore start — delegation wrappers; these just forward to the real functions */
|
|
151
|
+
runAgent: (msgs, cb, ch, sig, opts) => (0, core_1.runAgent)(msgs, cb, ch, sig, opts),
|
|
152
|
+
postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
|
|
153
|
+
(0, context_2.postTurn)(turnMessages, sessionPathArg, usage, hooks, state);
|
|
154
|
+
sessionState = state;
|
|
155
|
+
},
|
|
156
|
+
/* v8 ignore stop */
|
|
157
|
+
accumulateFriendTokens: tokens_1.accumulateFriendTokens,
|
|
158
|
+
});
|
|
159
|
+
// Detect ponder deferral
|
|
160
|
+
const ponderDeferred = result.turnOutcome === "pondered";
|
|
161
|
+
// Build response
|
|
162
|
+
let finalResponse;
|
|
163
|
+
if (ponderDeferred) {
|
|
164
|
+
finalResponse = "the agent is pondering this — check back shortly via check_response.";
|
|
165
|
+
}
|
|
166
|
+
else if (responseText.length === 0) {
|
|
167
|
+
// Agent settled but no text came through callbacks — check session transcript for the settle answer
|
|
168
|
+
const postTurnSession = (0, context_1.loadSession)(sessPath);
|
|
169
|
+
if (postTurnSession?.messages) {
|
|
170
|
+
const lastAssistant = [...postTurnSession.messages].reverse().find(m => m.role === "assistant");
|
|
171
|
+
if (lastAssistant && typeof lastAssistant.content === "string" && lastAssistant.content.trim()) {
|
|
172
|
+
finalResponse = lastAssistant.content;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
finalResponse = "(agent responded but response was empty)";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
finalResponse = "(agent responded but response was empty)";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
finalResponse = responseText;
|
|
184
|
+
}
|
|
185
|
+
// Cap response length
|
|
186
|
+
if (finalResponse.length > RESPONSE_CAP) {
|
|
187
|
+
finalResponse = finalResponse.slice(0, RESPONSE_CAP) + "\n\n[truncated — response exceeded 50K characters]";
|
|
188
|
+
}
|
|
189
|
+
(0, runtime_1.emitNervesEvent)({
|
|
190
|
+
component: "senses",
|
|
191
|
+
event: "senses.shared_turn_end",
|
|
192
|
+
message: "shared turn runner complete",
|
|
193
|
+
meta: { agentName, channel, sessionKey, friendId, ponderDeferred, responseLength: finalResponse.length },
|
|
194
|
+
});
|
|
195
|
+
return { response: finalResponse, ponderDeferred };
|
|
196
|
+
}
|