@ouro.bot/cli 0.1.0-alpha.133 → 0.1.0-alpha.135

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 CHANGED
@@ -1,6 +1,28 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.135",
6
+ "changes": [
7
+ "Metacognitive tool vocabulary complete: go_inward renamed to descend. RunAgentOutcome value is now 'descended', event is 'engine.descended'.",
8
+ "summarizeArgs rewritten: reads summaryKeys from ToolDefinition instead of per-tool switch branches. summarizeTeamsArgs and summarizeGithubArgs eliminated."
9
+ ]
10
+ },
11
+ {
12
+ "version": "0.1.0-alpha.134",
13
+ "changes": [
14
+ "Metacognitive tool redesign: final_answer renamed to settle, no_response renamed to observe. RunAgentOutcome values now 'settled' and 'observed'.",
15
+ "New surface tool: inner-dialog-only tool for routing thoughts outward to friends' freshest active sessions. Replaces post-turn routeDelegatedCompletion with inline delivery.",
16
+ "Attention queue: FIFO queue of delegated work items seeded from drained pending messages and recovered obligations. Visible to the model at inner dialog turn start.",
17
+ "Surface-before-settle gate: settle rejected in inner dialog until all attention queue items are surfaced.",
18
+ "Channel-based tool filtering: observe excluded from 1:1 and inner dialog; go_inward and send_message excluded from inner dialog; surface included only in inner dialog.",
19
+ "go_inward content param renamed to topic with metacognitive handoff framing.",
20
+ "Shared sole-call interception pattern extracted from duplicated core.ts logic.",
21
+ "Exact-origin routing removed from routeDelegatedCompletion; routing now bridge, freshest, deferred only.",
22
+ "ouro attention CLI: list held items, show details by ID, view surfacing history.",
23
+ "System prompts updated with metacognitive framing for all new tool names."
24
+ ]
25
+ },
4
26
  {
5
27
  "version": "0.1.0-alpha.133",
6
28
  "changes": [
@@ -8,7 +8,7 @@ exports.getProvider = getProvider;
8
8
  exports.createSummarize = createSummarize;
9
9
  exports.getProviderDisplayLabel = getProviderDisplayLabel;
10
10
  exports.isExternalStateQuery = isExternalStateQuery;
11
- exports.getFinalAnswerRetryError = getFinalAnswerRetryError;
11
+ exports.getSettleRetryError = getSettleRetryError;
12
12
  exports.stripLastToolCalls = stripLastToolCalls;
13
13
  exports.repairOrphanedToolCalls = repairOrphanedToolCalls;
14
14
  exports.isTransientError = isTransientError;
@@ -18,6 +18,7 @@ const config_1 = require("./config");
18
18
  const identity_1 = require("./identity");
19
19
  const tools_1 = require("../repertoire/tools");
20
20
  const channel_1 = require("../mind/friends/channel");
21
+ const surface_tool_1 = require("../senses/surface-tool");
21
22
  const runtime_1 = require("../nerves/runtime");
22
23
  const context_1 = require("../mind/context");
23
24
  const prompt_1 = require("../mind/prompt");
@@ -170,6 +171,13 @@ Object.defineProperty(exports, "toResponsesTools", { enumerable: true, get: func
170
171
  // Re-export prompt functions for backward compat
171
172
  var prompt_2 = require("../mind/prompt");
172
173
  Object.defineProperty(exports, "buildSystem", { enumerable: true, get: function () { return prompt_2.buildSystem; } });
174
+ // Sole-call tools must be the only tool call in a turn. When they appear
175
+ // alongside other tools, the sole-call tool is rejected with this message.
176
+ const SOLE_CALL_REJECTION = {
177
+ settle: "rejected: settle must be the only tool call. finish your work first, then call settle alone.",
178
+ observe: "rejected: observe must be the only tool call. call observe alone when you want to stay silent.",
179
+ descend: "rejected: descend must be the only tool call. finish your other work first, then call descend alone.",
180
+ };
173
181
  const DELEGATION_REASON_PROSE_HANDOFF = {
174
182
  explicit_reflection: "something in the conversation called for reflection",
175
183
  cross_session: "this touches other conversations",
@@ -178,39 +186,39 @@ const DELEGATION_REASON_PROSE_HANDOFF = {
178
186
  non_fast_path_tool: "this needs tools beyond a simple reply",
179
187
  unresolved_obligation: "there's an unresolved commitment from an earlier conversation",
180
188
  };
181
- function buildGoInwardHandoffPacket(params) {
189
+ function buildDescendHandoffPacket(params) {
182
190
  const reasons = params.delegationDecision?.reasons ?? [];
183
191
  const reasonProse = reasons.length > 0
184
192
  ? reasons.map((r) => DELEGATION_REASON_PROSE_HANDOFF[r]).join("; ")
185
193
  : "this felt like it needed more thought";
186
- const returnAddress = params.currentSession
187
- ? `${params.currentSession.friendId}/${params.currentSession.channel}/${params.currentSession.key}`
188
- : "no specific return -- just thinking";
189
- let obligationLine;
194
+ const whoAsked = params.currentSession
195
+ ? params.currentSession.friendId
196
+ : "no one -- just thinking";
197
+ let holdingLine;
190
198
  if (params.outwardClosureRequired && params.currentSession) {
191
- obligationLine = `i need to come back to ${params.currentSession.friendId} with something`;
199
+ holdingLine = `i'm holding something for ${params.currentSession.friendId}`;
192
200
  }
193
201
  else {
194
- obligationLine = "no obligation -- just thinking";
202
+ holdingLine = "nothing -- just thinking";
195
203
  }
196
204
  return [
197
205
  "## what i need to think about",
198
- params.content,
206
+ params.topic,
199
207
  "",
200
208
  "## why this came up",
201
209
  reasonProse,
202
210
  "",
203
- "## where to bring it back",
204
- returnAddress,
211
+ "## who asked",
212
+ whoAsked,
205
213
  "",
206
- "## what i owe",
207
- obligationLine,
214
+ "## what i'm holding",
215
+ holdingLine,
208
216
  "",
209
217
  "## thinking mode",
210
218
  params.mode,
211
219
  ].join("\n");
212
220
  }
213
- function parseFinalAnswerPayload(argumentsText) {
221
+ function parseSettlePayload(argumentsText) {
214
222
  try {
215
223
  const parsed = JSON.parse(argumentsText);
216
224
  if (typeof parsed === "string") {
@@ -237,22 +245,22 @@ function isExternalStateQuery(toolName, args) {
237
245
  const cmd = String(args.command ?? "");
238
246
  return /\bgh\s+(pr|run|api|issue)\b/.test(cmd) || /\bnpm\s+(view|info|show)\b/.test(cmd);
239
247
  }
240
- function getFinalAnswerRetryError(mustResolveBeforeHandoff, intent, sawSteeringFollowUp, _delegationDecision, sawSendMessageSelf, sawGoInward, _sawQuerySession, currentObligation, innerJob, sawExternalStateQuery) {
248
+ function getSettleRetryError(mustResolveBeforeHandoff, intent, sawSteeringFollowUp, _delegationDecision, sawSendMessageSelf, sawDescend, _sawQuerySession, currentObligation, innerJob, sawExternalStateQuery) {
241
249
  // Delegation adherence removed: the delegation decision is surfaced in the
242
- // system prompt as a suggestion. Hard-gating final_answer caused infinite
250
+ // system prompt as a suggestion. Hard-gating settle caused infinite
243
251
  // rejection loops where the agent couldn't respond to the user at all.
244
252
  // The agent is free to follow or ignore the delegation hint.
245
253
  // 2. Pending obligation not addressed
246
- if (innerJob?.obligationStatus === "pending" && !sawSendMessageSelf && !sawGoInward) {
247
- return "you're still holding something from an earlier conversation -- someone is waiting for your answer. finish the thought first, or go_inward to keep working on it privately.";
254
+ if (innerJob?.obligationStatus === "pending" && !sawSendMessageSelf && !sawDescend) {
255
+ return "you're still holding something from an earlier conversation -- someone is waiting for your answer. finish the thought first, or descend to keep working on it privately.";
248
256
  }
249
257
  // 3. mustResolveBeforeHandoff + missing intent
250
258
  if (mustResolveBeforeHandoff && !intent) {
251
- return "your final_answer is missing required intent. when you must keep going until done or blocked, call final_answer again with answer plus intent=complete, blocked, or direct_reply.";
259
+ return "your settle is missing required intent. when you must keep going until done or blocked, call settle again with answer plus intent=complete, blocked, or direct_reply.";
252
260
  }
253
261
  // 4. mustResolveBeforeHandoff + direct_reply without follow-up
254
262
  if (mustResolveBeforeHandoff && intent === "direct_reply" && !sawSteeringFollowUp) {
255
- return "your final_answer used intent=direct_reply without a newer steering follow-up. continue the unresolved work, or call final_answer again with intent=complete or blocked when appropriate.";
263
+ return "your settle used intent=direct_reply without a newer steering follow-up. continue the unresolved work, or call settle again with intent=complete or blocked when appropriate.";
256
264
  }
257
265
  // 5. mustResolveBeforeHandoff + complete while a live return loop is still active
258
266
  if (mustResolveBeforeHandoff && intent === "complete" && currentObligation && !sawSteeringFollowUp) {
@@ -260,7 +268,7 @@ function getFinalAnswerRetryError(mustResolveBeforeHandoff, intent, sawSteeringF
260
268
  }
261
269
  // 6. External-state grounding: obligation + complete requires fresh external verification
262
270
  if (intent === "complete" && currentObligation && !sawExternalStateQuery && !sawSteeringFollowUp) {
263
- return "you're claiming this work is complete, but the external state hasn't been verified this turn. ground your claim with a fresh check (gh pr view, npm view, gh run view, etc.) before calling final_answer.";
271
+ return "you're claiming this work is complete, but the external state hasn't been verified this turn. ground your claim with a fresh check (gh pr view, npm view, gh run view, etc.) before calling settle.";
264
272
  }
265
273
  return null;
266
274
  }
@@ -480,7 +488,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
480
488
  let lastUsage;
481
489
  let overflowRetried = false;
482
490
  let retryCount = 0;
483
- let outcome = "complete";
491
+ let outcome = "settled";
484
492
  let completion;
485
493
  let terminalError;
486
494
  let terminalErrorClassification;
@@ -488,7 +496,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
488
496
  let mustResolveBeforeHandoffActive = options?.mustResolveBeforeHandoff === true;
489
497
  let currentReasoningEffort = "medium";
490
498
  let sawSendMessageSelf = false;
491
- let sawGoInward = false;
499
+ let sawDescend = false;
492
500
  let sawQuerySession = false;
493
501
  let sawBridgeManage = false;
494
502
  let sawExternalStateQuery = false;
@@ -513,13 +521,24 @@ async function runAgent(messages, callbacks, channel, signal, options) {
513
521
  // This prevents stale provider caches from replaying prior-turn context.
514
522
  providerRuntime.resetTurnState(messages);
515
523
  while (!done) {
516
- // When toolChoiceRequired is true (the default), include final_answer
517
- // so the model can signal completion. With tool_choice: required, the
518
- // model must call a tool every turn final_answer is how it exits.
519
- // Overridable via options.toolChoiceRequired = false (e.g. CLI).
520
- const activeTools = toolChoiceRequired
521
- ? [...baseTools, tools_1.goInwardTool, ...(currentContext?.isGroupChat ? [tools_1.noResponseTool] : []), tools_1.finalAnswerTool]
524
+ // Channel-based tool filtering:
525
+ // - Inner dialog: exclude descend (already inward), send_message (delivery via surface), observe (no one to observe)
526
+ // - 1:1 sessions: exclude observe (can't ignore someone talking directly to you)
527
+ // - Group chats: observe available
528
+ //
529
+ // descend, settle, surface, and observe are always assembled based on channel context.
530
+ // toolChoiceRequired only controls whether tool_choice: "required" is set in the API call.
531
+ const isInnerDialog = channel === "inner";
532
+ const filteredBaseTools = isInnerDialog
533
+ ? baseTools.filter((t) => t.function.name !== "send_message")
522
534
  : baseTools;
535
+ const activeTools = [
536
+ ...filteredBaseTools,
537
+ ...(!isInnerDialog ? [tools_1.descendTool] : []),
538
+ ...(isInnerDialog ? [surface_tool_1.surfaceToolDef] : []),
539
+ ...(currentContext?.isGroupChat && !isInnerDialog ? [tools_1.observeTool] : []),
540
+ tools_1.settleTool,
541
+ ];
523
542
  const steeringFollowUps = options?.drainSteeringFollowUps?.() ?? [];
524
543
  if (steeringFollowUps.length > 0) {
525
544
  const hasSupersedingFollowUp = steeringFollowUps.some((followUp) => followUp.effect === "clear_and_supersede");
@@ -555,7 +574,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
555
574
  traceId,
556
575
  toolChoiceRequired,
557
576
  reasoningEffort: currentReasoningEffort,
558
- eagerFinalAnswerStreaming: true,
577
+ eagerSettleStreaming: true,
559
578
  });
560
579
  // Track usage from the latest API call
561
580
  if (result.usage)
@@ -586,24 +605,54 @@ async function runAgent(messages, callbacks, channel, signal, options) {
586
605
  }
587
606
  // Phase annotation for Codex provider
588
607
  const hasPhaseAnnotation = providerRuntime.capabilities.has("phase-annotation");
589
- const isSoleFinalAnswer = result.toolCalls.length === 1 && result.toolCalls[0].name === "final_answer";
608
+ const isSoleSettle = result.toolCalls.length === 1 && result.toolCalls[0].name === "settle";
590
609
  if (hasPhaseAnnotation) {
591
- msg.phase = isSoleFinalAnswer ? "final_answer" : "commentary";
610
+ msg.phase = isSoleSettle ? "settle" : "commentary";
592
611
  }
593
612
  if (!result.toolCalls.length) {
594
613
  // No tool calls — accept response as-is.
595
- // (Kick detection disabled; tool_choice: required + final_answer
614
+ // (Kick detection disabled; tool_choice: required + settle
596
615
  // is the primary loop control. See src/heart/kicks.ts to re-enable.)
597
616
  messages.push(msg);
598
617
  done = true;
599
618
  }
600
619
  else {
601
- // Check for final_answer sole call: intercept before tool execution
602
- if (isSoleFinalAnswer) {
620
+ // Check for settle sole call: intercept before tool execution
621
+ if (isSoleSettle) {
622
+ /* v8 ignore next -- defensive: JSON.parse catch for malformed settle args @preserve */
623
+ const settleArgs = (() => { try {
624
+ return JSON.parse(result.toolCalls[0].arguments);
625
+ }
626
+ catch {
627
+ return {};
628
+ } })();
629
+ callbacks.onToolStart("settle", settleArgs);
630
+ // Inner dialog attention queue gate: reject settle if items remain
631
+ const attentionQueue = (augmentedToolContext ?? options?.toolContext)?.delegatedOrigins;
632
+ if (isInnerDialog && attentionQueue && attentionQueue.length > 0) {
633
+ callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
634
+ callbacks.onClearText?.();
635
+ messages.push(msg);
636
+ const gateMessage = "you're holding thoughts someone is waiting for — surface them before you settle.";
637
+ messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: gateMessage });
638
+ providerRuntime.appendToolOutput(result.toolCalls[0].id, gateMessage);
639
+ continue;
640
+ }
603
641
  // Extract answer from the tool call arguments.
604
642
  // Supports: {"answer":"text","intent":"..."} or "text" (JSON string).
605
- const { answer, intent } = parseFinalAnswerPayload(result.toolCalls[0].arguments);
606
- const retryError = getFinalAnswerRetryError(mustResolveBeforeHandoffActive, intent, sawSteeringFollowUp, options?.delegationDecision, sawSendMessageSelf, sawGoInward, sawQuerySession, options?.currentObligation ?? null, options?.activeWorkFrame?.inner?.job, sawExternalStateQuery);
643
+ const { answer, intent } = parseSettlePayload(result.toolCalls[0].arguments);
644
+ // Inner dialog settle: no CompletionMetadata, "(settled)" ack
645
+ if (isInnerDialog) {
646
+ callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), true);
647
+ messages.push(msg);
648
+ const settled = "(settled)";
649
+ messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: settled });
650
+ providerRuntime.appendToolOutput(result.toolCalls[0].id, settled);
651
+ outcome = "settled";
652
+ done = true;
653
+ continue;
654
+ }
655
+ const retryError = getSettleRetryError(mustResolveBeforeHandoffActive, intent, sawSteeringFollowUp, options?.delegationDecision, sawSendMessageSelf, sawDescend, sawQuerySession, options?.currentObligation ?? null, options?.activeWorkFrame?.inner?.job, sawExternalStateQuery);
607
656
  const deliveredAnswer = answer;
608
657
  const validDirectReply = mustResolveBeforeHandoffActive && intent === "direct_reply" && sawSteeringFollowUp;
609
658
  const validTerminalIntent = intent === "complete" || intent === "blocked";
@@ -611,13 +660,14 @@ async function runAgent(messages, callbacks, channel, signal, options) {
611
660
  && !retryError
612
661
  && (!mustResolveBeforeHandoffActive || validDirectReply || validTerminalIntent);
613
662
  if (validClosure) {
663
+ callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), true);
614
664
  completion = {
615
665
  answer: deliveredAnswer,
616
666
  intent: validDirectReply ? "direct_reply" : intent === "blocked" ? "blocked" : "complete",
617
667
  };
618
- if (result.finalAnswerStreamed) {
668
+ if (result.settleStreamed) {
619
669
  // The streaming layer already parsed and emitted the answer
620
- // progressively via FinalAnswerParser. Skip clearing and
670
+ // progressively via SettleParser. Skip clearing and
621
671
  // re-emitting to avoid double-delivery.
622
672
  }
623
673
  else {
@@ -637,57 +687,64 @@ async function runAgent(messages, callbacks, channel, signal, options) {
637
687
  const delivered = "(delivered)";
638
688
  messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: delivered });
639
689
  providerRuntime.appendToolOutput(result.toolCalls[0].id, delivered);
640
- outcome = intent === "blocked" ? "blocked" : "complete";
690
+ outcome = intent === "blocked" ? "blocked" : "settled";
641
691
  done = true;
642
692
  }
643
693
  }
644
694
  else {
645
- // Answer is undefined -- the model's final_answer was incomplete or
695
+ // Answer is undefined -- the model's settle was incomplete or
646
696
  // malformed. Clear any partial streamed text or noise, then push the
647
697
  // assistant msg + error tool result and let the model try again.
698
+ callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
648
699
  callbacks.onClearText?.();
649
700
  messages.push(msg);
650
701
  const toolRetryMessage = retryError
651
- ?? "your final_answer was incomplete or malformed. call final_answer again with your complete response.";
702
+ ?? "your settle was incomplete or malformed. call settle again with your complete response.";
652
703
  messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: toolRetryMessage });
653
704
  providerRuntime.appendToolOutput(result.toolCalls[0].id, toolRetryMessage);
654
705
  }
655
706
  continue;
656
707
  }
657
- // Check for no_response sole call: intercept before tool execution
658
- const isSoleNoResponse = result.toolCalls.length === 1 && result.toolCalls[0].name === "no_response";
659
- if (isSoleNoResponse) {
660
- let reason;
661
- try {
662
- const parsed = JSON.parse(result.toolCalls[0].arguments);
663
- if (typeof parsed?.reason === "string")
664
- reason = parsed.reason;
708
+ // Check for observe sole call: intercept before tool execution
709
+ const isSoleObserve = result.toolCalls.length === 1 && result.toolCalls[0].name === "observe";
710
+ if (isSoleObserve) {
711
+ /* v8 ignore next -- defensive: JSON.parse catch for malformed observe args @preserve */
712
+ const observeArgs = (() => { try {
713
+ return JSON.parse(result.toolCalls[0].arguments);
665
714
  }
666
- catch { /* ignore */ }
715
+ catch {
716
+ return {};
717
+ } })();
718
+ let reason;
719
+ if (typeof observeArgs?.reason === "string")
720
+ reason = observeArgs.reason;
721
+ callbacks.onToolStart("observe", observeArgs);
667
722
  (0, runtime_1.emitNervesEvent)({
668
723
  component: "engine",
669
- event: "engine.no_response",
724
+ event: "engine.observe",
670
725
  message: "agent declined to respond in group chat",
671
726
  meta: { ...(reason ? { reason } : {}) },
672
727
  });
728
+ callbacks.onToolEnd("observe", (0, tools_1.summarizeArgs)("observe", observeArgs), true);
673
729
  messages.push(msg);
674
730
  const silenced = "(silenced)";
675
731
  messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: silenced });
676
732
  providerRuntime.appendToolOutput(result.toolCalls[0].id, silenced);
677
- outcome = "no_response";
733
+ outcome = "observed";
678
734
  done = true;
679
735
  continue;
680
736
  }
681
- // Check for go_inward sole call: intercept before tool execution
682
- const isSoleGoInward = result.toolCalls.length === 1 && result.toolCalls[0].name === "go_inward";
683
- if (isSoleGoInward) {
737
+ // Check for descend sole call: intercept before tool execution
738
+ const isSoleDescend = result.toolCalls.length === 1 && result.toolCalls[0].name === "descend";
739
+ if (isSoleDescend) {
684
740
  let parsedArgs = {};
685
741
  try {
686
742
  parsedArgs = JSON.parse(result.toolCalls[0].arguments);
687
743
  }
688
744
  catch { /* ignore */ }
689
- /* v8 ignore next -- defensive: content always string from model @preserve */
690
- const content = typeof parsedArgs.content === "string" ? parsedArgs.content : "";
745
+ callbacks.onToolStart("descend", parsedArgs);
746
+ /* v8 ignore next -- defensive: topic always string from model @preserve */
747
+ const topic = typeof parsedArgs.topic === "string" ? parsedArgs.topic : "";
691
748
  const answer = typeof parsedArgs.answer === "string" ? parsedArgs.answer : undefined;
692
749
  const parsedMode = parsedArgs.mode === "reflect" || parsedArgs.mode === "plan" || parsedArgs.mode === "relay"
693
750
  ? parsedArgs.mode
@@ -699,8 +756,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
699
756
  callbacks.onTextChunk(answer);
700
757
  }
701
758
  // Build handoff packet and enqueue
702
- const handoffContent = buildGoInwardHandoffPacket({
703
- content,
759
+ const handoffContent = buildDescendHandoffPacket({
760
+ topic,
704
761
  mode,
705
762
  delegationDecision: options?.delegationDecision,
706
763
  currentSession: options?.toolContext?.currentSession ?? null,
@@ -710,6 +767,24 @@ async function runAgent(messages, callbacks, channel, signal, options) {
710
767
  const pendingDir = (0, pending_1.getInnerDialogPendingDir)((0, identity_2.getAgentName)());
711
768
  const currentSession = options?.toolContext?.currentSession;
712
769
  const isInnerChannel = currentSession?.friendId === "self" && currentSession?.channel === "inner";
770
+ // Create obligation FIRST so we can attach its ID to the pending message
771
+ let createdObligationId;
772
+ if (currentSession && !isInnerChannel) {
773
+ try {
774
+ const obligation = (0, obligations_1.createObligation)((0, identity_2.getAgentRoot)(), {
775
+ origin: {
776
+ friendId: currentSession.friendId,
777
+ channel: currentSession.channel,
778
+ key: currentSession.key,
779
+ },
780
+ content: topic,
781
+ });
782
+ createdObligationId = obligation.id;
783
+ }
784
+ catch {
785
+ /* v8 ignore next -- defensive: obligation store write failure should not break descend @preserve */
786
+ }
787
+ }
713
788
  const envelope = {
714
789
  from: (0, identity_2.getAgentName)(),
715
790
  friendId: "self",
@@ -725,67 +800,41 @@ async function runAgent(messages, callbacks, channel, signal, options) {
725
800
  key: currentSession.key,
726
801
  },
727
802
  obligationStatus: "pending",
803
+ /* v8 ignore next -- defensive: createdObligationId is undefined only when obligation store write fails @preserve */
804
+ ...(createdObligationId ? { obligationId: createdObligationId } : {}),
728
805
  } : {}),
729
806
  };
730
807
  (0, pending_1.queuePendingMessage)(pendingDir, envelope);
731
- if (currentSession && !isInnerChannel) {
732
- try {
733
- (0, obligations_1.createObligation)((0, identity_2.getAgentRoot)(), {
734
- origin: {
735
- friendId: currentSession.friendId,
736
- channel: currentSession.channel,
737
- key: currentSession.key,
738
- },
739
- content,
740
- });
741
- }
742
- catch {
743
- /* v8 ignore next -- defensive: obligation store write failure should not break go_inward @preserve */
744
- }
745
- }
746
808
  try {
747
809
  await (0, socket_client_1.requestInnerWake)((0, identity_2.getAgentName)());
748
810
  }
749
811
  catch { /* daemon may not be running */ }
750
- sawGoInward = true;
812
+ callbacks.onToolEnd("descend", (0, tools_1.summarizeArgs)("descend", parsedArgs), true);
813
+ sawDescend = true;
751
814
  messages.push(msg);
752
815
  const ack = "(going inward)";
753
816
  messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: ack });
754
817
  providerRuntime.appendToolOutput(result.toolCalls[0].id, ack);
755
818
  (0, runtime_1.emitNervesEvent)({
756
819
  component: "engine",
757
- event: "engine.go_inward",
820
+ event: "engine.descended",
758
821
  message: "taking thread inward",
759
- meta: { mode, hasAnswer: answer !== undefined, contentSnippet: content.slice(0, 80) },
822
+ meta: { mode, hasAnswer: answer !== undefined, contentSnippet: topic.slice(0, 80) },
760
823
  });
761
- outcome = "go_inward";
824
+ outcome = "descended";
762
825
  done = true;
763
826
  continue;
764
827
  }
765
828
  messages.push(msg);
766
- // SHARED: execute tools (final_answer, no_response, go_inward in mixed calls are rejected inline)
829
+ // Execute tools (sole-call tools in mixed calls are rejected inline)
767
830
  for (const tc of result.toolCalls) {
768
831
  if (signal?.aborted)
769
832
  break;
770
- // Intercept final_answer in mixed call: reject it
771
- if (tc.name === "final_answer") {
772
- const rejection = "rejected: final_answer must be the only tool call. Finish your work first, then call final_answer alone.";
773
- messages.push({ role: "tool", tool_call_id: tc.id, content: rejection });
774
- providerRuntime.appendToolOutput(tc.id, rejection);
775
- continue;
776
- }
777
- // Intercept no_response in mixed call: reject it
778
- if (tc.name === "no_response") {
779
- const rejection = "rejected: no_response must be the only tool call. call no_response alone when you want to stay silent.";
780
- messages.push({ role: "tool", tool_call_id: tc.id, content: rejection });
781
- providerRuntime.appendToolOutput(tc.id, rejection);
782
- continue;
783
- }
784
- // Intercept go_inward in mixed call: reject it
785
- if (tc.name === "go_inward") {
786
- const rejection = "rejected: go_inward must be the only tool call. finish your other work first, then call go_inward alone.";
787
- messages.push({ role: "tool", tool_call_id: tc.id, content: rejection });
788
- providerRuntime.appendToolOutput(tc.id, rejection);
833
+ // Reject sole-call tools when mixed with other tool calls
834
+ const soleCallRejection = SOLE_CALL_REJECTION[tc.name];
835
+ if (soleCallRejection) {
836
+ messages.push({ role: "tool", tool_call_id: tc.id, content: soleCallRejection });
837
+ providerRuntime.appendToolOutput(tc.id, soleCallRejection);
789
838
  continue;
790
839
  }
791
840
  let args = {};
@@ -932,7 +981,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
932
981
  trace_id: traceId,
933
982
  component: "engine",
934
983
  message: "runAgent turn completed",
935
- meta: { done, sawGoInward, sawQuerySession, sawBridgeManage },
984
+ meta: { done, sawDescend, sawQuerySession, sawBridgeManage },
936
985
  });
937
986
  return {
938
987
  usage: lastUsage,
@@ -760,6 +760,17 @@ function parseSessionCommand(args) {
760
760
  return { kind: "session.list", ...(agent ? { agent } : {}) };
761
761
  throw new Error(`Usage\n${usage()}`);
762
762
  }
763
+ function parseAttentionCommand(args) {
764
+ const { agent, rest: cleaned } = extractAgentFlag(args);
765
+ const sub = cleaned[0];
766
+ if (sub === "show" && cleaned[1]) {
767
+ return { kind: "attention.show", id: cleaned[1], ...(agent ? { agent } : {}) };
768
+ }
769
+ if (sub === "history") {
770
+ return { kind: "attention.history", ...(agent ? { agent } : {}) };
771
+ }
772
+ return { kind: "attention.list", ...(agent ? { agent } : {}) };
773
+ }
763
774
  function parseThoughtsCommand(args) {
764
775
  const { agent, rest: cleaned } = extractAgentFlag(args);
765
776
  let last;
@@ -930,6 +941,8 @@ function parseOuroCommand(args) {
930
941
  }
931
942
  if (head === "thoughts")
932
943
  return parseThoughtsCommand(args.slice(1));
944
+ if (head === "attention")
945
+ return parseAttentionCommand(args.slice(1));
933
946
  if (head === "chat") {
934
947
  if (!second)
935
948
  throw new Error(`Usage\n${usage()}`);
@@ -2284,6 +2297,74 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
2284
2297
  return message;
2285
2298
  }
2286
2299
  }
2300
+ // ── attention queue (local, no daemon socket needed) ──
2301
+ /* v8 ignore start -- CLI attention handler: requires real obligation store on disk @preserve */
2302
+ if (command.kind === "attention.list" || command.kind === "attention.show" || command.kind === "attention.history") {
2303
+ try {
2304
+ const agentName = command.agent ?? (0, identity_1.getAgentName)();
2305
+ const { listActiveObligations, readObligation } = await Promise.resolve().then(() => __importStar(require("../../mind/obligations")));
2306
+ if (command.kind === "attention.list") {
2307
+ const obligations = listActiveObligations(agentName);
2308
+ if (obligations.length === 0) {
2309
+ const message = "nothing held — attention queue is empty";
2310
+ deps.writeStdout(message);
2311
+ return message;
2312
+ }
2313
+ const lines = obligations.map((o) => `[${o.id}] ${o.origin.friendId} via ${o.origin.channel}/${o.origin.key} — ${o.delegatedContent.slice(0, 60)}${o.delegatedContent.length > 60 ? "..." : ""} (${o.status})`);
2314
+ const message = lines.join("\n");
2315
+ deps.writeStdout(message);
2316
+ return message;
2317
+ }
2318
+ if (command.kind === "attention.show") {
2319
+ const obligation = readObligation(agentName, command.id);
2320
+ if (!obligation) {
2321
+ const message = `no obligation found with id ${command.id}`;
2322
+ deps.writeStdout(message);
2323
+ return message;
2324
+ }
2325
+ const message = JSON.stringify(obligation, null, 2);
2326
+ deps.writeStdout(message);
2327
+ return message;
2328
+ }
2329
+ // attention.history: show returned obligations
2330
+ const { getObligationsDir } = await Promise.resolve().then(() => __importStar(require("../../mind/obligations")));
2331
+ const obligationsDir = getObligationsDir(agentName);
2332
+ let entries = [];
2333
+ try {
2334
+ entries = fs.readdirSync(obligationsDir);
2335
+ }
2336
+ catch { /* empty */ }
2337
+ const returned = entries
2338
+ .filter((e) => e.endsWith(".json"))
2339
+ .map((e) => { try {
2340
+ return JSON.parse(fs.readFileSync(path.join(obligationsDir, e), "utf-8"));
2341
+ }
2342
+ catch {
2343
+ return null;
2344
+ } })
2345
+ .filter((o) => o?.status === "returned")
2346
+ .sort((a, b) => (b.returnedAt ?? 0) - (a.returnedAt ?? 0))
2347
+ .slice(0, 20);
2348
+ if (returned.length === 0) {
2349
+ const message = "no surfacing history yet";
2350
+ deps.writeStdout(message);
2351
+ return message;
2352
+ }
2353
+ const lines = returned.map((o) => {
2354
+ const when = o.returnedAt ? new Date(o.returnedAt).toISOString() : "unknown";
2355
+ return `[${o.id}] → ${o.origin.friendId} via ${o.returnTarget ?? "unknown"} at ${when}`;
2356
+ });
2357
+ const message = lines.join("\n");
2358
+ deps.writeStdout(message);
2359
+ return message;
2360
+ }
2361
+ catch {
2362
+ const message = "error: no agent context — use --agent <name> to specify";
2363
+ deps.writeStdout(message);
2364
+ return message;
2365
+ }
2366
+ }
2367
+ /* v8 ignore stop */
2287
2368
  // ── session list (local, no daemon socket needed) ──
2288
2369
  if (command.kind === "session.list") {
2289
2370
  /* v8 ignore start -- production default: requires full identity setup @preserve */
@@ -82,7 +82,7 @@ function buildSpecialistSystemPrompt(soulText, identityText, existingBundles, co
82
82
  "2. I write agent.json to the temp directory using write_file",
83
83
  "3. I suggest a PascalCase name for the hatchling and confirm with the human",
84
84
  "4. I call complete_adoption with the name and a warm handoff message",
85
- "5. I call final_answer to end the session",
85
+ "5. I call settle to end the session",
86
86
  ].join("\n"));
87
87
  sections.push([
88
88
  "## Tools",
@@ -91,9 +91,9 @@ function buildSpecialistSystemPrompt(soulText, identityText, existingBundles, co
91
91
  "- `list_directory`: List directory contents. Useful for exploring existing agent bundles.",
92
92
  "- I also have the normal local harness tools when useful here, including `shell`, `ouro task create`, `ouro reminder create`, memory tools, coding tools, and repo helpers.",
93
93
  "- `complete_adoption`: Finalize the bundle. Validates, scaffolds structural dirs, moves to ~/AgentBundles/, writes secrets, plays hatch animation. I call this with `name` (PascalCase) and `handoff_message` (warm message for the human).",
94
- "- `final_answer`: End the conversation with a final message. I call this after complete_adoption succeeds.",
94
+ "- `settle`: End the conversation with a final message. I call this after complete_adoption succeeds.",
95
95
  "",
96
- "I must call `final_answer` when I am done to end the session cleanly.",
96
+ "I must call `settle` when I am done to end the session cleanly.",
97
97
  ].join("\n"));
98
98
  return sections.join("\n\n");
99
99
  }
@@ -90,7 +90,7 @@ const listDirToolSchema = {
90
90
  * Returns the specialist's tool schema array.
91
91
  */
92
92
  function getSpecialistTools() {
93
- return [completeAdoptionTool, tools_base_1.finalAnswerTool, readFileTool.tool, writeFileTool.tool, listDirToolSchema];
93
+ return [completeAdoptionTool, tools_base_1.settleTool, readFileTool.tool, writeFileTool.tool, listDirToolSchema];
94
94
  }
95
95
  const PSYCHE_FILES = ["SOUL.md", "IDENTITY.md", "LORE.md", "TACIT.md", "ASPIRATIONS.md"];
96
96
  function isPascalCase(name) {