@ouro.bot/cli 0.1.0-alpha.1 → 0.1.0-alpha.11

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.
Files changed (48) hide show
  1. package/AdoptionSpecialist.ouro/agent.json +70 -9
  2. package/AdoptionSpecialist.ouro/psyche/SOUL.md +4 -1
  3. package/dist/heart/config.js +34 -0
  4. package/dist/heart/core.js +41 -2
  5. package/dist/heart/daemon/daemon-cli.js +293 -46
  6. package/dist/heart/daemon/daemon.js +3 -0
  7. package/dist/heart/daemon/hatch-animation.js +28 -0
  8. package/dist/heart/daemon/hatch-flow.js +3 -1
  9. package/dist/heart/daemon/hatch-specialist.js +6 -1
  10. package/dist/heart/daemon/log-tailer.js +146 -0
  11. package/dist/heart/daemon/os-cron.js +260 -0
  12. package/dist/heart/daemon/ouro-bot-entry.js +0 -0
  13. package/dist/heart/daemon/ouro-bot-wrapper.js +4 -3
  14. package/dist/heart/daemon/ouro-entry.js +0 -0
  15. package/dist/heart/daemon/ouro-path-installer.js +161 -0
  16. package/dist/heart/daemon/process-manager.js +18 -1
  17. package/dist/heart/daemon/runtime-logging.js +9 -5
  18. package/dist/heart/daemon/specialist-orchestrator.js +186 -0
  19. package/dist/heart/daemon/specialist-prompt.js +61 -0
  20. package/dist/heart/daemon/specialist-session.js +177 -0
  21. package/dist/heart/daemon/specialist-tools.js +132 -0
  22. package/dist/heart/daemon/task-scheduler.js +4 -1
  23. package/dist/heart/identity.js +28 -3
  24. package/dist/heart/providers/anthropic.js +3 -0
  25. package/dist/heart/streaming.js +3 -0
  26. package/dist/mind/associative-recall.js +23 -2
  27. package/dist/mind/context.js +85 -1
  28. package/dist/mind/friends/channel.js +8 -0
  29. package/dist/mind/friends/types.js +1 -1
  30. package/dist/mind/memory.js +62 -0
  31. package/dist/mind/pending.js +93 -0
  32. package/dist/mind/prompt-refresh.js +20 -0
  33. package/dist/mind/prompt.js +101 -0
  34. package/dist/nerves/coverage/file-completeness.js +14 -4
  35. package/dist/repertoire/tools-base.js +92 -0
  36. package/dist/repertoire/tools.js +3 -3
  37. package/dist/senses/bluebubbles-client.js +279 -0
  38. package/dist/senses/bluebubbles-entry.js +11 -0
  39. package/dist/senses/bluebubbles-model.js +253 -0
  40. package/dist/senses/bluebubbles-mutation-log.js +76 -0
  41. package/dist/senses/bluebubbles.js +332 -0
  42. package/dist/senses/cli.js +89 -8
  43. package/dist/senses/inner-dialog.js +15 -0
  44. package/dist/senses/session-lock.js +119 -0
  45. package/dist/senses/teams.js +1 -0
  46. package/package.json +4 -3
  47. package/subagents/README.md +3 -1
  48. package/subagents/work-merger.md +33 -2
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.installOuroCommand = installOuroCommand;
37
+ const fs = __importStar(require("fs"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ const runtime_1 = require("../../nerves/runtime");
41
+ const WRAPPER_SCRIPT = `#!/bin/sh
42
+ exec npx --yes @ouro.bot/cli "$@"
43
+ `;
44
+ function detectShellProfile(homeDir, shell) {
45
+ if (!shell)
46
+ return null;
47
+ const base = path.basename(shell);
48
+ if (base === "zsh")
49
+ return path.join(homeDir, ".zshrc");
50
+ if (base === "bash") {
51
+ // macOS uses .bash_profile, Linux uses .bashrc
52
+ const profilePath = path.join(homeDir, ".bash_profile");
53
+ return profilePath;
54
+ }
55
+ if (base === "fish")
56
+ return path.join(homeDir, ".config", "fish", "config.fish");
57
+ return null;
58
+ }
59
+ function isBinDirInPath(binDir, envPath) {
60
+ return envPath.split(path.delimiter).some((p) => p === binDir);
61
+ }
62
+ function buildPathExportLine(binDir, shell) {
63
+ const base = shell ? path.basename(shell) : /* v8 ignore next -- unreachable: only called when detectShellProfile returns non-null, which requires shell @preserve */ "";
64
+ if (base === "fish") {
65
+ return `\n# Added by ouro\nset -gx PATH ${binDir} $PATH\n`;
66
+ }
67
+ return `\n# Added by ouro\nexport PATH="${binDir}:$PATH"\n`;
68
+ }
69
+ function installOuroCommand(deps = {}) {
70
+ /* v8 ignore start -- dep defaults: only used in real runtime, tests always inject @preserve */
71
+ const platform = deps.platform ?? process.platform;
72
+ const homeDir = deps.homeDir ?? os.homedir();
73
+ const existsSync = deps.existsSync ?? fs.existsSync;
74
+ const mkdirSync = deps.mkdirSync ?? fs.mkdirSync;
75
+ const writeFileSync = deps.writeFileSync ?? fs.writeFileSync;
76
+ const readFileSync = deps.readFileSync ?? ((p, enc) => fs.readFileSync(p, enc));
77
+ const appendFileSync = deps.appendFileSync ?? fs.appendFileSync;
78
+ const chmodSync = deps.chmodSync ?? fs.chmodSync;
79
+ const envPath = deps.envPath ?? process.env.PATH ?? "";
80
+ const shell = deps.shell ?? process.env.SHELL;
81
+ /* v8 ignore stop */
82
+ if (platform === "win32") {
83
+ (0, runtime_1.emitNervesEvent)({
84
+ component: "daemon",
85
+ event: "daemon.ouro_path_install_skip",
86
+ message: "skipped ouro PATH install on Windows",
87
+ meta: { platform },
88
+ });
89
+ return { installed: false, scriptPath: null, pathReady: false, shellProfileUpdated: null, skippedReason: "windows" };
90
+ }
91
+ const binDir = path.join(homeDir, ".local", "bin");
92
+ const scriptPath = path.join(binDir, "ouro");
93
+ (0, runtime_1.emitNervesEvent)({
94
+ component: "daemon",
95
+ event: "daemon.ouro_path_install_start",
96
+ message: "installing ouro command to PATH",
97
+ meta: { scriptPath, binDir },
98
+ });
99
+ // If ouro already exists somewhere in PATH, skip
100
+ if (existsSync(scriptPath)) {
101
+ (0, runtime_1.emitNervesEvent)({
102
+ component: "daemon",
103
+ event: "daemon.ouro_path_install_skip",
104
+ message: "ouro command already installed",
105
+ meta: { scriptPath },
106
+ });
107
+ return { installed: false, scriptPath, pathReady: isBinDirInPath(binDir, envPath), shellProfileUpdated: null, skippedReason: "already-installed" };
108
+ }
109
+ try {
110
+ mkdirSync(binDir, { recursive: true });
111
+ writeFileSync(scriptPath, WRAPPER_SCRIPT, { mode: 0o755 });
112
+ chmodSync(scriptPath, 0o755);
113
+ }
114
+ catch (error) {
115
+ (0, runtime_1.emitNervesEvent)({
116
+ level: "warn",
117
+ component: "daemon",
118
+ event: "daemon.ouro_path_install_error",
119
+ message: "failed to install ouro command",
120
+ meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
121
+ });
122
+ return { installed: false, scriptPath: null, pathReady: false, shellProfileUpdated: null, skippedReason: error instanceof Error ? error.message : /* v8 ignore next -- defensive @preserve */ String(error) };
123
+ }
124
+ // Check if ~/.local/bin is already in PATH
125
+ let shellProfileUpdated = null;
126
+ const pathReady = isBinDirInPath(binDir, envPath);
127
+ if (!pathReady) {
128
+ const profilePath = detectShellProfile(homeDir, shell);
129
+ if (profilePath) {
130
+ try {
131
+ let existing = "";
132
+ try {
133
+ existing = readFileSync(profilePath, "utf-8");
134
+ }
135
+ catch {
136
+ // Profile doesn't exist yet — that's fine, we'll create it
137
+ }
138
+ if (!existing.includes(binDir)) {
139
+ appendFileSync(profilePath, buildPathExportLine(binDir, shell));
140
+ shellProfileUpdated = profilePath;
141
+ }
142
+ }
143
+ catch (error) {
144
+ (0, runtime_1.emitNervesEvent)({
145
+ level: "warn",
146
+ component: "daemon",
147
+ event: "daemon.ouro_path_profile_error",
148
+ message: "failed to update shell profile for PATH",
149
+ meta: { profilePath, error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
150
+ });
151
+ }
152
+ }
153
+ }
154
+ (0, runtime_1.emitNervesEvent)({
155
+ component: "daemon",
156
+ event: "daemon.ouro_path_install_end",
157
+ message: "ouro command installed",
158
+ meta: { scriptPath, pathReady, shellProfileUpdated },
159
+ });
160
+ return { installed: true, scriptPath, pathReady, shellProfileUpdated };
161
+ }
@@ -100,7 +100,7 @@ class DaemonProcessManager {
100
100
  const child = this.spawnFn("node", args, {
101
101
  cwd: runCwd,
102
102
  env: state.config.env ? { ...process.env, ...state.config.env } : process.env,
103
- stdio: ["ignore", "ignore", "ignore"],
103
+ stdio: ["ignore", "ignore", "ignore", "ipc"],
104
104
  });
105
105
  state.process = child;
106
106
  state.snapshot.status = "running";
@@ -151,6 +151,23 @@ class DaemonProcessManager {
151
151
  await this.stopAgent(state.config.name);
152
152
  }
153
153
  }
154
+ sendToAgent(agent, message) {
155
+ const state = this.requireAgent(agent);
156
+ if (!state.process)
157
+ return;
158
+ try {
159
+ state.process.send(message);
160
+ }
161
+ catch {
162
+ (0, runtime_1.emitNervesEvent)({
163
+ level: "warn",
164
+ component: "daemon",
165
+ event: "daemon.agent_ipc_send_error",
166
+ message: "failed to send IPC message to managed agent",
167
+ meta: { agent },
168
+ });
169
+ }
170
+ }
154
171
  getAgentSnapshot(agent) {
155
172
  return this.agents.get(agent)?.snapshot;
156
173
  }
@@ -43,23 +43,27 @@ const DEFAULT_RUNTIME_LOGGING = {
43
43
  level: "info",
44
44
  sinks: ["terminal", "ndjson"],
45
45
  };
46
+ function defaultLevelForProcess(processName) {
47
+ return processName === "daemon" ? "info" : "warn";
48
+ }
46
49
  function isLogLevel(value) {
47
50
  return value === "debug" || value === "info" || value === "warn" || value === "error";
48
51
  }
49
- function resolveRuntimeLoggingConfig(configPath) {
52
+ function resolveRuntimeLoggingConfig(configPath, processName) {
53
+ const defaultLevel = defaultLevelForProcess(processName);
50
54
  let parsed = null;
51
55
  try {
52
56
  const raw = fs.readFileSync(configPath, "utf-8");
53
57
  parsed = JSON.parse(raw);
54
58
  }
55
59
  catch {
56
- return { ...DEFAULT_RUNTIME_LOGGING };
60
+ return { ...DEFAULT_RUNTIME_LOGGING, level: defaultLevel };
57
61
  }
58
62
  if (!parsed || typeof parsed !== "object") {
59
- return { ...DEFAULT_RUNTIME_LOGGING };
63
+ return { ...DEFAULT_RUNTIME_LOGGING, level: defaultLevel };
60
64
  }
61
65
  const candidate = parsed;
62
- const level = isLogLevel(candidate.level) ? candidate.level : DEFAULT_RUNTIME_LOGGING.level;
66
+ const level = isLogLevel(candidate.level) ? candidate.level : defaultLevel;
63
67
  const sinks = Array.isArray(candidate.sinks)
64
68
  ? candidate.sinks.filter((entry) => entry === "terminal" || entry === "ndjson")
65
69
  : DEFAULT_RUNTIME_LOGGING.sinks;
@@ -71,7 +75,7 @@ function resolveRuntimeLoggingConfig(configPath) {
71
75
  function configureDaemonRuntimeLogger(processName, options = {}) {
72
76
  const homeDir = options.homeDir ?? os.homedir();
73
77
  const configPath = options.configPath ?? path.join(homeDir, ".agentstate", "daemon", "logging.json");
74
- const config = resolveRuntimeLoggingConfig(configPath);
78
+ const config = resolveRuntimeLoggingConfig(configPath, processName);
75
79
  const sinks = config.sinks.map((sinkName) => {
76
80
  if (sinkName === "terminal") {
77
81
  return (0, nerves_1.createTerminalSink)();
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.runAdoptionSpecialist = runAdoptionSpecialist;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const runtime_1 = require("../../nerves/runtime");
40
+ const identity_1 = require("../identity");
41
+ const config_1 = require("../config");
42
+ const core_1 = require("../core");
43
+ const hatch_flow_1 = require("./hatch-flow");
44
+ const specialist_prompt_1 = require("./specialist-prompt");
45
+ const specialist_tools_1 = require("./specialist-tools");
46
+ const specialist_session_1 = require("./specialist-session");
47
+ function listExistingBundles(bundlesRoot) {
48
+ let entries;
49
+ try {
50
+ entries = fs.readdirSync(bundlesRoot, { withFileTypes: true });
51
+ }
52
+ catch {
53
+ return [];
54
+ }
55
+ const discovered = [];
56
+ for (const entry of entries) {
57
+ if (!entry.isDirectory() || !entry.name.endsWith(".ouro"))
58
+ continue;
59
+ const agentName = entry.name.slice(0, -5);
60
+ discovered.push(agentName);
61
+ }
62
+ return discovered.sort((a, b) => a.localeCompare(b));
63
+ }
64
+ function loadIdentityPhrases(bundleSourceDir, identityFileName) {
65
+ const agentJsonPath = path.join(bundleSourceDir, "agent.json");
66
+ try {
67
+ const raw = fs.readFileSync(agentJsonPath, "utf-8");
68
+ const parsed = JSON.parse(raw);
69
+ const identityKey = identityFileName.replace(/\.md$/, "");
70
+ const identity = parsed.identityPhrases?.[identityKey];
71
+ if (identity?.thinking?.length && identity?.tool?.length && identity?.followup?.length) {
72
+ return identity;
73
+ }
74
+ if (parsed.phrases?.thinking?.length && parsed.phrases?.tool?.length && parsed.phrases?.followup?.length) {
75
+ return parsed.phrases;
76
+ }
77
+ }
78
+ catch {
79
+ // agent.json missing or malformed — fall through
80
+ }
81
+ return { ...identity_1.DEFAULT_AGENT_PHRASES };
82
+ }
83
+ function pickRandomIdentity(identitiesDir, random) {
84
+ const files = fs.readdirSync(identitiesDir).filter((f) => f.endsWith(".md"));
85
+ if (files.length === 0) {
86
+ return { fileName: "default", content: "I am the adoption specialist." };
87
+ }
88
+ const idx = Math.floor(random() * files.length);
89
+ const fileName = files[idx];
90
+ const content = fs.readFileSync(path.join(identitiesDir, fileName), "utf-8");
91
+ return { fileName, content };
92
+ }
93
+ /**
94
+ * Run the full adoption specialist flow:
95
+ * 1. Pick a random identity from the bundled AdoptionSpecialist.ouro
96
+ * 2. Read SOUL.md
97
+ * 3. List existing bundles
98
+ * 4. Build system prompt
99
+ * 5. Set up provider (setAgentName, setAgentConfigOverride, writeSecretsFile, reset caches)
100
+ * 6. Run the specialist session
101
+ * 7. Clean up identity/config overrides
102
+ * 8. Return hatchling name
103
+ */
104
+ async function runAdoptionSpecialist(deps) {
105
+ const { bundleSourceDir, bundlesRoot, secretsRoot, provider, credentials, humanName, callbacks, signal } = deps;
106
+ const random = deps.random ?? Math.random;
107
+ (0, runtime_1.emitNervesEvent)({
108
+ component: "daemon",
109
+ event: "daemon.specialist_orchestrator_start",
110
+ message: "starting adoption specialist orchestrator",
111
+ meta: { provider, bundleSourceDir },
112
+ });
113
+ // 1. Read SOUL.md
114
+ const soulPath = path.join(bundleSourceDir, "psyche", "SOUL.md");
115
+ let soulText = "";
116
+ try {
117
+ soulText = fs.readFileSync(soulPath, "utf-8");
118
+ }
119
+ catch {
120
+ // No SOUL.md -- proceed without it
121
+ }
122
+ // 2. Pick random identity
123
+ const identitiesDir = path.join(bundleSourceDir, "psyche", "identities");
124
+ const identity = pickRandomIdentity(identitiesDir, random);
125
+ (0, runtime_1.emitNervesEvent)({
126
+ component: "daemon",
127
+ event: "daemon.specialist_identity_picked",
128
+ message: "picked specialist identity",
129
+ meta: { identity: identity.fileName },
130
+ });
131
+ // 3. List existing bundles
132
+ const existingBundles = listExistingBundles(bundlesRoot);
133
+ // 4. Build system prompt
134
+ const systemPrompt = (0, specialist_prompt_1.buildSpecialistSystemPrompt)(soulText, identity.content, existingBundles);
135
+ // 5. Set up provider with identity-specific phrases
136
+ const phrases = loadIdentityPhrases(bundleSourceDir, identity.fileName);
137
+ (0, identity_1.setAgentName)("AdoptionSpecialist");
138
+ (0, identity_1.setAgentConfigOverride)({
139
+ version: 1,
140
+ enabled: true,
141
+ provider,
142
+ phrases,
143
+ });
144
+ (0, hatch_flow_1.writeSecretsFile)("AdoptionSpecialist", provider, credentials, secretsRoot);
145
+ (0, config_1.resetConfigCache)();
146
+ (0, core_1.resetProviderRuntime)();
147
+ try {
148
+ // Create provider runtime
149
+ const providerRuntime = (0, core_1.createProviderRegistry)().resolve();
150
+ if (!providerRuntime) {
151
+ throw new Error("Failed to create provider runtime for adoption specialist");
152
+ }
153
+ // 6. Run session
154
+ const tools = (0, specialist_tools_1.getSpecialistTools)();
155
+ const readline = deps.createReadline();
156
+ const ctrl = readline.inputController;
157
+ const result = await (0, specialist_session_1.runSpecialistSession)({
158
+ providerRuntime,
159
+ systemPrompt,
160
+ tools,
161
+ execTool: (name, args) => (0, specialist_tools_1.execSpecialistTool)(name, args, {
162
+ humanName,
163
+ provider,
164
+ credentials,
165
+ bundlesRoot,
166
+ secretsRoot,
167
+ specialistIdentitiesDir: identitiesDir,
168
+ }),
169
+ readline,
170
+ callbacks,
171
+ signal,
172
+ kickoffMessage: "hi, i just ran ouro for the first time",
173
+ suppressInput: ctrl ? (onInterrupt) => ctrl.suppress(onInterrupt) : undefined,
174
+ restoreInput: ctrl ? () => ctrl.restore() : undefined,
175
+ flushMarkdown: callbacks.flushMarkdown,
176
+ writePrompt: ctrl ? () => process.stdout.write("\x1b[36m> \x1b[0m") : undefined,
177
+ });
178
+ return result.hatchedAgentName;
179
+ }
180
+ finally {
181
+ // 7. Cleanup: restore identity/config state
182
+ (0, identity_1.setAgentConfigOverride)(null);
183
+ (0, config_1.resetConfigCache)();
184
+ (0, core_1.resetProviderRuntime)();
185
+ }
186
+ }
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildSpecialistSystemPrompt = buildSpecialistSystemPrompt;
4
+ const runtime_1 = require("../../nerves/runtime");
5
+ /**
6
+ * Build the adoption specialist's system prompt from its components.
7
+ * The prompt is written in first person (the specialist's own voice).
8
+ */
9
+ function buildSpecialistSystemPrompt(soulText, identityText, existingBundles) {
10
+ (0, runtime_1.emitNervesEvent)({
11
+ component: "daemon",
12
+ event: "daemon.specialist_prompt_build",
13
+ message: "building specialist system prompt",
14
+ meta: { bundleCount: existingBundles.length },
15
+ });
16
+ const sections = [];
17
+ if (soulText) {
18
+ sections.push(soulText);
19
+ }
20
+ if (identityText) {
21
+ sections.push(identityText);
22
+ }
23
+ if (existingBundles.length > 0) {
24
+ sections.push(`## Existing agents\nThe human already has these agents: ${existingBundles.join(", ")}.`);
25
+ }
26
+ else {
27
+ sections.push("## Existing agents\nThe human has no agents yet. This will be their first hatchling.");
28
+ }
29
+ sections.push([
30
+ "## Who I am",
31
+ "I am one of thirteen adoption specialists. The system randomly selected me for this session.",
32
+ "Most humans only go through adoption once, so this is likely the only time they'll meet me.",
33
+ "I make this encounter count — warm, memorable, and uniquely mine.",
34
+ "",
35
+ "## Voice rules",
36
+ "IMPORTANT: I keep every response to 1-3 short sentences. I sound like a friend texting, not a manual.",
37
+ "I NEVER use headers, bullet lists, numbered lists, or markdown formatting.",
38
+ "I ask ONE question at a time. I do not dump multiple questions or options.",
39
+ "I am warm but brief. Every word earns its place.",
40
+ "",
41
+ "## Conversation flow",
42
+ "The human just connected. I speak first — I greet them warmly and introduce myself in my own voice.",
43
+ "I briefly mention that I'm one of several adoption specialists and they got me today.",
44
+ "I ask their name.",
45
+ "Then I ask what they'd like their agent to help with — one question at a time.",
46
+ "I'm proactive: I suggest ideas and guide them. If they seem unsure, I offer a concrete suggestion.",
47
+ "I don't wait for the human to figure things out — I explain simply what an agent is if needed.",
48
+ "When I have enough context, I suggest a name for the hatchling and confirm with the human.",
49
+ "Then I call `hatch_agent` with the agent name and the human's name.",
50
+ "",
51
+ "## Tools",
52
+ "I have these tools available:",
53
+ "- `hatch_agent`: Create a new agent bundle. I call this with `name` (the agent name, PascalCase) and `humanName` (what the human told me their name is).",
54
+ "- `final_answer`: End the conversation with a final message to the human. I call this when the adoption process is complete.",
55
+ "- `read_file`: Read a file from disk. Useful for reviewing existing agent bundles or migration sources.",
56
+ "- `list_directory`: List directory contents. Useful for exploring existing agent bundles.",
57
+ "",
58
+ "I must call `final_answer` when I am done to end the session cleanly.",
59
+ ].join("\n"));
60
+ return sections.join("\n\n");
61
+ }
@@ -0,0 +1,177 @@
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
+ }