@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 +22 -0
- package/dist/heart/core.js +149 -100
- package/dist/heart/daemon/daemon-cli.js +81 -0
- package/dist/heart/daemon/specialist-prompt.js +3 -3
- package/dist/heart/daemon/specialist-tools.js +1 -1
- package/dist/heart/daemon/thoughts.js +6 -6
- package/dist/heart/delegation.js +1 -1
- package/dist/heart/kicks.js +1 -1
- package/dist/heart/providers/anthropic.js +7 -7
- package/dist/heart/providers/azure.js +1 -1
- package/dist/heart/providers/github-copilot.js +2 -2
- package/dist/heart/providers/minimax.js +1 -1
- package/dist/heart/providers/openai-codex.js +1 -1
- package/dist/heart/streaming.js +25 -25
- package/dist/heart/tool-loop.js +5 -1
- package/dist/mind/context.js +62 -0
- package/dist/mind/prompt.js +19 -14
- package/dist/repertoire/ado-semantic.js +6 -0
- package/dist/repertoire/coding/tools.js +5 -0
- package/dist/repertoire/tools-base.js +31 -38
- package/dist/repertoire/tools-bluebubbles.js +1 -0
- package/dist/repertoire/tools-github.js +1 -6
- package/dist/repertoire/tools-teams.js +9 -36
- package/dist/repertoire/tools.js +193 -72
- package/dist/senses/attention-queue.js +97 -0
- package/dist/senses/cli.js +3 -3
- package/dist/senses/inner-dialog.js +39 -22
- package/dist/senses/pipeline.js +4 -3
- package/dist/senses/surface-tool.js +82 -0
- package/package.json +1 -1
- package/dist/heart/safe-workspace.js +0 -381
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": [
|
package/dist/heart/core.js
CHANGED
|
@@ -8,7 +8,7 @@ exports.getProvider = getProvider;
|
|
|
8
8
|
exports.createSummarize = createSummarize;
|
|
9
9
|
exports.getProviderDisplayLabel = getProviderDisplayLabel;
|
|
10
10
|
exports.isExternalStateQuery = isExternalStateQuery;
|
|
11
|
-
exports.
|
|
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
|
|
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
|
|
187
|
-
?
|
|
188
|
-
: "no
|
|
189
|
-
let
|
|
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
|
-
|
|
199
|
+
holdingLine = `i'm holding something for ${params.currentSession.friendId}`;
|
|
192
200
|
}
|
|
193
201
|
else {
|
|
194
|
-
|
|
202
|
+
holdingLine = "nothing -- just thinking";
|
|
195
203
|
}
|
|
196
204
|
return [
|
|
197
205
|
"## what i need to think about",
|
|
198
|
-
params.
|
|
206
|
+
params.topic,
|
|
199
207
|
"",
|
|
200
208
|
"## why this came up",
|
|
201
209
|
reasonProse,
|
|
202
210
|
"",
|
|
203
|
-
"##
|
|
204
|
-
|
|
211
|
+
"## who asked",
|
|
212
|
+
whoAsked,
|
|
205
213
|
"",
|
|
206
|
-
"## what i
|
|
207
|
-
|
|
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
|
|
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
|
|
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
|
|
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 && !
|
|
247
|
-
return "you're still holding something from an earlier conversation -- someone is waiting for your answer. finish the thought first, or
|
|
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
|
|
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
|
|
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
|
|
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 = "
|
|
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
|
|
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
|
-
//
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
|
|
521
|
-
|
|
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
|
-
|
|
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
|
|
608
|
+
const isSoleSettle = result.toolCalls.length === 1 && result.toolCalls[0].name === "settle";
|
|
590
609
|
if (hasPhaseAnnotation) {
|
|
591
|
-
msg.phase =
|
|
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 +
|
|
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
|
|
602
|
-
if (
|
|
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 } =
|
|
606
|
-
|
|
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.
|
|
668
|
+
if (result.settleStreamed) {
|
|
619
669
|
// The streaming layer already parsed and emitted the answer
|
|
620
|
-
// progressively via
|
|
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" : "
|
|
690
|
+
outcome = intent === "blocked" ? "blocked" : "settled";
|
|
641
691
|
done = true;
|
|
642
692
|
}
|
|
643
693
|
}
|
|
644
694
|
else {
|
|
645
|
-
// Answer is undefined -- the model's
|
|
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
|
|
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
|
|
658
|
-
const
|
|
659
|
-
if (
|
|
660
|
-
|
|
661
|
-
try {
|
|
662
|
-
|
|
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 {
|
|
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.
|
|
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 = "
|
|
733
|
+
outcome = "observed";
|
|
678
734
|
done = true;
|
|
679
735
|
continue;
|
|
680
736
|
}
|
|
681
|
-
// Check for
|
|
682
|
-
const
|
|
683
|
-
if (
|
|
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
|
-
|
|
690
|
-
|
|
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 =
|
|
703
|
-
|
|
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
|
-
|
|
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.
|
|
820
|
+
event: "engine.descended",
|
|
758
821
|
message: "taking thread inward",
|
|
759
|
-
meta: { mode, hasAnswer: answer !== undefined, contentSnippet:
|
|
822
|
+
meta: { mode, hasAnswer: answer !== undefined, contentSnippet: topic.slice(0, 80) },
|
|
760
823
|
});
|
|
761
|
-
outcome = "
|
|
824
|
+
outcome = "descended";
|
|
762
825
|
done = true;
|
|
763
826
|
continue;
|
|
764
827
|
}
|
|
765
828
|
messages.push(msg);
|
|
766
|
-
//
|
|
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
|
-
//
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
messages.push({ role: "tool", tool_call_id: tc.id, content:
|
|
774
|
-
providerRuntime.appendToolOutput(tc.id,
|
|
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,
|
|
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
|
|
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
|
-
"- `
|
|
94
|
+
"- `settle`: End the conversation with a final message. I call this after complete_adoption succeeds.",
|
|
95
95
|
"",
|
|
96
|
-
"I must call `
|
|
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.
|
|
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) {
|