@ouro.bot/cli 0.1.0-alpha.4 → 0.1.0-alpha.41

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 (84) 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/README.md +117 -188
  5. package/assets/ouroboros.png +0 -0
  6. package/changelog.json +170 -0
  7. package/dist/heart/config.js +81 -8
  8. package/dist/heart/core.js +78 -45
  9. package/dist/heart/daemon/agent-discovery.js +81 -0
  10. package/dist/heart/daemon/daemon-cli.js +987 -77
  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 +177 -9
  14. package/dist/heart/daemon/hatch-animation.js +35 -0
  15. package/dist/heart/daemon/hatch-flow.js +4 -20
  16. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  17. package/dist/heart/daemon/launchd.js +134 -0
  18. package/dist/heart/daemon/message-router.js +15 -6
  19. package/dist/heart/daemon/ouro-bot-entry.js +0 -0
  20. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  21. package/dist/heart/daemon/ouro-entry.js +0 -0
  22. package/dist/heart/daemon/ouro-path-installer.js +178 -0
  23. package/dist/heart/daemon/ouro-uti.js +11 -2
  24. package/dist/heart/daemon/process-manager.js +1 -1
  25. package/dist/heart/daemon/run-hooks.js +37 -0
  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 +99 -0
  30. package/dist/heart/daemon/specialist-tools.js +283 -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 +111 -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 +96 -4
  37. package/dist/heart/kicks.js +1 -19
  38. package/dist/heart/providers/anthropic.js +16 -2
  39. package/dist/heart/sense-truth.js +61 -0
  40. package/dist/heart/streaming.js +96 -21
  41. package/dist/mind/bundle-manifest.js +70 -0
  42. package/dist/mind/context.js +7 -7
  43. package/dist/mind/first-impressions.js +2 -1
  44. package/dist/mind/friends/channel.js +43 -0
  45. package/dist/mind/friends/store-file.js +19 -0
  46. package/dist/mind/friends/types.js +9 -1
  47. package/dist/mind/memory.js +10 -3
  48. package/dist/mind/pending.js +10 -2
  49. package/dist/mind/phrases.js +1 -0
  50. package/dist/mind/prompt.js +222 -7
  51. package/dist/mind/token-estimate.js +8 -12
  52. package/dist/nerves/cli-logging.js +15 -2
  53. package/dist/repertoire/ado-client.js +4 -2
  54. package/dist/repertoire/coding/feedback.js +134 -0
  55. package/dist/repertoire/coding/index.js +4 -1
  56. package/dist/repertoire/coding/manager.js +62 -4
  57. package/dist/repertoire/coding/spawner.js +3 -3
  58. package/dist/repertoire/coding/tools.js +41 -2
  59. package/dist/repertoire/data/ado-endpoints.json +188 -0
  60. package/dist/repertoire/tasks/index.js +2 -9
  61. package/dist/repertoire/tasks/transitions.js +1 -2
  62. package/dist/repertoire/tools-base.js +202 -219
  63. package/dist/repertoire/tools-bluebubbles.js +93 -0
  64. package/dist/repertoire/tools-teams.js +58 -25
  65. package/dist/repertoire/tools.js +55 -35
  66. package/dist/senses/bluebubbles-client.js +434 -0
  67. package/dist/senses/bluebubbles-entry.js +11 -0
  68. package/dist/senses/bluebubbles-media.js +338 -0
  69. package/dist/senses/bluebubbles-model.js +261 -0
  70. package/dist/senses/bluebubbles-mutation-log.js +74 -0
  71. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  72. package/dist/senses/bluebubbles.js +832 -0
  73. package/dist/senses/cli.js +327 -138
  74. package/dist/senses/debug-activity.js +127 -0
  75. package/dist/senses/inner-dialog.js +103 -55
  76. package/dist/senses/pipeline.js +124 -0
  77. package/dist/senses/teams.js +427 -112
  78. package/dist/senses/trust-gate.js +112 -2
  79. package/package.json +14 -3
  80. package/subagents/README.md +40 -53
  81. package/subagents/work-doer.md +26 -24
  82. package/subagents/work-merger.md +24 -30
  83. package/subagents/work-planner.md +34 -25
  84. 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,15 +33,18 @@ 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;
40
40
  exports.getAgentBundlesRoot = getAgentBundlesRoot;
41
41
  exports.getAgentRoot = getAgentRoot;
42
+ exports.getAgentStateRoot = getAgentStateRoot;
42
43
  exports.getAgentSecretsPath = getAgentSecretsPath;
43
44
  exports.loadAgentConfig = loadAgentConfig;
