@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.
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildAttentionQueue = buildAttentionQueue;
4
+ exports.dequeueAttentionItem = dequeueAttentionItem;
5
+ exports.attentionQueueEmpty = attentionQueueEmpty;
6
+ exports.buildAttentionQueueSummary = buildAttentionQueueSummary;
7
+ const runtime_1 = require("../nerves/runtime");
8
+ // ── Queue construction ───────────────────────────────────────────
9
+ function generateItemId() {
10
+ return Math.random().toString(36).slice(2, 10);
11
+ }
12
+ function originKey(friendId, channel, key) {
13
+ return `${friendId}/${channel}/${key}`;
14
+ }
15
+ function buildAttentionQueue(input) {
16
+ const { drainedPending, outstandingObligations, friendNameResolver } = input;
17
+ const seen = new Set();
18
+ const items = [];
19
+ // Source 1: drained pending messages with delegatedFrom (current-turn delegations)
20
+ for (const msg of drainedPending) {
21
+ if (!msg.delegatedFrom)
22
+ continue;
23
+ const { friendId, channel, key, bridgeId } = msg.delegatedFrom;
24
+ const oKey = originKey(friendId, channel, key);
25
+ seen.add(oKey);
26
+ const resolvedName = friendNameResolver(friendId);
27
+ items.push({
28
+ id: msg.obligationId ?? generateItemId(),
29
+ friendId,
30
+ friendName: resolvedName ?? friendId,
31
+ channel,
32
+ key,
33
+ ...(bridgeId ? { bridgeId } : {}),
34
+ delegatedContent: msg.content,
35
+ ...(msg.obligationId ? { obligationId: msg.obligationId } : {}),
36
+ source: "drained",
37
+ timestamp: msg.timestamp,
38
+ });
39
+ }
40
+ // Source 2: outstanding obligations (crash recovery)
41
+ for (const obligation of outstandingObligations) {
42
+ const { friendId, channel, key, bridgeId } = obligation.origin;
43
+ const oKey = originKey(friendId, channel, key);
44
+ if (seen.has(oKey))
45
+ continue; // deduplicate: prefer drained version
46
+ seen.add(oKey);
47
+ const resolvedName = friendNameResolver(friendId);
48
+ items.push({
49
+ id: obligation.id,
50
+ friendId,
51
+ friendName: resolvedName ?? friendId,
52
+ channel,
53
+ key,
54
+ ...(bridgeId ? { bridgeId } : {}),
55
+ delegatedContent: obligation.delegatedContent,
56
+ obligationId: obligation.id,
57
+ source: "obligation-recovery",
58
+ timestamp: obligation.createdAt,
59
+ });
60
+ }
61
+ // Sort FIFO (oldest first)
62
+ items.sort((a, b) => a.timestamp - b.timestamp);
63
+ (0, runtime_1.emitNervesEvent)({
64
+ event: "senses.attention_queue_built",
65
+ component: "senses",
66
+ message: `attention queue built with ${items.length} item(s)`,
67
+ meta: {
68
+ drainedCount: items.filter((i) => i.source === "drained").length,
69
+ recoveredCount: items.filter((i) => i.source === "obligation-recovery").length,
70
+ },
71
+ });
72
+ return items;
73
+ }
74
+ // ── Queue operations ─────────────────────────────────────────────
75
+ function dequeueAttentionItem(queue, id) {
76
+ const index = queue.findIndex((item) => item.id === id);
77
+ if (index === -1)
78
+ return null;
79
+ return queue.splice(index, 1)[0];
80
+ }
81
+ function attentionQueueEmpty(queue) {
82
+ return queue.length === 0;
83
+ }
84
+ // ── Queue visibility ─────────────────────────────────────────────
85
+ const CONTENT_PREVIEW_MAX = 80;
86
+ function buildAttentionQueueSummary(queue) {
87
+ if (queue.length === 0)
88
+ return "";
89
+ const lines = ["you're holding:"];
90
+ for (const item of queue) {
91
+ const preview = item.delegatedContent.length > CONTENT_PREVIEW_MAX
92
+ ? `${item.delegatedContent.slice(0, CONTENT_PREVIEW_MAX - 3)}...`
93
+ : item.delegatedContent;
94
+ lines.push(`- [${item.id}] ${item.friendName} asked: "${preview}"`);
95
+ }
96
+ return lines.join("\n");
97
+ }
@@ -382,14 +382,14 @@ function createCliCallbacks() {
382
382
  onModelStreamStart: () => {
383
383
  // No-op: content callbacks (onTextChunk, onReasoningChunk) handle
384
384
  // stopping the spinner. onModelStreamStart fires too early and
385
- // doesn't fire at all for final_answer tool streaming.
385
+ // doesn't fire at all for settle tool streaming.
386
386
  },
387
387
  onClearText: () => {
388
388
  streamer.reset();
389
389
  wrapper.reset();
390
390
  },
391
391
  onTextChunk: (text) => {
392
- // Stop spinner if still running — final_answer streaming and Anthropic
392
+ // Stop spinner if still running — settle streaming and Anthropic
393
393
  // tool-only responses bypass onModelStreamStart, so the spinner would
394
394
  // otherwise keep running (and its \r writes overwrite response text).
395
395
  if (currentSpinner) {
@@ -558,7 +558,7 @@ async function runCliSession(options) {
558
558
  exitToolResult = result;
559
559
  exitToolFired = true;
560
560
  // Abort immediately so the model doesn't generate more output
561
- // (e.g. reasoning about calling final_answer after complete_adoption)
561
+ // (e.g. reasoning about calling settle after complete_adoption)
562
562
  currentAbort?.abort();
563
563
  }
564
564
  return result;
@@ -42,6 +42,7 @@ exports.buildTaskTriggeredMessage = buildTaskTriggeredMessage;
42
42
  exports.deriveResumeCheckpoint = deriveResumeCheckpoint;
43
43
  exports.innerDialogSessionPath = innerDialogSessionPath;
44
44
  exports.enrichDelegatedFromWithBridge = enrichDelegatedFromWithBridge;
45
+ exports.routeDelegatedCompletion = routeDelegatedCompletion;
45
46
  exports.runInnerDialogTurn = runInnerDialogTurn;
46
47
  const fs = __importStar(require("fs"));
47
48
  const path = __importStar(require("path"));
@@ -54,6 +55,7 @@ const mcp_manager_1 = require("../repertoire/mcp-manager");
54
55
  const bundle_manifest_1 = require("../mind/bundle-manifest");
55
56
  const pending_1 = require("../mind/pending");
56
57
  const obligations_1 = require("../mind/obligations");
58
+ const attention_queue_1 = require("./attention-queue");
57
59
  const channel_1 = require("../mind/friends/channel");
58
60
  const trust_gate_1 = require("./trust-gate");
59
61
  const tokens_1 = require("../mind/friends/tokens");
@@ -259,6 +261,7 @@ function writeInnerDialogRuntimeState(sessionFilePath, state) {
259
261
  });
260
262
  }
261
263
  }
