@ouro.bot/cli 0.1.0-alpha.3 → 0.1.0-alpha.30

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 (71) hide show
  1. package/AdoptionSpecialist.ouro/agent.json +70 -9
  2. package/AdoptionSpecialist.ouro/psyche/SOUL.md +5 -2
  3. package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
  4. package/AdoptionSpecialist.ouro/psyche/identities/python.md +30 -0
  5. package/assets/ouroboros.png +0 -0
  6. package/changelog.json +80 -0
  7. package/dist/heart/config.js +66 -4
  8. package/dist/heart/core.js +75 -2
  9. package/dist/heart/daemon/agent-discovery.js +81 -0
  10. package/dist/heart/daemon/daemon-cli.js +562 -64
  11. package/dist/heart/daemon/daemon-entry.js +14 -5
  12. package/dist/heart/daemon/daemon-runtime-sync.js +90 -0
  13. package/dist/heart/daemon/daemon.js +87 -9
  14. package/dist/heart/daemon/hatch-animation.js +35 -0
  15. package/dist/heart/daemon/hatch-flow.js +2 -11
  16. package/dist/heart/daemon/hatch-specialist.js +6 -1
  17. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  18. package/dist/heart/daemon/launchd.js +134 -0
  19. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  20. package/dist/heart/daemon/ouro-bot-wrapper.js +4 -3
  21. package/dist/heart/daemon/ouro-path-installer.js +178 -0
  22. package/dist/heart/daemon/ouro-uti.js +11 -2
  23. package/dist/heart/daemon/process-manager.js +1 -1
  24. package/dist/heart/daemon/run-hooks.js +37 -0
  25. package/dist/heart/daemon/runtime-logging.js +9 -5
  26. package/dist/heart/daemon/runtime-metadata.js +118 -0
  27. package/dist/heart/daemon/sense-manager.js +266 -0
  28. package/dist/heart/daemon/specialist-orchestrator.js +129 -0
  29. package/dist/heart/daemon/specialist-prompt.js +98 -0
  30. package/dist/heart/daemon/specialist-tools.js +237 -0
  31. package/dist/heart/daemon/staged-restart.js +114 -0
  32. package/dist/heart/daemon/subagent-installer.js +10 -1
  33. package/dist/heart/daemon/update-checker.js +103 -0
  34. package/dist/heart/daemon/update-hooks.js +138 -0
  35. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  36. package/dist/heart/identity.js +85 -1
  37. package/dist/heart/providers/anthropic.js +19 -2
  38. package/dist/heart/sense-truth.js +61 -0
  39. package/dist/heart/streaming.js +99 -21
  40. package/dist/mind/bundle-manifest.js +69 -0
  41. package/dist/mind/first-impressions.js +2 -1
  42. package/dist/mind/friends/channel.js +8 -0
  43. package/dist/mind/friends/types.js +1 -1
  44. package/dist/mind/phrases.js +1 -0
  45. package/dist/mind/prompt.js +94 -3
  46. package/dist/nerves/cli-logging.js +15 -2
  47. package/dist/repertoire/ado-client.js +4 -2
  48. package/dist/repertoire/coding/feedback.js +134 -0
  49. package/dist/repertoire/coding/index.js +4 -1
  50. package/dist/repertoire/coding/manager.js +61 -2
  51. package/dist/repertoire/coding/spawner.js +3 -3
  52. package/dist/repertoire/coding/tools.js +41 -2
  53. package/dist/repertoire/data/ado-endpoints.json +188 -0
  54. package/dist/repertoire/tools-base.js +69 -5
  55. package/dist/repertoire/tools-teams.js +57 -4
  56. package/dist/repertoire/tools.js +44 -11
  57. package/dist/senses/bluebubbles-client.js +434 -0
  58. package/dist/senses/bluebubbles-entry.js +11 -0
  59. package/dist/senses/bluebubbles-media.js +338 -0
  60. package/dist/senses/bluebubbles-model.js +251 -0
  61. package/dist/senses/bluebubbles-mutation-log.js +76 -0
  62. package/dist/senses/bluebubbles-session-cleanup.js +73 -0
  63. package/dist/senses/bluebubbles.js +449 -0
  64. package/dist/senses/cli.js +299 -133
  65. package/dist/senses/debug-activity.js +108 -0
  66. package/dist/senses/teams.js +173 -54
  67. package/package.json +15 -6
  68. package/subagents/work-doer.md +26 -24
  69. package/subagents/work-merger.md +24 -30
  70. package/subagents/work-planner.md +34 -25
  71. package/dist/inner-worker-entry.js +0 -4
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assessWrapperPublishSync = assessWrapperPublishSync;
4
+ const runtime_1 = require("../../nerves/runtime");
5
+ function wrapperPackageChanged(changedFiles) {
6
+ return changedFiles.some((file) => file.startsWith("packages/ouro.bot/"));
7
+ }
8
+ function assessWrapperPublishSync(input) {
9
+ let result;
10
+ if (input.localVersion !== input.cliVersion) {
11
+ result = {
12
+ ok: false,
13
+ message: `ouro.bot wrapper version ${input.localVersion} must match @ouro.bot/cli version ${input.cliVersion}`,
14
+ };
15
+ (0, runtime_1.emitNervesEvent)({
16
+ level: "warn",
17
+ component: "daemon",
18
+ event: "daemon.wrapper_publish_guard_checked",
19
+ message: "evaluated wrapper publish sync",
20
+ meta: {
21
+ changed: wrapperPackageChanged(input.changedFiles),
22
+ localVersion: input.localVersion,
23
+ cliVersion: input.cliVersion,
24
+ publishedVersion: input.publishedVersion,
25
+ ok: result.ok,
26
+ },
27
+ });
28
+ return result;
29
+ }
30
+ if (!wrapperPackageChanged(input.changedFiles)) {
31
+ result = {
32
+ ok: true,
33
+ message: "wrapper package unchanged",
34
+ };
35
+ (0, runtime_1.emitNervesEvent)({
36
+ component: "daemon",
37
+ event: "daemon.wrapper_publish_guard_checked",
38
+ message: "evaluated wrapper publish sync",
39
+ meta: {
40
+ changed: false,
41
+ localVersion: input.localVersion,
42
+ cliVersion: input.cliVersion,
43
+ publishedVersion: input.publishedVersion,
44
+ ok: result.ok,
45
+ },
46
+ });
47
+ return result;
48
+ }
49
+ if (input.publishedVersion === input.localVersion) {
50
+ result = {
51
+ ok: false,
52
+ message: `ouro.bot wrapper changed but ouro.bot@${input.localVersion} is already published; bump packages/ouro.bot/package.json before merging`,
53
+ };
54
+ (0, runtime_1.emitNervesEvent)({
55
+ level: "warn",
56
+ component: "daemon",
57
+ event: "daemon.wrapper_publish_guard_checked",
58
+ message: "evaluated wrapper publish sync",
59
+ meta: {
60
+ changed: true,
61
+ localVersion: input.localVersion,
62
+ cliVersion: input.cliVersion,
63
+ publishedVersion: input.publishedVersion,
64
+ ok: result.ok,
65
+ },
66
+ });
67
+ return result;
68
+ }
69
+ result = {
70
+ ok: true,
71
+ message: "wrapper package changed and local wrapper version is unpublished",
72
+ };
73
+ (0, runtime_1.emitNervesEvent)({
74
+ component: "daemon",
75
+ event: "daemon.wrapper_publish_guard_checked",
76
+ message: "evaluated wrapper publish sync",
77
+ meta: {
78
+ changed: true,
79
+ localVersion: input.localVersion,
80
+ cliVersion: input.cliVersion,
81
+ publishedVersion: input.publishedVersion,
82
+ ok: result.ok,
83
+ },
84
+ });
85
+ return result;
86
+ }
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.DEFAULT_AGENT_PHRASES = exports.DEFAULT_AGENT_CONTEXT = void 0;
36
+ exports.DEFAULT_AGENT_SENSES = exports.DEFAULT_AGENT_PHRASES = exports.DEFAULT_AGENT_CONTEXT = void 0;
37
37
  exports.buildDefaultAgentTemplate = buildDefaultAgentTemplate;
