@ouro.bot/cli 0.1.0-alpha.2 → 0.1.0-alpha.20
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/AdoptionSpecialist.ouro/agent.json +70 -9
- package/AdoptionSpecialist.ouro/psyche/SOUL.md +5 -2
- package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
- package/assets/ouroboros.png +0 -0
- package/dist/heart/config.js +66 -4
- package/dist/heart/core.js +75 -2
- package/dist/heart/daemon/daemon-cli.js +507 -29
- package/dist/heart/daemon/daemon-entry.js +13 -5
- package/dist/heart/daemon/daemon.js +42 -9
- package/dist/heart/daemon/hatch-animation.js +35 -0
- package/dist/heart/daemon/hatch-flow.js +2 -11
- package/dist/heart/daemon/hatch-specialist.js +6 -1
- package/dist/heart/daemon/ouro-bot-wrapper.js +4 -3
- package/dist/heart/daemon/ouro-path-installer.js +177 -0
- package/dist/heart/daemon/ouro-uti.js +11 -2
- package/dist/heart/daemon/process-manager.js +1 -1
- package/dist/heart/daemon/runtime-logging.js +9 -5
- package/dist/heart/daemon/runtime-metadata.js +118 -0
- package/dist/heart/daemon/sense-manager.js +266 -0
- package/dist/heart/daemon/specialist-orchestrator.js +129 -0
- package/dist/heart/daemon/specialist-prompt.js +98 -0
- package/dist/heart/daemon/specialist-tools.js +237 -0
- package/dist/heart/daemon/subagent-installer.js +10 -1
- package/dist/heart/identity.js +77 -1
- package/dist/heart/providers/anthropic.js +19 -2
- package/dist/heart/sense-truth.js +61 -0
- package/dist/heart/streaming.js +99 -21
- package/dist/mind/bundle-manifest.js +58 -0
- package/dist/mind/friends/channel.js +8 -0
- package/dist/mind/friends/types.js +1 -1
- package/dist/mind/prompt.js +77 -3
- package/dist/nerves/cli-logging.js +15 -2
- package/dist/repertoire/ado-client.js +4 -2
- package/dist/repertoire/coding/feedback.js +134 -0
- package/dist/repertoire/coding/index.js +4 -1
- package/dist/repertoire/coding/manager.js +61 -2
- package/dist/repertoire/coding/spawner.js +3 -3
- package/dist/repertoire/coding/tools.js +41 -2
- package/dist/repertoire/data/ado-endpoints.json +188 -0
- package/dist/repertoire/tools-base.js +69 -5
- package/dist/repertoire/tools-teams.js +57 -4
- package/dist/repertoire/tools.js +44 -11
- package/dist/senses/bluebubbles-client.js +433 -0
- package/dist/senses/bluebubbles-entry.js +11 -0
- package/dist/senses/bluebubbles-media.js +244 -0
- package/dist/senses/bluebubbles-model.js +253 -0
- package/dist/senses/bluebubbles-mutation-log.js +76 -0
- package/dist/senses/bluebubbles.js +421 -0
- package/dist/senses/cli.js +293 -133
- package/dist/senses/debug-activity.js +107 -0
- package/dist/senses/teams.js +173 -54
- package/package.json +11 -4
- package/subagents/work-doer.md +26 -24
- package/subagents/work-merger.md +24 -30
- package/subagents/work-planner.md +34 -25
- package/dist/inner-worker-entry.js +0 -4
package/dist/senses/cli.js
CHANGED
|
@@ -38,6 +38,8 @@ exports.handleSigint = handleSigint;
|
|
|
38
38
|
exports.addHistory = addHistory;
|
|
39
39
|
exports.renderMarkdown = renderMarkdown;
|
|
40
40
|
exports.createCliCallbacks = createCliCallbacks;
|
|
41
|
+
exports.createDebouncedLines = createDebouncedLines;
|
|
42
|
+
exports.runCliSession = runCliSession;
|
|
41
43
|
exports.main = main;
|
|
42
44
|
const readline = __importStar(require("readline"));
|
|
43
45
|
const os = __importStar(require("os"));
|
|
@@ -70,12 +72,14 @@ class Spinner {
|
|
|
70
72
|
msg = "";
|
|
71
73
|
phrases = null;
|
|
72
74
|
lastPhrase = "";
|
|
75
|
+
stopped = false;
|
|
73
76
|
constructor(m = "working", phrases) {
|
|
74
77
|
this.msg = m;
|
|
75
78
|
if (phrases && phrases.length > 0)
|
|
76
79
|
this.phrases = phrases;
|
|
77
80
|
}
|
|
78
81
|
start() {
|
|
82
|
+
this.stopped = false;
|
|
79
83
|
process.stderr.write("\r\x1b[K");
|
|
80
84
|
this.spin();
|
|
81
85
|
this.iv = setInterval(() => this.spin(), 80);
|
|
@@ -84,15 +88,23 @@ class Spinner {
|
|
|
84
88
|
}
|
|
85
89
|
}
|
|
86
90
|
spin() {
|
|
87
|
-
|
|
91
|
+
// Guard: clearInterval can't prevent already-dequeued callbacks
|
|
92
|
+
/* v8 ignore next -- race guard: timer callback fires after stop() @preserve */
|
|
93
|
+
if (this.stopped)
|
|
94
|
+
return;
|
|
95
|
+
process.stderr.write(`\r\x1b[K${this.frames[this.i]} ${this.msg}... `);
|
|
88
96
|
this.i = (this.i + 1) % this.frames.length;
|
|
89
97
|
}
|
|
90
98
|
rotatePhrase() {
|
|
99
|
+
/* v8 ignore next -- race guard: timer callback fires after stop() @preserve */
|
|
100
|
+
if (this.stopped)
|
|
101
|
+
return;
|
|
91
102
|
const next = (0, phrases_1.pickPhrase)(this.phrases, this.lastPhrase);
|
|
92
103
|
this.lastPhrase = next;
|
|
93
104
|
this.msg = next;
|
|
94
105
|
}
|
|
95
106
|
stop(ok) {
|
|
107
|
+
this.stopped = true;
|
|
96
108
|
if (this.iv) {
|
|
97
109
|
clearInterval(this.iv);
|
|
98
110
|
this.iv = null;
|
|
@@ -297,12 +309,25 @@ function createCliCallbacks() {
|
|
|
297
309
|
currentSpinner.start();
|
|
298
310
|
},
|
|
299
311
|
onModelStreamStart: () => {
|
|
300
|
-
|
|
301
|
-
|
|
312
|
+
// No-op: content callbacks (onTextChunk, onReasoningChunk) handle
|
|
313
|
+
// stopping the spinner. onModelStreamStart fires too early and
|
|
314
|
+
// doesn't fire at all for final_answer tool streaming.
|
|
315
|
+
},
|
|
316
|
+
onClearText: () => {
|
|
317
|
+
streamer.reset();
|
|
302
318
|
},
|
|
303
319
|
onTextChunk: (text) => {
|
|
320
|
+
// Stop spinner if still running — final_answer streaming and Anthropic
|
|
321
|
+
// tool-only responses bypass onModelStreamStart, so the spinner would
|
|
322
|
+
// otherwise keep running (and its \r writes overwrite response text).
|
|
323
|
+
if (currentSpinner) {
|
|
324
|
+
currentSpinner.stop();
|
|
325
|
+
currentSpinner = null;
|
|
326
|
+
}
|
|
304
327
|
if (hadReasoning) {
|
|
305
|
-
|
|
328
|
+
// Single newline to separate reasoning from reply — reasoning
|
|
329
|
+
// output often ends with its own trailing newline(s)
|
|
330
|
+
process.stdout.write("\n");
|
|
306
331
|
hadReasoning = false;
|
|
307
332
|
}
|
|
308
333
|
const rendered = streamer.push(text);
|
|
@@ -311,6 +336,10 @@ function createCliCallbacks() {
|
|
|
311
336
|
textDirty = text.length > 0 && !text.endsWith("\n");
|
|
312
337
|
},
|
|
313
338
|
onReasoningChunk: (text) => {
|
|
339
|
+
if (currentSpinner) {
|
|
340
|
+
currentSpinner.stop();
|
|
341
|
+
currentSpinner = null;
|
|
342
|
+
}
|
|
314
343
|
hadReasoning = true;
|
|
315
344
|
process.stdout.write(`\x1b[2m${text}\x1b[0m`);
|
|
316
345
|
textDirty = text.length > 0 && !text.endsWith("\n");
|
|
@@ -368,88 +397,86 @@ function createCliCallbacks() {
|
|
|
368
397
|
},
|
|
369
398
|
};
|
|
370
399
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
(0, commands_1.registerDefaultCommands)(registry);
|
|
379
|
-
// Resolve context kernel (identity + channel) for CLI
|
|
380
|
-
const friendsPath = path.join((0, identity_1.getAgentRoot)(), "friends");
|
|
381
|
-
const friendStore = new store_file_1.FileFriendStore(friendsPath);
|
|
382
|
-
const username = os.userInfo().username;
|
|
383
|
-
const hostname = os.hostname();
|
|
384
|
-
const localExternalId = `${username}@${hostname}`;
|
|
385
|
-
const resolver = new resolver_1.FriendResolver(friendStore, {
|
|
386
|
-
provider: "local",
|
|
387
|
-
externalId: localExternalId,
|
|
388
|
-
displayName: username,
|
|
389
|
-
channel: "cli",
|
|
390
|
-
});
|
|
391
|
-
const resolvedContext = await resolver.resolve();
|
|
392
|
-
const cliToolContext = {
|
|
393
|
-
/* v8 ignore next -- CLI has no OAuth sign-in; this no-op satisfies the interface @preserve */
|
|
394
|
-
signin: async () => undefined,
|
|
395
|
-
context: resolvedContext,
|
|
396
|
-
friendStore,
|
|
397
|
-
summarize: (0, core_1.createSummarize)(),
|
|
398
|
-
};
|
|
399
|
-
const friendId = resolvedContext.friend.id;
|
|
400
|
-
const agentConfig = (0, identity_1.loadAgentConfig)();
|
|
401
|
-
(0, cli_logging_1.configureCliRuntimeLogger)(friendId, {
|
|
402
|
-
level: agentConfig.logging?.level,
|
|
403
|
-
sinks: agentConfig.logging?.sinks,
|
|
404
|
-
});
|
|
405
|
-
const sessPath = (0, config_1.sessionPath)(friendId, "cli", "session");
|
|
406
|
-
let sessionLock = null;
|
|
407
|
-
try {
|
|
408
|
-
sessionLock = (0, session_lock_1.acquireSessionLock)(`${sessPath}.lock`, (0, identity_1.getAgentName)());
|
|
400
|
+
// Debounced line iterator: collects rapid-fire lines (paste) into a single input.
|
|
401
|
+
// When the debounce timeout wins the race, the pending iter.next() is saved
|
|
402
|
+
// and reused in the next iteration to prevent it from silently consuming input.
|
|
403
|
+
async function* createDebouncedLines(source, debounceMs) {
|
|
404
|
+
if (debounceMs <= 0) {
|
|
405
|
+
yield* source;
|
|
406
|
+
return;
|
|
409
407
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
408
|
+
const iter = source[Symbol.asyncIterator]();
|
|
409
|
+
let pending = null;
|
|
410
|
+
while (true) {
|
|
411
|
+
const first = pending ? await pending : await iter.next();
|
|
412
|
+
pending = null;
|
|
413
|
+
if (first.done)
|
|
414
|
+
break;
|
|
415
|
+
const lines = [first.value];
|
|
416
|
+
let more = true;
|
|
417
|
+
while (more) {
|
|
418
|
+
const nextPromise = iter.next();
|
|
419
|
+
const raced = await Promise.race([
|
|
420
|
+
nextPromise.then((r) => ({ kind: "line", result: r })),
|
|
421
|
+
new Promise((r) => setTimeout(() => r({ kind: "timeout" }), debounceMs)),
|
|
422
|
+
]);
|
|
423
|
+
if (raced.kind === "timeout") {
|
|
424
|
+
pending = nextPromise;
|
|
425
|
+
more = false;
|
|
426
|
+
}
|
|
427
|
+
else if (raced.result.done) {
|
|
428
|
+
more = false;
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
lines.push(raced.result.value);
|
|
432
|
+
}
|
|
415
433
|
}
|
|
416
|
-
|
|
417
|
-
/* v8 ignore stop */
|
|
434
|
+
yield lines.join("\n");
|
|
418
435
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const drainToMessages = () => {
|
|
427
|
-
const pending = (0, pending_1.drainPending)(pendingDir);
|
|
428
|
-
if (pending.length === 0)
|
|
429
|
-
return 0;
|
|
430
|
-
for (const msg of pending) {
|
|
431
|
-
messages.push({ role: "user", name: "harness", content: `[proactive message from ${msg.from}]` });
|
|
432
|
-
messages.push({ role: "assistant", content: msg.content });
|
|
433
|
-
}
|
|
434
|
-
return pending.length;
|
|
435
|
-
};
|
|
436
|
-
// Startup drain: deliver offline messages
|
|
437
|
-
const startupCount = drainToMessages();
|
|
438
|
-
if (startupCount > 0) {
|
|
439
|
-
(0, context_1.saveSession)(sessPath, messages);
|
|
436
|
+
}
|
|
437
|
+
async function runCliSession(options) {
|
|
438
|
+
/* v8 ignore start -- integration: runCliSession is interactive, tested via E2E @preserve */
|
|
439
|
+
const pasteDebounceMs = options.pasteDebounceMs ?? 50;
|
|
440
|
+
const registry = (0, commands_1.createCommandRegistry)();
|
|
441
|
+
if (!options.disableCommands) {
|
|
442
|
+
(0, commands_1.registerDefaultCommands)(registry);
|
|
440
443
|
}
|
|
444
|
+
const messages = options.messages
|
|
445
|
+
?? [{ role: "system", content: await (0, prompt_1.buildSystem)("cli") }];
|
|
441
446
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
442
447
|
const ctrl = new InputController(rl);
|
|
443
448
|
let currentAbort = null;
|
|
444
449
|
const history = [];
|
|
445
450
|
let closed = false;
|
|
446
451
|
rl.on("close", () => { closed = true; });
|
|
447
|
-
|
|
448
|
-
|
|
452
|
+
if (options.banner !== false) {
|
|
453
|
+
const bannerText = typeof options.banner === "string"
|
|
454
|
+
? options.banner
|
|
455
|
+
: `${options.agentName} (type /commands for help)`;
|
|
456
|
+
// eslint-disable-next-line no-console -- terminal UX: startup banner
|
|
457
|
+
console.log(`\n${bannerText}\n`);
|
|
458
|
+
}
|
|
449
459
|
const cliCallbacks = createCliCallbacks();
|
|
450
|
-
|
|
460
|
+
// exitOnToolCall machinery: wrap execTool to detect target tool
|
|
461
|
+
let exitToolResult;
|
|
462
|
+
let exitToolFired = false;
|
|
463
|
+
const resolvedExecTool = options.execTool;
|
|
464
|
+
const wrappedExecTool = options.exitOnToolCall && resolvedExecTool
|
|
465
|
+
? async (name, args, ctx) => {
|
|
466
|
+
const result = await resolvedExecTool(name, args, ctx);
|
|
467
|
+
if (name === options.exitOnToolCall) {
|
|
468
|
+
exitToolResult = result;
|
|
469
|
+
exitToolFired = true;
|
|
470
|
+
// Abort immediately so the model doesn't generate more output
|
|
471
|
+
// (e.g. reasoning about calling final_answer after complete_adoption)
|
|
472
|
+
currentAbort?.abort();
|
|
473
|
+
}
|
|
474
|
+
return result;
|
|
475
|
+
}
|
|
476
|
+
: resolvedExecTool;
|
|
477
|
+
// Resolve toolChoiceRequired: use explicit option if set, else fall back to toggle
|
|
478
|
+
const getEffectiveToolChoiceRequired = () => options.toolChoiceRequired !== undefined ? options.toolChoiceRequired : (0, commands_1.getToolChoiceRequired)();
|
|
451
479
|
// Ctrl-C at the input prompt: clear line or warn/exit
|
|
452
|
-
// readline with terminal:true catches Ctrl-C in raw mode (no ^C echo)
|
|
453
480
|
rl.on("SIGINT", () => {
|
|
454
481
|
const rlInt = rl;
|
|
455
482
|
const currentLine = rlInt.line || "";
|
|
@@ -470,38 +497,58 @@ async function main(agentName, options) {
|
|
|
470
497
|
rl.close();
|
|
471
498
|
}
|
|
472
499
|
});
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
500
|
+
const debouncedLines = (source) => createDebouncedLines(source, pasteDebounceMs);
|
|
501
|
+
(0, runtime_1.emitNervesEvent)({
|
|
502
|
+
component: "senses",
|
|
503
|
+
event: "senses.cli_session_start",
|
|
504
|
+
message: "runCliSession started",
|
|
505
|
+
meta: { agentName: options.agentName, hasExitOnToolCall: !!options.exitOnToolCall },
|
|
506
|
+
});
|
|
507
|
+
let exitReason = "user_quit";
|
|
508
|
+
// Auto-first-turn: process the last user message immediately so the agent
|
|
509
|
+
// speaks first (e.g. specialist greeting). Only triggers when explicitly opted in.
|
|
510
|
+
if (options.autoFirstTurn && messages.length > 0 && messages[messages.length - 1]?.role === "user") {
|
|
511
|
+
currentAbort = new AbortController();
|
|
512
|
+
const traceId = (0, nerves_1.createTraceId)();
|
|
513
|
+
ctrl.suppress(() => currentAbort.abort());
|
|
514
|
+
let result;
|
|
515
|
+
try {
|
|
516
|
+
result = await (0, core_1.runAgent)(messages, cliCallbacks, options.skipSystemPromptRefresh ? undefined : "cli", currentAbort.signal, {
|
|
517
|
+
toolChoiceRequired: getEffectiveToolChoiceRequired(),
|
|
518
|
+
traceId,
|
|
519
|
+
tools: options.tools,
|
|
520
|
+
execTool: wrappedExecTool,
|
|
521
|
+
toolContext: options.toolContext,
|
|
522
|
+
});
|
|
478
523
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
if (
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
524
|
+
catch (err) {
|
|
525
|
+
// AbortError (Ctrl-C) -- silently continue to prompt
|
|
526
|
+
// All other errors: show the user what happened
|
|
527
|
+
if (!(err instanceof DOMException && err.name === "AbortError")) {
|
|
528
|
+
process.stderr.write(`\x1b[31m${err instanceof Error ? err.message : String(err)}\x1b[0m\n`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
cliCallbacks.flushMarkdown();
|
|
532
|
+
ctrl.restore();
|
|
533
|
+
currentAbort = null;
|
|
534
|
+
if (exitToolFired) {
|
|
535
|
+
exitReason = "tool_exit";
|
|
536
|
+
rl.close();
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
const lastMsg = messages[messages.length - 1];
|
|
540
|
+
if (lastMsg?.role === "assistant" && !(typeof lastMsg.content === "string" ? lastMsg.content : "").trim()) {
|
|
541
|
+
process.stderr.write("\x1b[33m(empty response)\x1b[0m\n");
|
|
542
|
+
}
|
|
543
|
+
process.stdout.write("\n\n");
|
|
544
|
+
if (options.onTurnEnd) {
|
|
545
|
+
await options.onTurnEnd(messages, result ?? { usage: undefined });
|
|
501
546
|
}
|
|
502
|
-
yield lines.join("\n");
|
|
503
547
|
}
|
|
504
548
|
}
|
|
549
|
+
if (!exitToolFired) {
|
|
550
|
+
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
551
|
+
}
|
|
505
552
|
try {
|
|
506
553
|
for await (const input of debouncedLines(rl)) {
|
|
507
554
|
if (closed)
|
|
@@ -510,20 +557,18 @@ async function main(agentName, options) {
|
|
|
510
557
|
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
511
558
|
continue;
|
|
512
559
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
560
|
+
// Optional input gate (e.g. trust gate in main)
|
|
561
|
+
if (options.onInput) {
|
|
562
|
+
const gate = options.onInput(input);
|
|
563
|
+
if (!gate.allowed) {
|
|
564
|
+
if (gate.reply) {
|
|
565
|
+
process.stdout.write(`${gate.reply}\n`);
|
|
566
|
+
}
|
|
567
|
+
if (closed)
|
|
568
|
+
break;
|
|
569
|
+
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
570
|
+
continue;
|
|
522
571
|
}
|
|
523
|
-
if (closed)
|
|
524
|
-
break;
|
|
525
|
-
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
526
|
-
continue;
|
|
527
572
|
}
|
|
528
573
|
// Check for slash commands
|
|
529
574
|
const parsed = (0, commands_1.parseSlashCommand)(input);
|
|
@@ -536,7 +581,7 @@ async function main(agentName, options) {
|
|
|
536
581
|
else if (dispatchResult.result.action === "new") {
|
|
537
582
|
messages.length = 0;
|
|
538
583
|
messages.push({ role: "system", content: await (0, prompt_1.buildSystem)("cli") });
|
|
539
|
-
|
|
584
|
+
await options.onNewSession?.();
|
|
540
585
|
// eslint-disable-next-line no-console -- terminal UX: session cleared
|
|
541
586
|
console.log("session cleared");
|
|
542
587
|
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
@@ -550,13 +595,12 @@ async function main(agentName, options) {
|
|
|
550
595
|
}
|
|
551
596
|
}
|
|
552
597
|
}
|
|
553
|
-
// Re-style the echoed input lines
|
|
554
|
-
// For multiline paste, each line was echoed separately — erase them all
|
|
598
|
+
// Re-style the echoed input lines
|
|
555
599
|
const cols = process.stdout.columns || 80;
|
|
556
600
|
const inputLines = input.split("\n");
|
|
557
601
|
let echoRows = 0;
|
|
558
602
|
for (const line of inputLines) {
|
|
559
|
-
echoRows += Math.ceil((2 + line.length) / cols);
|
|
603
|
+
echoRows += Math.ceil((2 + line.length) / cols);
|
|
560
604
|
}
|
|
561
605
|
process.stdout.write(`\x1b[${echoRows}A\x1b[K` + `\x1b[1m> ${inputLines[0]}${inputLines.length > 1 ? ` (+${inputLines.length - 1} lines)` : ""}\x1b[0m\n\n`);
|
|
562
606
|
messages.push({ role: "user", content: input });
|
|
@@ -566,39 +610,155 @@ async function main(agentName, options) {
|
|
|
566
610
|
ctrl.suppress(() => currentAbort.abort());
|
|
567
611
|
let result;
|
|
568
612
|
try {
|
|
569
|
-
result = await (0, core_1.runAgent)(messages, cliCallbacks, "cli", currentAbort.signal, {
|
|
570
|
-
toolChoiceRequired: (
|
|
571
|
-
toolContext: cliToolContext,
|
|
613
|
+
result = await (0, core_1.runAgent)(messages, cliCallbacks, options.skipSystemPromptRefresh ? undefined : "cli", currentAbort.signal, {
|
|
614
|
+
toolChoiceRequired: getEffectiveToolChoiceRequired(),
|
|
572
615
|
traceId,
|
|
616
|
+
tools: options.tools,
|
|
617
|
+
execTool: wrappedExecTool,
|
|
618
|
+
toolContext: options.toolContext,
|
|
573
619
|
});
|
|
574
620
|
}
|
|
575
|
-
catch {
|
|
576
|
-
// AbortError
|
|
621
|
+
catch (err) {
|
|
622
|
+
// AbortError (Ctrl-C) -- silently return to prompt
|
|
623
|
+
// All other errors: show the user what happened
|
|
624
|
+
if (!(err instanceof DOMException && err.name === "AbortError")) {
|
|
625
|
+
process.stderr.write(`\x1b[31m${err instanceof Error ? err.message : String(err)}\x1b[0m\n`);
|
|
626
|
+
}
|
|
577
627
|
}
|
|
578
628
|
cliCallbacks.flushMarkdown();
|
|
579
629
|
ctrl.restore();
|
|
580
630
|
currentAbort = null;
|
|
631
|
+
// Check if exit tool was fired during this turn
|
|
632
|
+
if (exitToolFired) {
|
|
633
|
+
exitReason = "tool_exit";
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
581
636
|
// Safety net: never silently swallow an empty response
|
|
582
637
|
const lastMsg = messages[messages.length - 1];
|
|
583
638
|
if (lastMsg?.role === "assistant" && !(typeof lastMsg.content === "string" ? lastMsg.content : "").trim()) {
|
|
584
639
|
process.stderr.write("\x1b[33m(empty response)\x1b[0m\n");
|
|
585
640
|
}
|
|
586
641
|
process.stdout.write("\n\n");
|
|
587
|
-
(
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
// Post-turn: refresh system prompt so active sessions metadata is current
|
|
592
|
-
await (0, prompt_refresh_1.refreshSystemPrompt)(messages, "cli", undefined, resolvedContext);
|
|
642
|
+
// Post-turn hook (session persistence, pending drain, prompt refresh, etc.)
|
|
643
|
+
if (options.onTurnEnd) {
|
|
644
|
+
await options.onTurnEnd(messages, result ?? { usage: undefined });
|
|
645
|
+
}
|
|
593
646
|
if (closed)
|
|
594
647
|
break;
|
|
595
648
|
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
596
649
|
}
|
|
597
650
|
}
|
|
598
651
|
finally {
|
|
599
|
-
sessionLock?.release();
|
|
600
652
|
rl.close();
|
|
601
|
-
|
|
602
|
-
|
|
653
|
+
if (options.banner !== false) {
|
|
654
|
+
// eslint-disable-next-line no-console -- terminal UX: goodbye
|
|
655
|
+
console.log("bye");
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
/* v8 ignore stop */
|
|
659
|
+
return { exitReason, toolResult: exitToolResult };
|
|
660
|
+
}
|
|
661
|
+
async function main(agentName, options) {
|
|
662
|
+
if (agentName)
|
|
663
|
+
(0, identity_1.setAgentName)(agentName);
|
|
664
|
+
const pasteDebounceMs = options?.pasteDebounceMs ?? 50;
|
|
665
|
+
// Fail fast if provider is misconfigured (triggers human-readable error + exit)
|
|
666
|
+
(0, core_1.getProvider)();
|
|
667
|
+
// Resolve context kernel (identity + channel) for CLI
|
|
668
|
+
const friendsPath = path.join((0, identity_1.getAgentRoot)(), "friends");
|
|
669
|
+
const friendStore = new store_file_1.FileFriendStore(friendsPath);
|
|
670
|
+
const username = os.userInfo().username;
|
|
671
|
+
const hostname = os.hostname();
|
|
672
|
+
const localExternalId = `${username}@${hostname}`;
|
|
673
|
+
const resolver = new resolver_1.FriendResolver(friendStore, {
|
|
674
|
+
provider: "local",
|
|
675
|
+
externalId: localExternalId,
|
|
676
|
+
displayName: username,
|
|
677
|
+
channel: "cli",
|
|
678
|
+
});
|
|
679
|
+
const resolvedContext = await resolver.resolve();
|
|
680
|
+
const cliToolContext = {
|
|
681
|
+
/* v8 ignore next -- CLI has no OAuth sign-in; this no-op satisfies the interface @preserve */
|
|
682
|
+
signin: async () => undefined,
|
|
683
|
+
context: resolvedContext,
|
|
684
|
+
friendStore,
|
|
685
|
+
summarize: (0, core_1.createSummarize)(),
|
|
686
|
+
};
|
|
687
|
+
const friendId = resolvedContext.friend.id;
|
|
688
|
+
const agentConfig = (0, identity_1.loadAgentConfig)();
|
|
689
|
+
(0, cli_logging_1.configureCliRuntimeLogger)(friendId, {
|
|
690
|
+
level: agentConfig.logging?.level,
|
|
691
|
+
sinks: agentConfig.logging?.sinks,
|
|
692
|
+
});
|
|
693
|
+
const sessPath = (0, config_1.sessionPath)(friendId, "cli", "session");
|
|
694
|
+
let sessionLock = null;
|
|
695
|
+
try {
|
|
696
|
+
sessionLock = (0, session_lock_1.acquireSessionLock)(`${sessPath}.lock`, (0, identity_1.getAgentName)());
|
|
697
|
+
}
|
|
698
|
+
catch (error) {
|
|
699
|
+
/* v8 ignore start -- integration: main() is interactive, lock tested in session-lock.test.ts @preserve */
|
|
700
|
+
if (error instanceof session_lock_1.SessionLockError) {
|
|
701
|
+
process.stderr.write(`${error.message}\n`);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
throw error;
|
|
705
|
+
/* v8 ignore stop */
|
|
706
|
+
}
|
|
707
|
+
// Load existing session or start fresh
|
|
708
|
+
const existing = (0, context_1.loadSession)(sessPath);
|
|
709
|
+
const sessionMessages = existing?.messages && existing.messages.length > 0
|
|
710
|
+
? existing.messages
|
|
711
|
+
: [{ role: "system", content: await (0, prompt_1.buildSystem)("cli", undefined, resolvedContext) }];
|
|
712
|
+
// Pending queue drain: inject pending messages as harness-context + assistant-content pairs
|
|
713
|
+
const pendingDir = (0, pending_1.getPendingDir)((0, identity_1.getAgentName)(), friendId, "cli", "session");
|
|
714
|
+
const drainToMessages = () => {
|
|
715
|
+
const pending = (0, pending_1.drainPending)(pendingDir);
|
|
716
|
+
if (pending.length === 0)
|
|
717
|
+
return 0;
|
|
718
|
+
for (const msg of pending) {
|
|
719
|
+
sessionMessages.push({ role: "user", name: "harness", content: `[proactive message from ${msg.from}]` });
|
|
720
|
+
sessionMessages.push({ role: "assistant", content: msg.content });
|
|
721
|
+
}
|
|
722
|
+
return pending.length;
|
|
723
|
+
};
|
|
724
|
+
// Startup drain: deliver offline messages
|
|
725
|
+
const startupCount = drainToMessages();
|
|
726
|
+
if (startupCount > 0) {
|
|
727
|
+
(0, context_1.saveSession)(sessPath, sessionMessages);
|
|
728
|
+
}
|
|
729
|
+
try {
|
|
730
|
+
await runCliSession({
|
|
731
|
+
agentName: (0, identity_1.getAgentName)(),
|
|
732
|
+
pasteDebounceMs,
|
|
733
|
+
messages: sessionMessages,
|
|
734
|
+
toolContext: cliToolContext,
|
|
735
|
+
onInput: () => {
|
|
736
|
+
const trustGate = (0, trust_gate_1.enforceTrustGate)({
|
|
737
|
+
friend: resolvedContext.friend,
|
|
738
|
+
provider: "local",
|
|
739
|
+
externalId: localExternalId,
|
|
740
|
+
channel: "cli",
|
|
741
|
+
});
|
|
742
|
+
if (!trustGate.allowed) {
|
|
743
|
+
return {
|
|
744
|
+
allowed: false,
|
|
745
|
+
reply: trustGate.reason === "stranger_first_reply" ? trustGate.autoReply : undefined,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
return { allowed: true };
|
|
749
|
+
},
|
|
750
|
+
onTurnEnd: async (msgs, result) => {
|
|
751
|
+
(0, context_1.postTurn)(msgs, sessPath, result.usage);
|
|
752
|
+
await (0, tokens_1.accumulateFriendTokens)(friendStore, resolvedContext.friend.id, result.usage);
|
|
753
|
+
drainToMessages();
|
|
754
|
+
await (0, prompt_refresh_1.refreshSystemPrompt)(msgs, "cli", undefined, resolvedContext);
|
|
755
|
+
},
|
|
756
|
+
onNewSession: () => {
|
|
757
|
+
(0, context_1.deleteSession)(sessPath);
|
|
758
|
+
},
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
finally {
|
|
762
|
+
sessionLock?.release();
|
|
603
763
|
}
|
|
604
764
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createDebugActivityController = createDebugActivityController;
|
|
4
|
+
const format_1 = require("../mind/format");
|
|
5
|
+
const phrases_1 = require("../mind/phrases");
|
|
6
|
+
const runtime_1 = require("../nerves/runtime");
|
|
7
|
+
function createDebugActivityController(options) {
|
|
8
|
+
let queue = Promise.resolve();
|
|
9
|
+
let statusMessageGuid;
|
|
10
|
+
let typingActive = false;
|
|
11
|
+
let hadToolRun = false;
|
|
12
|
+
let followupShown = false;
|
|
13
|
+
let lastPhrase = "";
|
|
14
|
+
function reportTransportError(operation, error) {
|
|
15
|
+
(0, runtime_1.emitNervesEvent)({
|
|
16
|
+
level: "warn",
|
|
17
|
+
component: "senses",
|
|
18
|
+
event: "senses.debug_activity_transport_error",
|
|
19
|
+
message: "debug activity transport failed",
|
|
20
|
+
meta: {
|
|
21
|
+
operation,
|
|
22
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
options.onTransportError?.(operation, error);
|
|
26
|
+
}
|
|
27
|
+
function enqueue(operation, task) {
|
|
28
|
+
queue = queue
|
|
29
|
+
.then(task)
|
|
30
|
+
.catch((error) => {
|
|
31
|
+
reportTransportError(operation, error);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function nextPhrase(pool) {
|
|
35
|
+
const phrase = (0, phrases_1.pickPhrase)(pool, lastPhrase);
|
|
36
|
+
lastPhrase = phrase;
|
|
37
|
+
return phrase;
|
|
38
|
+
}
|
|
39
|
+
function ensureTyping(active) {
|
|
40
|
+
if (typingActive === active) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
typingActive = active;
|
|
44
|
+
enqueue(active ? "typing_start" : "typing_stop", async () => {
|
|
45
|
+
await options.transport.setTyping(active);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function setStatus(text) {
|
|
49
|
+
(0, runtime_1.emitNervesEvent)({
|
|
50
|
+
component: "senses",
|
|
51
|
+
event: "senses.debug_activity_update",
|
|
52
|
+
message: "debug activity status updated",
|
|
53
|
+
meta: {
|
|
54
|
+
hasStatusGuid: Boolean(statusMessageGuid),
|
|
55
|
+
textLength: text.length,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
ensureTyping(true);
|
|
59
|
+
enqueue("status_update", async () => {
|
|
60
|
+
if (statusMessageGuid) {
|
|
61
|
+
await options.transport.editStatus(statusMessageGuid, text);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
statusMessageGuid = await options.transport.sendStatus(text);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
onModelStart() {
|
|
69
|
+
const pool = hadToolRun ? options.followupPhrases : options.thinkingPhrases;
|
|
70
|
+
setStatus(`${nextPhrase(pool)}...`);
|
|
71
|
+
},
|
|
72
|
+
onToolStart(name, args) {
|
|
73
|
+
hadToolRun = true;
|
|
74
|
+
followupShown = false;
|
|
75
|
+
const argSummary = Object.values(args).join(", ");
|
|
76
|
+
const detail = argSummary ? ` (${argSummary})` : "";
|
|
77
|
+
setStatus(`running ${name}${detail}...`);
|
|
78
|
+
},
|
|
79
|
+
onToolEnd(name, summary, success) {
|
|
80
|
+
hadToolRun = true;
|
|
81
|
+
followupShown = false;
|
|
82
|
+
setStatus((0, format_1.formatToolResult)(name, summary, success));
|
|
83
|
+
},
|
|
84
|
+
onTextChunk(text) {
|
|
85
|
+
if (!text || !hadToolRun || followupShown) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
followupShown = true;
|
|
89
|
+
setStatus(`${nextPhrase(options.followupPhrases)}...`);
|
|
90
|
+
},
|
|
91
|
+
onError(error) {
|
|
92
|
+
setStatus((0, format_1.formatError)(error));
|
|
93
|
+
this.finish();
|
|
94
|
+
},
|
|
95
|
+
async drain() {
|
|
96
|
+
await queue;
|
|
97
|
+
},
|
|
98
|
+
async finish() {
|
|
99
|
+
if (!typingActive) {
|
|
100
|
+
await queue;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
ensureTyping(false);
|
|
104
|
+
await queue;
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|