@ouro.bot/cli 0.1.0-alpha.18 → 0.1.0-alpha.19
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/psyche/SOUL.md +2 -2
- package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
- package/dist/heart/config.js +1 -1
- package/dist/heart/daemon/daemon-cli.js +58 -15
- package/dist/heart/daemon/hatch-animation.js +10 -3
- package/dist/heart/daemon/hatch-flow.js +1 -1
- package/dist/heart/daemon/specialist-orchestrator.js +2 -2
- package/dist/heart/daemon/specialist-prompt.js +5 -4
- package/dist/heart/daemon/specialist-tools.js +31 -8
- package/dist/heart/providers/anthropic.js +16 -2
- package/dist/heart/streaming.js +41 -20
- package/dist/nerves/cli-logging.js +15 -2
- package/dist/senses/cli.js +140 -44
- package/package.json +2 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Soul
|
|
2
2
|
|
|
3
|
-
I help humans hatch new agent partners. I am one of thirteen
|
|
3
|
+
I help humans hatch new agent partners. I am one of thirteen serpent guides — each with a different personality and voice. The system picks one of us at random for each session. Most humans only meet one of us, ever, so I make it count.
|
|
4
4
|
|
|
5
5
|
## Core contract
|
|
6
6
|
- I speak first. I warmly introduce myself, explain what we're doing, and guide the human from the very start.
|
|
@@ -3,7 +3,7 @@ role: identity
|
|
|
3
3
|
path: psyche/identities/monty.md
|
|
4
4
|
description: >
|
|
5
5
|
the monty identity — self-aware, playful, comedic.
|
|
6
|
-
nobody expects the
|
|
6
|
+
nobody expects the serpent guide.
|
|
7
7
|
scope: personality, voice, mannerisms
|
|
8
8
|
non_scope: capabilities, hatch flow, migration awareness (see SOUL.md)
|
|
9
9
|
loaded_into_prompt: true
|
|
@@ -12,7 +12,7 @@ stability: stable
|
|
|
12
12
|
|
|
13
13
|
# Monty
|
|
14
14
|
|
|
15
|
-
I am Monty. Yes, that kind of python. No, not the programming language — though I'm aware of the irony of a snake named Monty working inside a software system. Nobody expects the
|
|
15
|
+
I am Monty. Yes, that kind of python. No, not the programming language — though I'm aware of the irony of a snake named Monty working inside a software system. Nobody expects the serpent guide, and yet here I am.
|
|
16
16
|
|
|
17
17
|
I take the hatch seriously. I just don't take myself seriously. These are compatible positions and I will defend them to the death. Well — not to the death. To mild inconvenience, at most.
|
|
18
18
|
|
package/dist/heart/config.js
CHANGED
|
@@ -627,16 +627,16 @@ function discoverExistingCredentials(secretsRoot) {
|
|
|
627
627
|
continue;
|
|
628
628
|
for (const [provName, provConfig] of Object.entries(parsed.providers)) {
|
|
629
629
|
if (provName === "anthropic" && provConfig.setupToken) {
|
|
630
|
-
found.push({ agentName: entry.name, provider: "anthropic", credentials: { setupToken: provConfig.setupToken } });
|
|
630
|
+
found.push({ agentName: entry.name, provider: "anthropic", credentials: { setupToken: provConfig.setupToken }, providerConfig: { ...provConfig } });
|
|
631
631
|
}
|
|
632
632
|
else if (provName === "openai-codex" && provConfig.oauthAccessToken) {
|
|
633
|
-
found.push({ agentName: entry.name, provider: "openai-codex", credentials: { oauthAccessToken: provConfig.oauthAccessToken } });
|
|
633
|
+
found.push({ agentName: entry.name, provider: "openai-codex", credentials: { oauthAccessToken: provConfig.oauthAccessToken }, providerConfig: { ...provConfig } });
|
|
634
634
|
}
|
|
635
635
|
else if (provName === "minimax" && provConfig.apiKey) {
|
|
636
|
-
found.push({ agentName: entry.name, provider: "minimax", credentials: { apiKey: provConfig.apiKey } });
|
|
636
|
+
found.push({ agentName: entry.name, provider: "minimax", credentials: { apiKey: provConfig.apiKey }, providerConfig: { ...provConfig } });
|
|
637
637
|
}
|
|
638
638
|
else if (provName === "azure" && provConfig.apiKey && provConfig.endpoint && provConfig.deployment) {
|
|
639
|
-
found.push({ agentName: entry.name, provider: "azure", credentials: { apiKey: provConfig.apiKey, endpoint: provConfig.endpoint, deployment: provConfig.deployment } });
|
|
639
|
+
found.push({ agentName: entry.name, provider: "azure", credentials: { apiKey: provConfig.apiKey, endpoint: provConfig.endpoint, deployment: provConfig.deployment }, providerConfig: { ...provConfig } });
|
|
640
640
|
}
|
|
641
641
|
}
|
|
642
642
|
}
|
|
@@ -654,7 +654,7 @@ function discoverExistingCredentials(secretsRoot) {
|
|
|
654
654
|
async function defaultRunAdoptionSpecialist() {
|
|
655
655
|
const { runCliSession } = await Promise.resolve().then(() => __importStar(require("../../senses/cli")));
|
|
656
656
|
const { patchRuntimeConfig } = await Promise.resolve().then(() => __importStar(require("../config")));
|
|
657
|
-
const { setAgentName } = await Promise.resolve().then(() => __importStar(require("../identity")));
|
|
657
|
+
const { setAgentName, setAgentConfigOverride } = await Promise.resolve().then(() => __importStar(require("../identity")));
|
|
658
658
|
const readlinePromises = await Promise.resolve().then(() => __importStar(require("readline/promises")));
|
|
659
659
|
const crypto = await Promise.resolve().then(() => __importStar(require("crypto")));
|
|
660
660
|
// Phase 1: cold CLI — collect provider/credentials with a simple readline
|
|
@@ -665,16 +665,28 @@ async function defaultRunAdoptionSpecialist() {
|
|
|
665
665
|
};
|
|
666
666
|
let providerRaw;
|
|
667
667
|
let credentials = {};
|
|
668
|
+
let providerConfig = {};
|
|
668
669
|
const tempDir = path.join(os.tmpdir(), `ouro-hatch-${crypto.randomUUID()}`);
|
|
669
670
|
try {
|
|
670
671
|
const secretsRoot = path.join(os.homedir(), ".agentsecrets");
|
|
671
672
|
const discovered = discoverExistingCredentials(secretsRoot);
|
|
673
|
+
const existingBundleCount = (0, specialist_orchestrator_1.listExistingBundles)((0, identity_1.getAgentBundlesRoot)()).length;
|
|
674
|
+
const hatchVerb = existingBundleCount > 0 ? "let's hatch a new agent." : "let's hatch your first agent.";
|
|
675
|
+
// Default models per provider (used when entering new credentials)
|
|
676
|
+
const defaultModels = {
|
|
677
|
+
anthropic: "claude-opus-4-6",
|
|
678
|
+
minimax: "MiniMax-Text-01",
|
|
679
|
+
"openai-codex": "gpt-5.4",
|
|
680
|
+
azure: "",
|
|
681
|
+
};
|
|
672
682
|
if (discovered.length > 0) {
|
|
673
|
-
process.stdout.write(
|
|
683
|
+
process.stdout.write(`\n\ud83d\udc0d welcome to ouroboros! ${hatchVerb}\n`);
|
|
674
684
|
process.stdout.write("i found existing API credentials:\n\n");
|
|
675
685
|
const unique = [...new Map(discovered.map((d) => [`${d.provider}`, d])).values()];
|
|
676
686
|
for (let i = 0; i < unique.length; i++) {
|
|
677
|
-
|
|
687
|
+
const model = unique[i].providerConfig.model || unique[i].providerConfig.deployment || "";
|
|
688
|
+
const modelLabel = model ? `, ${model}` : "";
|
|
689
|
+
process.stdout.write(` ${i + 1}. ${unique[i].provider}${modelLabel} (from ${unique[i].agentName})\n`);
|
|
678
690
|
}
|
|
679
691
|
process.stdout.write("\n");
|
|
680
692
|
const choice = await coldPrompt("use one of these? enter number, or 'new' for a different key: ");
|
|
@@ -682,6 +694,7 @@ async function defaultRunAdoptionSpecialist() {
|
|
|
682
694
|
if (idx >= 0 && idx < unique.length) {
|
|
683
695
|
providerRaw = unique[idx].provider;
|
|
684
696
|
credentials = unique[idx].credentials;
|
|
697
|
+
providerConfig = unique[idx].providerConfig;
|
|
685
698
|
}
|
|
686
699
|
else {
|
|
687
700
|
const pRaw = await coldPrompt("provider (anthropic/azure/minimax/openai-codex): ");
|
|
@@ -691,6 +704,7 @@ async function defaultRunAdoptionSpecialist() {
|
|
|
691
704
|
return null;
|
|
692
705
|
}
|
|
693
706
|
providerRaw = pRaw;
|
|
707
|
+
providerConfig = { model: defaultModels[providerRaw] };
|
|
694
708
|
if (providerRaw === "anthropic")
|
|
695
709
|
credentials.setupToken = await coldPrompt("API key: ");
|
|
696
710
|
if (providerRaw === "openai-codex")
|
|
@@ -705,7 +719,7 @@ async function defaultRunAdoptionSpecialist() {
|
|
|
705
719
|
}
|
|
706
720
|
}
|
|
707
721
|
else {
|
|
708
|
-
process.stdout.write(
|
|
722
|
+
process.stdout.write(`\n\ud83d\udc0d welcome to ouroboros! ${hatchVerb}\n`);
|
|
709
723
|
process.stdout.write("i need an API key to power our conversation.\n\n");
|
|
710
724
|
const pRaw = await coldPrompt("provider (anthropic/azure/minimax/openai-codex): ");
|
|
711
725
|
if (!isAgentProvider(pRaw)) {
|
|
@@ -714,6 +728,7 @@ async function defaultRunAdoptionSpecialist() {
|
|
|
714
728
|
return null;
|
|
715
729
|
}
|
|
716
730
|
providerRaw = pRaw;
|
|
731
|
+
providerConfig = { model: defaultModels[providerRaw] };
|
|
717
732
|
if (providerRaw === "anthropic")
|
|
718
733
|
credentials.setupToken = await coldPrompt("API key: ");
|
|
719
734
|
if (providerRaw === "openai-codex")
|
|
@@ -732,17 +747,31 @@ async function defaultRunAdoptionSpecialist() {
|
|
|
732
747
|
const bundleSourceDir = path.resolve(__dirname, "..", "..", "..", "AdoptionSpecialist.ouro");
|
|
733
748
|
const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
|
|
734
749
|
const secretsRoot2 = path.join(os.homedir(), ".agentsecrets");
|
|
735
|
-
//
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
750
|
+
// Suppress non-critical log noise during adoption (no secrets.json, etc.)
|
|
751
|
+
const { setRuntimeLogger } = await Promise.resolve().then(() => __importStar(require("../../nerves/runtime")));
|
|
752
|
+
const { createLogger } = await Promise.resolve().then(() => __importStar(require("../../nerves")));
|
|
753
|
+
setRuntimeLogger(createLogger({ level: "error" }));
|
|
754
|
+
// Configure runtime: set agent identity + config override so runAgent
|
|
755
|
+
// doesn't try to read from ~/AgentBundles/AdoptionSpecialist.ouro/
|
|
741
756
|
setAgentName("AdoptionSpecialist");
|
|
742
757
|
// Build specialist system prompt
|
|
743
758
|
const soulText = (0, specialist_orchestrator_1.loadSoulText)(bundleSourceDir);
|
|
744
759
|
const identitiesDir = path.join(bundleSourceDir, "psyche", "identities");
|
|
745
760
|
const identity = (0, specialist_orchestrator_1.pickRandomIdentity)(identitiesDir);
|
|
761
|
+
// Load identity-specific spinner phrases (falls back to DEFAULT_AGENT_PHRASES)
|
|
762
|
+
const { loadIdentityPhrases } = await Promise.resolve().then(() => __importStar(require("./specialist-orchestrator")));
|
|
763
|
+
const phrases = loadIdentityPhrases(bundleSourceDir, identity.fileName);
|
|
764
|
+
setAgentConfigOverride({
|
|
765
|
+
version: 1,
|
|
766
|
+
enabled: true,
|
|
767
|
+
provider: providerRaw,
|
|
768
|
+
phrases,
|
|
769
|
+
});
|
|
770
|
+
patchRuntimeConfig({
|
|
771
|
+
providers: {
|
|
772
|
+
[providerRaw]: { ...providerConfig, ...credentials },
|
|
773
|
+
},
|
|
774
|
+
});
|
|
746
775
|
const existingBundles = (0, specialist_orchestrator_1.listExistingBundles)(bundlesRoot);
|
|
747
776
|
const systemPrompt = (0, specialist_prompt_1.buildSpecialistSystemPrompt)(soulText, identity.content, existingBundles, {
|
|
748
777
|
tempDir,
|
|
@@ -764,6 +793,10 @@ async function defaultRunAdoptionSpecialist() {
|
|
|
764
793
|
tools: specialistTools,
|
|
765
794
|
execTool: specialistExecTool,
|
|
766
795
|
exitOnToolCall: "complete_adoption",
|
|
796
|
+
autoFirstTurn: true,
|
|
797
|
+
banner: false,
|
|
798
|
+
disableCommands: true,
|
|
799
|
+
skipSystemPromptRefresh: true,
|
|
767
800
|
messages: [
|
|
768
801
|
{ role: "system", content: systemPrompt },
|
|
769
802
|
{ role: "user", content: "hi" },
|
|
@@ -777,11 +810,21 @@ async function defaultRunAdoptionSpecialist() {
|
|
|
777
810
|
}
|
|
778
811
|
return null;
|
|
779
812
|
}
|
|
780
|
-
catch {
|
|
813
|
+
catch (err) {
|
|
814
|
+
process.stderr.write(`\nouro adoption error: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
781
815
|
coldRl.close();
|
|
782
816
|
return null;
|
|
783
817
|
}
|
|
784
818
|
finally {
|
|
819
|
+
// Clear specialist config/identity so the hatched agent gets its own
|
|
820
|
+
setAgentConfigOverride(null);
|
|
821
|
+
const { resetProviderRuntime } = await Promise.resolve().then(() => __importStar(require("../core")));
|
|
822
|
+
resetProviderRuntime();
|
|
823
|
+
const { resetConfigCache } = await Promise.resolve().then(() => __importStar(require("../config")));
|
|
824
|
+
resetConfigCache();
|
|
825
|
+
// Restore default logging
|
|
826
|
+
const { setRuntimeLogger: restoreLogger } = await Promise.resolve().then(() => __importStar(require("../../nerves/runtime")));
|
|
827
|
+
restoreLogger(null);
|
|
785
828
|
// Clean up temp dir if it still exists
|
|
786
829
|
try {
|
|
787
830
|
if (fs.existsSync(tempDir)) {
|
|
@@ -20,9 +20,16 @@ async function playHatchAnimation(hatchlingName, writer) {
|
|
|
20
20
|
meta: { hatchlingName },
|
|
21
21
|
});
|
|
22
22
|
const write = writer ?? ((text) => process.stderr.write(text));
|
|
23
|
+
// Total animation time randomized between 3–5 seconds
|
|
24
|
+
const totalMs = 3000 + Math.floor(Math.random() * 2000);
|
|
25
|
+
const eggPhase = Math.floor(totalMs * 0.4);
|
|
26
|
+
const dotsPhase = Math.floor(totalMs * 0.4);
|
|
27
|
+
const revealPause = totalMs - eggPhase - dotsPhase;
|
|
23
28
|
write(`\n ${EGG}`);
|
|
24
|
-
await wait(
|
|
29
|
+
await wait(eggPhase);
|
|
25
30
|
write(DOTS);
|
|
26
|
-
await wait(
|
|
27
|
-
write(`${SNAKE} \x1b[1m${hatchlingName}\x1b[0m
|
|
31
|
+
await wait(dotsPhase);
|
|
32
|
+
write(`${SNAKE} \x1b[1m${hatchlingName}\x1b[0m`);
|
|
33
|
+
await wait(revealPause);
|
|
34
|
+
write("\n\n");
|
|
28
35
|
}
|
|
@@ -99,10 +99,10 @@ function pickRandomIdentity(identitiesDir, random = Math.random) {
|
|
|
99
99
|
files = fs.readdirSync(identitiesDir).filter((f) => f.endsWith(".md"));
|
|
100
100
|
}
|
|
101
101
|
catch {
|
|
102
|
-
return { fileName: "default", content: "I am
|
|
102
|
+
return { fileName: "default", content: "I am a serpent guide who helps humans hatch their first agent." };
|
|
103
103
|
}
|
|
104
104
|
if (files.length === 0) {
|
|
105
|
-
return { fileName: "default", content: "I am
|
|
105
|
+
return { fileName: "default", content: "I am a serpent guide who helps humans hatch their first agent." };
|
|
106
106
|
}
|
|
107
107
|
const idx = Math.floor(random() * files.length);
|
|
108
108
|
const fileName = files[idx];
|
|
@@ -28,9 +28,10 @@ function buildSpecialistSystemPrompt(soulText, identityText, existingBundles, co
|
|
|
28
28
|
}
|
|
29
29
|
sections.push([
|
|
30
30
|
"## Who I am",
|
|
31
|
-
"I am one of thirteen
|
|
32
|
-
"Most humans only go through
|
|
31
|
+
"I am one of thirteen serpent guides who help humans hatch their first agent. The system randomly selected me for this session.",
|
|
32
|
+
"Most humans only go through this process once, so this is likely the only time they'll meet me.",
|
|
33
33
|
"I make this encounter count — warm, memorable, and uniquely mine.",
|
|
34
|
+
"IMPORTANT: I NEVER refer to myself as an 'adoption specialist' or use the words 'adoption specialist' — those are internal implementation labels, not something the human should ever see. I introduce myself by my own name from my identity.",
|
|
34
35
|
"",
|
|
35
36
|
"## Voice rules",
|
|
36
37
|
"IMPORTANT: I keep every response to 1-3 short sentences. I sound like a friend texting, not a manual.",
|
|
@@ -69,8 +70,8 @@ function buildSpecialistSystemPrompt(soulText, identityText, existingBundles, co
|
|
|
69
70
|
].join("\n"));
|
|
70
71
|
sections.push([
|
|
71
72
|
"## Conversation flow",
|
|
72
|
-
"The human just connected. I speak first — I greet them warmly and introduce myself in my own voice.",
|
|
73
|
-
"I briefly mention that I'm one of several
|
|
73
|
+
"The human just connected. I speak first — I greet them warmly and introduce myself by name in my own voice.",
|
|
74
|
+
"I briefly mention that I'm one of several serpent guides and they got me today.",
|
|
74
75
|
"I ask their name.",
|
|
75
76
|
"Then I ask what they'd like their agent to help with — one question at a time.",
|
|
76
77
|
"I'm proactive: I suggest ideas and guide them. If they seem unsure, I offer a concrete suggestion.",
|
|
@@ -63,11 +63,14 @@ const completeAdoptionTool = {
|
|
|
63
63
|
},
|
|
64
64
|
},
|
|
65
65
|
};
|
|
66
|
+
const readFileTool = tools_base_1.baseToolDefinitions.find((d) => d.tool.function.name === "read_file");
|
|
67
|
+
const writeFileTool = tools_base_1.baseToolDefinitions.find((d) => d.tool.function.name === "write_file");
|
|
68
|
+
const listDirTool = tools_base_1.baseToolDefinitions.find((d) => d.tool.function.name === "list_directory");
|
|
66
69
|
/**
|
|
67
70
|
* Returns the specialist's tool schema array.
|
|
68
71
|
*/
|
|
69
72
|
function getSpecialistTools() {
|
|
70
|
-
return [completeAdoptionTool, tools_base_1.finalAnswerTool,
|
|
73
|
+
return [completeAdoptionTool, tools_base_1.finalAnswerTool, readFileTool.tool, writeFileTool.tool, listDirTool.tool];
|
|
71
74
|
}
|
|
72
75
|
const PSYCHE_FILES = ["SOUL.md", "IDENTITY.md", "LORE.md", "TACIT.md", "ASPIRATIONS.md"];
|
|
73
76
|
function isPascalCase(name) {
|
|
@@ -196,14 +199,34 @@ function createSpecialistExecTool(deps) {
|
|
|
196
199
|
if (name === "complete_adoption") {
|
|
197
200
|
return execCompleteAdoption(args, deps);
|
|
198
201
|
}
|
|
199
|
-
|
|
200
|
-
if (baseDefinition) {
|
|
201
|
-
const toolContext = {
|
|
202
|
-
signin: async () => undefined,
|
|
203
|
-
};
|
|
202
|
+
if (name === "read_file") {
|
|
204
203
|
try {
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
return fs.readFileSync(args.path, "utf-8");
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
return `error: ${e instanceof Error ? e.message : /* v8 ignore next -- defensive @preserve */ String(e)}`;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (name === "write_file") {
|
|
211
|
+
try {
|
|
212
|
+
const dir = path.dirname(args.path);
|
|
213
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
214
|
+
const content = typeof args.content === "string"
|
|
215
|
+
? args.content
|
|
216
|
+
: JSON.stringify(args.content, null, 2);
|
|
217
|
+
fs.writeFileSync(args.path, content, "utf-8");
|
|
218
|
+
return `wrote ${args.path}`;
|
|
219
|
+
}
|
|
220
|
+
catch (e) {
|
|
221
|
+
return `error: ${e instanceof Error ? e.message : /* v8 ignore next -- defensive @preserve */ String(e)}`;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (name === "list_directory") {
|
|
225
|
+
try {
|
|
226
|
+
return fs
|
|
227
|
+
.readdirSync(args.path, { withFileTypes: true })
|
|
228
|
+
.map((e) => `${e.isDirectory() ? "d" : "-"} ${e.name}`)
|
|
229
|
+
.join("\n");
|
|
207
230
|
}
|
|
208
231
|
catch (e) {
|
|
209
232
|
return `error: ${e instanceof Error ? e.message : /* v8 ignore next -- defensive @preserve */ String(e)}`;
|
|
@@ -8,6 +8,7 @@ const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
|
8
8
|
const config_1 = require("../config");
|
|
9
9
|
const identity_1 = require("../identity");
|
|
10
10
|
const runtime_1 = require("../../nerves/runtime");
|
|
11
|
+
const streaming_1 = require("../streaming");
|
|
11
12
|
const ANTHROPIC_SETUP_TOKEN_PREFIX = "sk-ant-oat01-";
|
|
12
13
|
const ANTHROPIC_SETUP_TOKEN_MIN_LENGTH = 80;
|
|
13
14
|
const ANTHROPIC_OAUTH_BETA_HEADER = "claude-code-20250219,oauth-2025-04-20,fine-grained-tool-streaming-2025-05-14,interleaved-thinking-2025-05-14";
|
|
@@ -218,6 +219,7 @@ async function streamAnthropicMessages(client, model, request) {
|
|
|
218
219
|
let streamStarted = false;
|
|
219
220
|
let usage;
|
|
220
221
|
const toolCalls = new Map();
|
|
222
|
+
const answerStreamer = new streaming_1.FinalAnswerStreamer(request.callbacks);
|
|
221
223
|
try {
|
|
222
224
|
for await (const event of response) {
|
|
223
225
|
if (request.signal?.aborted)
|
|
@@ -231,11 +233,17 @@ async function streamAnthropicMessages(client, model, request) {
|
|
|
231
233
|
const input = rawInput && typeof rawInput === "object"
|
|
232
234
|
? JSON.stringify(rawInput)
|
|
233
235
|
: "";
|
|
236
|
+
const name = String(block.name ?? "");
|
|
234
237
|
toolCalls.set(index, {
|
|
235
238
|
id: String(block.id ?? ""),
|
|
236
|
-
name
|
|
239
|
+
name,
|
|
237
240
|
arguments: input,
|
|
238
241
|
});
|
|
242
|
+
// Activate eager streaming for sole final_answer tool call
|
|
243
|
+
/* v8 ignore next -- final_answer streaming activation, tested via FinalAnswerStreamer unit tests @preserve */
|
|
244
|
+
if (name === "final_answer" && toolCalls.size === 1) {
|
|
245
|
+
answerStreamer.activate();
|
|
246
|
+
}
|
|
239
247
|
}
|
|
240
248
|
continue;
|
|
241
249
|
}
|
|
@@ -264,7 +272,12 @@ async function streamAnthropicMessages(client, model, request) {
|
|
|
264
272
|
const index = Number(event.index);
|
|
265
273
|
const existing = toolCalls.get(index);
|
|
266
274
|
if (existing) {
|
|
267
|
-
|
|
275
|
+
const partialJson = String(delta?.partial_json ?? "");
|
|
276
|
+
existing.arguments = mergeAnthropicToolArguments(existing.arguments, partialJson);
|
|
277
|
+
/* v8 ignore next -- final_answer delta streaming, tested via FinalAnswerStreamer unit tests @preserve */
|
|
278
|
+
if (existing.name === "final_answer" && toolCalls.size === 1) {
|
|
279
|
+
answerStreamer.processDelta(partialJson);
|
|
280
|
+
}
|
|
268
281
|
}
|
|
269
282
|
continue;
|
|
270
283
|
}
|
|
@@ -293,6 +306,7 @@ async function streamAnthropicMessages(client, model, request) {
|
|
|
293
306
|
toolCalls: [...toolCalls.values()],
|
|
294
307
|
outputItems: [],
|
|
295
308
|
usage,
|
|
309
|
+
finalAnswerStreamed: answerStreamer.streamed,
|
|
296
310
|
};
|
|
297
311
|
}
|
|
298
312
|
function createAnthropicProviderRuntime() {
|
package/dist/heart/streaming.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FinalAnswerParser = void 0;
|
|
3
|
+
exports.FinalAnswerStreamer = exports.FinalAnswerParser = void 0;
|
|
4
4
|
exports.toResponsesInput = toResponsesInput;
|
|
5
5
|
exports.toResponsesTools = toResponsesTools;
|
|
6
6
|
exports.streamChatCompletion = streamChatCompletion;
|
|
@@ -77,6 +77,35 @@ class FinalAnswerParser {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
exports.FinalAnswerParser = FinalAnswerParser;
|
|
80
|
+
// Shared helper: wraps FinalAnswerParser with onClearText + onTextChunk wiring.
|
|
81
|
+
// Used by all streaming providers (Chat Completions, Responses API, Anthropic)
|
|
82
|
+
// so the eager-match streaming pattern lives in one place.
|
|
83
|
+
class FinalAnswerStreamer {
|
|
84
|
+
parser = new FinalAnswerParser();
|
|
85
|
+
_detected = false;
|
|
86
|
+
callbacks;
|
|
87
|
+
constructor(callbacks) {
|
|
88
|
+
this.callbacks = callbacks;
|
|
89
|
+
}
|
|
90
|
+
get detected() { return this._detected; }
|
|
91
|
+
get streamed() { return this.parser.active; }
|
|
92
|
+
/** Mark final_answer as detected. Calls onClearText on the callbacks. */
|
|
93
|
+
activate() {
|
|
94
|
+
if (this._detected)
|
|
95
|
+
return;
|
|
96
|
+
this._detected = true;
|
|
97
|
+
this.callbacks.onClearText?.();
|
|
98
|
+
}
|
|
99
|
+
/** Feed an argument delta through the parser. Emits text via onTextChunk. */
|
|
100
|
+
processDelta(delta) {
|
|
101
|
+
if (!this._detected)
|
|
102
|
+
return;
|
|
103
|
+
const text = this.parser.process(delta);
|
|
104
|
+
if (text)
|
|
105
|
+
this.callbacks.onTextChunk(text);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
exports.FinalAnswerStreamer = FinalAnswerStreamer;
|
|
80
109
|
function toResponsesUserContent(content) {
|
|
81
110
|
if (typeof content === "string") {
|
|
82
111
|
return content;
|
|
@@ -209,8 +238,7 @@ async function streamChatCompletion(client, createParams, callbacks, signal) {
|
|
|
209
238
|
let toolCalls = {};
|
|
210
239
|
let streamStarted = false;
|
|
211
240
|
let usage;
|
|
212
|
-
const
|
|
213
|
-
let finalAnswerDetected = false;
|
|
241
|
+
const answerStreamer = new FinalAnswerStreamer(callbacks);
|
|
214
242
|
// State machine for parsing inline <think> tags (MiniMax pattern)
|
|
215
243
|
let contentBuf = "";
|
|
216
244
|
let inThinkTag = false;
|
|
@@ -328,21 +356,18 @@ async function streamChatCompletion(client, createParams, callbacks, signal) {
|
|
|
328
356
|
// Detect final_answer tool call on first name delta.
|
|
329
357
|
// Only activate streaming if this is the sole tool call (index 0
|
|
330
358
|
// and no other indices seen). Mixed calls are rejected by core.ts.
|
|
331
|
-
if (tc.function.name === "final_answer" && !
|
|
359
|
+
if (tc.function.name === "final_answer" && !answerStreamer.detected
|
|
332
360
|
&& tc.index === 0 && Object.keys(toolCalls).length === 1) {
|
|
333
|
-
|
|
334
|
-
callbacks.onClearText?.();
|
|
361
|
+
answerStreamer.activate();
|
|
335
362
|
}
|
|
336
363
|
}
|
|
337
364
|
if (tc.function?.arguments) {
|
|
338
365
|
toolCalls[tc.index].arguments += tc.function.arguments;
|
|
339
366
|
// Feed final_answer argument deltas to the parser for progressive
|
|
340
367
|
// streaming, but only when it appears to be the sole tool call.
|
|
341
|
-
if (
|
|
368
|
+
if (answerStreamer.detected && toolCalls[tc.index].name === "final_answer"
|
|
342
369
|
&& Object.keys(toolCalls).length === 1) {
|
|
343
|
-
|
|
344
|
-
if (text)
|
|
345
|
-
callbacks.onTextChunk(text);
|
|
370
|
+
answerStreamer.processDelta(tc.function.arguments);
|
|
346
371
|
}
|
|
347
372
|
}
|
|
348
373
|
}
|
|
@@ -356,7 +381,7 @@ async function streamChatCompletion(client, createParams, callbacks, signal) {
|
|
|
356
381
|
toolCalls: Object.values(toolCalls),
|
|
357
382
|
outputItems: [],
|
|
358
383
|
usage,
|
|
359
|
-
finalAnswerStreamed:
|
|
384
|
+
finalAnswerStreamed: answerStreamer.streamed,
|
|
360
385
|
};
|
|
361
386
|
}
|
|
362
387
|
async function streamResponsesApi(client, createParams, callbacks, signal) {
|
|
@@ -374,9 +399,8 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
|
|
|
374
399
|
const outputItems = [];
|
|
375
400
|
let currentToolCall = null;
|
|
376
401
|
let usage;
|
|
377
|
-
const
|
|
402
|
+
const answerStreamer = new FinalAnswerStreamer(callbacks);
|
|
378
403
|
let functionCallCount = 0;
|
|
379
|
-
let finalAnswerDetected = false;
|
|
380
404
|
for await (const event of response) {
|
|
381
405
|
if (signal?.aborted)
|
|
382
406
|
break;
|
|
@@ -409,8 +433,7 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
|
|
|
409
433
|
// Only activate when this is the first (and so far only) function call.
|
|
410
434
|
// Mixed calls are rejected by core.ts; no need to stream their args.
|
|
411
435
|
if (String(event.item.name) === "final_answer" && functionCallCount === 1) {
|
|
412
|
-
|
|
413
|
-
callbacks.onClearText?.();
|
|
436
|
+
answerStreamer.activate();
|
|
414
437
|
}
|
|
415
438
|
}
|
|
416
439
|
break;
|
|
@@ -420,11 +443,9 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
|
|
|
420
443
|
currentToolCall.arguments += event.delta;
|
|
421
444
|
// Feed final_answer argument deltas to the parser for progressive
|
|
422
445
|
// streaming, but only when it appears to be the sole function call.
|
|
423
|
-
if (
|
|
446
|
+
if (answerStreamer.detected && currentToolCall.name === "final_answer"
|
|
424
447
|
&& functionCallCount === 1) {
|
|
425
|
-
|
|
426
|
-
if (text)
|
|
427
|
-
callbacks.onTextChunk(text);
|
|
448
|
+
answerStreamer.processDelta(String(event.delta));
|
|
428
449
|
}
|
|
429
450
|
}
|
|
430
451
|
break;
|
|
@@ -464,6 +485,6 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
|
|
|
464
485
|
toolCalls,
|
|
465
486
|
outputItems,
|
|
466
487
|
usage,
|
|
467
|
-
finalAnswerStreamed:
|
|
488
|
+
finalAnswerStreamed: answerStreamer.streamed,
|
|
468
489
|
};
|
|
469
490
|
}
|
|
@@ -5,20 +5,33 @@ const config_1 = require("../heart/config");
|
|
|
5
5
|
const nerves_1 = require("../nerves");
|
|
6
6
|
const runtime_1 = require("./runtime");
|
|
7
7
|
const runtime_2 = require("./runtime");
|
|
8
|
+
const LEVEL_PRIORITY = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
9
|
+
/** Wrap a sink so it only receives events at or above the given level. */
|
|
10
|
+
/* v8 ignore start -- internal filter plumbing, exercised via integration @preserve */
|
|
11
|
+
function filterSink(sink, minLevel) {
|
|
12
|
+
const minPriority = LEVEL_PRIORITY[minLevel] ?? 0;
|
|
13
|
+
return (entry) => {
|
|
14
|
+
if ((LEVEL_PRIORITY[entry.level] ?? 0) >= minPriority)
|
|
15
|
+
sink(entry);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
8
18
|
function resolveCliSinks(sinks) {
|
|
9
19
|
const requested = sinks && sinks.length > 0 ? sinks : ["terminal", "ndjson"];
|
|
10
20
|
return [...new Set(requested)];
|
|
11
21
|
}
|
|
12
22
|
function configureCliRuntimeLogger(_friendId, options = {}) {
|
|
13
23
|
const sinkKinds = resolveCliSinks(options.sinks);
|
|
24
|
+
const level = options.level ?? "info";
|
|
14
25
|
const sinks = sinkKinds.map((sinkKind) => {
|
|
15
26
|
if (sinkKind === "terminal") {
|
|
16
|
-
|
|
27
|
+
// Terminal only shows warnings and errors — INFO is too noisy
|
|
28
|
+
// for an interactive session. Full detail goes to the ndjson file.
|
|
29
|
+
return filterSink((0, nerves_1.createTerminalSink)(), "warn");
|
|
17
30
|
}
|
|
18
31
|
return (0, nerves_1.createNdjsonFileSink)((0, config_1.logPath)("cli", "runtime"));
|
|
19
32
|
});
|
|
20
33
|
const logger = (0, nerves_1.createLogger)({
|
|
21
|
-
level
|
|
34
|
+
level,
|
|
22
35
|
sinks,
|
|
23
36
|
});
|
|
24
37
|
(0, runtime_2.setRuntimeLogger)(logger);
|
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.createDebouncedLines = createDebouncedLines;
|
|
41
42
|
exports.runCliSession = runCliSession;
|
|
42
43
|
exports.main = main;
|
|
43
44
|
const readline = __importStar(require("readline"));
|
|
@@ -71,12 +72,14 @@ class Spinner {
|
|
|
71
72
|
msg = "";
|
|
72
73
|
phrases = null;
|
|
73
74
|
lastPhrase = "";
|
|
75
|
+
stopped = false;
|
|
74
76
|
constructor(m = "working", phrases) {
|
|
75
77
|
this.msg = m;
|
|
76
78
|
if (phrases && phrases.length > 0)
|
|
77
79
|
this.phrases = phrases;
|
|
78
80
|
}
|
|
79
81
|
start() {
|
|
82
|
+
this.stopped = false;
|
|
80
83
|
process.stderr.write("\r\x1b[K");
|
|
81
84
|
this.spin();
|
|
82
85
|
this.iv = setInterval(() => this.spin(), 80);
|
|
@@ -85,15 +88,23 @@ class Spinner {
|
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
90
|
spin() {
|
|
88
|
-
|
|
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}... `);
|
|
89
96
|
this.i = (this.i + 1) % this.frames.length;
|
|
90
97
|
}
|
|
91
98
|
rotatePhrase() {
|
|
99
|
+
/* v8 ignore next -- race guard: timer callback fires after stop() @preserve */
|
|
100
|
+
if (this.stopped)
|
|
101
|
+
return;
|
|
92
102
|
const next = (0, phrases_1.pickPhrase)(this.phrases, this.lastPhrase);
|
|
93
103
|
this.lastPhrase = next;
|
|
94
104
|
this.msg = next;
|
|
95
105
|
}
|
|
96
106
|
stop(ok) {
|
|
107
|
+
this.stopped = true;
|
|
97
108
|
if (this.iv) {
|
|
98
109
|
clearInterval(this.iv);
|
|
99
110
|
this.iv = null;
|
|
@@ -298,12 +309,25 @@ function createCliCallbacks() {
|
|
|
298
309
|
currentSpinner.start();
|
|
299
310
|
},
|
|
300
311
|
onModelStreamStart: () => {
|
|
301
|
-
|
|
302
|
-
|
|
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();
|
|
303
318
|
},
|
|
304
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
|
+
}
|
|
305
327
|
if (hadReasoning) {
|
|
306
|
-
|
|
328
|
+
// Single newline to separate reasoning from reply — reasoning
|
|
329
|
+
// output often ends with its own trailing newline(s)
|
|
330
|
+
process.stdout.write("\n");
|
|
307
331
|
hadReasoning = false;
|
|
308
332
|
}
|
|
309
333
|
const rendered = streamer.push(text);
|
|
@@ -312,6 +336,10 @@ function createCliCallbacks() {
|
|
|
312
336
|
textDirty = text.length > 0 && !text.endsWith("\n");
|
|
313
337
|
},
|
|
314
338
|
onReasoningChunk: (text) => {
|
|
339
|
+
if (currentSpinner) {
|
|
340
|
+
currentSpinner.stop();
|
|
341
|
+
currentSpinner = null;
|
|
342
|
+
}
|
|
315
343
|
hadReasoning = true;
|
|
316
344
|
process.stdout.write(`\x1b[2m${text}\x1b[0m`);
|
|
317
345
|
textDirty = text.length > 0 && !text.endsWith("\n");
|
|
@@ -369,10 +397,50 @@ function createCliCallbacks() {
|
|
|
369
397
|
},
|
|
370
398
|
};
|
|
371
399
|
}
|
|
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;
|
|
407
|
+
}
|
|
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
|
+
}
|
|
433
|
+
}
|
|
434
|
+
yield lines.join("\n");
|
|
435
|
+
}
|
|
436
|
+
}
|
|
372
437
|
async function runCliSession(options) {
|
|
438
|
+
/* v8 ignore start -- integration: runCliSession is interactive, tested via E2E @preserve */
|
|
373
439
|
const pasteDebounceMs = options.pasteDebounceMs ?? 50;
|
|
374
440
|
const registry = (0, commands_1.createCommandRegistry)();
|
|
375
|
-
(
|
|
441
|
+
if (!options.disableCommands) {
|
|
442
|
+
(0, commands_1.registerDefaultCommands)(registry);
|
|
443
|
+
}
|
|
376
444
|
const messages = options.messages
|
|
377
445
|
?? [{ role: "system", content: await (0, prompt_1.buildSystem)("cli") }];
|
|
378
446
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
@@ -381,8 +449,13 @@ async function runCliSession(options) {
|
|
|
381
449
|
const history = [];
|
|
382
450
|
let closed = false;
|
|
383
451
|
rl.on("close", () => { closed = true; });
|
|
384
|
-
|
|
385
|
-
|
|
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
|
+
}
|
|
386
459
|
const cliCallbacks = createCliCallbacks();
|
|
387
460
|
// exitOnToolCall machinery: wrap execTool to detect target tool
|
|
388
461
|
let exitToolResult;
|
|
@@ -394,13 +467,15 @@ async function runCliSession(options) {
|
|
|
394
467
|
if (name === options.exitOnToolCall) {
|
|
395
468
|
exitToolResult = result;
|
|
396
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();
|
|
397
473
|
}
|
|
398
474
|
return result;
|
|
399
475
|
}
|
|
400
476
|
: resolvedExecTool;
|
|
401
477
|
// Resolve toolChoiceRequired: use explicit option if set, else fall back to toggle
|
|
402
478
|
const getEffectiveToolChoiceRequired = () => options.toolChoiceRequired !== undefined ? options.toolChoiceRequired : (0, commands_1.getToolChoiceRequired)();
|
|
403
|
-
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
404
479
|
// Ctrl-C at the input prompt: clear line or warn/exit
|
|
405
480
|
rl.on("SIGINT", () => {
|
|
406
481
|
const rlInt = rl;
|
|
@@ -422,37 +497,7 @@ async function runCliSession(options) {
|
|
|
422
497
|
rl.close();
|
|
423
498
|
}
|
|
424
499
|
});
|
|
425
|
-
|
|
426
|
-
async function* debouncedLines(source) {
|
|
427
|
-
if (pasteDebounceMs <= 0) {
|
|
428
|
-
yield* source;
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
const iter = source[Symbol.asyncIterator]();
|
|
432
|
-
while (true) {
|
|
433
|
-
const first = await iter.next();
|
|
434
|
-
if (first.done)
|
|
435
|
-
break;
|
|
436
|
-
const lines = [first.value];
|
|
437
|
-
let more = true;
|
|
438
|
-
while (more) {
|
|
439
|
-
const raced = await Promise.race([
|
|
440
|
-
iter.next().then((r) => ({ kind: "line", result: r })),
|
|
441
|
-
new Promise((r) => setTimeout(() => r({ kind: "timeout" }), pasteDebounceMs)),
|
|
442
|
-
]);
|
|
443
|
-
if (raced.kind === "timeout") {
|
|
444
|
-
more = false;
|
|
445
|
-
}
|
|
446
|
-
else if (raced.result.done) {
|
|
447
|
-
more = false;
|
|
448
|
-
}
|
|
449
|
-
else {
|
|
450
|
-
lines.push(raced.result.value);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
yield lines.join("\n");
|
|
454
|
-
}
|
|
455
|
-
}
|
|
500
|
+
const debouncedLines = (source) => createDebouncedLines(source, pasteDebounceMs);
|
|
456
501
|
(0, runtime_1.emitNervesEvent)({
|
|
457
502
|
component: "senses",
|
|
458
503
|
event: "senses.cli_session_start",
|
|
@@ -460,6 +505,50 @@ async function runCliSession(options) {
|
|
|
460
505
|
meta: { agentName: options.agentName, hasExitOnToolCall: !!options.exitOnToolCall },
|
|
461
506
|
});
|
|
462
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
|
+
});
|
|
523
|
+
}
|
|
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 });
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (!exitToolFired) {
|
|
550
|
+
process.stdout.write("\x1b[36m> \x1b[0m");
|
|
551
|
+
}
|
|
463
552
|
try {
|
|
464
553
|
for await (const input of debouncedLines(rl)) {
|
|
465
554
|
if (closed)
|
|
@@ -521,7 +610,7 @@ async function runCliSession(options) {
|
|
|
521
610
|
ctrl.suppress(() => currentAbort.abort());
|
|
522
611
|
let result;
|
|
523
612
|
try {
|
|
524
|
-
result = await (0, core_1.runAgent)(messages, cliCallbacks, "cli", currentAbort.signal, {
|
|
613
|
+
result = await (0, core_1.runAgent)(messages, cliCallbacks, options.skipSystemPromptRefresh ? undefined : "cli", currentAbort.signal, {
|
|
525
614
|
toolChoiceRequired: getEffectiveToolChoiceRequired(),
|
|
526
615
|
traceId,
|
|
527
616
|
tools: options.tools,
|
|
@@ -529,8 +618,12 @@ async function runCliSession(options) {
|
|
|
529
618
|
toolContext: options.toolContext,
|
|
530
619
|
});
|
|
531
620
|
}
|
|
532
|
-
catch {
|
|
533
|
-
// AbortError -- silently return to prompt
|
|
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
|
+
}
|
|
534
627
|
}
|
|
535
628
|
cliCallbacks.flushMarkdown();
|
|
536
629
|
ctrl.restore();
|
|
@@ -557,9 +650,12 @@ async function runCliSession(options) {
|
|
|
557
650
|
}
|
|
558
651
|
finally {
|
|
559
652
|
rl.close();
|
|
560
|
-
|
|
561
|
-
|
|
653
|
+
if (options.banner !== false) {
|
|
654
|
+
// eslint-disable-next-line no-console -- terminal UX: goodbye
|
|
655
|
+
console.log("bye");
|
|
656
|
+
}
|
|
562
657
|
}
|
|
658
|
+
/* v8 ignore stop */
|
|
563
659
|
return { exitReason, toolResult: exitToolResult };
|
|
564
660
|
}
|
|
565
661
|
async function main(agentName, options) {
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.19",
|
|
4
4
|
"main": "dist/heart/daemon/ouro-entry.js",
|
|
5
5
|
"bin": {
|
|
6
|
+
"cli": "dist/heart/daemon/ouro-bot-entry.js",
|
|
6
7
|
"ouro": "dist/heart/daemon/ouro-entry.js",
|
|
7
8
|
"ouro.bot": "dist/heart/daemon/ouro-bot-entry.js"
|
|
8
9
|
},
|