@ouro.bot/cli 0.1.0-alpha.38 → 0.1.0-alpha.39

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.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.teamsTools = exports.finalAnswerTool = exports.tools = void 0;
3
+ exports.REMOTE_BLOCKED_LOCAL_TOOLS = exports.teamsTools = exports.finalAnswerTool = exports.tools = void 0;
4
4
  exports.getToolsForChannel = getToolsForChannel;
5
5
  exports.isConfirmationRequired = isConfirmationRequired;
6
6
  exports.execTool = execTool;
@@ -10,6 +10,8 @@ const tools_teams_1 = require("./tools-teams");
10
10
  const tools_bluebubbles_1 = require("./tools-bluebubbles");
11
11
  const ado_semantic_1 = require("./ado-semantic");
12
12
  const tools_github_1 = require("./tools-github");
13
+ const types_1 = require("../mind/friends/types");
14
+ const channel_1 = require("../mind/friends/channel");
13
15
  const runtime_1 = require("../nerves/runtime");
14
16
  // Re-export types and constants used by the rest of the codebase
15
17
  var tools_base_2 = require("./tools-base");
@@ -19,32 +21,25 @@ var tools_teams_2 = require("./tools-teams");
19
21
  Object.defineProperty(exports, "teamsTools", { enumerable: true, get: function () { return tools_teams_2.teamsTools; } });
20
22
  // All tool definitions in a single registry
21
23
  const allDefinitions = [...tools_base_1.baseToolDefinitions, ...tools_bluebubbles_1.bluebubblesToolDefinitions, ...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions];
22
- const REMOTE_BLOCKED_LOCAL_TOOLS = new Set(["shell", "read_file", "write_file", "edit_file", "glob", "grep"]);
23
- function isRemoteChannel(capabilities) {
24
- return capabilities?.channel === "teams" || capabilities?.channel === "bluebubbles";
25
- }
26
- function isSharedRemoteContext(friend) {
27
- const externalIds = friend.externalIds ?? [];
28
- return externalIds.some((externalId) => externalId.externalId.startsWith("group:") || externalId.provider === "teams-conversation");
29
- }
24
+ /** Tool names blocked for untrusted remote contexts. Shared with prompt.ts for restriction messaging. */
25
+ exports.REMOTE_BLOCKED_LOCAL_TOOLS = new Set(["shell", "read_file", "write_file", "edit_file", "glob", "grep"]);
30
26
  function isTrustedRemoteContext(context) {
31
- if (!context?.friend || !isRemoteChannel(context.channel))
27
+ if (!context?.friend || !(0, channel_1.isRemoteChannel)(context.channel))
32
28
  return false;
33
- const trustLevel = context.friend.trustLevel ?? "stranger";
34
- return trustLevel !== "stranger" && !isSharedRemoteContext(context.friend);
29
+ return (0, types_1.isTrustedLevel)(context.friend.trustLevel);
35
30
  }
36
31
  function shouldBlockLocalTools(capabilities, context) {
37
- if (!isRemoteChannel(capabilities))
32
+ if (!(0, channel_1.isRemoteChannel)(capabilities))
38
33
  return false;
39
34
  return !isTrustedRemoteContext(context);
40
35
  }
41
36
  function blockedLocalToolMessage() {
42
- return "I can't do that from here because I'm talking to multiple people in a shared remote channel, and local shell/file/git/gh operations could let conversations interfere with each other. Ask me for a remote-safe alternative (Graph/ADO/web), or run that operation from CLI.";
37
+ return "I can't do that because my trust level with you isn't high enough for local shell/file operations. Ask me for a remote-safe alternative (Graph/ADO/web), or run that operation from CLI.";
43
38
  }
44
39
  function baseToolsForCapabilities(capabilities, context) {
45
40
  if (!shouldBlockLocalTools(capabilities, context))
46
41
  return tools_base_1.tools;
47
- return tools_base_1.tools.filter((tool) => !REMOTE_BLOCKED_LOCAL_TOOLS.has(tool.function.name));
42
+ return tools_base_1.tools.filter((tool) => !exports.REMOTE_BLOCKED_LOCAL_TOOLS.has(tool.function.name));
48
43
  }
49
44
  // Apply a single tool preference to a tool schema, returning a new object.
50
45
  function applyPreference(tool, pref) {
@@ -113,7 +108,7 @@ async function execTool(name, args, ctx) {
113
108
  });