44
45
  exports.setAgentName = setAgentName;
46
+ exports.setAgentConfigOverride = setAgentConfigOverride;
47
+ exports.resetAgentConfigCache = resetAgentConfigCache;
45
48
  exports.resetIdentity = resetIdentity;
46
49
  const fs = __importStar(require("fs"));
47
50
  const os = __importStar(require("os"));
@@ -56,12 +59,73 @@ exports.DEFAULT_AGENT_PHRASES = {
56
59
  tool: ["running tool"],
57
60
  followup: ["processing"],
58
61
  };
62
+ exports.DEFAULT_AGENT_SENSES = {
63
+ cli: { enabled: true },
64
+ teams: { enabled: false },
65
+ bluebubbles: { enabled: false },
66
+ };
67
+ function normalizeSenses(value, configFile) {
68
+ const defaults = {
69
+ cli: { ...exports.DEFAULT_AGENT_SENSES.cli },
70
+ teams: { ...exports.DEFAULT_AGENT_SENSES.teams },
71
+ bluebubbles: { ...exports.DEFAULT_AGENT_SENSES.bluebubbles },
72
+ };
73
+ if (value === undefined) {
74
+ return defaults;
75
+ }
76
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
77
+ (0, runtime_1.emitNervesEvent)({
78
+ level: "error",
79
+ event: "config_identity.error",
80
+ component: "config/identity",
81
+ message: "agent config has invalid senses block",
82
+ meta: { path: configFile },
83
+ });
84
+ throw new Error(`agent.json at ${configFile} must include senses as an object when present.`);
85
+ }
86
+ const raw = value;
87
+ const senseNames = ["cli", "teams", "bluebubbles"];
88
+ for (const senseName of senseNames) {
89
+ const rawSense = raw[senseName];
90
+ if (rawSense === undefined) {
91
+ continue;
92
+ }
93
+ if (!rawSense || typeof rawSense !== "object" || Array.isArray(rawSense)) {
94
+ (0, runtime_1.emitNervesEvent)({
95
+ level: "error",
96
+ event: "config_identity.error",
97
+ component: "config/identity",
98
+ message: "agent config has invalid sense config",
99
+ meta: { path: configFile, sense: senseName },
100
+ });
101
+ throw new Error(`agent.json at ${configFile} has invalid senses.${senseName} config.`);
102
+ }
103
+ const enabled = rawSense.enabled;
104
+ if (typeof enabled !== "boolean") {
105
+ (0, runtime_1.emitNervesEvent)({
106
+ level: "error",
107
+ event: "config_identity.error",
108
+ component: "config/identity",
109
+ message: "agent config has invalid sense enabled flag",
110
+ meta: { path: configFile, sense: senseName, enabled: enabled ?? null },
111
+ });
112
+ throw new Error(`agent.json at ${configFile} must include senses.${senseName}.enabled as boolean.`);
113
+ }
114
+ defaults[senseName] = { enabled };
115
+ }
116
+ return defaults;
117
+ }
59
118
  function buildDefaultAgentTemplate(_agentName) {
60
119
  return {
61
120
  version: 1,
62
121
  enabled: true,
63
122
  provider: "anthropic",
64
123
  context: { ...exports.DEFAULT_AGENT_CONTEXT },
124
+ senses: {
125
+ cli: { ...exports.DEFAULT_AGENT_SENSES.cli },
126
+ teams: { ...exports.DEFAULT_AGENT_SENSES.teams },
127
+ bluebubbles: { ...exports.DEFAULT_AGENT_SENSES.bluebubbles },
128
+ },
65
129
  phrases: {
66
130
  thinking: [...exports.DEFAULT_AGENT_PHRASES.thinking],
67
131
  tool: [...exports.DEFAULT_AGENT_PHRASES.tool],
@@ -71,6 +135,7 @@ function buildDefaultAgentTemplate(_agentName) {
71
135
  }
72
136
  let _cachedAgentName = null;
73
137
  let _cachedAgentConfig = null;
138
+ let _agentConfigOverride = null;
74
139
  /**
75
140
  * Parse `--agent <name>` from process.argv.
76
141
  * Caches the result after first parse.
@@ -111,13 +176,20 @@ function getRepoRoot() {
111
176
  * Returns the shared bundle root directory: `~/AgentBundles/`
112
177
  */
113
178
  function getAgentBundlesRoot() {
114
- return path.join(os.homedir(), "AgentBundles");
179
+ const homeBase = process.env.WEBSITE_SITE_NAME ? "/home" : os.homedir();
180
+ return path.join(homeBase, "AgentBundles");
115
181
  }
116
182
  /**
117
183
  * Returns the agent-specific bundle directory: `~/AgentBundles/<agentName>.ouro/`
118
184
  */
119
- function getAgentRoot() {
120
- return path.join(getAgentBundlesRoot(), `${getAgentName()}.ouro`);
185
+ function getAgentRoot(agentName = getAgentName()) {
186
+ return path.join(getAgentBundlesRoot(), `${agentName}.ouro`);
187
+ }
188
+ /**
189
+ * Returns the bundle-local runtime state directory: `~/AgentBundles/<agentName>.ouro/state/`
190
+ */
191
+ function getAgentStateRoot(agentName = getAgentName()) {
192
+ return path.join(getAgentRoot(agentName), "state");
121
193
  }
122
194
  /**
123
195
  * Returns the conventional secrets path: `~/.agentsecrets/<agentName>/secrets.json`
@@ -131,6 +203,9 @@ function getAgentSecretsPath(agentName = getAgentName()) {
131
203
  * Throws descriptive error if file is missing or contains invalid JSON.
132
204
  */
133
205
  function loadAgentConfig() {
206
+ if (_agentConfigOverride) {
207
+ return _agentConfigOverride;
208
+ }
134
209
  if (_cachedAgentConfig) {
135
210
  (0, runtime_1.emitNervesEvent)({
136
211
  event: "identity.resolve",
@@ -252,6 +327,7 @@ function loadAgentConfig() {
252
327
  provider: rawProvider,
253
328
  context: parsed.context,
254
329
  logging: parsed.logging,
330
+ senses: normalizeSenses(parsed.senses, configFile),
255
331
  phrases: parsed.phrases,
256
332
  };
257
333
  (0, runtime_1.emitNervesEvent)({
@@ -271,6 +347,21 @@ function loadAgentConfig() {
271
347
  function setAgentName(name) {
272
348
  _cachedAgentName = name;
273
349
  }
350
+ /**
351
+ * Override the agent config returned by loadAgentConfig().
352
+ * When set to a non-null AgentConfig, loadAgentConfig() returns the override
353
+ * instead of reading from disk. When set to null, normal disk-based loading resumes.
354
+ */
355
+ function setAgentConfigOverride(config) {
356
+ _agentConfigOverride = config;
357
+ }
358
+ /**
359
+ * Clear only the cached agent config while preserving the resolved agent identity.
360
+ * Used when a running agent should pick up updated disk-backed config on the next turn.
361
+ */
362
+ function resetAgentConfigCache() {
363
+ _cachedAgentConfig = null;
364
+ }
274
365
  /**
275
366
  * Clear all cached identity state.
276
367
  * Used in tests and when switching agent context.
@@ -278,4 +369,5 @@ function setAgentName(name) {
278
369
  function resetIdentity() {
279
370
  _cachedAgentName = null;
280
371
  _cachedAgentConfig = null;
372
+ _agentConfigOverride = null;
281
373
  }
@@ -1,25 +1,8 @@
1
1
  "use strict";
2
- // TODO: Kicks enforce "any action" but not "meaningful action". After a narration
3
- // kick, the model can satisfy the constraint by calling a no-op tool like
4
- // get_current_time({}). We need to detect trivial compliance and either re-kick
5
- // or discount the tool call. Ideally, the kick message would suggest a specific
6
- // tool call based on conversation context (what the user asked, what tools are
7
- // relevant) rather than just saying "call a tool". That's a bigger piece of work —
8
- // it requires the kick system to be context-aware.
9
- // See ouroboros' observation: "i'm not chickening out. i'm satisfying a crude
10
- // constraint. poorly."
11
- //
12
- // A kick is a self-correction. When the harness detects a malformed response,
13
- // it injects an assistant-role message as if the model caught its own mistake.
14
- //
15
- // Kicks are:
16
- // - assistant role (self-correction, not external rebuke)
17
- // - first person ("I" not "you")
18
- // - forward-looking (what I'm doing next, not what I did wrong)
19
- // - short (one sentence)
20
2
  Object.defineProperty(exports, "__esModule", { value: true });
21
3
  exports.hasToolIntent = hasToolIntent;
22
4
  exports.detectKick = detectKick;
5
+ const runtime_1 = require("../nerves/runtime");
23
6
  const KICK_MESSAGES = {
24
7
  empty: "I sent an empty message by accident — let me try again.",
25
8
  narration: "I narrated instead of acting. Using the tool now -- if done, calling final_answer.",
@@ -141,4 +124,3 @@ function detectKick(content, options) {
141
124
  }
142
125
  return null;
143
126
  }
144
- const runtime_1 = require("../nerves/runtime");
@@ -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: String(block.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
- 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
+ }
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() {
@@ -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") {
@@ -155,8 +238,7 @@ async function streamChatCompletion(client, createParams, callbacks, signal) {
155
238
  let toolCalls = {};
156
239
  let streamStarted = false;
157
240
  let usage;
158
- const answerParser = new FinalAnswerParser();
159
- let finalAnswerDetected = false;
241
+ const answerStreamer = new FinalAnswerStreamer(callbacks);
160
242
  // State machine for parsing inline <think> tags (MiniMax pattern)
161
243
  let contentBuf = "";
162
244
  let inThinkTag = false;
@@ -274,21 +356,18 @@ async function streamChatCompletion(client, createParams, callbacks, signal) {
274
356
  // Detect final_answer tool call on first name delta.
275
357
  // Only activate streaming if this is the sole tool call (index 0
276
358
  // and no other indices seen). Mixed calls are rejected by core.ts.
277
- if (tc.function.name === "final_answer" && !finalAnswerDetected
359
+ if (tc.function.name === "final_answer" && !answerStreamer.detected
278
360
  && tc.index === 0 && Object.keys(toolCalls).length === 1) {
279
- finalAnswerDetected = true;
280
- callbacks.onClearText?.();
361
+ answerStreamer.activate();
281
362
  }
282
363
  }
283
364
  if (tc.function?.arguments) {
284
365
  toolCalls[tc.index].arguments += tc.function.arguments;
285
366
  // Feed final_answer argument deltas to the parser for progressive
286
367
  // streaming, but only when it appears to be the sole tool call.
287
- if (finalAnswerDetected && toolCalls[tc.index].name === "final_answer"
368
+ if (answerStreamer.detected && toolCalls[tc.index].name === "final_answer"
288
369
  && Object.keys(toolCalls).length === 1) {
289
- const text = answerParser.process(tc.function.arguments);
290
- if (text)
291
- callbacks.onTextChunk(text);
370
+ answerStreamer.processDelta(tc.function.arguments);
292
371
  }
293
372
  }
294
373
  }
@@ -302,7 +381,7 @@ async function streamChatCompletion(client, createParams, callbacks, signal) {
302
381
  toolCalls: Object.values(toolCalls),
303
382
  outputItems: [],
304
383
  usage,
305
- finalAnswerStreamed: answerParser.active,
384
+ finalAnswerStreamed: answerStreamer.streamed,
306
385
  };
307
386
  }
308
387
  async function streamResponsesApi(client, createParams, callbacks, signal) {
@@ -320,9 +399,8 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
320
399
  const outputItems = [];
321
400
  let currentToolCall = null;
322
401
  let usage;
323
- const answerParser = new FinalAnswerParser();
402
+ const answerStreamer = new FinalAnswerStreamer(callbacks);
324
403
  let functionCallCount = 0;
325
- let finalAnswerDetected = false;
326
404
  for await (const event of response) {
327
405
  if (signal?.aborted)
328
406
  break;
@@ -355,8 +433,7 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
355
433
  // Only activate when this is the first (and so far only) function call.
356
434
  // Mixed calls are rejected by core.ts; no need to stream their args.
357
435
  if (String(event.item.name) === "final_answer" && functionCallCount === 1) {
358
- finalAnswerDetected = true;
359
- callbacks.onClearText?.();
436
+ answerStreamer.activate();
360
437
  }
361
438
  }
362
439
  break;
@@ -366,11 +443,9 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
366
443
  currentToolCall.arguments += event.delta;
367
444
  // Feed final_answer argument deltas to the parser for progressive
368
445
  // streaming, but only when it appears to be the sole function call.
369
- if (finalAnswerDetected && currentToolCall.name === "final_answer"
446
+ if (answerStreamer.detected && currentToolCall.name === "final_answer"
370
447
  && functionCallCount === 1) {
371
- const text = answerParser.process(String(event.delta));
372
- if (text)
373
- callbacks.onTextChunk(text);
448
+ answerStreamer.processDelta(String(event.delta));
374
449
  }
375
450
  }
376
451
  break;
@@ -410,6 +485,6 @@ async function streamResponsesApi(client, createParams, callbacks, signal) {
410
485
  toolCalls,
411
486
  outputItems,
412
487
  usage,
413
- finalAnswerStreamed: answerParser.active,
488
+ finalAnswerStreamed: answerStreamer.streamed,
414
489
  };
415
490
  }