@ouro.bot/cli 0.1.0-alpha.44 → 0.1.0-alpha.46
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 +21 -0
- package/dist/heart/daemon/daemon-cli.js +106 -0
- package/dist/heart/daemon/launchd.js +21 -4
- package/dist/heart/daemon/sense-manager.js +26 -2
- package/dist/heart/daemon/thoughts.js +225 -0
- package/dist/senses/bluebubbles-client.js +51 -1
- package/dist/senses/bluebubbles-entry.js +2 -0
- package/dist/senses/bluebubbles-inbound-log.js +109 -0
- package/dist/senses/bluebubbles-mutation-log.js +42 -0
- package/dist/senses/bluebubbles-runtime-state.js +109 -0
- package/dist/senses/bluebubbles.js +177 -3
- package/dist/senses/inner-dialog-worker.js +7 -4
- package/dist/senses/inner-dialog.js +88 -14
- package/package.json +1 -1
- package/subagents/work-doer.md +22 -20
- package/subagents/work-planner.md +14 -6
package/changelog.json
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
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.46",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Inner dialog now knows which task triggered it: taskId flows from daemon poke through the worker into the turn, and the agent gets the full task file content instead of a generic heartbeat prompt.",
|
|
8
|
+
"Inner dialog boot message includes aspirations and state summary instead of a vacuous placeholder, so the agent wakes up with context about what matters and what's happening.",
|
|
9
|
+
"Vestigial `drainInbox` removed from inner dialog — pipeline already handles pending drain correctly.",
|
|
10
|
+
"Inner dialog nerves events now include assistant response preview, tool call names, token usage, and taskId for meaningful observability.",
|
|
11
|
+
"`ouro thoughts` command reads and formats inner dialog session turns with `--last`, `--json`, `--follow`, and `--agent` flags — humans can now see what the agent has been thinking.",
|
|
12
|
+
"`readTaskFile` searches collection subdirectories (one-shots, ongoing, habits) since the scheduler sends bare task stems without collection prefixes.",
|
|
13
|
+
"`ouro reminder create` accepts `--requester` to track who requested a reminder for notification round-trip.",
|
|
14
|
+
"Response extraction handles `tool_choice=required` models by falling back to `final_answer` tool call arguments when assistant message content is empty."
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"version": "0.1.0-alpha.45",
|
|
19
|
+
"changes": [
|
|
20
|
+
"`ouro up` now persists a boot-time launch agent with `RunAtLoad`, so the daemon comes back reliably after reboot instead of only starting for the current session.",
|
|
21
|
+
"BlueBubbles sense status is now truthful about upstream health: it probes the real BlueBubbles API, records runtime state, and surfaces `error` when the webhook listener is alive but the upstream server is unreachable.",
|
|
22
|
+
"BlueBubbles intake now replays recoverable read/delivery mutation backlogs through the normal inbound agent path, with a small dedupe ledger so missed messages can be recovered without silent drops or duplicate delivery."
|
|
23
|
+
]
|
|
24
|
+
},
|
|
4
25
|
{
|
|
5
26
|
"version": "0.1.0-alpha.44",
|
|
6
27
|
"changes": [
|
|
@@ -62,7 +62,9 @@ const update_hooks_1 = require("./update-hooks");
|
|
|
62
62
|
const bundle_meta_1 = require("./hooks/bundle-meta");
|
|
63
63
|
const bundle_manifest_1 = require("../../mind/bundle-manifest");
|
|
64
64
|
const tasks_1 = require("../../repertoire/tasks");
|
|
65
|
+
const thoughts_1 = require("./thoughts");
|
|
65
66
|
const ouro_bot_global_installer_1 = require("./ouro-bot-global-installer");
|
|
67
|
+
const launchd_1 = require("./launchd");
|
|
66
68
|
function stringField(value) {
|
|
67
69
|
return typeof value === "string" ? value : null;
|
|
68
70
|
}
|
|
@@ -257,6 +259,7 @@ function usage() {
|
|
|
257
259
|
" ouro friend list [--agent <name>]",
|
|
258
260
|
" ouro friend show <id> [--agent <name>]",
|
|
259
261
|
" ouro friend create --name <name> [--trust <level>] [--agent <name>]",
|
|
262
|
+
" ouro thoughts [--last <n>] [--json] [--follow] [--agent <name>]",
|
|
260
263
|
" ouro friend link <agent> --friend <id> --provider <p> --external-id <eid>",
|
|
261
264
|
" ouro friend unlink <agent> --friend <id> --provider <p> --external-id <eid>",
|
|
262
265
|
" ouro whoami [--agent <name>]",
|
|
@@ -519,6 +522,7 @@ function parseReminderCommand(args) {
|
|
|
519
522
|
let scheduledAt;
|
|
520
523
|
let cadence;
|
|
521
524
|
let category;
|
|
525
|
+
let requester;
|
|
522
526
|
for (let i = 1; i < rest.length; i++) {
|
|
523
527
|
if (rest[i] === "--body" && rest[i + 1]) {
|
|
524
528
|
body = rest[i + 1];
|
|
@@ -536,6 +540,10 @@ function parseReminderCommand(args) {
|
|
|
536
540
|
category = rest[i + 1];
|
|
537
541
|
i += 1;
|
|
538
542
|
}
|
|
543
|
+
else if (rest[i] === "--requester" && rest[i + 1]) {
|
|
544
|
+
requester = rest[i + 1];
|
|
545
|
+
i += 1;
|
|
546
|
+
}
|
|
539
547
|
}
|
|
540
548
|
if (!body)
|
|
541
549
|
throw new Error(`Usage\n${usage()}`);
|
|
@@ -548,6 +556,7 @@ function parseReminderCommand(args) {
|
|
|
548
556
|
...(scheduledAt ? { scheduledAt } : {}),
|
|
549
557
|
...(cadence ? { cadence } : {}),
|
|
550
558
|
...(category ? { category } : {}),
|
|
559
|
+
...(requester ? { requester } : {}),
|
|
551
560
|
...(agent ? { agent } : {}),
|
|
552
561
|
};
|
|
553
562
|
}
|
|
@@ -562,6 +571,23 @@ function parseSessionCommand(args) {
|
|
|
562
571
|
return { kind: "session.list", ...(agent ? { agent } : {}) };
|
|
563
572
|
throw new Error(`Usage\n${usage()}`);
|
|
564
573
|
}
|
|
574
|
+
function parseThoughtsCommand(args) {
|
|
575
|
+
const { agent, rest: cleaned } = extractAgentFlag(args);
|
|
576
|
+
let last;
|
|
577
|
+
let json = false;
|
|
578
|
+
let follow = false;
|
|
579
|
+
for (let i = 0; i < cleaned.length; i++) {
|
|
580
|
+
if (cleaned[i] === "--last" && i + 1 < cleaned.length) {
|
|
581
|
+
last = Number.parseInt(cleaned[i + 1], 10);
|
|
582
|
+
i++;
|
|
583
|
+
}
|
|
584
|
+
if (cleaned[i] === "--json")
|
|
585
|
+
json = true;
|
|
586
|
+
if (cleaned[i] === "--follow" || cleaned[i] === "-f")
|
|
587
|
+
follow = true;
|
|
588
|
+
}
|
|
589
|
+
return { kind: "thoughts", ...(agent ? { agent } : {}), ...(last ? { last } : {}), ...(json ? { json } : {}), ...(follow ? { follow } : {}) };
|
|
590
|
+
}
|
|
565
591
|
function parseFriendCommand(args) {
|
|
566
592
|
const { agent, rest: cleaned } = extractAgentFlag(args);
|
|
567
593
|
const [sub, ...rest] = cleaned;
|
|
@@ -629,6 +655,8 @@ function parseOuroCommand(args) {
|
|
|
629
655
|
}
|
|
630
656
|
if (head === "session")
|
|
631
657
|
return parseSessionCommand(args.slice(1));
|
|
658
|
+
if (head === "thoughts")
|
|
659
|
+
return parseThoughtsCommand(args.slice(1));
|
|
632
660
|
if (head === "chat") {
|
|
633
661
|
if (!second)
|
|
634
662
|
throw new Error(`Usage\n${usage()}`);
|
|
@@ -761,6 +789,25 @@ function defaultFallbackPendingMessage(command) {
|
|
|
761
789
|
});
|
|
762
790
|
return pendingPath;
|
|
763
791
|
}
|
|
792
|
+
function defaultEnsureDaemonBootPersistence(socketPath) {
|
|
793
|
+
if (process.platform !== "darwin") {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const homeDir = os.homedir();
|
|
797
|
+
const launchdDeps = {
|
|
798
|
+
writeFile: (filePath, content) => fs.writeFileSync(filePath, content, "utf-8"),
|
|
799
|
+
mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
|
|
800
|
+
homeDir,
|
|
801
|
+
};
|
|
802
|
+
const entryPath = path.join((0, identity_1.getRepoRoot)(), "dist", "heart", "daemon", "daemon-entry.js");
|
|
803
|
+
const logDir = path.join(homeDir, ".agentstate", "daemon", "logs");
|
|
804
|
+
(0, launchd_1.writeLaunchAgentPlist)(launchdDeps, {
|
|
805
|
+
nodePath: process.execPath,
|
|
806
|
+
entryPath,
|
|
807
|
+
socketPath,
|
|
808
|
+
logDir,
|
|
809
|
+
});
|
|
810
|
+
}
|
|
764
811
|
async function defaultInstallSubagents() {
|
|
765
812
|
return (0, subagent_installer_1.installSubagentsForAvailableCli)({
|
|
766
813
|
repoRoot: (0, identity_1.getRepoRoot)(),
|
|
@@ -1045,6 +1092,7 @@ function createDefaultOuroCliDeps(socketPath = "/tmp/ouroboros-daemon.sock") {
|
|
|
1045
1092
|
registerOuroBundleType: ouro_uti_1.registerOuroBundleUti,
|
|
1046
1093
|
installOuroCommand: ouro_path_installer_1.installOuroCommand,
|
|
1047
1094
|
syncGlobalOuroBotWrapper: ouro_bot_global_installer_1.syncGlobalOuroBotWrapper,
|
|
1095
|
+
ensureDaemonBootPersistence: defaultEnsureDaemonBootPersistence,
|
|
1048
1096
|
/* v8 ignore next 3 -- integration: launches interactive CLI session @preserve */
|
|
1049
1097
|
startChat: async (agentName) => {
|
|
1050
1098
|
const { main } = await Promise.resolve().then(() => __importStar(require("../../senses/cli")));
|
|
@@ -1317,6 +1365,7 @@ function executeReminderCommand(command, taskMod) {
|
|
|
1317
1365
|
body: command.body,
|
|
1318
1366
|
scheduledAt: command.scheduledAt,
|
|
1319
1367
|
cadence: command.cadence,
|
|
1368
|
+
requester: command.requester,
|
|
1320
1369
|
});
|
|
1321
1370
|
return `created: ${created}`;
|
|
1322
1371
|
}
|
|
@@ -1406,6 +1455,20 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
|
1406
1455
|
});
|
|
1407
1456
|
if (command.kind === "daemon.up") {
|
|
1408
1457
|
await performSystemSetup(deps);
|
|
1458
|
+
if (deps.ensureDaemonBootPersistence) {
|
|
1459
|
+
try {
|
|
1460
|
+
await Promise.resolve(deps.ensureDaemonBootPersistence(deps.socketPath));
|
|
1461
|
+
}
|
|
1462
|
+
catch (error) {
|
|
1463
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1464
|
+
level: "warn",
|
|
1465
|
+
component: "daemon",
|
|
1466
|
+
event: "daemon.system_setup_launchd_error",
|
|
1467
|
+
message: "failed to persist daemon boot startup",
|
|
1468
|
+
meta: { error: error instanceof Error ? error.message : String(error), socketPath: deps.socketPath },
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1409
1472
|
// Run update hooks before starting daemon so user sees the output
|
|
1410
1473
|
(0, update_hooks_1.registerUpdateHook)(bundle_meta_1.bundleMetaHook);
|
|
1411
1474
|
const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
|
|
@@ -1501,6 +1564,49 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
|
1501
1564
|
}
|
|
1502
1565
|
/* v8 ignore stop */
|
|
1503
1566
|
}
|
|
1567
|
+
// ── thoughts (local, no daemon socket needed) ──
|
|
1568
|
+
if (command.kind === "thoughts") {
|
|
1569
|
+
try {
|
|
1570
|
+
const agentName = command.agent ?? (0, identity_1.getAgentName)();
|
|
1571
|
+
const agentRoot = path.join((0, identity_1.getAgentBundlesRoot)(), `${agentName}.ouro`);
|
|
1572
|
+
const sessionFilePath = (0, thoughts_1.getInnerDialogSessionPath)(agentRoot);
|
|
1573
|
+
if (command.json) {
|
|
1574
|
+
try {
|
|
1575
|
+
const raw = fs.readFileSync(sessionFilePath, "utf-8");
|
|
1576
|
+
deps.writeStdout(raw);
|
|
1577
|
+
return raw;
|
|
1578
|
+
}
|
|
1579
|
+
catch {
|
|
1580
|
+
const message = "no inner dialog session found";
|
|
1581
|
+
deps.writeStdout(message);
|
|
1582
|
+
return message;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
const turns = (0, thoughts_1.parseInnerDialogSession)(sessionFilePath);
|
|
1586
|
+
const message = (0, thoughts_1.formatThoughtTurns)(turns, command.last ?? 10);
|
|
1587
|
+
deps.writeStdout(message);
|
|
1588
|
+
if (command.follow) {
|
|
1589
|
+
deps.writeStdout("\n\n--- following (ctrl+c to stop) ---\n");
|
|
1590
|
+
/* v8 ignore start -- callback tested via followThoughts unit tests @preserve */
|
|
1591
|
+
const stop = (0, thoughts_1.followThoughts)(sessionFilePath, (formatted) => {
|
|
1592
|
+
deps.writeStdout("\n" + formatted);
|
|
1593
|
+
});
|
|
1594
|
+
/* v8 ignore stop */
|
|
1595
|
+
// Block until process exit; cleanup watcher on SIGINT/SIGTERM
|
|
1596
|
+
return new Promise((resolve) => {
|
|
1597
|
+
const cleanup = () => { stop(); resolve(message); };
|
|
1598
|
+
process.once("SIGINT", cleanup);
|
|
1599
|
+
process.once("SIGTERM", cleanup);
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
return message;
|
|
1603
|
+
}
|
|
1604
|
+
catch {
|
|
1605
|
+
const message = "error: no agent context — use --agent <name> to specify";
|
|
1606
|
+
deps.writeStdout(message);
|
|
1607
|
+
return message;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1504
1610
|
// ── session list (local, no daemon socket needed) ──
|
|
1505
1611
|
if (command.kind === "session.list") {
|
|
1506
1612
|
/* v8 ignore start -- production default: requires full identity setup @preserve */
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.DAEMON_PLIST_LABEL = void 0;
|
|
37
37
|
exports.generateDaemonPlist = generateDaemonPlist;
|
|
38
|
+
exports.writeLaunchAgentPlist = writeLaunchAgentPlist;
|
|
38
39
|
exports.installLaunchAgent = installLaunchAgent;
|
|
39
40
|
exports.uninstallLaunchAgent = uninstallLaunchAgent;
|
|
40
41
|
exports.isDaemonInstalled = isDaemonInstalled;
|
|
@@ -65,6 +66,8 @@ function generateDaemonPlist(options) {
|
|
|
65
66
|
` <string>--socket</string>`,
|
|
66
67
|
` <string>${options.socketPath}</string>`,
|
|
67
68
|
` </array>`,
|
|
69
|
+
` <key>RunAtLoad</key>`,
|
|
70
|
+
` <true/>`,
|
|
68
71
|
` <key>KeepAlive</key>`,
|
|
69
72
|
` <true/>`,
|
|
70
73
|
];
|
|
@@ -74,6 +77,23 @@ function generateDaemonPlist(options) {
|
|
|
74
77
|
lines.push(`</dict>`, `</plist>`, ``);
|
|
75
78
|
return lines.join("\n");
|
|
76
79
|
}
|
|
80
|
+
function writeLaunchAgentPlist(deps, options) {
|
|
81
|
+
const launchAgentsDir = path.join(deps.homeDir, "Library", "LaunchAgents");
|
|
82
|
+
deps.mkdirp(launchAgentsDir);
|
|
83
|
+
if (options.logDir) {
|
|
84
|
+
deps.mkdirp(options.logDir);
|
|
85
|
+
}
|
|
86
|
+
const fullPath = plistFilePath(deps.homeDir);
|
|
87
|
+
const xml = generateDaemonPlist(options);
|
|
88
|
+
deps.writeFile(fullPath, xml);
|
|
89
|
+
(0, runtime_1.emitNervesEvent)({
|
|
90
|
+
component: "daemon",
|
|
91
|
+
event: "daemon.launchd_plist_written",
|
|
92
|
+
message: "daemon launch agent plist written",
|
|
93
|
+
meta: { plistPath: fullPath, entryPath: options.entryPath, socketPath: options.socketPath },
|
|
94
|
+
});
|
|
95
|
+
return fullPath;
|
|
96
|
+
}
|
|
77
97
|
function installLaunchAgent(deps, options) {
|
|
78
98
|
(0, runtime_1.emitNervesEvent)({
|
|
79
99
|
component: "daemon",
|
|
@@ -81,8 +101,6 @@ function installLaunchAgent(deps, options) {
|
|
|
81
101
|
message: "installing launch agent",
|
|
82
102
|
meta: { entryPath: options.entryPath, socketPath: options.socketPath },
|
|
83
103
|
});
|
|
84
|
-
const launchAgentsDir = path.join(deps.homeDir, "Library", "LaunchAgents");
|
|
85
|
-
deps.mkdirp(launchAgentsDir);
|
|
86
104
|
const fullPath = plistFilePath(deps.homeDir);
|
|
87
105
|
// Unload existing (best effort) for idempotent re-install
|
|
88
106
|
if (deps.existsFile(fullPath)) {
|
|
@@ -91,8 +109,7 @@ function installLaunchAgent(deps, options) {
|
|
|
91
109
|
}
|
|
92
110
|
catch { /* best effort */ }
|
|
93
111
|
}
|
|
94
|
-
|
|
95
|
-
deps.writeFile(fullPath, xml);
|
|
112
|
+
writeLaunchAgentPlist(deps, options);
|
|
96
113
|
deps.exec(`launchctl load "${fullPath}"`);
|
|
97
114
|
(0, runtime_1.emitNervesEvent)({
|
|
98
115
|
component: "daemon",
|
|
@@ -38,6 +38,7 @@ const fs = __importStar(require("fs"));
|
|
|
38
38
|
const os = __importStar(require("os"));
|
|
39
39
|
const path = __importStar(require("path"));
|
|
40
40
|
const runtime_1 = require("../../nerves/runtime");
|
|
41
|
+
const bluebubbles_runtime_state_1 = require("../../senses/bluebubbles-runtime-state");
|
|
41
42
|
const identity_1 = require("../identity");
|
|
42
43
|
const sense_truth_1 = require("../sense-truth");
|
|
43
44
|
const process_manager_1 = require("./process-manager");
|
|
@@ -175,12 +176,29 @@ function runtimeInfoFor(status) {
|
|
|
175
176
|
return { runtime: "running" };
|
|
176
177
|
return { runtime: "error" };
|
|
177
178
|
}
|
|
179
|
+
function readBlueBubblesRuntimeFacts(agent, bundlesRoot, snapshot) {
|
|
180
|
+
const agentRoot = path.join(bundlesRoot, `${agent}.ouro`);
|
|
181
|
+
const runtimePath = path.join(agentRoot, "state", "senses", "bluebubbles", "runtime.json");
|
|
182
|
+
if (snapshot?.runtime !== "running" || !fs.existsSync(runtimePath)) {
|
|
183
|
+
return { runtime: snapshot?.runtime };
|
|
184
|
+
}
|
|
185
|
+
const state = (0, bluebubbles_runtime_state_1.readBlueBubblesRuntimeState)(agent, agentRoot);
|
|
186
|
+
if (state.upstreamStatus === "error") {
|
|
187
|
+
return {
|
|
188
|
+
runtime: "error",
|
|
189
|
+
detail: state.detail,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return { runtime: snapshot.runtime };
|
|
193
|
+
}
|
|
178
194
|
class DaemonSenseManager {
|
|
179
195
|
processManager;
|
|
180
196
|
contexts;
|
|
197
|
+
bundlesRoot;
|
|
181
198
|
constructor(options) {
|
|
182
199
|
const bundlesRoot = options.bundlesRoot ?? path.join(os.homedir(), "AgentBundles");
|
|
183
200
|
const secretsRoot = options.secretsRoot ?? path.join(os.homedir(), ".agentsecrets");
|
|
201
|
+
this.bundlesRoot = bundlesRoot;
|
|
184
202
|
this.contexts = new Map(options.agents.map((agent) => {
|
|
185
203
|
const senses = readAgentSenses(path.join(bundlesRoot, `${agent}.ouro`, "agent.json"));
|
|
186
204
|
const facts = senseFactsFromSecrets(agent, senses, path.join(secretsRoot, agent, "secrets.json"));
|
|
@@ -227,6 +245,7 @@ class DaemonSenseManager {
|
|
|
227
245
|
runtime.set(parsed.agent, current);
|
|
228
246
|
}
|
|
229
247
|
const rows = [...this.contexts.entries()].flatMap(([agent, context]) => {
|
|
248
|
+
const blueBubblesRuntimeFacts = readBlueBubblesRuntimeFacts(agent, this.bundlesRoot, runtime.get(agent)?.bluebubbles);
|
|
230
249
|
const runtimeInfo = {
|
|
231
250
|
cli: { configured: true },
|
|
232
251
|
teams: {
|
|
@@ -235,7 +254,7 @@ class DaemonSenseManager {
|
|
|
235
254
|
},
|
|
236
255
|
bluebubbles: {
|
|
237
256
|
configured: context.facts.bluebubbles.configured,
|
|
238
|
-
...
|
|
257
|
+
...blueBubblesRuntimeFacts,
|
|
239
258
|
},
|
|
240
259
|
};
|
|
241
260
|
const inventory = (0, sense_truth_1.getSenseInventory)({ senses: context.senses }, runtimeInfo);
|
|
@@ -245,7 +264,12 @@ class DaemonSenseManager {
|
|
|
245
264
|
label: entry.label,
|
|
246
265
|
enabled: entry.enabled,
|
|
247
266
|
status: entry.status,
|
|
248
|
-
detail: entry.enabled
|
|
267
|
+
detail: entry.enabled
|
|
268
|
+
? entry.sense === "bluebubbles"
|
|
269
|
+
? blueBubblesRuntimeFacts.detail
|
|
270
|
+
?? context.facts[entry.sense].detail
|
|
271
|
+
: context.facts[entry.sense].detail
|
|
272
|
+
: "not enabled in agent.json",
|
|
249
273
|
}));
|
|
250
274
|
});
|
|
251
275
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Formats inner dialog session turns for human consumption.
|
|
3
|
+
// Used by `ouro thoughts` CLI command to show what the agent has been thinking.
|
|
4
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
5
|
+
if (k2 === undefined) k2 = k;
|
|
6
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
7
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
8
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
9
|
+
}
|
|
10
|
+
Object.defineProperty(o, k2, desc);
|
|
11
|
+
}) : (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
o[k2] = m[k];
|
|
14
|
+
}));
|
|
15
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
16
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
17
|
+
}) : function(o, v) {
|
|
18
|
+
o["default"] = v;
|
|
19
|
+
});
|
|
20
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
21
|
+
var ownKeys = function(o) {
|
|
22
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
23
|
+
var ar = [];
|
|
24
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
25
|
+
return ar;
|
|
26
|
+
};
|
|
27
|
+
return ownKeys(o);
|
|
28
|
+
};
|
|
29
|
+
return function (mod) {
|
|
30
|
+
if (mod && mod.__esModule) return mod;
|
|
31
|
+
var result = {};
|
|
32
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
33
|
+
__setModuleDefault(result, mod);
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
})();
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.parseInnerDialogSession = parseInnerDialogSession;
|
|
39
|
+
exports.formatThoughtTurns = formatThoughtTurns;
|
|
40
|
+
exports.getInnerDialogSessionPath = getInnerDialogSessionPath;
|
|
41
|
+
exports.followThoughts = followThoughts;
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
45
|
+
function contentToText(content) {
|
|
46
|
+
if (typeof content === "string")
|
|
47
|
+
return content;
|
|
48
|
+
if (!Array.isArray(content))
|
|
49
|
+
return "";
|
|
50
|
+
return content
|
|
51
|
+
.map((part) => {
|
|
52
|
+
if (typeof part === "string")
|
|
53
|
+
return part;
|
|
54
|
+
if (part && typeof part === "object" && "text" in part && typeof part.text === "string") {
|
|
55
|
+
return part.text;
|
|
56
|
+
}
|
|
57
|
+
return "";
|
|
58
|
+
})
|
|
59
|
+
.join("\n");
|
|
60
|
+
}
|
|
61
|
+
function classifyTurn(userText) {
|
|
62
|
+
if (userText.includes("waking up."))
|
|
63
|
+
return { type: "boot" };
|
|
64
|
+
const taskMatch = /## task: (.+)$/m.exec(userText);
|
|
65
|
+
if (taskMatch)
|
|
66
|
+
return { type: "task", taskId: taskMatch[1] };
|
|
67
|
+
return { type: "heartbeat" };
|
|
68
|
+
}
|
|
69
|
+
function extractToolNames(messages) {
|
|
70
|
+
const names = [];
|
|
71
|
+
for (const msg of messages) {
|
|
72
|
+
if (msg.role === "assistant" && Array.isArray(msg.tool_calls)) {
|
|
73
|
+
for (const tc of msg.tool_calls) {
|
|
74
|
+
if (tc.function?.name && tc.function.name !== "final_answer")
|
|
75
|
+
names.push(tc.function.name);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return names;
|
|
80
|
+
}
|
|
81
|
+
/** Extract text from a final_answer tool call's arguments. */
|
|
82
|
+
function extractFinalAnswer(messages) {
|
|
83
|
+
for (let k = messages.length - 1; k >= 0; k--) {
|
|
84
|
+
const msg = messages[k];
|
|
85
|
+
if (msg.role !== "assistant" || !Array.isArray(msg.tool_calls))
|
|
86
|
+
continue;
|
|
87
|
+
for (const tc of msg.tool_calls) {
|
|
88
|
+
if (tc.function?.name !== "final_answer")
|
|
89
|
+
continue;
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(tc.function.arguments ?? "{}");
|
|
92
|
+
if (typeof parsed.answer === "string" && parsed.answer.trim())
|
|
93
|
+
return parsed.answer.trim();
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// malformed arguments — skip
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
102
|
+
function parseInnerDialogSession(sessionPath) {
|
|
103
|
+
(0, runtime_1.emitNervesEvent)({
|
|
104
|
+
component: "daemon",
|
|
105
|
+
event: "daemon.thoughts_parse",
|
|
106
|
+
message: "parsing inner dialog session",
|
|
107
|
+
meta: { sessionPath },
|
|
108
|
+
});
|
|
109
|
+
let raw;
|
|
110
|
+
try {
|
|
111
|
+
raw = fs.readFileSync(sessionPath, "utf-8");
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
let data;
|
|
117
|
+
try {
|
|
118
|
+
data = JSON.parse(raw);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
if (data.version !== 1 || !Array.isArray(data.messages))
|
|
124
|
+
return [];
|
|
125
|
+
const turns = [];
|
|
126
|
+
const messages = data.messages;
|
|
127
|
+
// Walk messages, pairing user → (tool calls) → assistant sequences
|
|
128
|
+
let i = 0;
|
|
129
|
+
while (i < messages.length) {
|
|
130
|
+
const msg = messages[i];
|
|
131
|
+
if (msg.role === "system") {
|
|
132
|
+
i++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (msg.role !== "user") {
|
|
136
|
+
i++;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const userText = contentToText(msg.content);
|
|
140
|
+
const classification = classifyTurn(userText);
|
|
141
|
+
// Collect all messages until the next user message (or end)
|
|
142
|
+
const turnMessages = [];
|
|
143
|
+
let j = i + 1;
|
|
144
|
+
while (j < messages.length && messages[j].role !== "user") {
|
|
145
|
+
turnMessages.push(messages[j]);
|
|
146
|
+
j++;
|
|
147
|
+
}
|
|
148
|
+
// Find the last assistant text response in this turn.
|
|
149
|
+
// With tool_choice="required", the response may be inside a final_answer tool call.
|
|
150
|
+
const assistantMsgs = turnMessages.filter((m) => m.role === "assistant");
|
|
151
|
+
const lastAssistant = assistantMsgs.reverse().find((m) => contentToText(m.content).trim().length > 0);
|
|
152
|
+
const response = lastAssistant
|
|
153
|
+
? contentToText(lastAssistant.content).trim()
|
|
154
|
+
: extractFinalAnswer(turnMessages);
|
|
155
|
+
const tools = extractToolNames(turnMessages);
|
|
156
|
+
turns.push({
|
|
157
|
+
type: classification.type,
|
|
158
|
+
prompt: userText.trim(),
|
|
159
|
+
response,
|
|
160
|
+
tools,
|
|
161
|
+
...(classification.taskId ? { taskId: classification.taskId } : {}),
|
|
162
|
+
});
|
|
163
|
+
i = j;
|
|
164
|
+
}
|
|
165
|
+
return turns;
|
|
166
|
+
}
|
|
167
|
+
function formatThoughtTurns(turns, lastN) {
|
|
168
|
+
if (turns.length === 0)
|
|
169
|
+
return "no inner dialog activity";
|
|
170
|
+
const selected = lastN > 0 ? turns.slice(-lastN) : turns;
|
|
171
|
+
/* v8 ignore next -- unreachable: turns.length > 0 checked above, slice always returns ≥1 @preserve */
|
|
172
|
+
if (selected.length === 0)
|
|
173
|
+
return "no inner dialog activity";
|
|
174
|
+
const lines = [];
|
|
175
|
+
for (const turn of selected) {
|
|
176
|
+
const typeLabel = turn.type === "task" && turn.taskId
|
|
177
|
+
? `task: ${turn.taskId}`
|
|
178
|
+
: turn.type;
|
|
179
|
+
lines.push(`--- ${typeLabel} ---`);
|
|
180
|
+
if (turn.tools.length > 0) {
|
|
181
|
+
lines.push(`tools: ${turn.tools.join(", ")}`);
|
|
182
|
+
}
|
|
183
|
+
if (turn.response) {
|
|
184
|
+
lines.push(turn.response);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
lines.push("(no response)");
|
|
188
|
+
}
|
|
189
|
+
lines.push("");
|
|
190
|
+
}
|
|
191
|
+
return lines.join("\n").trim();
|
|
192
|
+
}
|
|
193
|
+
function getInnerDialogSessionPath(agentRoot) {
|
|
194
|
+
return path.join(agentRoot, "state", "sessions", "self", "inner", "dialog.json");
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Watch a session file and emit new turns as they appear.
|
|
198
|
+
* Returns a cleanup function that stops the watcher.
|
|
199
|
+
*/
|
|
200
|
+
function followThoughts(sessionPath, onNewTurns, pollIntervalMs = 1000) {
|
|
201
|
+
let displayedCount = parseInnerDialogSession(sessionPath).length;
|
|
202
|
+
(0, runtime_1.emitNervesEvent)({
|
|
203
|
+
component: "daemon",
|
|
204
|
+
event: "daemon.thoughts_follow_start",
|
|
205
|
+
message: "started following inner dialog session",
|
|
206
|
+
meta: { sessionPath, initialTurns: displayedCount },
|
|
207
|
+
});
|
|
208
|
+
fs.watchFile(sessionPath, { interval: pollIntervalMs }, () => {
|
|
209
|
+
const turns = parseInnerDialogSession(sessionPath);
|
|
210
|
+
if (turns.length > displayedCount) {
|
|
211
|
+
const newTurns = turns.slice(displayedCount);
|
|
212
|
+
onNewTurns(formatThoughtTurns(newTurns, 0));
|
|
213
|
+
displayedCount = turns.length;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
return () => {
|
|
217
|
+
fs.unwatchFile(sessionPath);
|
|
218
|
+
(0, runtime_1.emitNervesEvent)({
|
|
219
|
+
component: "daemon",
|
|
220
|
+
event: "daemon.thoughts_follow_stop",
|
|
221
|
+
message: "stopped following inner dialog session",
|
|
222
|
+
meta: { sessionPath, totalTurns: displayedCount },
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
}
|
|
@@ -170,6 +170,12 @@ function applyRepairNotice(event, notice) {
|
|
|
170
170
|
repairNotice: notice,
|
|
171
171
|
};
|
|
172
172
|
}
|
|
173
|
+
function hasRecoverableMessageContent(event) {
|
|
174
|
+
return event.kind === "message"
|
|
175
|
+
&& (event.textForAgent.trim().length > 0
|
|
176
|
+
|| event.attachments.length > 0
|
|
177
|
+
|| event.hasPayloadData);
|
|
178
|
+
}
|
|
173
179
|
function hydrateTextForAgent(event, rawData) {
|
|
174
180
|
if (event.kind !== "message") {
|
|
175
181
|
return { ...event, requiresRepair: false };
|
|
@@ -323,6 +329,40 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
|
|
|
323
329
|
throw new Error(`BlueBubbles read failed (${response.status}): ${errorText || "unknown"}`);
|
|
324
330
|
}
|
|
325
331
|
},
|
|
332
|
+
async checkHealth() {
|
|
333
|
+
const url = buildBlueBubblesApiUrl(config.serverUrl, "/api/v1/message/count", config.password);
|
|
334
|
+
(0, runtime_1.emitNervesEvent)({
|
|
335
|
+
component: "senses",
|
|
336
|
+
event: "senses.bluebubbles_healthcheck_start",
|
|
337
|
+
message: "probing bluebubbles upstream health",
|
|
338
|
+
meta: { serverUrl: config.serverUrl },
|
|
339
|
+
});
|
|
340
|
+
const response = await fetch(url, {
|
|
341
|
+
method: "GET",
|
|
342
|
+
signal: AbortSignal.timeout(channelConfig.requestTimeoutMs),
|
|
343
|
+
});
|
|
344
|
+
if (!response.ok) {
|
|
345
|
+
const errorText = await response.text().catch(() => "");
|
|
346
|
+
(0, runtime_1.emitNervesEvent)({
|
|
347
|
+
level: "warn",
|
|
348
|
+
component: "senses",
|
|
349
|
+
event: "senses.bluebubbles_healthcheck_error",
|
|
350
|
+
message: "bluebubbles upstream health probe failed",
|
|
351
|
+
meta: {
|
|
352
|
+
serverUrl: config.serverUrl,
|
|
353
|
+
status: response.status,
|
|
354
|
+
reason: errorText || "unknown",
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
throw new Error(`BlueBubbles upstream health check failed (${response.status}): ${errorText || "unknown"}`);
|
|
358
|
+
}
|
|
359
|
+
(0, runtime_1.emitNervesEvent)({
|
|
360
|
+
component: "senses",
|
|
361
|
+
event: "senses.bluebubbles_healthcheck_end",
|
|
362
|
+
message: "bluebubbles upstream health probe succeeded",
|
|
363
|
+
meta: { serverUrl: config.serverUrl },
|
|
364
|
+
});
|
|
365
|
+
},
|
|
326
366
|
async repairEvent(event) {
|
|
327
367
|
if (!event.requiresRepair) {
|
|
328
368
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -387,7 +427,16 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
|
|
|
387
427
|
type: event.eventType,
|
|
388
428
|
data,
|
|
389
429
|
});
|
|
390
|
-
|
|
430
|
+
const recoveredMessage = event.kind === "mutation"
|
|
431
|
+
&& !event.shouldNotifyAgent
|
|
432
|
+
? (0, bluebubbles_model_1.normalizeBlueBubblesEvent)({
|
|
433
|
+
type: "new-message",
|
|
434
|
+
data,
|
|
435
|
+
})
|
|
436
|
+
: null;
|
|
437
|
+
let hydrated = recoveredMessage && hasRecoverableMessageContent(recoveredMessage)
|
|
438
|
+
? hydrateTextForAgent(recoveredMessage, data)
|
|
439
|
+
: hydrateTextForAgent(normalized, data);
|
|
391
440
|
if (hydrated.kind === "message" &&
|
|
392
441
|
hydrated.balloonBundleId !== "com.apple.messages.URLBalloonProvider" &&
|
|
393
442
|
hydrated.attachments.length > 0) {
|
|
@@ -411,6 +460,7 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
|
|
|
411
460
|
kind: hydrated.kind,
|
|
412
461
|
messageGuid: hydrated.messageGuid,
|
|
413
462
|
repairedFrom: event.kind,
|
|
463
|
+
promotedFromMutation: event.kind === "mutation" && hydrated.kind === "message",
|
|
414
464
|
},
|
|
415
465
|
});
|
|
416
466
|
return hydrated;
|
|
@@ -8,4 +8,6 @@ if (!process.argv.includes("--agent")) {
|
|
|
8
8
|
process.exit(1);
|
|
9
9
|
}
|
|
10
10
|
const bluebubbles_1 = require("./bluebubbles");
|
|
11
|
+
const runtime_logging_1 = require("../heart/daemon/runtime-logging");
|
|
12
|
+
(0, runtime_logging_1.configureDaemonRuntimeLogger)("bluebubbles");
|
|
11
13
|
(0, bluebubbles_1.startBlueBubblesApp)();
|