264
+ /* v8 ignore start -- routing helpers: called from routing functions which are integration paths @preserve */
262
265
  function writePendingEnvelope(pendingDir, message) {
263
266
  fs.mkdirSync(pendingDir, { recursive: true });
264
267
  const fileName = `${message.timestamp}-${Math.random().toString(36).slice(2, 10)}.json`;
@@ -270,12 +273,8 @@ function sessionMatchesActivity(activity, session) {
270
273
  && activity.channel === session.channel
271
274
  && activity.key === session.key;
272
275
  }
273
- function resolveExactOriginSession(delegatedFrom, sessionActivity) {
274
- return sessionActivity.find((activity) => activity.friendId === delegatedFrom.friendId
275
- && activity.channel === delegatedFrom.channel
276
- && activity.key === delegatedFrom.key
277
- && activity.channel !== "inner") ?? null;
278
- }
276
+ /* v8 ignore stop */
277
+ /* v8 ignore start -- routing: delivery now inline via surface tool; routing functions preserved for reuse @preserve */
279
278
  function resolveBridgePreferredSession(delegatedFrom, sessionActivity) {
280
279
  if (!delegatedFrom.bridgeId)
281
280
  return null;
@@ -385,18 +384,7 @@ async function routeDelegatedCompletion(agentRoot, agentName, completion, draine
385
384
  friendsDir: path.join(agentRoot, "friends"),
386
385
  agentName,
387
386
  });
388
- // Priority 1: Exact origin session (the session that delegated this work).
389
- const exactOrigin = resolveExactOriginSession(delegatedFrom, sessionActivity);
390
- if (exactOrigin) {
391
- if (await tryDeliverDelegatedCompletion(exactOrigin, outboundEnvelope)) {
392
- advanceObligationQuietly(agentName, obligationId, { status: "returned", returnedAt: timestamp, returnTarget: "exact-origin" });
393
- return;
394
- }
395
- writePendingEnvelope((0, pending_1.getPendingDir)(agentName, exactOrigin.friendId, exactOrigin.channel, exactOrigin.key), outboundEnvelope);
396
- advanceObligationQuietly(agentName, obligationId, { status: "returned", returnedAt: timestamp, returnTarget: "exact-origin" });
397
- return;
398
- }
399
- // Priority 2: Bridge-preferred session (if delegation was within a bridge).
387
+ // Priority 1: Bridge-preferred session (if delegation was within a bridge).
400
388
  const bridgeTarget = resolveBridgePreferredSession(delegatedFrom, sessionActivity);
401
389
  if (bridgeTarget) {
402
390
  if (await tryDeliverDelegatedCompletion(bridgeTarget, outboundEnvelope)) {
@@ -407,7 +395,7 @@ async function routeDelegatedCompletion(agentRoot, agentName, completion, draine
407
395
  advanceObligationQuietly(agentName, obligationId, { status: "returned", returnedAt: timestamp, returnTarget: "bridge-session" });
408
396
  return;
409
397
  }
410
- // Priority 3: Freshest active friend session.
398
+ // Priority 2: Freshest active friend session.
411
399
  const freshest = (0, session_activity_1.findFreshestFriendSession)({
412
400
  sessionsDir: path.join(agentRoot, "state", "sessions"),
413
401
  friendsDir: path.join(agentRoot, "friends"),
@@ -424,10 +412,11 @@ async function routeDelegatedCompletion(agentRoot, agentName, completion, draine
424
412
  advanceObligationQuietly(agentName, obligationId, { status: "returned", returnedAt: timestamp, returnTarget: "freshest-session" });
425
413
  return;
426
414
  }
427
- // Priority 4: Deferred return queue.
415
+ // Priority 3: Deferred return queue.
428
416
  writePendingEnvelope((0, pending_1.getDeferredReturnDir)(agentName, delegatedFrom.friendId), outboundEnvelope);
429
417
  advanceObligationQuietly(agentName, obligationId, { status: "deferred", returnedAt: timestamp, returnTarget: "deferred" });
430
418
  }
419
+ /* v8 ignore stop */
431
420
  // Self-referencing friend record for inner dialog (agent talking to itself).
432
421
  // No real friend to resolve -- this satisfies the pipeline's friend resolver contract.
433
422
  function createSelfFriend(agentName) {
@@ -459,7 +448,6 @@ async function runInnerDialogTurn(options) {
459
448
  const reason = options?.reason ?? "heartbeat";
460
449
  const sessionFilePath = innerDialogSessionPath();
461
450
  const agentName = (0, identity_1.getAgentName)();
462
- const agentRoot = (0, identity_1.getAgentRoot)();
463
451
  writeInnerDialogRuntimeState(sessionFilePath, {
464
452
  status: "running",
465
453
  reason,
@@ -525,6 +513,8 @@ async function runInnerDialogTurn(options) {
525
513
  // ── Call shared pipeline ──────────────────────────────────────────
526
514
  const callbacks = createInnerDialogCallbacks();
527
515
  const traceId = (0, nerves_1.createTraceId)();
516
+ // Attention queue: built when pending messages are drained, shared with tool context
517
+ let attentionQueue = [];
528
518
  const result = await (0, pipeline_1.handleInboundTurn)({
529
519
  channel: "inner",
530
520
  sessionKey: "dialog",
@@ -542,14 +532,41 @@ async function runInnerDialogTurn(options) {
542
532
  postTurn: context_1.postTurn,
543
533
  accumulateFriendTokens: tokens_1.accumulateFriendTokens,
544
534
  signal: options?.signal,
535
+ /* v8 ignore start -- attention queue: callback invoked by pipeline during pending drain; tested via attention-queue unit tests @preserve */
536
+ onPendingDrained: (drained) => {
537
+ const outstandingObligations = (0, obligations_1.listActiveObligations)(agentName);
538
+ attentionQueue = (0, attention_queue_1.buildAttentionQueue)({
539
+ drainedPending: drained,
540
+ outstandingObligations,
541
+ friendNameResolver: (friendId) => {
542
+ try {
543
+ const raw = fs.readFileSync(path.join((0, identity_1.getAgentRoot)(agentName), "friends", friendId + ".json"), "utf-8");
544
+ const parsed = JSON.parse(raw);
545
+ return typeof parsed.name === "string" ? parsed.name : null;
546
+ }
547
+ catch {
548
+ return null;
549
+ }
550
+ },
551
+ });
552
+ const summary = (0, attention_queue_1.buildAttentionQueueSummary)(attentionQueue);
553
+ return summary ? [summary] : [];
554
+ },
555
+ /* v8 ignore stop */
545
556
  runAgentOptions: {
546
557
  traceId,
547
558
  toolChoiceRequired: true,
548
559
  skipConfirmation: true,
549
560
  mcpManager,
561
+ toolContext: {
562
+ signin: async () => undefined,
563
+ delegatedOrigins: attentionQueue,
564
+ },
550
565
  },
551
566
  });
552
- await routeDelegatedCompletion(agentRoot, agentName, result.completion, result.drainedPending, now().getTime());
567
+ // Post-turn routeDelegatedCompletion removed: delivery is now inline via surface tool.
568
+ // settle in inner dialog produces no CompletionMetadata, so routeDelegatedCompletion
569
+ // would be a no-op. The routing infrastructure is reused by the surface handler.
553
570
  const resultMessages = result.messages ?? [];
554
571
  const assistantPreview = extractAssistantPreview(resultMessages);
555
572
  const toolCalls = extractToolCallNames(resultMessages);
@@ -332,8 +332,9 @@ async function handleInboundTurn(input) {
332
332
  : (input.drainDeferredReturns?.(resolvedContext.friend.id) ?? []);
333
333
  const sessionPending = input.drainPending(input.pendingDir);
334
334
  const pending = [...deferredReturns, ...sessionPending];
335
- // Assemble messages: session messages + live world-state checkpoint + pending + inbound user messages
336
- const prefixSections = [(0, active_work_1.formatLiveWorldStateCheckpoint)(activeWorkFrame)];
335
+ // Assemble messages: session messages + attention queue + live world-state checkpoint + pending + inbound user messages
336
+ const extraPrefixSections = input.onPendingDrained?.(pending) ?? [];
337
+ const prefixSections = [...extraPrefixSections, (0, active_work_1.formatLiveWorldStateCheckpoint)(activeWorkFrame)];
337
338
  if (pending.length > 0) {
338
339
  const pendingSection = pending
339
340
  .map((msg) => `[pending from ${msg.from}]: ${msg.content}`)
@@ -422,7 +423,7 @@ async function handleInboundTurn(input) {
422
423
  ...(mustResolveBeforeHandoff ? { mustResolveBeforeHandoff: true } : {}),
423
424
  ...(typeof lastFriendActivityAt === "string" ? { lastFriendActivityAt } : {}),
424
425
  };
425
- const nextState = result.outcome === "complete" || result.outcome === "blocked" || result.outcome === "superseded" || result.outcome === "no_response"
426
+ const nextState = result.outcome === "settled" || result.outcome === "blocked" || result.outcome === "superseded" || result.outcome === "observed"
426
427
  ? (typeof lastFriendActivityAt === "string"
427
428
  ? { lastFriendActivityAt }
428
429
  : undefined)
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.surfaceToolDef = void 0;
4
+ exports.handleSurface = handleSurface;
5
+ const attention_queue_1 = require("./attention-queue");
6
+ const runtime_1 = require("../nerves/runtime");
7
+ // ── Tool definition ──────────────────────────────────────────────
8
+ exports.surfaceToolDef = {
9
+ type: "function",
10
+ function: {
11
+ name: "surface",
12
+ description: "share a thought outward — deliver an answer, ask a follow-up, or surface progress to whoever needs to hear it. pass delegationId to address a held thought (see your attention queue above), or friendId for spontaneous outreach. does not end your turn.",
13
+ parameters: {
14
+ type: "object",
15
+ properties: {
16
+ content: {
17
+ type: "string",
18
+ description: "the message to deliver",
19
+ },
20
+ delegationId: {
21
+ type: "string",
22
+ description: "ID from your attention queue — addresses a specific held thought",
23
+ },
24
+ friendId: {
25
+ type: "string",
26
+ description: "friend to reach out to spontaneously (when not addressing a held thought)",
27
+ },
28
+ },
29
+ required: ["content"],
30
+ },
31
+ },
32
+ };
33
+ async function handleSurface(input) {
34
+ const { content, delegationId, friendId, queue, routeToFriend, advanceObligation } = input;
35
+ // Resolve target friend
36
+ let targetFriendId;
37
+ let queueItem;
38
+ if (delegationId) {
39
+ // Look up in attention queue
40
+ const found = queue.find((item) => item.id === delegationId);
41
+ if (!found) {
42
+ return `no delegation found with id ${delegationId} — check your attention queue`;
43
+ }
44
+ targetFriendId = found.friendId;
45
+ queueItem = found;
46
+ }
47
+ else if (friendId) {
48
+ targetFriendId = friendId;
49
+ }
50
+ else {
51
+ return "specify who this thought is for — use delegationId to address a held thought, or friendId for spontaneous outreach";
52
+ }
53
+ // Route to target
54
+ const result = await routeToFriend(targetFriendId, content, queueItem);
55
+ (0, runtime_1.emitNervesEvent)({
56
+ event: "senses.surface_routed",
57
+ component: "senses",
58
+ message: `surface routed to ${targetFriendId}: ${result.status}`,
59
+ meta: {
60
+ targetFriendId,
61
+ status: result.status,
62
+ hasDelegationId: !!delegationId,
63
+ ...(result.detail ? { detail: result.detail } : {}),
64
+ },
65
+ });
66
+ // On successful routing with delegationId:
67
+ // 1. Advance obligation to "returned" (disk FIRST — crash safety)
68
+ // 2. Dequeue from in-memory queue (AFTER obligation advance)
69
+ if (delegationId && queueItem && result.status !== "failed") {
70
+ if (queueItem.obligationId) {
71
+ advanceObligation(queueItem.obligationId, {
72
+ status: "returned",
73
+ returnedAt: Date.now(),
74
+ returnTarget: "surface",
75
+ });
76
+ }
77
+ (0, attention_queue_1.dequeueAttentionItem)(queue, delegationId);
78
+ }
79
+ // Return delivery status
80
+ const detail = result.detail ? ` — ${result.detail}` : "";
81
+ return `${result.status}${detail}`;
82
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.133",
3
+ "version": "0.1.0-alpha.135",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",