@ouro.bot/cli 0.1.0-alpha.132 → 0.1.0-alpha.134
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 +23 -0
- package/dist/heart/active-work.js +11 -0
- package/dist/heart/core.js +93 -68
- package/dist/heart/daemon/daemon-cli.js +81 -0
- package/dist/heart/daemon/ouro-path-installer.js +10 -5
- 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/mind/obligations.js +134 -0
- package/dist/mind/prompt.js +10 -10
- package/dist/repertoire/tools-base.js +25 -12
- package/dist/repertoire/tools.js +179 -4
- package/dist/senses/attention-queue.js +97 -0
- package/dist/senses/cli.js +3 -3
- package/dist/senses/inner-dialog.js +72 -5
- package/dist/senses/pipeline.js +13 -3
- package/dist/senses/surface-tool.js +82 -0
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
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.134",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Metacognitive tool redesign: final_answer renamed to settle, no_response renamed to observe. RunAgentOutcome values now 'settled' and 'observed'.",
|
|
8
|
+
"New surface tool: inner-dialog-only tool for routing thoughts outward to friends' freshest active sessions. Replaces post-turn routeDelegatedCompletion with inline delivery.",
|
|
9
|
+
"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.",
|
|
10
|
+
"Surface-before-settle gate: settle rejected in inner dialog until all attention queue items are surfaced.",
|
|
11
|
+
"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.",
|
|
12
|
+
"go_inward content param renamed to topic with metacognitive handoff framing.",
|
|
13
|
+
"Shared sole-call interception pattern extracted from duplicated core.ts logic.",
|
|
14
|
+
"Exact-origin routing removed from routeDelegatedCompletion; routing now bridge, freshest, deferred only.",
|
|
15
|
+
"ouro attention CLI: list held items, show details by ID, view surfacing history.",
|
|
16
|
+
"System prompts updated with metacognitive framing for all new tool names."
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"version": "0.1.0-alpha.133",
|
|
21
|
+
"changes": [
|
|
22
|
+
"Inner return obligations: delegated inner dialog work now tracks a ReturnObligation through queued → running → returned/deferred lifecycle.",
|
|
23
|
+
"Exact-origin routing: inner dialog completions route back to the session that delegated the work, not just the freshest active session.",
|
|
24
|
+
"Active work frame surfaces pending inner return obligations so the agent knows what's outstanding."
|
|
25
|
+
]
|
|
26
|
+
},
|
|
4
27
|
{
|
|
5
28
|
"version": "0.1.0-alpha.132",
|
|
6
29
|
"changes": [
|
|
@@ -439,6 +439,7 @@ function buildActiveWorkFrame(input) {
|
|
|
439
439
|
otherCodingSessions,
|
|
440
440
|
pendingObligations,
|
|
441
441
|
targetCandidates: input.targetCandidates ?? [],
|
|
442
|
+
innerReturnObligations: input.innerReturnObligations ?? [],
|
|
442
443
|
bridgeSuggestion: suggestBridgeForActiveWork({
|
|
443
444
|
currentSession: input.currentSession,
|
|
444
445
|
currentObligation: input.currentObligation,
|
|
@@ -588,6 +589,16 @@ function formatActiveWorkFrame(frame) {
|
|
|
588
589
|
lines.push(obligationLine);
|
|
589
590
|
}
|
|
590
591
|
}
|
|
592
|
+
if (frame.innerReturnObligations && frame.innerReturnObligations.length > 0) {
|
|
593
|
+
lines.push("");
|
|
594
|
+
lines.push("## inner return obligations");
|
|
595
|
+
for (const ob of frame.innerReturnObligations) {
|
|
596
|
+
const preview = ob.delegatedContent.length > 60
|
|
597
|
+
? `${ob.delegatedContent.slice(0, 57)}...`
|
|
598
|
+
: ob.delegatedContent;
|
|
599
|
+
lines.push(`- [${ob.status}] ${ob.origin.friendId}/${ob.origin.channel}/${ob.origin.key}: ${preview}`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
591
602
|
// Bridge suggestion
|
|
592
603
|
if (frame.bridgeSuggestion) {
|
|
593
604
|
lines.push("");
|
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
|
+
go_inward: "rejected: go_inward must be the only tool call. finish your other work first, then call go_inward 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",
|
|
@@ -183,34 +191,34 @@ function buildGoInwardHandoffPacket(params) {
|
|
|
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,9 +245,9 @@ 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, sawGoInward, _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
|
|
@@ -248,11 +256,11 @@ function getFinalAnswerRetryError(mustResolveBeforeHandoff, intent, sawSteeringF
|
|
|
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;
|
|
@@ -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 go_inward (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
|
+
// go_inward, 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.goInwardTool] : []),
|
|
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,44 @@ 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
|
+
// Inner dialog attention queue gate: reject settle if items remain
|
|
623
|
+
const attentionQueue = (augmentedToolContext ?? options?.toolContext)?.delegatedOrigins;
|
|
624
|
+
if (isInnerDialog && attentionQueue && attentionQueue.length > 0) {
|
|
625
|
+
callbacks.onClearText?.();
|
|
626
|
+
messages.push(msg);
|
|
627
|
+
const gateMessage = "you're holding thoughts someone is waiting for — surface them before you settle.";
|
|
628
|
+
messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: gateMessage });
|
|
629
|
+
providerRuntime.appendToolOutput(result.toolCalls[0].id, gateMessage);
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
603
632
|
// Extract answer from the tool call arguments.
|
|
604
633
|
// Supports: {"answer":"text","intent":"..."} or "text" (JSON string).
|
|
605
|
-
const { answer, intent } =
|
|
606
|
-
|
|
634
|
+
const { answer, intent } = parseSettlePayload(result.toolCalls[0].arguments);
|
|
635
|
+
// Inner dialog settle: no CompletionMetadata, "(settled)" ack
|
|
636
|
+
if (isInnerDialog) {
|
|
637
|
+
messages.push(msg);
|
|
638
|
+
const settled = "(settled)";
|
|
639
|
+
messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: settled });
|
|
640
|
+
providerRuntime.appendToolOutput(result.toolCalls[0].id, settled);
|
|
641
|
+
outcome = "settled";
|
|
642
|
+
done = true;
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
const retryError = getSettleRetryError(mustResolveBeforeHandoffActive, intent, sawSteeringFollowUp, options?.delegationDecision, sawSendMessageSelf, sawGoInward, sawQuerySession, options?.currentObligation ?? null, options?.activeWorkFrame?.inner?.job, sawExternalStateQuery);
|
|
607
646
|
const deliveredAnswer = answer;
|
|
608
647
|
const validDirectReply = mustResolveBeforeHandoffActive && intent === "direct_reply" && sawSteeringFollowUp;
|
|
609
648
|
const validTerminalIntent = intent === "complete" || intent === "blocked";
|
|
@@ -615,9 +654,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
615
654
|
answer: deliveredAnswer,
|
|
616
655
|
intent: validDirectReply ? "direct_reply" : intent === "blocked" ? "blocked" : "complete",
|
|
617
656
|
};
|
|
618
|
-
if (result.
|
|
657
|
+
if (result.settleStreamed) {
|
|
619
658
|
// The streaming layer already parsed and emitted the answer
|
|
620
|
-
// progressively via
|
|
659
|
+
// progressively via SettleParser. Skip clearing and
|
|
621
660
|
// re-emitting to avoid double-delivery.
|
|
622
661
|
}
|
|
623
662
|
else {
|
|
@@ -637,26 +676,26 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
637
676
|
const delivered = "(delivered)";
|
|
638
677
|
messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: delivered });
|
|
639
678
|
providerRuntime.appendToolOutput(result.toolCalls[0].id, delivered);
|
|
640
|
-
outcome = intent === "blocked" ? "blocked" : "
|
|
679
|
+
outcome = intent === "blocked" ? "blocked" : "settled";
|
|
641
680
|
done = true;
|
|
642
681
|
}
|
|
643
682
|
}
|
|
644
683
|
else {
|
|
645
|
-
// Answer is undefined -- the model's
|
|
684
|
+
// Answer is undefined -- the model's settle was incomplete or
|
|
646
685
|
// malformed. Clear any partial streamed text or noise, then push the
|
|
647
686
|
// assistant msg + error tool result and let the model try again.
|
|
648
687
|
callbacks.onClearText?.();
|
|
649
688
|
messages.push(msg);
|
|
650
689
|
const toolRetryMessage = retryError
|
|
651
|
-
?? "your
|
|
690
|
+
?? "your settle was incomplete or malformed. call settle again with your complete response.";
|
|
652
691
|
messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: toolRetryMessage });
|
|
653
692
|
providerRuntime.appendToolOutput(result.toolCalls[0].id, toolRetryMessage);
|
|
654
693
|
}
|
|
655
694
|
continue;
|
|
656
695
|
}
|
|
657
|
-
// Check for
|
|
658
|
-
const
|
|
659
|
-
if (
|
|
696
|
+
// Check for observe sole call: intercept before tool execution
|
|
697
|
+
const isSoleObserve = result.toolCalls.length === 1 && result.toolCalls[0].name === "observe";
|
|
698
|
+
if (isSoleObserve) {
|
|
660
699
|
let reason;
|
|
661
700
|
try {
|
|
662
701
|
const parsed = JSON.parse(result.toolCalls[0].arguments);
|
|
@@ -666,7 +705,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
666
705
|
catch { /* ignore */ }
|
|
667
706
|
(0, runtime_1.emitNervesEvent)({
|
|
668
707
|
component: "engine",
|
|
669
|
-
event: "engine.
|
|
708
|
+
event: "engine.observe",
|
|
670
709
|
message: "agent declined to respond in group chat",
|
|
671
710
|
meta: { ...(reason ? { reason } : {}) },
|
|
672
711
|
});
|
|
@@ -674,7 +713,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
674
713
|
const silenced = "(silenced)";
|
|
675
714
|
messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: silenced });
|
|
676
715
|
providerRuntime.appendToolOutput(result.toolCalls[0].id, silenced);
|
|
677
|
-
outcome = "
|
|
716
|
+
outcome = "observed";
|
|
678
717
|
done = true;
|
|
679
718
|
continue;
|
|
680
719
|
}
|
|
@@ -686,8 +725,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
686
725
|
parsedArgs = JSON.parse(result.toolCalls[0].arguments);
|
|
687
726
|
}
|
|
688
727
|
catch { /* ignore */ }
|
|
689
|
-
/* v8 ignore next -- defensive:
|
|
690
|
-
const
|
|
728
|
+
/* v8 ignore next -- defensive: topic always string from model @preserve */
|
|
729
|
+
const topic = typeof parsedArgs.topic === "string" ? parsedArgs.topic : "";
|
|
691
730
|
const answer = typeof parsedArgs.answer === "string" ? parsedArgs.answer : undefined;
|
|
692
731
|
const parsedMode = parsedArgs.mode === "reflect" || parsedArgs.mode === "plan" || parsedArgs.mode === "relay"
|
|
693
732
|
? parsedArgs.mode
|
|
@@ -700,7 +739,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
700
739
|
}
|
|
701
740
|
// Build handoff packet and enqueue
|
|
702
741
|
const handoffContent = buildGoInwardHandoffPacket({
|
|
703
|
-
|
|
742
|
+
topic,
|
|
704
743
|
mode,
|
|
705
744
|
delegationDecision: options?.delegationDecision,
|
|
706
745
|
currentSession: options?.toolContext?.currentSession ?? null,
|
|
@@ -736,7 +775,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
736
775
|
channel: currentSession.channel,
|
|
737
776
|
key: currentSession.key,
|
|
738
777
|
},
|
|
739
|
-
content,
|
|
778
|
+
content: topic,
|
|
740
779
|
});
|
|
741
780
|
}
|
|
742
781
|
catch {
|
|
@@ -756,36 +795,22 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
756
795
|
component: "engine",
|
|
757
796
|
event: "engine.go_inward",
|
|
758
797
|
message: "taking thread inward",
|
|
759
|
-
meta: { mode, hasAnswer: answer !== undefined, contentSnippet:
|
|
798
|
+
meta: { mode, hasAnswer: answer !== undefined, contentSnippet: topic.slice(0, 80) },
|
|
760
799
|
});
|
|
761
800
|
outcome = "go_inward";
|
|
762
801
|
done = true;
|
|
763
802
|
continue;
|
|
764
803
|
}
|
|
765
804
|
messages.push(msg);
|
|
766
|
-
//
|
|
805
|
+
// Execute tools (sole-call tools in mixed calls are rejected inline)
|
|
767
806
|
for (const tc of result.toolCalls) {
|
|
768
807
|
if (signal?.aborted)
|
|
769
808
|
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);
|
|
809
|
+
// Reject sole-call tools when mixed with other tool calls
|
|
810
|
+
const soleCallRejection = SOLE_CALL_REJECTION[tc.name];
|
|
811
|
+
if (soleCallRejection) {
|
|
812
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: soleCallRejection });
|
|
813
|
+
providerRuntime.appendToolOutput(tc.id, soleCallRejection);
|
|
789
814
|
continue;
|
|
790
815
|
}
|
|
791
816
|
let args = {};
|
|
@@ -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 */
|
|
@@ -46,6 +46,11 @@ if [ ! -e "$ENTRY" ]; then
|
|
|
46
46
|
fi
|
|
47
47
|
exec node "$ENTRY" "$@"
|
|
48
48
|
`;
|
|
49
|
+
function writeWrapperScript(scriptPath, mkdirSync, writeFileSync, chmodSync) {
|
|
50
|
+
mkdirSync(path.dirname(scriptPath), { recursive: true });
|
|
51
|
+
writeFileSync(scriptPath, WRAPPER_SCRIPT, { mode: 0o755 });
|
|
52
|
+
chmodSync(scriptPath, 0o755);
|
|
53
|
+
}
|
|
49
54
|
function detectShellProfile(homeDir, shell) {
|
|
50
55
|
if (!shell)
|
|
51
56
|
return null;
|
|
@@ -151,9 +156,9 @@ function installOuroCommand(deps = {}) {
|
|
|
151
156
|
meta: { scriptPath, binDir },
|
|
152
157
|
});
|
|
153
158
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
159
|
+
if (!modernCurrent) {
|
|
160
|
+
writeWrapperScript(scriptPath, mkdirSync, writeFileSync, chmodSync);
|
|
161
|
+
}
|
|
157
162
|
}
|
|
158
163
|
catch (error) {
|
|
159
164
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -166,8 +171,8 @@ function installOuroCommand(deps = {}) {
|
|
|
166
171
|
return { installed: false, scriptPath: null, pathReady: false, shellProfileUpdated: null, skippedReason: error instanceof Error ? error.message : /* v8 ignore next -- defensive @preserve */ String(error), repairedOldLauncher };
|
|
167
172
|
}
|
|
168
173
|
// Check if ~/.ouro-cli/bin is already in PATH
|
|
169
|
-
let shellProfileUpdated = null;
|
|
170
174
|
const pathReady = isBinDirInPath(binDir, envPath);
|
|
175
|
+
let shellProfileUpdated = null;
|
|
171
176
|
if (!pathReady) {
|
|
172
177
|
const profilePath = detectShellProfile(homeDir, shell);
|
|
173
178
|
if (profilePath) {
|
|
@@ -199,7 +204,7 @@ function installOuroCommand(deps = {}) {
|
|
|
199
204
|
component: "daemon",
|
|
200
205
|
event: "daemon.ouro_path_install_end",
|
|
201
206
|
message: "ouro command installed",
|
|
202
|
-
meta: { scriptPath, pathReady, shellProfileUpdated },
|
|
207
|
+
meta: { scriptPath, pathReady, shellProfileUpdated, oldScriptPath: oldExists ? oldScriptPath : null },
|
|
203
208
|
});
|
|
204
209
|
return { installed: true, scriptPath, pathReady, shellProfileUpdated, repairedOldLauncher };
|
|
205
210
|
}
|
|
@@ -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) {
|
|
@@ -93,7 +93,7 @@ function extractToolNames(messages) {
|
|
|
93
93
|
if (msg.role === "assistant" && Array.isArray(msg.tool_calls)) {
|
|
94
94
|
for (const tc of msg.tool_calls) {
|
|
95
95
|
const toolFunction = extractToolFunction(tc);
|
|
96
|
-
if (toolFunction?.name && toolFunction.name !== "
|
|
96
|
+
if (toolFunction?.name && toolFunction.name !== "settle")
|
|
97
97
|
names.push(toolFunction.name);
|
|
98
98
|
}
|
|
99
99
|
}
|
|
@@ -321,15 +321,15 @@ function formatInnerDialogStatus(status) {
|
|
|
321
321
|
}
|
|
322
322
|
return lines.join("\n");
|
|
323
323
|
}
|
|
324
|
-
/** Extract text from a
|
|
325
|
-
function
|
|
324
|
+
/** Extract text from a settle tool call's arguments. */
|
|
325
|
+
function extractSettleAnswer(messages) {
|
|
326
326
|
for (let k = messages.length - 1; k >= 0; k--) {
|
|
327
327
|
const msg = messages[k];
|
|
328
328
|
if (msg.role !== "assistant" || !Array.isArray(msg.tool_calls))
|
|
329
329
|
continue;
|
|
330
330
|
for (const tc of msg.tool_calls) {
|
|
331
331
|
const toolFunction = extractToolFunction(tc);
|
|
332
|
-
if (toolFunction?.name !== "
|
|
332
|
+
if (toolFunction?.name !== "settle")
|
|
333
333
|
continue;
|
|
334
334
|
try {
|
|
335
335
|
const parsed = JSON.parse(toolFunction.arguments ?? "{}");
|
|
@@ -348,7 +348,7 @@ function extractThoughtResponseFromMessages(messages) {
|
|
|
348
348
|
const lastAssistant = assistantMsgs.reverse().find((message) => contentToText(message.content).trim().length > 0);
|
|
349
349
|
return lastAssistant
|
|
350
350
|
? contentToText(lastAssistant.content).trim()
|
|
351
|
-
:
|
|
351
|
+
: extractSettleAnswer(messages);
|
|
352
352
|
}
|
|
353
353
|
function parseInnerDialogSession(sessionPath) {
|
|
354
354
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -397,7 +397,7 @@ function parseInnerDialogSession(sessionPath) {
|
|
|
397
397
|
j++;
|
|
398
398
|
}
|
|
399
399
|
// Find the last assistant text response in this turn.
|
|
400
|
-
// With tool_choice="required", the response may be inside a
|
|
400
|
+
// With tool_choice="required", the response may be inside a settle tool call.
|
|
401
401
|
const response = extractThoughtResponseFromMessages(turnMessages);
|
|
402
402
|
const tools = extractToolNames(turnMessages);
|
|
403
403
|
turns.push({
|
package/dist/heart/delegation.js
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.decideDelegation = decideDelegation;
|
|
4
4
|
const runtime_1 = require("../nerves/runtime");
|
|
5
5
|
const CROSS_SESSION_TOOLS = new Set(["query_session", "send_message", "bridge_manage"]);
|
|
6
|
-
const FAST_PATH_TOOLS = new Set(["
|
|
6
|
+
const FAST_PATH_TOOLS = new Set(["settle"]);
|
|
7
7
|
const REFLECTION_PATTERN = /\b(think|reflect|ponder|surface|surfaces|surfaced|sit with|metaboli[sz]e)\b/i;
|
|
8
8
|
const CROSS_SESSION_PATTERN = /\b(other chat|other session|across chats?|across sessions?|keep .* aligned|relay|carry .* across)\b/i;
|
|
9
9
|
function hasExplicitReflection(ingressTexts) {
|