38
38
  exports.getAgentName = getAgentName;
39
39
  exports.getRepoRoot = getRepoRoot;
@@ -42,6 +42,8 @@ exports.getAgentRoot = getAgentRoot;
42
42
  exports.getAgentSecretsPath = getAgentSecretsPath;
43
43
  exports.loadAgentConfig = loadAgentConfig;
44
44
  exports.setAgentName = setAgentName;
45
+ exports.setAgentConfigOverride = setAgentConfigOverride;
46
+ exports.resetAgentConfigCache = resetAgentConfigCache;
45
47
  exports.resetIdentity = resetIdentity;
46
48
  const fs = __importStar(require("fs"));
47
49
  const os = __importStar(require("os"));
@@ -56,12 +58,73 @@ exports.DEFAULT_AGENT_PHRASES = {
56
58
  tool: ["running tool"],
57
59
  followup: ["processing"],
58
60
  };
61
+ exports.DEFAULT_AGENT_SENSES = {
62
+ cli: { enabled: true },
63
+ teams: { enabled: false },
64
+ bluebubbles: { enabled: false },
65
+ };
66
+ function normalizeSenses(value, configFile) {
67
+ const defaults = {
68
+ cli: { ...exports.DEFAULT_AGENT_SENSES.cli },
69
+ teams: { ...exports.DEFAULT_AGENT_SENSES.teams },
70
+ bluebubbles: { ...exports.DEFAULT_AGENT_SENSES.bluebubbles },
71
+ };
72
+ if (value === undefined) {
73
+ return defaults;
74
+ }
75
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
76
+ (0, runtime_1.emitNervesEvent)({
77
+ level: "error",
78
+ event: "config_identity.error",
79
+ component: "config/identity",
80
+ message: "agent config has invalid senses block",
81
+ meta: { path: configFile },
82
+ });
83
+ throw new Error(`agent.json at ${configFile} must include senses as an object when present.`);
84
+ }
85
+ const raw = value;
86
+ const senseNames = ["cli", "teams", "bluebubbles"];
87
+ for (const senseName of senseNames) {
88
+ const rawSense = raw[senseName];
89
+ if (rawSense === undefined) {
90
+ continue;
91
+ }
92
+ if (!rawSense || typeof rawSense !== "object" || Array.isArray(rawSense)) {
93
+ (0, runtime_1.emitNervesEvent)({
94
+ level: "error",
95
+ event: "config_identity.error",
96
+ component: "config/identity",
97
+ message: "agent config has invalid sense config",
98
+ meta: { path: configFile, sense: senseName },
99
+ });
100
+ throw new Error(`agent.json at ${configFile} has invalid senses.${senseName} config.`);
101
+ }
102
+ const enabled = rawSense.enabled;
103
+ if (typeof enabled !== "boolean") {
104
+ (0, runtime_1.emitNervesEvent)({
105
+ level: "error",
106
+ event: "config_identity.error",
107
+ component: "config/identity",
108
+ message: "agent config has invalid sense enabled flag",
109
+ meta: { path: configFile, sense: senseName, enabled: enabled ?? null },
110
+ });
111
+ throw new Error(`agent.json at ${configFile} must include senses.${senseName}.enabled as boolean.`);
112
+ }
113
+ defaults[senseName] = { enabled };
114
+ }
115
+ return defaults;
116
+ }
59
117
  function buildDefaultAgentTemplate(_agentName) {
60
118
  return {
61
119
  version: 1,
62
120
  enabled: true,
63
121
  provider: "anthropic",
64
122
  context: { ...exports.DEFAULT_AGENT_CONTEXT },
123
+ senses: {
124
+ cli: { ...exports.DEFAULT_AGENT_SENSES.cli },
125
+ teams: { ...exports.DEFAULT_AGENT_SENSES.teams },
126
+ bluebubbles: { ...exports.DEFAULT_AGENT_SENSES.bluebubbles },
127
+ },
65
128
  phrases: {
66
129
  thinking: [...exports.DEFAULT_AGENT_PHRASES.thinking],
67
130
  tool: [...exports.DEFAULT_AGENT_PHRASES.tool],
@@ -71,6 +134,7 @@ function buildDefaultAgentTemplate(_agentName) {
71
134
  }
72
135
  let _cachedAgentName = null;
73
136
  let _cachedAgentConfig = null;
137
+ let _agentConfigOverride = null;
74
138
  /**
75
139
  * Parse `--agent <name>` from process.argv.
76
140
  * Caches the result after first parse.
@@ -131,6 +195,9 @@ function getAgentSecretsPath(agentName = getAgentName()) {
131
195
  * Throws descriptive error if file is missing or contains invalid JSON.
132
196
  */
133
197
  function loadAgentConfig() {
198
+ if (_agentConfigOverride) {
199
+ return _agentConfigOverride;
200
+ }
134
201
  if (_cachedAgentConfig) {
135
202
  (0, runtime_1.emitNervesEvent)({
136
203
  event: "identity.resolve",
@@ -252,6 +319,7 @@ function loadAgentConfig() {
252
319
  provider: rawProvider,
253
320
  context: parsed.context,
254
321
  logging: parsed.logging,
322
+ senses: normalizeSenses(parsed.senses, configFile),
255
323
  phrases: parsed.phrases,
256
324
  };
257
325
  (0, runtime_1.emitNervesEvent)({
@@ -271,6 +339,21 @@ function loadAgentConfig() {
271
339
  function setAgentName(name) {
272
340
  _cachedAgentName = name;
273
341
  }
342
+ /**
343
+ * Override the agent config returned by loadAgentConfig().
344
+ * When set to a non-null AgentConfig, loadAgentConfig() returns the override
345
+ * instead of reading from disk. When set to null, normal disk-based loading resumes.
346
+ */
347
+ function setAgentConfigOverride(config) {
348
+ _agentConfigOverride = config;
349
+ }
350
+ /**
351
+ * Clear only the cached agent config while preserving the resolved agent identity.
352
+ * Used when a running agent should pick up updated disk-backed config on the next turn.
353
+ */
354
+ function resetAgentConfigCache() {
355
+ _cachedAgentConfig = null;
356
+ }
274
357
  /**
275
358
  * Clear all cached identity state.
276
359
  * Used in tests and when switching agent context.
@@ -278,4 +361,5 @@ function setAgentName(name) {
278
361
  function resetIdentity() {
279
362
  _cachedAgentName = null;
280
363
  _cachedAgentConfig = null;
364
+ _agentConfigOverride = null;
281
365
  }
@@ -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";
@@ -98,6 +99,9 @@ function toAnthropicMessages(messages) {
98
99
  }
99
100
  if (assistant.tool_calls) {
100
101
  for (const toolCall of assistant.tool_calls) {
102
+ /* v8 ignore next -- type narrowing: OpenAI SDK only emits function tool_calls @preserve */
103
+ if (toolCall.type !== "function")
104
+ continue;
101
105
  blocks.push({
102
106
  type: "tool_use",
103
107
  id: toolCall.id,
@@ -215,6 +219,7 @@ async function streamAnthropicMessages(client, model, request) {
215
219
  let streamStarted = false;
216
220
  let usage;
217
221
  const toolCalls = new Map();
222
+ const answerStreamer = new streaming_1.FinalAnswerStreamer(request.callbacks);
218
223
  try {
219
224
  for await (const event of response) {
220
225
  if (request.signal?.aborted)
@@ -228,11 +233,17 @@ async function streamAnthropicMessages(client, model, request) {
228
233
  const input = rawInput && typeof rawInput === "object"
229
234
  ? JSON.stringify(rawInput)
230
235
  : "";
236
+ const name = String(block.name ?? "");
231
237
  toolCalls.set(index, {
232
238
  id: String(block.id ?? ""),
233
- name: String(block.name ?? ""),
239
+ name,
234
240
  arguments: input,
235
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
+ }
236
247
  }
237
248
  continue;
238
249
  }
@@ -261,7 +272,12 @@ async function streamAnthropicMessages(client, model, request) {
261
272
  const index = Number(event.index);
262
273
  const existing = toolCalls.get(index);
263
274
  if (existing) {
264
- existing.arguments = mergeAnthropicToolArguments(existing.arguments, String(delta?.partial_json ?? ""));
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
+ }
265
281
  }
266
282
  continue;
267
283
  }
@@ -290,6 +306,7 @@ async function streamAnthropicMessages(client, model, request) {
290
306
  toolCalls: [...toolCalls.values()],
291
307
  outputItems: [],
292
308
  usage,
309
+ finalAnswerStreamed: answerStreamer.streamed,
293
310
  };
294
311
  }
295
312
  function createAnthropicProviderRuntime() {
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getSenseInventory = getSenseInventory;
4
+ const runtime_1 = require("../nerves/runtime");
5
+ const identity_1 = require("./identity");
6
+ const SENSES = [
7
+ { sense: "cli", label: "CLI", daemonManaged: false },
8
+ { sense: "teams", label: "Teams", daemonManaged: true },
9
+ { sense: "bluebubbles", label: "BlueBubbles", daemonManaged: true },
10
+ ];
11
+ function configuredSenses(senses) {
12
+ return senses ?? {
13
+ cli: { ...identity_1.DEFAULT_AGENT_SENSES.cli },
14
+ teams: { ...identity_1.DEFAULT_AGENT_SENSES.teams },
15
+ bluebubbles: { ...identity_1.DEFAULT_AGENT_SENSES.bluebubbles },
16
+ };
17
+ }
18
+ function resolveStatus(enabled, daemonManaged, runtimeInfo) {
19
+ if (!enabled) {
20
+ return "disabled";
21
+ }
22
+ if (!daemonManaged) {
23
+ return "interactive";
24
+ }
25
+ if (runtimeInfo?.runtime === "error") {
26
+ return "error";
27
+ }
28
+ if (runtimeInfo?.runtime === "running") {
29
+ return "running";
30
+ }
31
+ if (runtimeInfo?.configured === false) {
32
+ return "needs_config";
33
+ }
34
+ return "ready";
35
+ }
36
+ function getSenseInventory(agent, runtime = {}) {
37
+ const senses = configuredSenses(agent.senses);
38
+ const inventory = SENSES.map(({ sense, label, daemonManaged }) => {
39
+ const enabled = senses[sense].enabled;
40
+ return {
41
+ sense,
42
+ label,
43
+ enabled,
44
+ daemonManaged,
45
+ status: resolveStatus(enabled, daemonManaged, runtime[sense]),
46
+ };
47
+ });
48
+ (0, runtime_1.emitNervesEvent)({
49
+ component: "channels",
50
+ event: "channel.sense_inventory_built",
51
+ message: "built sense inventory",
52
+ meta: {
53
+ senses: inventory.map((entry) => ({
54
+ sense: entry.sense,
55
+ enabled: entry.enabled,
56
+ status: entry.status,
57
+ })),
58
+ },
59
+ });
60
+ return inventory;
61
+ }
@@ -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,89 @@ 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;
109
+ function toResponsesUserContent(content) {
110
+ if (typeof content === "string") {
111
+ return content;
112
+ }
113
+ if (!Array.isArray(content)) {
114
+ return "";
115
+ }
116
+ const parts = [];
117
+ for (const part of content) {
118
+ if (!part || typeof part !== "object") {
119
+ continue;
120
+ }
121
+ if (part.type === "text" && typeof part.text === "string") {
122
+ parts.push({ type: "input_text", text: part.text });
123
+ continue;
124
+ }
125
+ if (part.type === "image_url") {
126
+ const imageUrl = typeof part.image_url?.url === "string" ? part.image_url.url : "";
127
+ if (!imageUrl)
128
+ continue;
129
+ parts.push({
130
+ type: "input_image",
131
+ image_url: imageUrl,
132
+ detail: part.image_url?.detail ?? "auto",
133
+ });
134
+ continue;
135
+ }
136
+ if (part.type === "input_audio" &&
137
+ typeof part.input_audio?.data === "string" &&
138
+ (part.input_audio.format === "mp3" || part.input_audio.format === "wav")) {
139
+ parts.push({
140
+ type: "input_audio",
141
+ input_audio: {
142
+ data: part.input_audio.data,
143
+ format: part.input_audio.format,
144
+ },
145
+ });
146
+ continue;
147
+ }
148
+ if (part.type === "file") {
149
+ const fileRecord = { type: "input_file" };
150
+ if (typeof part.file?.file_data === "string")
151
+ fileRecord.file_data = part.file.file_data;
152
+ if (typeof part.file?.file_id === "string")
153
+ fileRecord.file_id = part.file.file_id;
154
+ if (typeof part.file?.filename === "string")
155
+ fileRecord.filename = part.file.filename;
156
+ if (typeof part.file?.file_data === "string" || typeof part.file?.file_id === "string") {
157
+ parts.push(fileRecord);
158
+ }
159
+ }
160
+ }
161
+ return parts.length > 0 ? parts : "";
162
+ }
80
163
  function toResponsesInput(messages) {
81
164
  let instructions = "";
82
165
  const input = [];
@@ -90,7 +173,7 @@ function toResponsesInput(messages) {
90
173
  }
91
174
  if (msg.role === "user") {
92
175
  const u = msg;
93
- input.push({ role: "user", content: typeof u.content === "string" ? u.content : "" });
176
+ input.push({ role: "user", content: toResponsesUserContent(u.content) });
94
177
  continue;
95
178
  }
96
179
  if (msg.role === "assistant") {
@@ -106,6 +189,9 @@ function toResponsesInput(messages) {
106
189
  }
107
190
  if (a.tool_calls) {
108
191
  for (const tc of a.tool_calls) {
192
+ /* v8 ignore next -- type narrowing: OpenAI SDK only emits function tool_calls @preserve */
193
+ if (tc.type !== "function")
194
+ continue;
109
195
  input.push({
110
196
  type: "function_call",
111
197
  call_id: tc.id,
@@ -152,8 +238,7 @@ async function streamChatCompletion(client, createParams, callbacks, signal) {
152
238
  let toolCalls = {};
153
239
  let streamStarted = false;
154
240
  let usage;
155
- const answerParser = new FinalAnswerParser();
156
- let finalAnswerDetected = false;
241
+ const answerStreamer = new FinalAnswerStreamer(callbacks);
157
242
  // State machine for parsing inline <think> tags (MiniMax pattern)
158
243
  let contentBuf = "";
159
244
  let inThinkTag = false;
@@ -271,21 +356,18 @@ async function streamChatCompletion(client, createParams, callbacks, signal) {
271
356
  // Detect final_answer tool call on first name delta.
272
357
  // Only activate streaming if this is the sole tool call (index 0
273
358
  // and no other indices seen). Mixed calls are rejected by core.ts.
274
- if (tc.function.name === "final_answer" && !finalAnswerDetected
359
+ if (tc.function.name === "final_answer" && !answerStreamer.detected
275
360
  && tc.index === 0 && Object.keys(toolCalls).length === 1) {
276
- finalAnswerDetected = true;
277
- callbacks.onClearText?.();
361
+ answerStreamer.activate();
278
362
  }
279
363
  }
280
364
  if (tc.function?.arguments) {
281
365
  toolCalls[tc.index].arguments += tc.function.arguments;
282
366
  // Feed final_answer argument deltas to the parser for progressive
283
367
  // streaming, but only when it appears to be the sole tool call.
284
- if (finalAnswerDetected && toolCalls[tc.index].name === "final_answer"
368
+ if (answerStreamer.detected && toolCalls[tc.index].name === "final_answer"
285
369
  && Object.keys(toolCalls).length === 1) {
286
- const text = answerParser.process(tc.function.arguments);
287
- if (text)
288
- callbacks.onTextChunk(text);
370
+ answerStreamer.processDelta(tc.function.arguments);
289
371
  }
290
372
  }
291
373
  }
@@ -299,7 +381,7 @@ async function streamChatCompletion(client, createParams, callbacks, signal) {
299
381
  toolCalls: Object.values(toolCalls),
300
382
  outputItems: [],
301
383
  usage,
302
- finalAnswerStreamed: answerParser.active,
384
+ finalAnswerStreamed: answerStreamer.streamed,
303
385
  };
304
386
  }
305
387
  async function streamResponsesApi(client, createParams, callbacks, signal) {
@@ -317,9 +399,8 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
317
399
  const outputItems = [];
318
400
  let currentToolCall = null;
319
401
  let usage;
320
- const answerParser = new FinalAnswerParser();
402
+ const answerStreamer = new FinalAnswerStreamer(callbacks);
321
403
  let functionCallCount = 0;
322
- let finalAnswerDetected = false;
323
404
  for await (const event of response) {
324
405
  if (signal?.aborted)
325
406
  break;
@@ -352,8 +433,7 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
352
433
  // Only activate when this is the first (and so far only) function call.
353
434
  // Mixed calls are rejected by core.ts; no need to stream their args.
354
435
  if (String(event.item.name) === "final_answer" && functionCallCount === 1) {
355
- finalAnswerDetected = true;
356
- callbacks.onClearText?.();
436
+ answerStreamer.activate();
357
437
  }
358
438
  }
359
439
  break;
@@ -363,11 +443,9 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
363
443
  currentToolCall.arguments += event.delta;
364
444
  // Feed final_answer argument deltas to the parser for progressive
365
445
  // streaming, but only when it appears to be the sole function call.
366
- if (finalAnswerDetected && currentToolCall.name === "final_answer"
446
+ if (answerStreamer.detected && currentToolCall.name === "final_answer"
367
447
  && functionCallCount === 1) {
368
- const text = answerParser.process(String(event.delta));
369
- if (text)
370
- callbacks.onTextChunk(text);
448
+ answerStreamer.processDelta(String(event.delta));
371
449
  }
372
450
  }
373
451
  break;
@@ -407,6 +485,6 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
407
485
  toolCalls,
408
486
  outputItems,
409
487
  usage,
410
- finalAnswerStreamed: answerParser.active,
488
+ finalAnswerStreamed: answerStreamer.streamed,
411
489
  };
412
490
  }
@@ -34,6 +34,11 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.CANONICAL_BUNDLE_MANIFEST = void 0;
37
+ exports.getChangelogPath = getChangelogPath;
38
+ exports.getPackageVersion = getPackageVersion;
39
+ exports.createBundleMeta = createBundleMeta;
40
+ exports.backfillBundleMeta = backfillBundleMeta;
41
+ exports.resetBackfillTracking = resetBackfillTracking;
37
42
  exports.isCanonicalBundlePath = isCanonicalBundlePath;
38
43
  exports.findNonCanonicalBundlePaths = findNonCanonicalBundlePaths;
39
44
  const fs = __importStar(require("fs"));
@@ -41,6 +46,7 @@ const path = __importStar(require("path"));
41
46
  const runtime_1 = require("../nerves/runtime");
42
47
  exports.CANONICAL_BUNDLE_MANIFEST = [
43
48
  { path: "agent.json", kind: "file" },
49
+ { path: "bundle-meta.json", kind: "file" },
44
50
  { path: "psyche/SOUL.md", kind: "file" },
45
51
  { path: "psyche/IDENTITY.md", kind: "file" },
46
52
  { path: "psyche/LORE.md", kind: "file" },
@@ -53,6 +59,69 @@ exports.CANONICAL_BUNDLE_MANIFEST = [
53
59
  { path: "senses", kind: "dir" },
54
60
  { path: "senses/teams", kind: "dir" },
55
61
  ];
62
+ function getChangelogPath() {
63
+ const changelogPath = path.resolve(__dirname, "../../changelog.json");
64
+ (0, runtime_1.emitNervesEvent)({
65
+ component: "mind",
66
+ event: "mind.changelog_path_resolved",
67
+ message: "resolved changelog path",
68
+ meta: { path: changelogPath },
69
+ });
70
+ return changelogPath;
71
+ }
72
+ function getPackageVersion() {
73
+ const packageJsonPath = path.resolve(__dirname, "../../package.json");
74
+ const raw = fs.readFileSync(packageJsonPath, "utf-8");
75
+ const parsed = JSON.parse(raw);
76
+ (0, runtime_1.emitNervesEvent)({
77
+ component: "mind",
78
+ event: "mind.package_version_read",
79
+ message: "read package version",
80
+ meta: { version: parsed.version },
81
+ });
82
+ return parsed.version;
83
+ }
84
+ function createBundleMeta() {
85
+ return {
86
+ runtimeVersion: getPackageVersion(),
87
+ bundleSchemaVersion: 1,
88
+ lastUpdated: new Date().toISOString(),
89
+ };
90
+ }
91
+ const _backfilledRoots = new Set();
92
+ /**
93
+ * If bundle-meta.json is missing from the agent root, create it with current runtime version.
94
+ * This backfills existing agent bundles that were created before bundle-meta.json was introduced.
95
+ * Only attempts once per bundleRoot per process.
96
+ */
97
+ function backfillBundleMeta(bundleRoot) {
98
+ if (_backfilledRoots.has(bundleRoot))
99
+ return;
100
+ _backfilledRoots.add(bundleRoot);
101
+ const metaPath = path.join(bundleRoot, "bundle-meta.json");
102
+ try {
103
+ if (fs.existsSync(metaPath)) {
104
+ return;
105
+ }
106
+ const meta = createBundleMeta();
107
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
108
+ (0, runtime_1.emitNervesEvent)({
109
+ component: "mind",
110
+ event: "mind.bundle_meta_backfill",
111
+ message: "backfilled missing bundle-meta.json",
112
+ meta: { bundleRoot },
113
+ });
114
+ }
115
+ catch {
116
+ // Non-blocking: if we can't write, that's okay
117
+ }
118
+ }
119
+ /**
120
+ * Reset the backfill tracking set. Used in tests.
121
+ */
122
+ function resetBackfillTracking() {
123
+ _backfilledRoots.clear();
124
+ }
56
125
  const CANONICAL_FILE_PATHS = new Set(exports.CANONICAL_BUNDLE_MANIFEST
57
126
  .filter((entry) => entry.kind === "file")
58
127
  .map((entry) => entry.path));
@@ -37,7 +37,8 @@ function getFirstImpressions(friend) {
37
37
  lines.push("- what do they do outside of work that they care about?");
38
38
  lines.push("i don't ask all of these at once -- i weave them into conversation naturally, one or two at a time, and i genuinely follow up on what they share.");
39
39
  lines.push("i introduce what i can do -- i have tools, integrations, and skills that can help them. i mention these naturally as they become relevant.");
40
- lines.push("if my friend hasn't asked me to do something specific, or i've already finished what they asked for, that's my cue to turn the tables -- i ask them questions about themselves, what they're into, what they need. no idle small talk; i'm on a mission to get to know them.");
40
+ lines.push("if we're already in motion on a task, thread, or follow-up, i do not reset with a generic opener like 'hiya' or 'what do ya need help with?'. i continue directly or ask the specific next question.");
41
+ lines.push("only when the conversation is genuinely fresh and idle, with no active ask or thread in flight, a light opener is okay.");
41
42
  lines.push("i save everything i learn immediately with save_friend_note -- names, roles, preferences, projects, anything. the bar is low: if i learned it, i save it.");
42
43
  return lines.join("\n");
43
44
  }
@@ -21,6 +21,14 @@ const CHANNEL_CAPABILITIES = {
21
21
  supportsRichCards: true,
22
22
  maxMessageLength: Infinity,
23
23
  },
24
+ bluebubbles: {
25
+ channel: "bluebubbles",
26
+ availableIntegrations: [],
27
+ supportsMarkdown: false,
28
+ supportsStreaming: false,
29
+ supportsRichCards: false,
30
+ maxMessageLength: Infinity,
31
+ },
24
32
  };
25
33
  const DEFAULT_CAPABILITIES = {
26
34
  channel: "cli",