@ouro.bot/cli 0.1.0-alpha.12 → 0.1.0-alpha.14
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/assets/ouroboros.png +0 -0
- package/dist/heart/config.js +2 -2
- package/dist/heart/core.js +3 -2
- package/dist/heart/daemon/daemon-cli.js +65 -22
- package/dist/heart/daemon/hatch-flow.js +0 -10
- package/dist/heart/daemon/ouro-bot-entry.js +0 -0
- package/dist/heart/daemon/ouro-entry.js +0 -0
- package/dist/heart/daemon/ouro-path-installer.js +21 -5
- package/dist/heart/daemon/ouro-uti.js +11 -2
- package/dist/heart/daemon/specialist-orchestrator.js +37 -94
- package/dist/heart/daemon/specialist-prompt.js +43 -8
- package/dist/heart/daemon/specialist-tools.js +161 -59
- package/dist/mind/bundle-manifest.js +58 -0
- package/dist/mind/prompt.js +3 -0
- package/dist/senses/cli.js +162 -98
- package/package.json +7 -2
- package/dist/heart/daemon/specialist-session.js +0 -177
- package/dist/inner-worker-entry.js +0 -4
package/dist/senses/cli.js
CHANGED
|
@@ -38,6 +38,7 @@ exports.handleSigint = handleSigint;
|
|
|
38
38
|
exports.addHistory = addHistory;
|
|
39
39
|
exports.renderMarkdown = renderMarkdown;
|
|
40
40
|
exports.createCliCallbacks = createCliCallbacks;
|
|
41
|
+
exports.runCliSession = runCliSession;
|
|
41
42
|
exports.main = main;
|
|
42
43
|
const readline = __importStar(require("readline"));
|
|
43
44
|
const os = __importStar(require("os"));
|
|
@@ -368,76 +369,12 @@ function createCliCallbacks() {
|
|
|
368
369
|
},
|
|
369
370
|
};
|
|
370
371
|
}
|
|
371
|
-
async function
|
|
372
|
-
|
|
373
|
-
(0, identity_1.setAgentName)(agentName);
|
|
374
|
-
const pasteDebounceMs = options?.pasteDebounceMs ?? 50;
|
|
375
|
-
// Fail fast if provider is misconfigured (triggers human-readable error + exit)
|
|
376
|
-
(0, core_1.getProvider)();
|
|
372
|
+
async function runCliSession(options) {
|
|
373
|
+
const pasteDebounceMs = options.pasteDebounceMs ?? 50;
|
|
377
374
|
const registry = (0, commands_1.createCommandRegistry)();
|
|
378
375
|
(0, commands_1.registerDefaultCommands)(registry);
|
|
379
|
-
|
|
380
|
-
|
|
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)());
|
|
409
|
-
}
|
|
410
|
-
catch (error) {
|
|
411
|
-
/* v8 ignore start -- integration: main() is interactive, lock tested in session-lock.test.ts @preserve */
|
|
412
|
-
if (error instanceof session_lock_1.SessionLockError) {
|
|
413
|
-
process.stderr.write(`${error.message}\n`);
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
throw error;
|
|
417
|
-
/* v8 ignore stop */
|
|
418
|
-
}
|
|
419
|
-
// Load existing session or start fresh
|
|
420
|
-
const existing = (0, context_1.loadSession)(sessPath);
|
|
421
|
-
const messages = existing?.messages && existing.messages.length > 0
|
|
422
|
-
? existing.messages
|
|
423
|
-
: [{ role: "system", content: await (0, prompt_1.buildSystem)("cli", undefined, resolvedContext) }];
|
|
424
|
-
// Pending queue drain: inject pending messages as harness-context + assistant-content pairs
|
|
425
|
-
const pendingDir = (0, pending_1.getPendingDir)((0, identity_1.getAgentName)(), friendId, "cli", "session");
|
|
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);
|
|
440
|
-
}
|
|
376
|
+
const messages = options.messages
|
|
377
|
+
?? [{ role: "system", content: await (0, prompt_1.buildSystem)("cli") }];
|
|
441
378
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
442
379
|
const ctrl = new InputController(rl);
|
|
443
380
|
let currentAbort = null;
|
|
@@ -445,11 +382,26 @@ async function main(agentName, options) {
|
|
|
445
382
|
let closed = false;
|
|
446
383
|
rl.on("close", () => { closed = true; });
|
|
447
384
|
// eslint-disable-next-line no-console -- terminal UX: startup banner
|
|
448
|
-
console.log(`\n${
|
|
385
|
+
console.log(`\n${options.agentName} (type /commands for help)\n`);
|
|
449
386
|
const cliCallbacks = createCliCallbacks();
|
|
387
|
+
// exitOnToolCall machinery: wrap execTool to detect target tool
|
|
388
|
+
let exitToolResult;
|
|
389
|
+
let exitToolFired = false;
|
|
390
|
+
const resolvedExecTool = options.execTool;
|
|
391
|
+
const wrappedExecTool = options.exitOnToolCall && resolvedExecTool
|
|
392
|
+
? async (name, args, ctx) => {
|
|
393
|
+
const result = await resolvedExecTool(name, args, ctx);
|
|
394
|
+
if (name === options.exitOnToolCall) {
|
|
395
|
+
exitToolResult = result;
|
|
396
|
+
exitToolFired = true;
|
|
397
|
+
}
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
400
|
+
: resolvedExecTool;
|
|
401
|
+
// Resolve toolChoiceRequired: use explicit option if set, else fall back to toggle
|
|
402
|
+
const getEffectiveToolChoiceRequired = () => options.toolChoiceRequired !== undefined ? options.toolChoiceRequired : (0, commands_1.getToolChoiceRequired)();
|
|
450
403
|
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
451
404
|
// 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
405
|
rl.on("SIGINT", () => {
|
|
454
406
|
const rlInt = rl;
|
|
455
407
|
const currentLine = rlInt.line || "";
|
|
@@ -481,7 +433,6 @@ async function main(agentName, options) {
|
|
|
481
433
|
const first = await iter.next();
|
|
482
434
|
if (first.done)
|
|
483
435
|
break;
|
|
484
|
-
// Collect any lines that arrive within the debounce window (paste detection)
|
|
485
436
|
const lines = [first.value];
|
|
486
437
|
let more = true;
|
|
487
438
|
while (more) {
|
|
@@ -502,6 +453,13 @@ async function main(agentName, options) {
|
|
|
502
453
|
yield lines.join("\n");
|
|
503
454
|
}
|
|
504
455
|
}
|
|
456
|
+
(0, runtime_1.emitNervesEvent)({
|
|
457
|
+
component: "senses",
|
|
458
|
+
event: "senses.cli_session_start",
|
|
459
|
+
message: "runCliSession started",
|
|
460
|
+
meta: { agentName: options.agentName, hasExitOnToolCall: !!options.exitOnToolCall },
|
|
461
|
+
});
|
|
462
|
+
let exitReason = "user_quit";
|
|
505
463
|
try {
|
|
506
464
|
for await (const input of debouncedLines(rl)) {
|
|
507
465
|
if (closed)
|
|
@@ -510,20 +468,18 @@ async function main(agentName, options) {
|
|
|
510
468
|
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
511
469
|
continue;
|
|
512
470
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
471
|
+
// Optional input gate (e.g. trust gate in main)
|
|
472
|
+
if (options.onInput) {
|
|
473
|
+
const gate = options.onInput(input);
|
|
474
|
+
if (!gate.allowed) {
|
|
475
|
+
if (gate.reply) {
|
|
476
|
+
process.stdout.write(`${gate.reply}\n`);
|
|
477
|
+
}
|
|
478
|
+
if (closed)
|
|
479
|
+
break;
|
|
480
|
+
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
481
|
+
continue;
|
|
522
482
|
}
|
|
523
|
-
if (closed)
|
|
524
|
-
break;
|
|
525
|
-
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
526
|
-
continue;
|
|
527
483
|
}
|
|
528
484
|
// Check for slash commands
|
|
529
485
|
const parsed = (0, commands_1.parseSlashCommand)(input);
|
|
@@ -536,7 +492,7 @@ async function main(agentName, options) {
|
|
|
536
492
|
else if (dispatchResult.result.action === "new") {
|
|
537
493
|
messages.length = 0;
|
|
538
494
|
messages.push({ role: "system", content: await (0, prompt_1.buildSystem)("cli") });
|
|
539
|
-
|
|
495
|
+
await options.onNewSession?.();
|
|
540
496
|
// eslint-disable-next-line no-console -- terminal UX: session cleared
|
|
541
497
|
console.log("session cleared");
|
|
542
498
|
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
@@ -550,13 +506,12 @@ async function main(agentName, options) {
|
|
|
550
506
|
}
|
|
551
507
|
}
|
|
552
508
|
}
|
|
553
|
-
// Re-style the echoed input lines
|
|
554
|
-
// For multiline paste, each line was echoed separately — erase them all
|
|
509
|
+
// Re-style the echoed input lines
|
|
555
510
|
const cols = process.stdout.columns || 80;
|
|
556
511
|
const inputLines = input.split("\n");
|
|
557
512
|
let echoRows = 0;
|
|
558
513
|
for (const line of inputLines) {
|
|
559
|
-
echoRows += Math.ceil((2 + line.length) / cols);
|
|
514
|
+
echoRows += Math.ceil((2 + line.length) / cols);
|
|
560
515
|
}
|
|
561
516
|
process.stdout.write(`\x1b[${echoRows}A\x1b[K` + `\x1b[1m> ${inputLines[0]}${inputLines.length > 1 ? ` (+${inputLines.length - 1} lines)` : ""}\x1b[0m\n\n`);
|
|
562
517
|
messages.push({ role: "user", content: input });
|
|
@@ -567,38 +522,147 @@ async function main(agentName, options) {
|
|
|
567
522
|
let result;
|
|
568
523
|
try {
|
|
569
524
|
result = await (0, core_1.runAgent)(messages, cliCallbacks, "cli", currentAbort.signal, {
|
|
570
|
-
toolChoiceRequired: (
|
|
571
|
-
toolContext: cliToolContext,
|
|
525
|
+
toolChoiceRequired: getEffectiveToolChoiceRequired(),
|
|
572
526
|
traceId,
|
|
527
|
+
tools: options.tools,
|
|
528
|
+
execTool: wrappedExecTool,
|
|
529
|
+
toolContext: options.toolContext,
|
|
573
530
|
});
|
|
574
531
|
}
|
|
575
532
|
catch {
|
|
576
|
-
// AbortError
|
|
533
|
+
// AbortError -- silently return to prompt
|
|
577
534
|
}
|
|
578
535
|
cliCallbacks.flushMarkdown();
|
|
579
536
|
ctrl.restore();
|
|
580
537
|
currentAbort = null;
|
|
538
|
+
// Check if exit tool was fired during this turn
|
|
539
|
+
if (exitToolFired) {
|
|
540
|
+
exitReason = "tool_exit";
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
581
543
|
// Safety net: never silently swallow an empty response
|
|
582
544
|
const lastMsg = messages[messages.length - 1];
|
|
583
545
|
if (lastMsg?.role === "assistant" && !(typeof lastMsg.content === "string" ? lastMsg.content : "").trim()) {
|
|
584
546
|
process.stderr.write("\x1b[33m(empty response)\x1b[0m\n");
|
|
585
547
|
}
|
|
586
548
|
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);
|
|
549
|
+
// Post-turn hook (session persistence, pending drain, prompt refresh, etc.)
|
|
550
|
+
if (options.onTurnEnd) {
|
|
551
|
+
await options.onTurnEnd(messages, result ?? { usage: undefined });
|
|
552
|
+
}
|
|
593
553
|
if (closed)
|
|
594
554
|
break;
|
|
595
555
|
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
596
556
|
}
|
|
597
557
|
}
|
|
598
558
|
finally {
|
|
599
|
-
sessionLock?.release();
|
|
600
559
|
rl.close();
|
|
601
560
|
// eslint-disable-next-line no-console -- terminal UX: goodbye
|
|
602
561
|
console.log("bye");
|
|
603
562
|
}
|
|
563
|
+
return { exitReason, toolResult: exitToolResult };
|
|
564
|
+
}
|
|
565
|
+
async function main(agentName, options) {
|
|
566
|
+
if (agentName)
|
|
567
|
+
(0, identity_1.setAgentName)(agentName);
|
|
568
|
+
const pasteDebounceMs = options?.pasteDebounceMs ?? 50;
|
|
569
|
+
// Fail fast if provider is misconfigured (triggers human-readable error + exit)
|
|
570
|
+
(0, core_1.getProvider)();
|
|
571
|
+
// Resolve context kernel (identity + channel) for CLI
|
|
572
|
+
const friendsPath = path.join((0, identity_1.getAgentRoot)(), "friends");
|
|
573
|
+
const friendStore = new store_file_1.FileFriendStore(friendsPath);
|
|
574
|
+
const username = os.userInfo().username;
|
|
575
|
+
const hostname = os.hostname();
|
|
576
|
+
const localExternalId = `${username}@${hostname}`;
|
|
577
|
+
const resolver = new resolver_1.FriendResolver(friendStore, {
|
|
578
|
+
provider: "local",
|
|
579
|
+
externalId: localExternalId,
|
|
580
|
+
displayName: username,
|
|
581
|
+
channel: "cli",
|
|
582
|
+
});
|
|
583
|
+
const resolvedContext = await resolver.resolve();
|
|
584
|
+
const cliToolContext = {
|
|
585
|
+
/* v8 ignore next -- CLI has no OAuth sign-in; this no-op satisfies the interface @preserve */
|
|
586
|
+
signin: async () => undefined,
|
|
587
|
+
context: resolvedContext,
|
|
588
|
+
friendStore,
|
|
589
|
+
summarize: (0, core_1.createSummarize)(),
|
|
590
|
+
};
|
|
591
|
+
const friendId = resolvedContext.friend.id;
|
|
592
|
+
const agentConfig = (0, identity_1.loadAgentConfig)();
|
|
593
|
+
(0, cli_logging_1.configureCliRuntimeLogger)(friendId, {
|
|
594
|
+
level: agentConfig.logging?.level,
|
|
595
|
+
sinks: agentConfig.logging?.sinks,
|
|
596
|
+
});
|
|
597
|
+
const sessPath = (0, config_1.sessionPath)(friendId, "cli", "session");
|
|
598
|
+
let sessionLock = null;
|
|
599
|
+
try {
|
|
600
|
+
sessionLock = (0, session_lock_1.acquireSessionLock)(`${sessPath}.lock`, (0, identity_1.getAgentName)());
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
/* v8 ignore start -- integration: main() is interactive, lock tested in session-lock.test.ts @preserve */
|
|
604
|
+
if (error instanceof session_lock_1.SessionLockError) {
|
|
605
|
+
process.stderr.write(`${error.message}\n`);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
throw error;
|
|
609
|
+
/* v8 ignore stop */
|
|
610
|
+
}
|
|
611
|
+
// Load existing session or start fresh
|
|
612
|
+
const existing = (0, context_1.loadSession)(sessPath);
|
|
613
|
+
const sessionMessages = existing?.messages && existing.messages.length > 0
|
|
614
|
+
? existing.messages
|
|
615
|
+
: [{ role: "system", content: await (0, prompt_1.buildSystem)("cli", undefined, resolvedContext) }];
|
|
616
|
+
// Pending queue drain: inject pending messages as harness-context + assistant-content pairs
|
|
617
|
+
const pendingDir = (0, pending_1.getPendingDir)((0, identity_1.getAgentName)(), friendId, "cli", "session");
|
|
618
|
+
const drainToMessages = () => {
|
|
619
|
+
const pending = (0, pending_1.drainPending)(pendingDir);
|
|
620
|
+
if (pending.length === 0)
|
|
621
|
+
return 0;
|
|
622
|
+
for (const msg of pending) {
|
|
623
|
+
sessionMessages.push({ role: "user", name: "harness", content: `[proactive message from ${msg.from}]` });
|
|
624
|
+
sessionMessages.push({ role: "assistant", content: msg.content });
|
|
625
|
+
}
|
|
626
|
+
return pending.length;
|
|
627
|
+
};
|
|
628
|
+
// Startup drain: deliver offline messages
|
|
629
|
+
const startupCount = drainToMessages();
|
|
630
|
+
if (startupCount > 0) {
|
|
631
|
+
(0, context_1.saveSession)(sessPath, sessionMessages);
|
|
632
|
+
}
|
|
633
|
+
try {
|
|
634
|
+
await runCliSession({
|
|
635
|
+
agentName: (0, identity_1.getAgentName)(),
|
|
636
|
+
pasteDebounceMs,
|
|
637
|
+
messages: sessionMessages,
|
|
638
|
+
toolContext: cliToolContext,
|
|
639
|
+
onInput: () => {
|
|
640
|
+
const trustGate = (0, trust_gate_1.enforceTrustGate)({
|
|
641
|
+
friend: resolvedContext.friend,
|
|
642
|
+
provider: "local",
|
|
643
|
+
externalId: localExternalId,
|
|
644
|
+
channel: "cli",
|
|
645
|
+
});
|
|
646
|
+
if (!trustGate.allowed) {
|
|
647
|
+
return {
|
|
648
|
+
allowed: false,
|
|
649
|
+
reply: trustGate.reason === "stranger_first_reply" ? trustGate.autoReply : undefined,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
return { allowed: true };
|
|
653
|
+
},
|
|
654
|
+
onTurnEnd: async (msgs, result) => {
|
|
655
|
+
(0, context_1.postTurn)(msgs, sessPath, result.usage);
|
|
656
|
+
await (0, tokens_1.accumulateFriendTokens)(friendStore, resolvedContext.friend.id, result.usage);
|
|
657
|
+
drainToMessages();
|
|
658
|
+
await (0, prompt_refresh_1.refreshSystemPrompt)(msgs, "cli", undefined, resolvedContext);
|
|
659
|
+
},
|
|
660
|
+
onNewSession: () => {
|
|
661
|
+
(0, context_1.deleteSession)(sessPath);
|
|
662
|
+
},
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
finally {
|
|
666
|
+
sessionLock?.release();
|
|
667
|
+
}
|
|
604
668
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.14",
|
|
4
4
|
"main": "dist/heart/daemon/ouro-entry.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ouro": "dist/heart/daemon/ouro-entry.js",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"dist/",
|
|
11
11
|
"AdoptionSpecialist.ouro/",
|
|
12
|
-
"subagents/"
|
|
12
|
+
"subagents/",
|
|
13
|
+
"assets/"
|
|
13
14
|
],
|
|
14
15
|
"exports": {
|
|
15
16
|
".": "./dist/heart/daemon/daemon-cli.js",
|
|
@@ -34,6 +35,10 @@
|
|
|
34
35
|
"@microsoft/teams.dev": "^2.0.5",
|
|
35
36
|
"openai": "^6.27.0"
|
|
36
37
|
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/ouroborosbot/ouroboros"
|
|
41
|
+
},
|
|
37
42
|
"devDependencies": {
|
|
38
43
|
"@vitest/coverage-v8": "^4.0.18",
|
|
39
44
|
"eslint": "^10.0.2",
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.runSpecialistSession = runSpecialistSession;
|
|
4
|
-
const runtime_1 = require("../../nerves/runtime");
|
|
5
|
-
/**
|
|
6
|
-
* Run the specialist conversation session loop.
|
|
7
|
-
*
|
|
8
|
-
* The loop:
|
|
9
|
-
* 1. Initialize messages with system prompt
|
|
10
|
-
* 2. Prompt user -> add to messages -> call streamTurn -> process result
|
|
11
|
-
* 3. If result has no tool calls: push assistant message, re-prompt
|
|
12
|
-
* 4. If result has final_answer sole call: extract answer, emit via callbacks, done
|
|
13
|
-
* 5. If result has other tool calls: execute each, push tool results, continue loop
|
|
14
|
-
* 6. On abort signal: clean exit
|
|
15
|
-
* 7. Return { hatchedAgentName } -- name from hatch_agent if called
|
|
16
|
-
*/
|
|
17
|
-
async function runSpecialistSession(deps) {
|
|
18
|
-
const { providerRuntime, systemPrompt, tools, execTool, readline, callbacks, signal, kickoffMessage, suppressInput, restoreInput, flushMarkdown, writePrompt, } = deps;
|
|
19
|
-
(0, runtime_1.emitNervesEvent)({
|
|
20
|
-
component: "daemon",
|
|
21
|
-
event: "daemon.specialist_session_start",
|
|
22
|
-
message: "starting specialist session loop",
|
|
23
|
-
meta: {},
|
|
24
|
-
});
|
|
25
|
-
const messages = [
|
|
26
|
-
{ role: "system", content: systemPrompt },
|
|
27
|
-
];
|
|
28
|
-
let hatchedAgentName = null;
|
|
29
|
-
let done = false;
|
|
30
|
-
let isFirstTurn = true;
|
|
31
|
-
let currentAbort = null;
|
|
32
|
-
try {
|
|
33
|
-
while (!done) {
|
|
34
|
-
if (signal?.aborted)
|
|
35
|
-
break;
|
|
36
|
-
// On the first turn with a kickoff message, inject it so the specialist speaks first
|
|
37
|
-
if (isFirstTurn && kickoffMessage) {
|
|
38
|
-
isFirstTurn = false;
|
|
39
|
-
messages.push({ role: "user", content: kickoffMessage });
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
// Get user input
|
|
43
|
-
const userInput = await readline.question(writePrompt ? "" : "> ");
|
|
44
|
-
if (!userInput.trim()) {
|
|
45
|
-
if (writePrompt)
|
|
46
|
-
writePrompt();
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
messages.push({ role: "user", content: userInput });
|
|
50
|
-
}
|
|
51
|
-
providerRuntime.resetTurnState(messages);
|
|
52
|
-
// Suppress input during model execution
|
|
53
|
-
currentAbort = new AbortController();
|
|
54
|
-
const mergedSignal = signal;
|
|
55
|
-
if (suppressInput) {
|
|
56
|
-
suppressInput(() => currentAbort.abort());
|
|
57
|
-
}
|
|
58
|
-
// Inner loop: process tool calls until we get a final_answer or plain text
|
|
59
|
-
let turnDone = false;
|
|
60
|
-
while (!turnDone) {
|
|
61
|
-
if (mergedSignal?.aborted || currentAbort.signal.aborted) {
|
|
62
|
-
done = true;
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
65
|
-
callbacks.onModelStart();
|
|
66
|
-
const result = await providerRuntime.streamTurn({
|
|
67
|
-
messages,
|
|
68
|
-
activeTools: tools,
|
|
69
|
-
callbacks,
|
|
70
|
-
signal: mergedSignal,
|
|
71
|
-
});
|
|
72
|
-
// Build assistant message
|
|
73
|
-
const assistantMsg = {
|
|
74
|
-
role: "assistant",
|
|
75
|
-
};
|
|
76
|
-
if (result.content)
|
|
77
|
-
assistantMsg.content = result.content;
|
|
78
|
-
if (result.toolCalls.length) {
|
|
79
|
-
assistantMsg.tool_calls = result.toolCalls.map((tc) => ({
|
|
80
|
-
id: tc.id,
|
|
81
|
-
type: "function",
|
|
82
|
-
function: { name: tc.name, arguments: tc.arguments },
|
|
83
|
-
}));
|
|
84
|
-
}
|
|
85
|
-
if (!result.toolCalls.length) {
|
|
86
|
-
// Plain text response -- flush markdown, push and re-prompt
|
|
87
|
-
if (flushMarkdown)
|
|
88
|
-
flushMarkdown();
|
|
89
|
-
messages.push(assistantMsg);
|
|
90
|
-
turnDone = true;
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
// Check for final_answer
|
|
94
|
-
const isSoleFinalAnswer = result.toolCalls.length === 1 && result.toolCalls[0].name === "final_answer";
|
|
95
|
-
if (isSoleFinalAnswer) {
|
|
96
|
-
let answer;
|
|
97
|
-
try {
|
|
98
|
-
const parsed = JSON.parse(result.toolCalls[0].arguments);
|
|
99
|
-
if (typeof parsed === "string") {
|
|
100
|
-
answer = parsed;
|
|
101
|
-
}
|
|
102
|
-
else if (parsed.answer != null) {
|
|
103
|
-
answer = parsed.answer;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
// malformed
|
|
108
|
-
}
|
|
109
|
-
if (answer != null) {
|
|
110
|
-
callbacks.onTextChunk(answer);
|
|
111
|
-
if (flushMarkdown)
|
|
112
|
-
flushMarkdown();
|
|
113
|
-
messages.push(assistantMsg);
|
|
114
|
-
done = true;
|
|
115
|
-
turnDone = true;
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
// Malformed final_answer -- ask model to retry
|
|
119
|
-
messages.push(assistantMsg);
|
|
120
|
-
messages.push({
|
|
121
|
-
role: "tool",
|
|
122
|
-
tool_call_id: result.toolCalls[0].id,
|
|
123
|
-
content: "your final_answer was incomplete or malformed. call final_answer again with your complete response.",
|
|
124
|
-
});
|
|
125
|
-
providerRuntime.appendToolOutput(result.toolCalls[0].id, "retry");
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
// Execute tool calls
|
|
129
|
-
messages.push(assistantMsg);
|
|
130
|
-
for (const tc of result.toolCalls) {
|
|
131
|
-
if (mergedSignal?.aborted)
|
|
132
|
-
break;
|
|
133
|
-
let args = {};
|
|
134
|
-
try {
|
|
135
|
-
args = JSON.parse(tc.arguments);
|
|
136
|
-
}
|
|
137
|
-
catch {
|
|
138
|
-
// ignore parse error
|
|
139
|
-
}
|
|
140
|
-
callbacks.onToolStart(tc.name, args);
|
|
141
|
-
let toolResult;
|
|
142
|
-
try {
|
|
143
|
-
toolResult = await execTool(tc.name, args);
|
|
144
|
-
}
|
|
145
|
-
catch (e) {
|
|
146
|
-
toolResult = `error: ${e}`;
|
|
147
|
-
}
|
|
148
|
-
callbacks.onToolEnd(tc.name, tc.name, true);
|
|
149
|
-
// Track hatchling name
|
|
150
|
-
if (tc.name === "hatch_agent" && args.name) {
|
|
151
|
-
hatchedAgentName = args.name;
|
|
152
|
-
}
|
|
153
|
-
messages.push({ role: "tool", tool_call_id: tc.id, content: toolResult });
|
|
154
|
-
providerRuntime.appendToolOutput(tc.id, toolResult);
|
|
155
|
-
}
|
|
156
|
-
// After processing tool calls, continue inner loop for tool result processing
|
|
157
|
-
}
|
|
158
|
-
// Restore input and show prompt for next turn
|
|
159
|
-
if (flushMarkdown)
|
|
160
|
-
flushMarkdown();
|
|
161
|
-
if (restoreInput)
|
|
162
|
-
restoreInput();
|
|
163
|
-
currentAbort = null;
|
|
164
|
-
if (!done) {
|
|
165
|
-
process.stdout.write("\n\n");
|
|
166
|
-
if (writePrompt)
|
|
167
|
-
writePrompt();
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
finally {
|
|
172
|
-
if (restoreInput)
|
|
173
|
-
restoreInput();
|
|
174
|
-
readline.close();
|
|
175
|
-
}
|
|
176
|
-
return { hatchedAgentName };
|
|
177
|
-
}
|