@ouro.bot/cli 0.1.0-alpha.726 → 0.1.0-alpha.727
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 +9 -0
- package/dist/heart/core.js +181 -209
- package/dist/heart/daemon/daemon.js +79 -14
- package/dist/heart/daemon/process-manager.js +100 -18
- package/dist/heart/providers/anthropic.js +17 -12
- package/dist/heart/streaming.js +6 -6
- package/dist/mind/prompt-budget.js +104 -5
- package/dist/repertoire/tools.js +0 -10
- package/dist/senses/bluebubbles/client.js +43 -57
- package/dist/senses/bluebubbles/context-packet.js +104 -19
- package/dist/senses/bluebubbles/entry.js +14 -1
- package/dist/senses/bluebubbles/index.js +1382 -719
- package/dist/senses/bluebubbles/latest-turn.js +311 -0
- package/dist/senses/bluebubbles/outbound-state.js +4 -0
- package/dist/senses/bluebubbles/reaction-policy.js +4 -12
- package/dist/senses/bluebubbles/semantic-receipts.js +178 -10
- package/package.json +1 -1
|
@@ -39,6 +39,8 @@ exports.parseOrphanPidsFromPs = parseOrphanPidsFromPs;
|
|
|
39
39
|
exports.filterPidfilePidsToActualOrphans = filterPidfilePidsToActualOrphans;
|
|
40
40
|
exports.mergeUniqueOrphanPids = mergeUniqueOrphanPids;
|
|
41
41
|
exports.waitForOrphanProcessesToSettle = waitForOrphanProcessesToSettle;
|
|
42
|
+
exports.assertOrphanCleanupComplete = assertOrphanCleanupComplete;
|
|
43
|
+
exports.drainOrphanProcessesBeforeStartup = drainOrphanProcessesBeforeStartup;
|
|
42
44
|
exports.killOrphanProcesses = killOrphanProcesses;
|
|
43
45
|
exports.writePidfile = writePidfile;
|
|
44
46
|
exports.handleAgentSenseTurn = handleAgentSenseTurn;
|
|
@@ -124,7 +126,8 @@ function parseOrphanPidsFromPs(psOutput, selfPid) {
|
|
|
124
126
|
&& !line.includes("bluebubbles/entry.js")
|
|
125
127
|
&& !line.includes("mail-entry.js")
|
|
126
128
|
&& !line.includes("teams-entry.js")
|
|
127
|
-
&& !line.includes("a2a-entry.js")
|
|
129
|
+
&& !line.includes("a2a-entry.js")
|
|
130
|
+
&& !line.includes("voice-entry.js"))
|
|
128
131
|
continue;
|
|
129
132
|
// Parse `<pid> <ppid> <command...>`. ps pads these with leading spaces.
|
|
130
133
|
// Regex guarantees both groups are \d+ so parseInt can't produce NaN.
|
|
@@ -228,6 +231,41 @@ async function waitForOrphanProcessesToSettle(pids, deps = {}) {
|
|
|
228
231
|
}
|
|
229
232
|
return survivors;
|
|
230
233
|
}
|
|
234
|
+
function assertOrphanCleanupComplete(survivingPids) {
|
|
235
|
+
if (survivingPids.length === 0)
|
|
236
|
+
return;
|
|
237
|
+
const errorReason = `refusing startup while ${survivingPids.length} orphaned Ouro processes remain: ${survivingPids.join(", ")}`;
|
|
238
|
+
(0, runtime_1.emitNervesEvent)({
|
|
239
|
+
level: "error",
|
|
240
|
+
component: "daemon",
|
|
241
|
+
event: "daemon.orphan_cleanup_incomplete",
|
|
242
|
+
message: errorReason,
|
|
243
|
+
meta: { survivingPids },
|
|
244
|
+
});
|
|
245
|
+
throw new Error(errorReason);
|
|
246
|
+
}
|
|
247
|
+
const ORPHAN_CLEANUP_MAX_SIGNAL_PASSES = 8;
|
|
248
|
+
async function drainOrphanProcessesBeforeStartup(socketPath = socket_client_1.DEFAULT_DAEMON_SOCKET_PATH, deps = {}) {
|
|
249
|
+
const signalOrphans = deps.signalOrphans ?? killOrphanProcesses;
|
|
250
|
+
const settleOrphans = deps.settleOrphans ?? waitForOrphanProcessesToSettle;
|
|
251
|
+
const maxSignalPasses = deps.maxSignalPasses ?? ORPHAN_CLEANUP_MAX_SIGNAL_PASSES;
|
|
252
|
+
for (let pass = 0; pass < maxSignalPasses; pass += 1) {
|
|
253
|
+
const signaledPids = signalOrphans(socketPath);
|
|
254
|
+
if (signaledPids.length === 0)
|
|
255
|
+
return;
|
|
256
|
+
const survivingPids = await settleOrphans(signaledPids);
|
|
257
|
+
assertOrphanCleanupComplete(survivingPids);
|
|
258
|
+
}
|
|
259
|
+
const errorReason = `orphan cleanup did not reach a fixed point after ${maxSignalPasses} signaling round(s)`;
|
|
260
|
+
(0, runtime_1.emitNervesEvent)({
|
|
261
|
+
level: "error",
|
|
262
|
+
component: "daemon",
|
|
263
|
+
event: "daemon.orphan_cleanup_not_converged",
|
|
264
|
+
message: errorReason,
|
|
265
|
+
meta: { maxSignalPasses },
|
|
266
|
+
});
|
|
267
|
+
throw new Error(errorReason);
|
|
268
|
+
}
|
|
231
269
|
/* v8 ignore start -- shells out to ps; covered by filterPidfilePidsToActualOrphans unit tests via injected runner @preserve */
|
|
232
270
|
function runPsCheck(pids) {
|
|
233
271
|
try {
|
|
@@ -305,7 +343,10 @@ function killOrphanProcesses(socketPath = socket_client_1.DEFAULT_DAEMON_SOCKET_
|
|
|
305
343
|
const result = (0, child_process_1.execSync)("ps -eo pid,ppid,command", { encoding: "utf-8", timeout: 5000 });
|
|
306
344
|
scanOrphans = parseOrphanPidsFromPs(result, process.pid);
|
|
307
345
|
}
|
|
308
|
-
catch {
|
|
346
|
+
catch (error) {
|
|
347
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
348
|
+
throw new Error(`could not discover orphaned Ouro processes: ${detail}`);
|
|
349
|
+
}
|
|
309
350
|
const pidsToKill = mergeUniqueOrphanPids(pidfileOrphans, scanOrphans);
|
|
310
351
|
if (pidsToKill.length > 0) {
|
|
311
352
|
for (const pid of pidsToKill) {
|
|
@@ -325,13 +366,13 @@ function killOrphanProcesses(socketPath = socket_client_1.DEFAULT_DAEMON_SOCKET_
|
|
|
325
366
|
}
|
|
326
367
|
catch (error) {
|
|
327
368
|
(0, runtime_1.emitNervesEvent)({
|
|
328
|
-
level: "
|
|
369
|
+
level: "error",
|
|
329
370
|
component: "daemon",
|
|
330
371
|
event: "daemon.orphan_cleanup_error",
|
|
331
372
|
message: "failed to clean up orphaned ouro processes",
|
|
332
373
|
meta: { error: error instanceof Error ? error.message : String(error) },
|
|
333
374
|
});
|
|
334
|
-
|
|
375
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
335
376
|
}
|
|
336
377
|
}
|
|
337
378
|
/**
|
|
@@ -575,6 +616,7 @@ class OuroDaemon {
|
|
|
575
616
|
onStopCommandComplete;
|
|
576
617
|
externalEventRoot;
|
|
577
618
|
rsvpHabitRunner;
|
|
619
|
+
orphanStartupDrain;
|
|
578
620
|
constructor(options) {
|
|
579
621
|
this.socketPath = options.socketPath;
|
|
580
622
|
this.processManager = options.processManager;
|
|
@@ -589,6 +631,7 @@ class OuroDaemon {
|
|
|
589
631
|
this.onStopCommandComplete = options.onStopCommandComplete ?? null;
|
|
590
632
|
this.externalEventRoot = options.externalEventRoot ?? null;
|
|
591
633
|
this.rsvpHabitRunner = options.rsvpHabitRunner;
|
|
634
|
+
this.orphanStartupDrain = options.orphanStartupDrain ?? drainOrphanProcessesBeforeStartup;
|
|
592
635
|
}
|
|
593
636
|
/* v8 ignore start -- default mailbox server wiring: production-only path, tests inject mailboxServerFactory stub instead. startMailboxHttpServer itself has full coverage in mailbox-http.test.ts @preserve */
|
|
594
637
|
createDefaultMailboxServer() {
|
|
@@ -740,10 +783,7 @@ class OuroDaemon {
|
|
|
740
783
|
}
|
|
741
784
|
// MCP connections are lazily initialized per-agent during senseTurn
|
|
742
785
|
// (daemon manages multiple agents; agent identity must be set before loading MCP config)
|
|
743
|
-
|
|
744
|
-
const killedOrphanPids = killOrphanProcesses(this.socketPath);
|
|
745
|
-
await waitForOrphanProcessesToSettle(killedOrphanPids);
|
|
746
|
-
/* v8 ignore stop */
|
|
786
|
+
await this.orphanStartupDrain(this.socketPath);
|
|
747
787
|
await this.openCommandSocket();
|
|
748
788
|
this.triggerAutoStartAgents();
|
|
749
789
|
this.triggerAutoStartSensesWhenAgentsSettled();
|
|
@@ -1049,8 +1089,12 @@ class OuroDaemon {
|
|
|
1049
1089
|
clearTimeout(this.senseAutostartTimer);
|
|
1050
1090
|
this.senseAutostartTimer = null;
|
|
1051
1091
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1092
|
+
const workerStopTasks = [Promise.resolve().then(() => this.processManager.stopAll())];
|
|
1093
|
+
if (this.senseManager) {
|
|
1094
|
+
workerStopTasks.push(Promise.resolve().then(() => this.senseManager.stopAll()));
|
|
1095
|
+
}
|
|
1096
|
+
const workerStopResults = await Promise.allSettled(workerStopTasks);
|
|
1097
|
+
const workerStopFailures = workerStopResults.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
1054
1098
|
if (this.server) {
|
|
1055
1099
|
// DO NOT `await` server.close() here. server.close() resolves only
|
|
1056
1100
|
// after every open connection has closed. When stop() is invoked
|
|
@@ -1113,6 +1157,19 @@ class OuroDaemon {
|
|
|
1113
1157
|
});
|
|
1114
1158
|
}
|
|
1115
1159
|
this.socketIdentity = null;
|
|
1160
|
+
if (workerStopFailures.length > 0) {
|
|
1161
|
+
const errorReason = `daemon shutdown could not drain ${workerStopFailures.length} managed worker group(s)`;
|
|
1162
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1163
|
+
level: "error",
|
|
1164
|
+
component: "daemon",
|
|
1165
|
+
event: "daemon.worker_shutdown_error",
|
|
1166
|
+
message: errorReason,
|
|
1167
|
+
meta: {
|
|
1168
|
+
failures: workerStopFailures.map((error) => error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error rejection @preserve */ String(error)),
|
|
1169
|
+
},
|
|
1170
|
+
});
|
|
1171
|
+
throw new AggregateError(workerStopFailures, errorReason);
|
|
1172
|
+
}
|
|
1116
1173
|
}
|
|
1117
1174
|
async handleRawPayload(raw) {
|
|
1118
1175
|
try {
|
|
@@ -1477,8 +1534,12 @@ class OuroDaemon {
|
|
|
1477
1534
|
await this.start();
|
|
1478
1535
|
return { ok: true, message: "daemon started" };
|
|
1479
1536
|
case "daemon.stop":
|
|
1480
|
-
|
|
1481
|
-
|
|
1537
|
+
try {
|
|
1538
|
+
await this.stop();
|
|
1539
|
+
}
|
|
1540
|
+
finally {
|
|
1541
|
+
this.onStopCommandComplete?.();
|
|
1542
|
+
}
|
|
1482
1543
|
return { ok: true, message: "daemon stopped" };
|
|
1483
1544
|
case "daemon.restart": {
|
|
1484
1545
|
// Restart is "stop + let launchctl respawn." Under launchctl's KeepAlive
|
|
@@ -1494,8 +1555,12 @@ class OuroDaemon {
|
|
|
1494
1555
|
requestedBy: command.requestedBy ?? null,
|
|
1495
1556
|
},
|
|
1496
1557
|
});
|
|
1497
|
-
|
|
1498
|
-
|
|
1558
|
+
try {
|
|
1559
|
+
await this.stop();
|
|
1560
|
+
}
|
|
1561
|
+
finally {
|
|
1562
|
+
this.onStopCommandComplete?.();
|
|
1563
|
+
}
|
|
1499
1564
|
return {
|
|
1500
1565
|
ok: true,
|
|
1501
1566
|
message: "daemon restarting — launchctl will respawn",
|
|
@@ -56,6 +56,9 @@ exports.RESPAWN_GUARD_WINDOW_MS = 10 * 60_000;
|
|
|
56
56
|
const MAX_PENDING_IPC_MESSAGES = 20;
|
|
57
57
|
class DaemonProcessManager {
|
|
58
58
|
agents = new Map();
|
|
59
|
+
/** `stopAll()` is terminal for a manager instance. Once set, it fences
|
|
60
|
+
* detached restart/start continuations until the owning daemon exits. */
|
|
61
|
+
shutdownRequested = false;
|
|
59
62
|
maxRestartsPerHour;
|
|
60
63
|
stabilityThresholdMs;
|
|
61
64
|
initialBackoffMs;
|
|
@@ -67,6 +70,7 @@ class DaemonProcessManager {
|
|
|
67
70
|
cooldownRecoveryMs;
|
|
68
71
|
maxCooldownRetries;
|
|
69
72
|
startupStaleAfterMs;
|
|
73
|
+
stopTimeoutMs;
|
|
70
74
|
existsSyncFn;
|
|
71
75
|
configCheckFn;
|
|
72
76
|
statusWriterFn;
|
|
@@ -143,6 +147,7 @@ class DaemonProcessManager {
|
|
|
143
147
|
this.cooldownRecoveryMs = options.cooldownRecoveryMs ?? 5 * 60 * 1_000;
|
|
144
148
|
this.maxCooldownRetries = options.maxCooldownRetries ?? 3;
|
|
145
149
|
this.startupStaleAfterMs = options.startupStaleAfterMs ?? 45_000;
|
|
150
|
+
this.stopTimeoutMs = options.stopTimeoutMs ?? 4_000;
|
|
146
151
|
this.spawnFn = options.spawn ?? ((command, args, spawnOptions) => (0, child_process_1.spawn)(command, args, spawnOptions));
|
|
147
152
|
this.now = options.now ?? (() => Date.now());
|
|
148
153
|
this.setTimeoutFn = options.setTimeoutFn ?? ((cb, delay) => setTimeout(cb, delay));
|
|
@@ -169,6 +174,7 @@ class DaemonProcessManager {
|
|
|
169
174
|
cooldownTimer: null,
|
|
170
175
|
cooldownRetryCount: 0,
|
|
171
176
|
fastCrashCount: 0,
|
|
177
|
+
stopFailureActive: false,
|
|
172
178
|
snapshot: {
|
|
173
179
|
name: agent.name,
|
|
174
180
|
channel: agent.channel,
|
|
@@ -206,6 +212,8 @@ class DaemonProcessManager {
|
|
|
206
212
|
}
|
|
207
213
|
async startAgent(agent, options = {}) {
|
|
208
214
|
const state = this.requireAgent(agent);
|
|
215
|
+
if (this.shutdownRequested)
|
|
216
|
+
return;
|
|
209
217
|
if (state.process || state.startInFlight)
|
|
210
218
|
return;
|
|
211
219
|
const attemptId = state.startAttemptId + 1;
|
|
@@ -426,25 +434,87 @@ class DaemonProcessManager {
|
|
|
426
434
|
return;
|
|
427
435
|
}
|
|
428
436
|
const child = state.process;
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
437
|
+
const childPid = child.pid ?? null;
|
|
438
|
+
const childPidLabel = childPid === null ? "unknown" : String(childPid);
|
|
439
|
+
await new Promise((resolve, reject) => {
|
|
440
|
+
let settled = false;
|
|
441
|
+
let timeout = null;
|
|
442
|
+
const finish = (error) => {
|
|
443
|
+
/* v8 ignore next -- defensive idempotence if an injected timer fires after cancellation @preserve */
|
|
444
|
+
if (settled)
|
|
445
|
+
return;
|
|
446
|
+
settled = true;
|
|
447
|
+
child.removeListener("exit", onExit);
|
|
448
|
+
if (timeout !== null)
|
|
449
|
+
this.clearTimeoutFn(timeout);
|
|
450
|
+
if (error)
|
|
451
|
+
reject(error);
|
|
452
|
+
else
|
|
453
|
+
resolve();
|
|
454
|
+
};
|
|
455
|
+
const onExit = () => finish();
|
|
456
|
+
child.once("exit", onExit);
|
|
457
|
+
let signalAccepted = false;
|
|
458
|
+
try {
|
|
459
|
+
signalAccepted = child.kill("SIGTERM");
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
const errorReason = "failed to send SIGTERM to managed agent; worker remains tracked";
|
|
463
|
+
state.stopFailureActive = true;
|
|
464
|
+
state.snapshot.errorReason = errorReason;
|
|
465
|
+
state.snapshot.fixHint = `Inspect managed worker PID ${childPidLabel} before retrying the stop or restart.`;
|
|
466
|
+
(0, runtime_1.emitNervesEvent)({
|
|
467
|
+
level: "error",
|
|
468
|
+
component: "daemon",
|
|
469
|
+
event: "daemon.agent_stop_error",
|
|
470
|
+
message: errorReason,
|
|
471
|
+
meta: { agent, pid: childPid },
|
|
472
|
+
});
|
|
473
|
+
this.notifySnapshotChange(state.snapshot);
|
|
474
|
+
finish(new Error(errorReason));
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (!signalAccepted) {
|
|
478
|
+
const errorReason = "managed agent rejected SIGTERM; worker remains tracked";
|
|
479
|
+
state.stopFailureActive = true;
|
|
480
|
+
state.snapshot.errorReason = errorReason;
|
|
481
|
+
state.snapshot.fixHint = `Inspect managed worker PID ${childPidLabel} before retrying the stop or restart.`;
|
|
482
|
+
(0, runtime_1.emitNervesEvent)({
|
|
483
|
+
level: "error",
|
|
484
|
+
component: "daemon",
|
|
485
|
+
event: "daemon.agent_stop_error",
|
|
486
|
+
message: errorReason,
|
|
487
|
+
meta: { agent, pid: childPid },
|
|
488
|
+
});
|
|
489
|
+
this.notifySnapshotChange(state.snapshot);
|
|
490
|
+
finish(new Error(errorReason));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (settled)
|
|
494
|
+
return;
|
|
495
|
+
timeout = this.setTimeoutFn(() => {
|
|
496
|
+
if (settled)
|
|
497
|
+
return;
|
|
498
|
+
const errorReason = `managed agent did not exit within ${this.stopTimeoutMs}ms; replacement was not started`;
|
|
499
|
+
state.stopFailureActive = true;
|
|
500
|
+
state.snapshot.errorReason = errorReason;
|
|
501
|
+
state.snapshot.fixHint = `Inspect managed worker PID ${childPidLabel}; retry only after that worker has exited.`;
|
|
502
|
+
(0, runtime_1.emitNervesEvent)({
|
|
503
|
+
level: "error",
|
|
504
|
+
component: "daemon",
|
|
505
|
+
event: "daemon.agent_stop_timeout",
|
|
506
|
+
message: errorReason,
|
|
507
|
+
meta: { agent, pid: childPid, timeoutMs: this.stopTimeoutMs },
|
|
508
|
+
});
|
|
509
|
+
this.notifySnapshotChange(state.snapshot);
|
|
510
|
+
finish(new Error(errorReason));
|
|
511
|
+
}, this.stopTimeoutMs);
|
|
512
|
+
});
|
|
445
513
|
}
|
|
446
514
|
async restartAgent(agent, options = {}) {
|
|
447
515
|
const state = this.requireAgent(agent);
|
|
516
|
+
if (this.shutdownRequested)
|
|
517
|
+
return;
|
|
448
518
|
// Respawn-loop guard: prune timestamps outside the window, then check
|
|
449
519
|
// whether we've already restarted this agent too many times in it.
|
|
450
520
|
const now = this.now();
|
|
@@ -528,8 +598,14 @@ class DaemonProcessManager {
|
|
|
528
598
|
await this.startAgent(agent, options);
|
|
529
599
|
}
|
|
530
600
|
async stopAll() {
|
|
531
|
-
|
|
532
|
-
|
|
601
|
+
this.shutdownRequested = true;
|
|
602
|
+
const states = [...this.agents.values()];
|
|
603
|
+
const results = await Promise.allSettled(states.map((state) => this.stopAgent(state.config.name)));
|
|
604
|
+
const failures = results.flatMap((result, index) => result.status === "rejected"
|
|
605
|
+
? [{ agent: states[index].config.name, reason: result.reason }]
|
|
606
|
+
: []);
|
|
607
|
+
if (failures.length > 0) {
|
|
608
|
+
throw new AggregateError(failures.map((failure) => failure.reason), `failed to stop ${failures.length} managed worker(s): ${failures.map((failure) => failure.agent).join(", ")}`);
|
|
533
609
|
}
|
|
534
610
|
}
|
|
535
611
|
resetAgentFailureState(agent) {
|
|
@@ -582,6 +658,7 @@ class DaemonProcessManager {
|
|
|
582
658
|
return [...this.agents.values()].map((state) => state.snapshot);
|
|
583
659
|
}
|
|
584
660
|
onExit(state, child, code, signal) {
|
|
661
|
+
/* v8 ignore next -- defensive: replacement cannot start before this child's one-shot exit listener drains @preserve */
|
|
585
662
|
if (state.process !== child)
|
|
586
663
|
return;
|
|
587
664
|
state.process = null;
|
|
@@ -590,6 +667,11 @@ class DaemonProcessManager {
|
|
|
590
667
|
state.snapshot.pid = null;
|
|
591
668
|
state.snapshot.lastExitCode = code;
|
|
592
669
|
state.snapshot.lastSignal = signal;
|
|
670
|
+
if (state.stopFailureActive) {
|
|
671
|
+
state.stopFailureActive = false;
|
|
672
|
+
state.snapshot.errorReason = null;
|
|
673
|
+
state.snapshot.fixHint = null;
|
|
674
|
+
}
|
|
593
675
|
const crashed = !state.stopRequested && code !== 0;
|
|
594
676
|
const now = this.currentTimeMs();
|
|
595
677
|
const startedAt = state.snapshot.startedAt ? Date.parse(state.snapshot.startedAt) : now;
|
|
@@ -104,14 +104,13 @@ function parseToolCallInput(argumentsJson) {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
function toAnthropicMessages(messages) {
|
|
107
|
-
|
|
107
|
+
const systemParts = [];
|
|
108
108
|
const converted = [];
|
|
109
109
|
for (const msg of messages) {
|
|
110
110
|
if (msg.role === "system") {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
111
|
+
const text = toAnthropicTextContent(msg.content).trim();
|
|
112
|
+
if (text)
|
|
113
|
+
systemParts.push(text);
|
|
115
114
|
continue;
|
|
116
115
|
}
|
|
117
116
|
if (msg.role === "user") {
|
|
@@ -173,7 +172,7 @@ function toAnthropicMessages(messages) {
|
|
|
173
172
|
});
|
|
174
173
|
}
|
|
175
174
|
}
|
|
176
|
-
return { system, messages: converted };
|
|
175
|
+
return { system: systemParts.length > 0 ? systemParts.join("\n\n") : undefined, messages: converted };
|
|
177
176
|
}
|
|
178
177
|
function toAnthropicTools(tools) {
|
|
179
178
|
return tools.map((tool) => ({
|
|
@@ -231,6 +230,11 @@ function isAnthropicAuthFailure(error) {
|
|
|
231
230
|
}
|
|
232
231
|
async function streamAnthropicMessages(client, model, request) {
|
|
233
232
|
const { system, messages } = toAnthropicMessages(request.messages);
|
|
233
|
+
const additionalSystemParts = request.messages
|
|
234
|
+
.filter((message) => message.role === "system")
|
|
235
|
+
.slice(1)
|
|
236
|
+
.map((message) => toAnthropicTextContent(message.content).trim())
|
|
237
|
+
.filter((text) => text.length > 0);
|
|
234
238
|
const anthropicTools = toAnthropicTools(request.activeTools);
|
|
235
239
|
const modelCaps = (0, model_capabilities_1.getModelCapabilities)(model);
|
|
236
240
|
const maxTokens = modelCaps.maxOutputTokens ?? 16384;
|
|
@@ -255,12 +259,13 @@ async function streamAnthropicMessages(client, model, request) {
|
|
|
255
259
|
text: preambleText + "\n\n" + request.systemPrompt.stable,
|
|
256
260
|
cache_control: { type: "ephemeral" },
|
|
257
261
|
};
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
params.system = [
|
|
263
|
+
stableBlock,
|
|
264
|
+
...(request.systemPrompt.volatile
|
|
265
|
+
? [{ type: "text", text: request.systemPrompt.volatile }]
|
|
266
|
+
: []),
|
|
267
|
+
...additionalSystemParts.map((text) => ({ type: "text", text })),
|
|
268
|
+
];
|
|
264
269
|
}
|
|
265
270
|
else if (system) {
|
|
266
271
|
// Fallback: no structured prompt, extract from messages (legacy path)
|
package/dist/heart/streaming.js
CHANGED
|
@@ -496,14 +496,14 @@ function toResponsesUserContent(content) {
|
|
|
496
496
|
return parts.length > 0 ? parts : "";
|
|
497
497
|
}
|
|
498
498
|
function toResponsesInput(messages) {
|
|
499
|
-
|
|
499
|
+
const instructionParts = [];
|
|
500
500
|
const input = [];
|
|
501
501
|
for (const msg of messages) {
|
|
502
502
|
if (msg.role === "system") {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
503
|
+
const sys = msg;
|
|
504
|
+
const content = (typeof sys.content === "string" ? sys.content : "").trim();
|
|
505
|
+
if (content)
|
|
506
|
+
instructionParts.push(content);
|
|
507
507
|
continue;
|
|
508
508
|
}
|
|
509
509
|
if (msg.role === "user") {
|
|
@@ -551,7 +551,7 @@ function toResponsesInput(messages) {
|
|
|
551
551
|
continue;
|
|
552
552
|
}
|
|
553
553
|
}
|
|
554
|
-
return { instructions, input };
|
|
554
|
+
return { instructions: instructionParts.join("\n\n"), input };
|
|
555
555
|
}
|
|
556
556
|
function toResponsesTools(ccTools) {
|
|
557
557
|
return ccTools.map((t) => ({
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.PROMPT_BUDGET_FALLBACK_CONTEXT_WINDOW_TOKENS = exports.PROMPT_BUDGET_POLICY_VERSION = void 0;
|
|
4
4
|
exports.estimatePromptBudgetTokensForMessage = estimatePromptBudgetTokensForMessage;
|
|
5
5
|
exports.estimatePromptBudgetTokens = estimatePromptBudgetTokens;
|
|
6
|
+
exports.assessRequiredPromptEvidenceBudget = assessRequiredPromptEvidenceBudget;
|
|
6
7
|
exports.applyPromptBudget = applyPromptBudget;
|
|
7
8
|
const runtime_1 = require("../nerves/runtime");
|
|
8
9
|
exports.PROMPT_BUDGET_POLICY_VERSION = "prompt-budget/v1";
|
|
@@ -89,6 +90,42 @@ function itemTokens(item) {
|
|
|
89
90
|
function cloneMessage(message) {
|
|
90
91
|
return { ...message };
|
|
91
92
|
}
|
|
93
|
+
function requiredMessagesByIdentity(input) {
|
|
94
|
+
const evidence = input.requiredPromptEvidence;
|
|
95
|
+
if (!evidence)
|
|
96
|
+
return [];
|
|
97
|
+
const current = evidence.currentUserMessage;
|
|
98
|
+
if (current.role !== "user") {
|
|
99
|
+
throw new Error("required current user message must have role=user");
|
|
100
|
+
}
|
|
101
|
+
const currentMatches = input.messages.filter((message) => message === current);
|
|
102
|
+
if (currentMatches.length !== 1) {
|
|
103
|
+
throw new Error(currentMatches.length === 0
|
|
104
|
+
? "required current user message is not present by identity"
|
|
105
|
+
: "required current user message must appear exactly once by identity");
|
|
106
|
+
}
|
|
107
|
+
const predecessor = evidence.verifiedPredecessorMessage;
|
|
108
|
+
if (predecessor) {
|
|
109
|
+
if (predecessor === current)
|
|
110
|
+
throw new Error("required predecessor and current user message must be distinct objects");
|
|
111
|
+
if (predecessor.role !== "system") {
|
|
112
|
+
throw new Error("required verified predecessor message must have role=system");
|
|
113
|
+
}
|
|
114
|
+
const predecessorMatches = input.messages.filter((message) => message === predecessor);
|
|
115
|
+
if (predecessorMatches.length !== 1) {
|
|
116
|
+
throw new Error(predecessorMatches.length === 0
|
|
117
|
+
? "required verified predecessor message is not present by identity"
|
|
118
|
+
: "required verified predecessor message must appear exactly once by identity");
|
|
119
|
+
}
|
|
120
|
+
if (input.messages.indexOf(predecessor) !== input.messages.indexOf(current) - 1) {
|
|
121
|
+
throw new Error("required verified predecessor message must be immediately before the current user message");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return input.messages.filter((message) => message === current || message === predecessor);
|
|
125
|
+
}
|
|
126
|
+
function cloneUnlessRequired(message, requiredMessages) {
|
|
127
|
+
return requiredMessages.has(message) ? message : cloneMessage(message);
|
|
128
|
+
}
|
|
92
129
|
function messageText(message) {
|
|
93
130
|
return typeof message.content === "string" ? message.content : "";
|
|
94
131
|
}
|
|
@@ -197,11 +234,35 @@ function nonSystemSourceFor(messages) {
|
|
|
197
234
|
return "diagnostics";
|
|
198
235
|
return "recent-session-tail";
|
|
199
236
|
}
|
|
200
|
-
function buildPromptBudgetItems(messages) {
|
|
237
|
+
function buildPromptBudgetItems(messages, requiredPromptEvidence) {
|
|
201
238
|
const rawItems = [];
|
|
202
239
|
let index = 0;
|
|
203
240
|
while (index < messages.length) {
|
|
204
241
|
const message = messages[index];
|
|
242
|
+
if (message === requiredPromptEvidence?.currentUserMessage) {
|
|
243
|
+
rawItems.push({
|
|
244
|
+
id: `required:${index}:current`,
|
|
245
|
+
priority: 1,
|
|
246
|
+
source: "current-user-message",
|
|
247
|
+
messages: [message],
|
|
248
|
+
originalMessageCount: 1,
|
|
249
|
+
required: "current-user",
|
|
250
|
+
});
|
|
251
|
+
index += 1;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (message === requiredPromptEvidence?.verifiedPredecessorMessage) {
|
|
255
|
+
rawItems.push({
|
|
256
|
+
id: `required:${index}:predecessor`,
|
|
257
|
+
priority: 1,
|
|
258
|
+
source: "required-current-turn-state",
|
|
259
|
+
messages: [message],
|
|
260
|
+
originalMessageCount: 1,
|
|
261
|
+
required: "verified-predecessor",
|
|
262
|
+
});
|
|
263
|
+
index += 1;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
205
266
|
if (message.role === "system") {
|
|
206
267
|
rawItems.push(...systemItems(message, index));
|
|
207
268
|
index += 1;
|
|
@@ -236,6 +297,8 @@ function buildPromptBudgetItems(messages) {
|
|
|
236
297
|
const lastUserItemIndex = rawItems.findLastIndex((item) => item.messages.some((message) => message.role === "user"));
|
|
237
298
|
const latestToolItemIndex = rawItems.findLastIndex((item) => item.messages.some((message) => message.role === "tool" || assistantHasToolCalls(message)));
|
|
238
299
|
return rawItems.map((item, itemIndex) => {
|
|
300
|
+
if (item.required)
|
|
301
|
+
return item;
|
|
239
302
|
if (itemIndex === lastUserItemIndex) {
|
|
240
303
|
return { ...item, priority: 1, source: "current-user-message" };
|
|
241
304
|
}
|
|
@@ -254,7 +317,7 @@ function buildPromptBudgetItems(messages) {
|
|
|
254
317
|
});
|
|
255
318
|
}
|
|
256
319
|
function flattenItems(items) {
|
|
257
|
-
return items.flatMap((item) => item.messages.map(cloneMessage));
|
|
320
|
+
return items.flatMap((item) => item.messages.map((message) => item.required ? message : cloneMessage(message)));
|
|
258
321
|
}
|
|
259
322
|
function budgetFor(input) {
|
|
260
323
|
const contextWindowTokens = typeof input.contextWindowTokens === "number" && Number.isFinite(input.contextWindowTokens) && input.contextWindowTokens > 0
|
|
@@ -273,6 +336,19 @@ function budgetFor(input) {
|
|
|
273
336
|
inputTokenLimit: Math.max(0, contextWindowTokens - outputReserveTokens - protocolReserveTokens),
|
|
274
337
|
};
|
|
275
338
|
}
|
|
339
|
+
function assessRequiredPromptEvidenceBudget(input) {
|
|
340
|
+
const budget = budgetFor(input);
|
|
341
|
+
const messages = requiredMessagesByIdentity(input);
|
|
342
|
+
const estimatedTokens = estimatePromptBudgetTokens(messages);
|
|
343
|
+
return {
|
|
344
|
+
status: messages.length > 0 && estimatedTokens > budget.inputTokenLimit
|
|
345
|
+
? "required_evidence_over_budget"
|
|
346
|
+
: "within_budget",
|
|
347
|
+
messages,
|
|
348
|
+
estimatedTokens,
|
|
349
|
+
budget,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
276
352
|
function totalItemTokens(items) {
|
|
277
353
|
return estimatePromptBudgetTokens(flattenItems(items));
|
|
278
354
|
}
|
|
@@ -304,15 +380,38 @@ function emitPromptBudgetEvent(result) {
|
|
|
304
380
|
});
|
|
305
381
|
}
|
|
306
382
|
function applyPromptBudget(input) {
|
|
307
|
-
const
|
|
383
|
+
const requiredAssessment = assessRequiredPromptEvidenceBudget(input);
|
|
384
|
+
const budget = requiredAssessment.budget;
|
|
308
385
|
const estimatedBeforeTokens = estimatePromptBudgetTokens(input.messages);
|
|
309
386
|
const truncations = [];
|
|
387
|
+
const requiredMessages = requiredAssessment.messages;
|
|
388
|
+
const requiredMessageSet = new Set(requiredMessages);
|
|
310
389
|
let droppedMessages = 0;
|
|
390
|
+
const requiredTokens = requiredAssessment.estimatedTokens;
|
|
391
|
+
if (requiredAssessment.status === "required_evidence_over_budget") {
|
|
392
|
+
droppedMessages = input.messages.length - requiredMessages.length;
|
|
393
|
+
const result = {
|
|
394
|
+
policyVersion: exports.PROMPT_BUDGET_POLICY_VERSION,
|
|
395
|
+
status: "required_evidence_over_budget",
|
|
396
|
+
messages: requiredMessages,
|
|
397
|
+
budget,
|
|
398
|
+
stats: {
|
|
399
|
+
estimatedBeforeTokens,
|
|
400
|
+
estimatedAfterTokens: requiredTokens,
|
|
401
|
+
originalMessages: input.messages.length,
|
|
402
|
+
finalMessages: requiredMessages.length,
|
|
403
|
+
droppedMessages,
|
|
404
|
+
truncations,
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
emitPromptBudgetEvent(result);
|
|
408
|
+
return result;
|
|
409
|
+
}
|
|
311
410
|
if (estimatedBeforeTokens <= budget.inputTokenLimit) {
|
|
312
411
|
const result = {
|
|
313
412
|
policyVersion: exports.PROMPT_BUDGET_POLICY_VERSION,
|
|
314
413
|
status: "within_budget",
|
|
315
|
-
messages: input.messages.map(
|
|
414
|
+
messages: input.messages.map((message) => cloneUnlessRequired(message, requiredMessageSet)),
|
|
316
415
|
budget,
|
|
317
416
|
stats: {
|
|
318
417
|
estimatedBeforeTokens,
|
|
@@ -326,7 +425,7 @@ function applyPromptBudget(input) {
|
|
|
326
425
|
emitPromptBudgetEvent(result);
|
|
327
426
|
return result;
|
|
328
427
|
}
|
|
329
|
-
let items = buildPromptBudgetItems(input.messages);
|
|
428
|
+
let items = buildPromptBudgetItems(input.messages, input.requiredPromptEvidence);
|
|
330
429
|
for (const priority of [8, 7, 6, 5, 4, 3]) {
|
|
331
430
|
if (totalItemTokens(items) <= budget.inputTokenLimit)
|
|
332
431
|
break;
|
package/dist/repertoire/tools.js
CHANGED
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.surfaceToolDef = exports.speakTool = exports.restTool = exports.ponderTool = exports.observeTool = exports.settleTool = exports.tools = void 0;
|
|
4
4
|
exports.resetMcpDefinitions = resetMcpDefinitions;
|
|
5
5
|
exports.getToolsForChannel = getToolsForChannel;
|
|
6
|
-
exports.getRestrictedReactionFeedbackTools = getRestrictedReactionFeedbackTools;
|
|
7
6
|
exports.resolveToolDefinition = resolveToolDefinition;
|
|
8
7
|
exports.shellRiskProfile = shellRiskProfile;
|
|
9
8
|
exports.riskProfileForTool = riskProfileForTool;
|
|
@@ -127,15 +126,6 @@ function getToolsForChannel(capabilities, toolPreferences, _context, providerCap
|
|
|
127
126
|
}
|
|
128
127
|
return filterByCapability(result, providerCapabilities);
|
|
129
128
|
}
|
|
130
|
-
/**
|
|
131
|
-
* The complete provider-facing tool surface for trusted reaction feedback.
|
|
132
|
-
* Keep these schemas sourced from the normal registry so restricted turns
|
|
133
|
-
* cannot drift onto lookalike definitions or inherit channel/MCP tools.
|
|
134
|
-
*/
|
|
135
|
-
function getRestrictedReactionFeedbackTools() {
|
|
136
|
-
const orientationGetTool = tools_base_1.baseToolDefinitions.find((definition) => definition.tool.function.name === "orientation_get").tool;
|
|
137
|
-
return [tools_base_1.settleTool, tools_base_1.observeTool, orientationGetTool];
|
|
138
|
-
}
|
|
139
129
|
// Look up a tool definition from the live combined registry (native + MCP).
|
|
140
130
|
// The base registry is intentionally consulted at call time so tests and
|
|
141
131
|
// runtime extensions cannot leave execution metadata behind a stale snapshot.
|