114
109
  return `unknown: ${name}`;
115
110
  }
116
- if (shouldBlockLocalTools(ctx?.context?.channel, ctx?.context) && REMOTE_BLOCKED_LOCAL_TOOLS.has(name)) {
111
+ if (shouldBlockLocalTools(ctx?.context?.channel, ctx?.context) && exports.REMOTE_BLOCKED_LOCAL_TOOLS.has(name)) {
117
112
  const message = blockedLocalToolMessage();
118
113
  (0, runtime_1.emitNervesEvent)({
119
114
  level: "warn",
@@ -52,6 +52,15 @@ function buildChatRef(data, threadOriginatorGuid) {
52
52
  const sessionKey = chatGuid?.trim()
53
53
  ? `chat:${chatGuid.trim()}`
54
54
  : `chat_identifier:${(chatIdentifier ?? "unknown").trim()}`;
55
+ // Extract participant handles from chat.participants (when available from BB API)
56
+ const rawParticipants = Array.isArray(chat?.participants) ? chat.participants : [];
57
+ const participantHandles = rawParticipants
58
+ .map((p) => {
59
+ const rec = asRecord(p);
60
+ const addr = readString(rec, "address") ?? readString(rec, "id");
61
+ return addr ? normalizeHandle(addr) : "";
62
+ })
63
+ .filter(Boolean);
55
64
  return {
56
65
  chatGuid: chatGuid?.trim() || undefined,
57
66
  chatIdentifier: chatIdentifier?.trim() || undefined,
@@ -61,6 +70,7 @@ function buildChatRef(data, threadOriginatorGuid) {
61
70
  sendTarget: chatGuid?.trim()
62
71
  ? { kind: "chat_guid", value: chatGuid.trim() }
63
72
  : { kind: "chat_identifier", value: (chatIdentifier ?? "unknown").trim() },
73
+ participantHandles,
64
74
  };
65
75
  }
66
76
  function extractSender(data, chat) {
@@ -47,6 +47,9 @@ const context_1 = require("../mind/context");
47
47
  const tokens_1 = require("../mind/friends/tokens");
48
48
  const resolver_1 = require("../mind/friends/resolver");
49
49
  const store_file_1 = require("../mind/friends/store-file");
50
+ const types_1 = require("../mind/friends/types");
51
+ const channel_1 = require("../mind/friends/channel");
52
+ const pending_1 = require("../mind/pending");
50
53
  const prompt_1 = require("../mind/prompt");
51
54
  const phrases_1 = require("../mind/phrases");
52
55
  const runtime_1 = require("../nerves/runtime");
@@ -55,6 +58,8 @@ const bluebubbles_client_1 = require("./bluebubbles-client");
55
58
  const bluebubbles_mutation_log_1 = require("./bluebubbles-mutation-log");
56
59
  const bluebubbles_session_cleanup_1 = require("./bluebubbles-session-cleanup");
57
60
  const debug_activity_1 = require("./debug-activity");
61
+ const trust_gate_1 = require("./trust-gate");
62
+ const pipeline_1 = require("./pipeline");
58
63
  const defaultDeps = {
59
64
  getAgentName: identity_1.getAgentName,
60
65
  buildSystem: prompt_1.buildSystem,
@@ -86,6 +91,47 @@ function resolveFriendParams(event) {
86
91
  channel: "bluebubbles",
87
92
  };
88
93
  }
94
+ /**
95
+ * Check if any participant in a group chat is a known family member.
96
+ * Looks up each participant handle in the friend store.
97
+ */
98
+ async function checkGroupHasFamilyMember(store, event) {
99
+ if (!event.chat.isGroup)
100
+ return false;
101
+ for (const handle of event.chat.participantHandles ?? []) {
102
+ const friend = await store.findByExternalId("imessage-handle", handle);
103
+ if (friend?.trustLevel === "family")
104
+ return true;
105
+ }
106
+ return false;
107
+ }
108
+ /**
109
+ * Check if an acquaintance shares any group chat with a family member.
110
+ * Compares group-prefixed externalIds between the acquaintance and all family members.
111
+ */
112
+ async function checkHasExistingGroupWithFamily(store, senderFriend) {
113
+ const trustLevel = senderFriend.trustLevel ?? "friend";
114
+ if (trustLevel !== "acquaintance")
115
+ return false;
116
+ const acquaintanceGroups = new Set((senderFriend.externalIds ?? [])
117
+ .filter((eid) => eid.externalId.startsWith("group:"))
118
+ .map((eid) => eid.externalId));
119
+ if (acquaintanceGroups.size === 0)
120
+ return false;
121
+ const allFriends = await (store.listAll?.() ?? Promise.resolve([]));
122
+ for (const friend of allFriends) {
123
+ if (friend.trustLevel !== "family")
124
+ continue;
125
+ const friendGroups = (friend.externalIds ?? [])
126
+ .filter((eid) => eid.externalId.startsWith("group:"))
127
+ .map((eid) => eid.externalId);
128
+ for (const group of friendGroups) {
129
+ if (acquaintanceGroups.has(group))
130
+ return true;
131
+ }
132
+ }
133
+ return false;
134
+ }
89
135
  function extractMessageText(content) {
90
136
  if (typeof content === "string")
91
137
  return content;
@@ -414,28 +460,12 @@ async function handleBlueBubblesEvent(payload, deps = {}) {
414
460
  });
415
461
  return { handled: true, notifiedAgent: false, kind: event.kind, reason: "mutation_state_only" };
416
462
  }
463
+ // ── Adapter setup: friend, session, content, callbacks ──────────
417
464
  const store = resolvedDeps.createFriendStore();
418
465
  const resolver = resolvedDeps.createFriendResolver(store, resolveFriendParams(event));
419
- const context = await resolver.resolve();
466
+ const baseContext = await resolver.resolve();
467
+ const context = { ...baseContext, isGroupChat: event.chat.isGroup };
420
468
  const replyTarget = createReplyTargetController(event);
421
- const toolContext = {
422
- signin: async () => undefined,
423
- friendStore: store,
424
- summarize: (0, core_1.createSummarize)(),
425
- context,
426
- bluebubblesReplyTarget: {
427
- setSelection: (selection) => replyTarget.setSelection(selection),
428
- },
429
- codingFeedback: {
430
- send: async (message) => {
431
- await client.sendText({
432
- chat: event.chat,
433
- text: message,
434
- replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
435
- });
436
- },
437
- },
438
- };
439
469
  const friendId = context.friend.id;
440
470
  const sessPath = resolvedDeps.sessionPath(friendId, "bluebubbles", event.chat.sessionKey);
441
471
  try {
@@ -453,21 +483,87 @@ async function handleBlueBubblesEvent(payload, deps = {}) {
453
483
  },
454
484
  });
455
485
  }
486
+ // Pre-load session (adapter needs existing messages for lane history in content building)
456
487
  const existing = resolvedDeps.loadSession(sessPath);
457
- const messages = existing?.messages && existing.messages.length > 0
488
+ const sessionMessages = existing?.messages && existing.messages.length > 0
458
489
  ? existing.messages
459
490
  : [{ role: "system", content: await resolvedDeps.buildSystem("bluebubbles", undefined, context) }];
460
- messages.push({ role: "user", content: buildInboundContent(event, existing?.messages ?? messages) });
491
+ // Build inbound user message (adapter concern: BB-specific content formatting)
492
+ const userMessage = {
493
+ role: "user",
494
+ content: buildInboundContent(event, existing?.messages ?? sessionMessages),
495
+ };
461
496
  const callbacks = createBlueBubblesCallbacks(client, event.chat, replyTarget);
462
497
  const controller = new AbortController();
463
- const agentOptions = {
464
- toolContext,
465
- };
498
+ // BB-specific tool context wrappers
499
+ const summarize = (0, core_1.createSummarize)();
500
+ const bbCapabilities = (0, channel_1.getChannelCapabilities)("bluebubbles");
501
+ const pendingDir = (0, pending_1.getPendingDir)(resolvedDeps.getAgentName(), friendId, "bluebubbles", event.chat.sessionKey);
502
+ // ── Compute trust gate context for group/acquaintance rules ─────
503
+ const groupHasFamilyMember = await checkGroupHasFamilyMember(store, event);
504
+ const hasExistingGroupWithFamily = event.chat.isGroup
505
+ ? false
506
+ : await checkHasExistingGroupWithFamily(store, context.friend);
507
+ // ── Call shared pipeline ──────────────────────────────────────────
466
508
  try {
467
- const result = await resolvedDeps.runAgent(messages, callbacks, "bluebubbles", controller.signal, agentOptions);
509
+ const result = await (0, pipeline_1.handleInboundTurn)({
510
+ channel: "bluebubbles",
511
+ capabilities: bbCapabilities,
512
+ messages: [userMessage],
513
+ callbacks,
514
+ friendResolver: { resolve: () => Promise.resolve(context) },
515
+ sessionLoader: { loadOrCreate: () => Promise.resolve({ messages: sessionMessages, sessionPath: sessPath }) },
516
+ pendingDir,
517
+ friendStore: store,
518
+ provider: "imessage-handle",
519
+ externalId: event.sender.externalId || event.sender.rawId,
520
+ isGroupChat: event.chat.isGroup,
521
+ groupHasFamilyMember,
522
+ hasExistingGroupWithFamily,
523
+ enforceTrustGate: trust_gate_1.enforceTrustGate,
524
+ drainPending: pending_1.drainPending,
525
+ runAgent: (msgs, cb, channel, sig, opts) => resolvedDeps.runAgent(msgs, cb, channel, sig, {
526
+ ...opts,
527
+ toolContext: {
528
+ /* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
529
+ signin: async () => undefined,
530
+ ...opts?.toolContext,
531
+ summarize,
532
+ bluebubblesReplyTarget: {
533
+ setSelection: (selection) => replyTarget.setSelection(selection),
534
+ },
535
+ codingFeedback: {
536
+ send: async (message) => {
537
+ await client.sendText({
538
+ chat: event.chat,
539
+ text: message,
540
+ replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
541
+ });
542
+ },
543
+ },
544
+ },
545
+ }),
546
+ postTurn: resolvedDeps.postTurn,
547
+ accumulateFriendTokens: resolvedDeps.accumulateFriendTokens,
548
+ signal: controller.signal,
549
+ });
550
+ // ── Handle gate result ────────────────────────────────────────
551
+ if (!result.gateResult.allowed) {
552
+ // Send auto-reply via BB API if the gate provides one
553
+ if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
554
+ await client.sendText({
555
+ chat: event.chat,
556
+ text: result.gateResult.autoReply,
557
+ });
558
+ }
559
+ return {
560
+ handled: true,
561
+ notifiedAgent: false,
562
+ kind: event.kind,
563
+ };
564
+ }
565
+ // Gate allowed — flush the agent's reply
468
566
  await callbacks.flush();
469
- resolvedDeps.postTurn(messages, sessPath, result.usage);
470
- await resolvedDeps.accumulateFriendTokens(store, friendId, result.usage);
471
567
  (0, runtime_1.emitNervesEvent)({
472
568
  component: "senses",
473
569
  event: "senses.bluebubbles_turn_end",
@@ -543,7 +639,6 @@ function createBlueBubblesWebhookHandler(deps = {}) {
543
639
  }
544
640
  };
545
641
  }
546
- const PROACTIVE_SEND_ALLOWED_TRUST = new Set(["family", "friend"]);
547
642
  function findImessageHandle(friend) {
548
643
  for (const ext of friend.externalIds) {
549
644
  if (ext.provider === "imessage-handle" && !ext.externalId.startsWith("group:")) {
@@ -644,7 +739,7 @@ async function drainAndSendPendingBlueBubbles(deps = {}, pendingRoot) {
644
739
  });
645
740
  continue;
646
741
  }
647
- if (!PROACTIVE_SEND_ALLOWED_TRUST.has(friend.trustLevel ?? "stranger")) {
742
+ if (!types_1.TRUSTED_LEVELS.has(friend.trustLevel ?? "stranger")) {
648
743
  result.skipped++;
649
744
  try {
650
745
  fs.unlinkSync(filePath);
@@ -679,6 +774,7 @@ async function drainAndSendPendingBlueBubbles(deps = {}, pendingRoot) {
679
774
  isGroup: false,
680
775
  sessionKey: friendId,
681
776
  sendTarget: { kind: "chat_identifier", value: handle },
777
+ participantHandles: [],
682
778
  };
683
779
  try {
684
780
  await client.sendText({ chat, text: messageText });
@@ -52,7 +52,6 @@ const format_1 = require("../mind/format");
52
52
  const config_1 = require("../heart/config");
53
53
  const context_1 = require("../mind/context");
54
54
  const pending_1 = require("../mind/pending");
55
- const prompt_refresh_1 = require("../mind/prompt-refresh");
56
55
  const commands_1 = require("./commands");
57
56
  const identity_1 = require("../heart/identity");
58
57
  const nerves_1 = require("../nerves");
@@ -62,6 +61,8 @@ const tokens_1 = require("../mind/friends/tokens");
62
61
  const cli_logging_1 = require("../nerves/cli-logging");
63
62
  const runtime_1 = require("../nerves/runtime");
64
63
  const trust_gate_1 = require("./trust-gate");
64
+ const pipeline_1 = require("./pipeline");
65
+ const channel_1 = require("../mind/friends/channel");
65
66
  const session_lock_1 = require("./session-lock");
66
67
  const update_hooks_1 = require("../heart/daemon/update-hooks");
67
68
  const bundle_meta_1 = require("../heart/daemon/hooks/bundle-meta");
@@ -620,21 +621,29 @@ async function runCliSession(options) {
620
621
  echoRows += Math.ceil((2 + line.length) / cols);
621
622
  }
622
623
  process.stdout.write(`\x1b[${echoRows}A\x1b[K` + `\x1b[1m> ${inputLines[0]}${inputLines.length > 1 ? ` (+${inputLines.length - 1} lines)` : ""}\x1b[0m\n\n`);
623
- const prefix = options.getContentPrefix?.();
624
- messages.push({ role: "user", content: prefix ? `${prefix}\n\n${input}` : input });
625
624
  addHistory(history, input);
626
625
  currentAbort = new AbortController();
627
- const traceId = (0, nerves_1.createTraceId)();
628
626
  ctrl.suppress(() => currentAbort.abort());
629
627
  let result;
630
628
  try {
631
- result = await (0, core_1.runAgent)(messages, cliCallbacks, options.skipSystemPromptRefresh ? undefined : "cli", currentAbort.signal, {
632
- toolChoiceRequired: getEffectiveToolChoiceRequired(),
633
- traceId,
634
- tools: options.tools,
635
- execTool: wrappedExecTool,
636
- toolContext: options.toolContext,
637
- });
629
+ if (options.runTurn) {
630
+ // Pipeline-based turn: the runTurn callback handles user message assembly,
631
+ // pending drain, trust gate, runAgent, postTurn, and token accumulation.
632
+ result = await options.runTurn(messages, input, cliCallbacks, currentAbort.signal);
633
+ }
634
+ else {
635
+ // Legacy path: inline runAgent (used by adoption specialist and tests)
636
+ const prefix = options.getContentPrefix?.();
637
+ messages.push({ role: "user", content: prefix ? `${prefix}\n\n${input}` : input });
638
+ const traceId = (0, nerves_1.createTraceId)();
639
+ result = await (0, core_1.runAgent)(messages, cliCallbacks, options.skipSystemPromptRefresh ? undefined : "cli", currentAbort.signal, {
640
+ toolChoiceRequired: getEffectiveToolChoiceRequired(),
641
+ traceId,
642
+ tools: options.tools,
643
+ execTool: wrappedExecTool,
644
+ toolContext: options.toolContext,
645
+ });
646
+ }
638
647
  }
639
648
  catch (err) {
640
649
  // AbortError (Ctrl-C) -- silently return to prompt
@@ -698,13 +707,6 @@ async function main(agentName, options) {
698
707
  channel: "cli",
699
708
  });
700
709
  const resolvedContext = await resolver.resolve();
701
- const cliToolContext = {
702
- /* v8 ignore next -- CLI has no OAuth sign-in; this no-op satisfies the interface @preserve */
703
- signin: async () => undefined,
704
- context: resolvedContext,
705
- friendStore,
706
- summarize: (0, core_1.createSummarize)(),
707
- };
708
710
  const friendId = resolvedContext.friend.id;
709
711
  const agentConfig = (0, identity_1.loadAgentConfig)();
710
712
  (0, cli_logging_1.configureCliRuntimeLogger)(friendId, {
@@ -730,52 +732,55 @@ async function main(agentName, options) {
730
732
  const sessionMessages = existing?.messages && existing.messages.length > 0
731
733
  ? existing.messages
732
734
  : [{ role: "system", content: await (0, prompt_1.buildSystem)("cli", undefined, resolvedContext) }];
733
- // Pending queue drain: format as content-prefix for next user message
735
+ // Per-turn pipeline input: CLI capabilities and pending dir
736
+ const cliCapabilities = (0, channel_1.getChannelCapabilities)("cli");
734
737
  const pendingDir = (0, pending_1.getPendingDir)((0, identity_1.getAgentName)(), friendId, "cli", "session");
735
- let pendingPrefix;
736
- const drainToPrefix = () => {
737
- const pending = (0, pending_1.drainPending)(pendingDir);
738
- if (pending.length === 0)
739
- return 0;
740
- pendingPrefix = formatPendingPrefix(pending, (0, identity_1.getAgentName)());
741
- return pending.length;
742
- };
743
- // Startup drain: collect offline messages as prefix for next user message
744
- const startupCount = drainToPrefix();
745
- if (startupCount > 0) {
746
- (0, context_1.saveSession)(sessPath, sessionMessages);
747
- }
738
+ const summarize = (0, core_1.createSummarize)();
748
739
  try {
749
740
  await runCliSession({
750
741
  agentName: (0, identity_1.getAgentName)(),
751
742
  pasteDebounceMs,
752
743
  messages: sessionMessages,
753
- toolContext: cliToolContext,
754
- onInput: () => {
755
- const trustGate = (0, trust_gate_1.enforceTrustGate)({
756
- friend: resolvedContext.friend,
744
+ runTurn: async (messages, userInput, callbacks, signal) => {
745
+ // Run the full per-turn pipeline: resolve -> gate -> session -> drain -> runAgent -> postTurn -> tokens
746
+ // User message passed via input.messages so the pipeline can prepend pending messages to it.
747
+ const result = await (0, pipeline_1.handleInboundTurn)({
748
+ channel: "cli",
749
+ capabilities: cliCapabilities,
750
+ messages: [{ role: "user", content: userInput }],
751
+ callbacks,
752
+ friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
753
+ sessionLoader: { loadOrCreate: () => Promise.resolve({ messages, sessionPath: sessPath }) },
754
+ pendingDir,
755
+ friendStore,
757
756
  provider: "local",
758
757
  externalId: localExternalId,
759
- channel: "cli",
758
+ enforceTrustGate: trust_gate_1.enforceTrustGate,
759
+ drainPending: pending_1.drainPending,
760
+ runAgent: (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
761
+ ...opts,
762
+ toolContext: {
763
+ /* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
764
+ signin: async () => undefined,
765
+ ...opts?.toolContext,
766
+ summarize,
767
+ },
768
+ }),
769
+ postTurn: context_1.postTurn,
770
+ accumulateFriendTokens: tokens_1.accumulateFriendTokens,
771
+ signal,
772
+ runAgentOptions: {
773
+ toolChoiceRequired: (0, commands_1.getToolChoiceRequired)(),
774
+ traceId: (0, nerves_1.createTraceId)(),
775
+ },
760
776
  });
761
- if (!trustGate.allowed) {
762
- return {
763
- allowed: false,
764
- reply: trustGate.reason === "stranger_first_reply" ? trustGate.autoReply : undefined,
765
- };
777
+ // Handle gate rejection: display auto-reply if present
778
+ if (!result.gateResult.allowed) {
779
+ if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
780
+ process.stdout.write(`${result.gateResult.autoReply}\n`);
781
+ }
766
782
  }
767
- return { allowed: true };
768
- },
769
- getContentPrefix: () => {
770
- const prefix = pendingPrefix;
771
- pendingPrefix = undefined;
772
- return prefix;
773
- },
774
- onTurnEnd: async (msgs, result) => {
775
- (0, context_1.postTurn)(msgs, sessPath, result.usage);
776
- await (0, tokens_1.accumulateFriendTokens)(friendStore, resolvedContext.friend.id, result.usage);
777
- drainToPrefix();
778
- await (0, prompt_refresh_1.refreshSystemPrompt)(msgs, "cli", undefined, resolvedContext);
783
+ return { usage: result.usage };
779
784
  },
780
785
  onNewSession: () => {
781
786
  (0, context_1.deleteSession)(sessPath);
@@ -49,6 +49,10 @@ const context_1 = require("../mind/context");
49
49
  const prompt_1 = require("../mind/prompt");
50
50
  const bundle_manifest_1 = require("../mind/bundle-manifest");
51
51
  const pending_1 = require("../mind/pending");
52
+ const channel_1 = require("../mind/friends/channel");
53
+ const trust_gate_1 = require("./trust-gate");
54
+ const tokens_1 = require("../mind/friends/tokens");
55
+ const pipeline_1 = require("./pipeline");
52
56
  const nerves_1 = require("../nerves");
53
57
  const runtime_1 = require("../nerves/runtime");
54
58
  const DEFAULT_INNER_DIALOG_INSTINCTS = [
@@ -148,77 +152,116 @@ function createInnerDialogCallbacks() {
148
152
  };
149
153
  }
150
154
  function innerDialogSessionPath() {
151
- return (0, config_1.sessionPath)("self", "inner", "dialog");
155
+ return (0, config_1.sessionPath)(pending_1.INNER_DIALOG_PENDING.friendId, pending_1.INNER_DIALOG_PENDING.channel, pending_1.INNER_DIALOG_PENDING.key);
156
+ }
157
+ // Self-referencing friend record for inner dialog (agent talking to itself).
158
+ // No real friend to resolve -- this satisfies the pipeline's friend resolver contract.
159
+ function createSelfFriend(agentName) {
160
+ return {
161
+ id: "self",
162
+ name: agentName,
163
+ trustLevel: "family",
164
+ externalIds: [],
165
+ tenantMemberships: [],
166
+ toolPreferences: {},
167
+ notes: {},
168
+ totalTokens: 0,
169
+ createdAt: new Date().toISOString(),
170
+ updatedAt: new Date().toISOString(),
171
+ schemaVersion: 1,
172
+ };
173
+ }
174
+ // No-op friend store for inner dialog. Inner dialog doesn't track token usage per-friend.
175
+ function createNoOpFriendStore() {
176
+ return {
177
+ get: async () => null,
178
+ put: async () => { },
179
+ delete: async () => { },
180
+ findByExternalId: async () => null,
181
+ };
152
182
  }
153
183
  async function runInnerDialogTurn(options) {
154
184
  const now = options?.now ?? (() => new Date());
155
185
  const reason = options?.reason ?? "heartbeat";
156
186
  const sessionFilePath = innerDialogSessionPath();
157
187
  const loaded = (0, context_1.loadSession)(sessionFilePath);
158
- const messages = loaded?.messages ? [...loaded.messages] : [];
188
+ const existingMessages = loaded?.messages ? [...loaded.messages] : [];
159
189
  const instincts = options?.instincts ?? loadInnerDialogInstincts();
160
190
  const state = {
161
191
  cycleCount: 1,
162
192
  resting: false,
163
193
  lastHeartbeatAt: now().toISOString(),
164
194
  };
165
- if (messages.length === 0) {
166
- const systemPrompt = await (0, prompt_1.buildSystem)("inner", { toolChoiceRequired: true });
167
- messages.push({ role: "system", content: systemPrompt });
195
+ // ── Adapter concern: build user message ──────────────────────────
196
+ let userContent;
197
+ if (existingMessages.length === 0) {
198
+ // Fresh session: bootstrap message with non-canonical cleanup nudge
168
199
  const aspirations = readAspirations((0, identity_1.getAgentRoot)());
169
200
  const nonCanonical = (0, bundle_manifest_1.findNonCanonicalBundlePaths)((0, identity_1.getAgentRoot)());
170
201
  const cleanupNudge = buildNonCanonicalCleanupNudge(nonCanonical);
171
- const bootstrapMessage = [
202
+ userContent = [
172
203
  buildInnerDialogBootstrapMessage(aspirations, "No prior inner dialog session found."),
173
204
  cleanupNudge,
174
205
  ].filter(Boolean).join("\n\n");
175
- messages.push({ role: "user", content: bootstrapMessage });
176
206
  }
177
207
  else {
178
- const assistantTurns = messages.filter((message) => message.role === "assistant").length;
208
+ // Resumed session: instinct message with checkpoint context
209
+ const assistantTurns = existingMessages.filter((message) => message.role === "assistant").length;
179
210
  state.cycleCount = assistantTurns + 1;
180
- state.checkpoint = deriveResumeCheckpoint(messages);
181
- const instinctPrompt = buildInstinctUserMessage(instincts, reason, state);
182
- messages.push({ role: "user", content: instinctPrompt });
183
- }
184
- const pendingMessages = (0, pending_1.drainPending)((0, pending_1.getPendingDir)((0, identity_1.getAgentName)(), "self", "inner", "dialog"));
185
- if (pendingMessages.length > 0) {
186
- const lastUserIdx = messages.length - 1;
187
- const lastUser = messages[lastUserIdx];
188
- /* v8 ignore next -- defensive: all code paths push a user message before here @preserve */
189
- if (lastUser?.role === "user" && typeof lastUser.content === "string") {
190
- const section = pendingMessages
191
- .map((msg) => `- **${msg.from}**: ${msg.content}`)
192
- .join("\n");
193
- messages[lastUserIdx] = {
194
- ...lastUser,
195
- content: `${lastUser.content}\n\n## pending messages\n${section}`,
196
- };
197
- }
211
+ state.checkpoint = deriveResumeCheckpoint(existingMessages);
212
+ userContent = buildInstinctUserMessage(instincts, reason, state);
198
213
  }
214
+ // ── Adapter concern: inbox drain (inner-dialog-specific) ─────────
199
215
  const inboxMessages = options?.drainInbox?.() ?? [];
200
216
  if (inboxMessages.length > 0) {
201
- const lastUserIdx = messages.length - 1;
202
- const lastUser = messages[lastUserIdx];
203
- /* v8 ignore next -- defensive: all code paths push a user message before here @preserve */
204
- if (lastUser?.role === "user" && typeof lastUser.content === "string") {
205
- const section = inboxMessages
206
- .map((msg) => `- **${msg.from}**: ${msg.content}`)
207
- .join("\n");
208
- messages[lastUserIdx] = {
209
- ...lastUser,
210
- content: `${lastUser.content}\n\n## incoming messages\n${section}`,
211
- };
212
- }
217
+ const section = inboxMessages
218
+ .map((msg) => `- **${msg.from}**: ${msg.content}`)
219
+ .join("\n");
220
+ userContent = `${userContent}\n\n## incoming messages\n${section}`;
213
221
  }
222
+ const userMessage = { role: "user", content: userContent };
223
+ // ── Session loader: wraps existing session logic ──────────────────
224
+ const innerCapabilities = (0, channel_1.getChannelCapabilities)("inner");
225
+ const pendingDir = (0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)());
226
+ const selfFriend = createSelfFriend((0, identity_1.getAgentName)());
227
+ const selfContext = { friend: selfFriend, channel: innerCapabilities };
228
+ const sessionLoader = {
229
+ loadOrCreate: async () => {
230
+ if (existingMessages.length > 0) {
231
+ return { messages: existingMessages, sessionPath: sessionFilePath };
232
+ }
233
+ // Fresh session: build system prompt
234
+ const systemPrompt = await (0, prompt_1.buildSystem)("inner", { toolChoiceRequired: true });
235
+ return {
236
+ messages: [{ role: "system", content: systemPrompt }],
237
+ sessionPath: sessionFilePath,
238
+ };
239
+ },
240
+ };
241
+ // ── Call shared pipeline ──────────────────────────────────────────
214
242
  const callbacks = createInnerDialogCallbacks();
215
243
  const traceId = (0, nerves_1.createTraceId)();
216
- const result = await (0, core_1.runAgent)(messages, callbacks, "inner", options?.signal, {
217
- traceId,
218
- toolChoiceRequired: true,
219
- skipConfirmation: true,
244
+ const result = await (0, pipeline_1.handleInboundTurn)({
245
+ channel: "inner",
246
+ capabilities: innerCapabilities,
247
+ messages: [userMessage],
248
+ callbacks,
249
+ friendResolver: { resolve: () => Promise.resolve(selfContext) },
250
+ sessionLoader,
251
+ pendingDir,
252
+ friendStore: createNoOpFriendStore(),
253
+ enforceTrustGate: trust_gate_1.enforceTrustGate,
254
+ drainPending: pending_1.drainPending,
255
+ runAgent: core_1.runAgent,
256
+ postTurn: context_1.postTurn,
257
+ accumulateFriendTokens: tokens_1.accumulateFriendTokens,
258
+ signal: options?.signal,
259
+ runAgentOptions: {
260
+ traceId,
261
+ toolChoiceRequired: true,
262
+ skipConfirmation: true,
263
+ },
220
264
  });
221
- (0, context_1.postTurn)(messages, sessionFilePath, result.usage);
222
265
  (0, runtime_1.emitNervesEvent)({
223
266
  component: "senses",
224
267
  event: "senses.inner_dialog_turn",
@@ -226,8 +269,8 @@ async function runInnerDialogTurn(options) {
226
269
  meta: { reason, session: sessionFilePath },
227
270
  });
228
271
  return {
229
- messages,
272
+ messages: result.messages ?? [],
230
273
  usage: result.usage,
231
- sessionPath: sessionFilePath,
274
+ sessionPath: result.sessionPath ?? sessionFilePath,
232
275
  };
233
276
  }