@ouro.bot/cli 0.1.0-alpha.5 → 0.1.0-alpha.50

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 (102) 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 +242 -0
  7. package/dist/heart/active-work.js +157 -0
  8. package/dist/heart/bridges/manager.js +358 -0
  9. package/dist/heart/bridges/state-machine.js +135 -0
  10. package/dist/heart/bridges/store.js +123 -0
  11. package/dist/heart/config.js +81 -8
  12. package/dist/heart/core.js +145 -50
  13. package/dist/heart/daemon/agent-discovery.js +81 -0
  14. package/dist/heart/daemon/daemon-cli.js +1099 -164
  15. package/dist/heart/daemon/daemon-entry.js +14 -5
  16. package/dist/heart/daemon/daemon-runtime-sync.js +90 -0
  17. package/dist/heart/daemon/daemon.js +184 -9
  18. package/dist/heart/daemon/hatch-animation.js +10 -3
  19. package/dist/heart/daemon/hatch-flow.js +3 -20
  20. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  21. package/dist/heart/daemon/launchd.js +151 -0
  22. package/dist/heart/daemon/message-router.js +15 -6
  23. package/dist/heart/daemon/ouro-bot-entry.js +0 -0
  24. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  25. package/dist/heart/daemon/ouro-entry.js +0 -0
  26. package/dist/heart/daemon/ouro-path-installer.js +178 -0
  27. package/dist/heart/daemon/ouro-uti.js +11 -2
  28. package/dist/heart/daemon/process-manager.js +1 -1
  29. package/dist/heart/daemon/run-hooks.js +37 -0
  30. package/dist/heart/daemon/runtime-metadata.js +118 -0
  31. package/dist/heart/daemon/sense-manager.js +290 -0
  32. package/dist/heart/daemon/socket-client.js +202 -0
  33. package/dist/heart/daemon/specialist-orchestrator.js +53 -84
  34. package/dist/heart/daemon/specialist-prompt.js +64 -5
  35. package/dist/heart/daemon/specialist-tools.js +213 -58
  36. package/dist/heart/daemon/staged-restart.js +114 -0
  37. package/dist/heart/daemon/subagent-installer.js +48 -7
  38. package/dist/heart/daemon/thoughts.js +379 -0
  39. package/dist/heart/daemon/update-checker.js +111 -0
  40. package/dist/heart/daemon/update-hooks.js +138 -0
  41. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  42. package/dist/heart/delegation.js +62 -0
  43. package/dist/heart/identity.js +82 -4
  44. package/dist/heart/kicks.js +1 -19
  45. package/dist/heart/progress-story.js +42 -0
  46. package/dist/heart/providers/anthropic.js +16 -2
  47. package/dist/heart/sense-truth.js +61 -0
  48. package/dist/heart/session-activity.js +169 -0
  49. package/dist/heart/session-recall.js +116 -0
  50. package/dist/heart/streaming.js +96 -21
  51. package/dist/heart/turn-coordinator.js +28 -0
  52. package/dist/mind/associative-recall.js +14 -2
  53. package/dist/mind/bundle-manifest.js +70 -0
  54. package/dist/mind/context.js +27 -11
  55. package/dist/mind/first-impressions.js +16 -2
  56. package/dist/mind/friends/channel.js +43 -0
  57. package/dist/mind/friends/store-file.js +19 -0
  58. package/dist/mind/friends/types.js +9 -1
  59. package/dist/mind/memory.js +10 -3
  60. package/dist/mind/pending.js +72 -9
  61. package/dist/mind/phrases.js +1 -0
  62. package/dist/mind/prompt.js +266 -77
  63. package/dist/mind/token-estimate.js +8 -12
  64. package/dist/nerves/cli-logging.js +15 -2
  65. package/dist/repertoire/ado-client.js +4 -2
  66. package/dist/repertoire/coding/feedback.js +134 -0
  67. package/dist/repertoire/coding/index.js +4 -1
  68. package/dist/repertoire/coding/manager.js +62 -4
  69. package/dist/repertoire/coding/spawner.js +3 -3
  70. package/dist/repertoire/coding/tools.js +41 -2
  71. package/dist/repertoire/data/ado-endpoints.json +188 -0
  72. package/dist/repertoire/tasks/board.js +12 -0
  73. package/dist/repertoire/tasks/index.js +23 -9
  74. package/dist/repertoire/tasks/transitions.js +1 -2
  75. package/dist/repertoire/tools-base.js +462 -245
  76. package/dist/repertoire/tools-bluebubbles.js +93 -0
  77. package/dist/repertoire/tools-teams.js +58 -25
  78. package/dist/repertoire/tools.js +57 -35
  79. package/dist/senses/bluebubbles-client.js +484 -0
  80. package/dist/senses/bluebubbles-entry.js +13 -0
  81. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  82. package/dist/senses/bluebubbles-media.js +338 -0
  83. package/dist/senses/bluebubbles-model.js +261 -0
  84. package/dist/senses/bluebubbles-mutation-log.js +116 -0
  85. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  86. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  87. package/dist/senses/bluebubbles.js +1142 -0
  88. package/dist/senses/cli.js +340 -138
  89. package/dist/senses/continuity.js +94 -0
  90. package/dist/senses/debug-activity.js +148 -0
  91. package/dist/senses/inner-dialog-worker.js +47 -18
  92. package/dist/senses/inner-dialog.js +330 -84
  93. package/dist/senses/pipeline.js +256 -0
  94. package/dist/senses/teams.js +541 -129
  95. package/dist/senses/trust-gate.js +112 -2
  96. package/package.json +14 -3
  97. package/subagents/README.md +46 -33
  98. package/subagents/work-doer.md +28 -24
  99. package/subagents/work-merger.md +24 -30
  100. package/subagents/work-planner.md +44 -27
  101. package/dist/heart/daemon/specialist-session.js +0 -142
  102. package/dist/inner-worker-entry.js +0 -4
@@ -35,23 +35,29 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.loadConfig = loadConfig;
37
37
  exports.resetConfigCache = resetConfigCache;
38
- exports.setTestConfig = setTestConfig;
38
+ exports.patchRuntimeConfig = patchRuntimeConfig;
39
39
  exports.getAzureConfig = getAzureConfig;
40
40
  exports.getMinimaxConfig = getMinimaxConfig;
41
41
  exports.getAnthropicConfig = getAnthropicConfig;
42
42
  exports.getOpenAICodexConfig = getOpenAICodexConfig;
43
43
  exports.getTeamsConfig = getTeamsConfig;
44
+ exports.getTeamsSecondaryConfig = getTeamsSecondaryConfig;
44
45
  exports.getContextConfig = getContextConfig;
45
46
  exports.getOAuthConfig = getOAuthConfig;
47
+ exports.resolveOAuthForTenant = resolveOAuthForTenant;
46
48
  exports.getTeamsChannelConfig = getTeamsChannelConfig;
49
+ exports.getBlueBubblesConfig = getBlueBubblesConfig;
50
+ exports.getBlueBubblesChannelConfig = getBlueBubblesChannelConfig;
47
51
  exports.getIntegrationsConfig = getIntegrationsConfig;
48
52
  exports.getOpenAIEmbeddingsApiKey = getOpenAIEmbeddingsApiKey;
49
53
  exports.getLogsDir = getLogsDir;
54
+ exports.sanitizeKey = sanitizeKey;
55
+ exports.slugify = slugify;
56
+ exports.resolveSessionPath = resolveSessionPath;
50
57
  exports.sessionPath = sessionPath;
51
58
  exports.logPath = logPath;
52
59
  const fs = __importStar(require("fs"));
53
60
  const path = __importStar(require("path"));
54
- const os = __importStar(require("os"));
55
61
  const identity_1 = require("./identity");
56
62
  const runtime_1 = require("../nerves/runtime");
57
63
  const DEFAULT_SECRETS_TEMPLATE = {
@@ -74,7 +80,7 @@ const DEFAULT_SECRETS_TEMPLATE = {
74
80
  setupToken: "",
75
81
  },
76
82
  "openai-codex": {
77
- model: "gpt-5.2",
83
+ model: "gpt-5.4",
78
84
  oauthAccessToken: "",
79
85
  },
80
86
  },
@@ -82,16 +88,33 @@ const DEFAULT_SECRETS_TEMPLATE = {
82
88
  clientId: "",
83
89
  clientSecret: "",
84
90
  tenantId: "",
91
+ managedIdentityClientId: "",
85
92
  },
86
93
  oauth: {
87
94
  graphConnectionName: "graph",
88
95
  adoConnectionName: "ado",
89
96
  githubConnectionName: "",
90
97
  },
98
+ teamsSecondary: {
99
+ clientId: "",
100
+ clientSecret: "",
101
+ tenantId: "",
102
+ managedIdentityClientId: "",
103
+ },
91
104
  teamsChannel: {
92
105
  skipConfirmation: true,
93
106
  port: 3978,
94
107
  },
108
+ bluebubbles: {
109
+ serverUrl: "",
110
+ password: "",
111
+ accountId: "default",
112
+ },
113
+ bluebubblesChannel: {
114
+ port: 18790,
115
+ webhookPath: "/bluebubbles-webhook",
116
+ requestTimeoutMs: 30000,
117
+ },
95
118
  integrations: {
96
119
  perplexityApiKey: "",
97
120
  openaiEmbeddingsApiKey: "",
@@ -106,9 +129,12 @@ function defaultRuntimeConfig() {
106
129
  "openai-codex": { ...DEFAULT_SECRETS_TEMPLATE.providers["openai-codex"] },
107
130
  },
108
131
  teams: { ...DEFAULT_SECRETS_TEMPLATE.teams },
132
+ teamsSecondary: { ...DEFAULT_SECRETS_TEMPLATE.teamsSecondary },
109
133
  oauth: { ...DEFAULT_SECRETS_TEMPLATE.oauth },
110
134
  context: { ...identity_1.DEFAULT_AGENT_CONTEXT },
111
135
  teamsChannel: { ...DEFAULT_SECRETS_TEMPLATE.teamsChannel },
136
+ bluebubbles: { ...DEFAULT_SECRETS_TEMPLATE.bluebubbles },
137
+ bluebubblesChannel: { ...DEFAULT_SECRETS_TEMPLATE.bluebubblesChannel },
112
138
  integrations: { ...DEFAULT_SECRETS_TEMPLATE.integrations },
113
139
  };
114
140
  }
@@ -219,7 +245,7 @@ function resetConfigCache() {
219
245
  _cachedConfig = null;
220
246
  _testContextOverride = null;
221
247
  }
222
- function setTestConfig(partial) {
248
+ function patchRuntimeConfig(partial) {
223
249
  loadConfig(); // ensure _cachedConfig exists
224
250
  const contextPatch = partial.context;
225
251
  if (contextPatch) {
@@ -248,6 +274,10 @@ function getTeamsConfig() {
248
274
  const config = loadConfig();
249
275
  return { ...config.teams };
250
276
  }
277
+ function getTeamsSecondaryConfig() {
278
+ const config = loadConfig();
279
+ return { ...config.teamsSecondary };
280
+ }
251
281
  function getContextConfig() {
252
282
  if (_testContextOverride) {
253
283
  return { ..._testContextOverride };
@@ -268,11 +298,41 @@ function getOAuthConfig() {
268
298
  const config = loadConfig();
269
299
  return { ...config.oauth };
270
300
  }
301
+ /** Resolve OAuth connection names for a specific tenant, falling back to defaults. */
302
+ function resolveOAuthForTenant(tenantId) {
303
+ const base = getOAuthConfig();
304
+ const overrides = tenantId ? base.tenantOverrides?.[tenantId] : undefined;
305
+ return {
306
+ graphConnectionName: overrides?.graphConnectionName ?? base.graphConnectionName,
307
+ adoConnectionName: overrides?.adoConnectionName ?? base.adoConnectionName,
308
+ githubConnectionName: overrides?.githubConnectionName ?? base.githubConnectionName,
309
+ };
310
+ }
271
311
  function getTeamsChannelConfig() {
272
312
  const config = loadConfig();
273
313
  const { skipConfirmation, flushIntervalMs, port } = config.teamsChannel;
274
314
  return { skipConfirmation, flushIntervalMs, port };
275
315
  }
316
+ function getBlueBubblesConfig() {
317
+ const config = loadConfig();
318
+ const { serverUrl, password, accountId } = config.bluebubbles;
319
+ if (!serverUrl.trim()) {
320
+ throw new Error("bluebubbles.serverUrl is required in secrets.json to run the BlueBubbles sense.");
321
+ }
322
+ if (!password.trim()) {
323
+ throw new Error("bluebubbles.password is required in secrets.json to run the BlueBubbles sense.");
324
+ }
325
+ return {
326
+ serverUrl: serverUrl.trim(),
327
+ password: password.trim(),
328
+ accountId: accountId.trim() || "default",
329
+ };
330
+ }
331
+ function getBlueBubblesChannelConfig() {
332
+ const config = loadConfig();
333
+ const { port, webhookPath, requestTimeoutMs } = config.bluebubblesChannel;
334
+ return { port, webhookPath, requestTimeoutMs };
335
+ }
276
336
  function getIntegrationsConfig() {
277
337
  const config = loadConfig();
278
338
  return { ...config.integrations };
@@ -281,16 +341,29 @@ function getOpenAIEmbeddingsApiKey() {
281
341
  return getIntegrationsConfig().openaiEmbeddingsApiKey;
282
342
  }
283
343
  function getLogsDir() {
284
- return path.join(os.homedir(), ".agentstate", (0, identity_1.getAgentName)(), "logs");
344
+ return path.join((0, identity_1.getAgentRoot)(), "state", "logs");
285
345
  }
286
346
  function sanitizeKey(key) {
287
347
  return key.replace(/[/:]/g, "_");
288
348
  }
289
- function sessionPath(friendId, channel, key) {
290
- const dir = path.join(os.homedir(), ".agentstate", (0, identity_1.getAgentName)(), "sessions", friendId, channel);
291
- fs.mkdirSync(dir, { recursive: true });
349
+ function slugify(value) {
350
+ return value
351
+ .trim()
352
+ .toLowerCase()
353
+ .replace(/[^a-z0-9]+/g, "-")
354
+ .replace(/^-+/, "")
355
+ .replace(/-+$/, "");
356
+ }
357
+ function resolveSessionPath(friendId, channel, key, options) {
358
+ const dir = path.join((0, identity_1.getAgentRoot)(), "state", "sessions", friendId, channel);
359
+ if (options?.ensureDir) {
360
+ fs.mkdirSync(dir, { recursive: true });
361
+ }
292
362
  return path.join(dir, sanitizeKey(key) + ".json");
293
363
  }
364
+ function sessionPath(friendId, channel, key) {
365
+ return resolveSessionPath(friendId, channel, key, { ensureDir: true });
366
+ }
294
367
  function logPath(channel, key) {
295
368
  return path.join(getLogsDir(), channel, sanitizeKey(key) + ".ndjson");
296
369
  }
@@ -8,6 +8,7 @@ exports.getProvider = getProvider;
8
8
  exports.createSummarize = createSummarize;
9
9
  exports.getProviderDisplayLabel = getProviderDisplayLabel;
10
10
  exports.stripLastToolCalls = stripLastToolCalls;
11
+ exports.repairOrphanedToolCalls = repairOrphanedToolCalls;
11
12
  exports.isTransientError = isTransientError;
12
13
  exports.classifyTransientError = classifyTransientError;
13
14
  exports.runAgent = runAgent;
@@ -15,9 +16,6 @@ const config_1 = require("./config");
15
16
  const identity_1 = require("./identity");
16
17
  const tools_1 = require("../repertoire/tools");
17
18
  const channel_1 = require("../mind/friends/channel");
18
- // Kick detection preserved but disabled — see comment in agent loop below.
19
- // import { detectKick } from "./kicks";
20
- // import type { KickReason } from "./kicks";
21
19
  const runtime_1 = require("../nerves/runtime");
22
20
  const context_1 = require("../mind/context");
23
21
  const prompt_1 = require("../mind/prompt");
@@ -128,6 +126,35 @@ Object.defineProperty(exports, "toResponsesTools", { enumerable: true, get: func
128
126
  // Re-export prompt functions for backward compat
129
127
  var prompt_2 = require("../mind/prompt");
130
128
  Object.defineProperty(exports, "buildSystem", { enumerable: true, get: function () { return prompt_2.buildSystem; } });
129
+ function parseFinalAnswerPayload(argumentsText) {
130
+ try {
131
+ const parsed = JSON.parse(argumentsText);
132
+ if (typeof parsed === "string") {
133
+ return { answer: parsed };
134
+ }
135
+ if (!parsed || typeof parsed !== "object") {
136
+ return {};
137
+ }
138
+ const answer = typeof parsed.answer === "string" ? parsed.answer : undefined;
139
+ const rawIntent = parsed.intent;
140
+ const intent = rawIntent === "complete" || rawIntent === "blocked" || rawIntent === "direct_reply"
141
+ ? rawIntent
142
+ : undefined;
143
+ return { answer, intent };
144
+ }
145
+ catch {
146
+ return {};
147
+ }
148
+ }
149
+ function getFinalAnswerRetryError(mustResolveBeforeHandoff, intent, sawSteeringFollowUp) {
150
+ if (mustResolveBeforeHandoff && !intent) {
151
+ return "your final_answer is missing required intent. when you must keep going until done or blocked, call final_answer again with answer plus intent=complete, blocked, or direct_reply.";
152
+ }
153
+ if (mustResolveBeforeHandoff && intent === "direct_reply" && !sawSteeringFollowUp) {
154
+ return "your final_answer used intent=direct_reply without a newer steering follow-up. continue the unresolved work, or call final_answer again with intent=complete or blocked when appropriate.";
155
+ }
156
+ return "your final_answer was incomplete or malformed. call final_answer again with your complete response.";
157
+ }
131
158
  // Re-export kick utilities for backward compat
132
159
  var kicks_1 = require("./kicks");
133
160
  Object.defineProperty(exports, "hasToolIntent", { enumerable: true, get: function () { return kicks_1.hasToolIntent; } });
@@ -160,6 +187,68 @@ function stripLastToolCalls(messages) {
160
187
  }
161
188
  }
162
189
  }
190
+ // Roles that end a tool-result scan. When scanning forward from an assistant
191
+ // message, stop at the next assistant or user message (tool results must be
192
+ // adjacent to their originating assistant message).
193
+ const TOOL_SCAN_BOUNDARY_ROLES = new Set(["assistant", "user"]);
194
+ // Repair orphaned tool_calls and tool results anywhere in the message history.
195
+ // 1. If an assistant message has tool_calls but missing tool results, inject synthetic error results.
196
+ // 2. If a tool result's tool_call_id doesn't match any tool_calls in a preceding assistant message, remove it.
197
+ // This prevents 400 errors from the API after an aborted turn.
198
+ function repairOrphanedToolCalls(messages) {
199
+ // Pass 1: collect all valid tool_call IDs from assistant messages
200
+ const validCallIds = new Set();
201
+ for (const msg of messages) {
202
+ if (msg.role === "assistant") {
203
+ const asst = msg;
204
+ if (asst.tool_calls) {
205
+ for (const tc of asst.tool_calls)
206
+ validCallIds.add(tc.id);
207
+ }
208
+ }
209
+ }
210
+ // Pass 2: remove orphaned tool results (tool_call_id not in any assistant's tool_calls)
211
+ for (let i = messages.length - 1; i >= 0; i--) {
212
+ if (messages[i].role === "tool") {
213
+ const toolMsg = messages[i];
214
+ if (!validCallIds.has(toolMsg.tool_call_id)) {
215
+ messages.splice(i, 1);
216
+ }
217
+ }
218
+ }
219
+ // Pass 3: inject synthetic results for tool_calls missing their tool results
220
+ for (let i = 0; i < messages.length; i++) {
221
+ const msg = messages[i];
222
+ if (msg.role !== "assistant")
223
+ continue;
224
+ const asst = msg;
225
+ if (!asst.tool_calls || asst.tool_calls.length === 0)
226
+ continue;
227
+ // Collect tool result IDs that follow this assistant message
228
+ const resultIds = new Set();
229
+ for (let j = i + 1; j < messages.length; j++) {
230
+ const following = messages[j];
231
+ if (following.role === "tool") {
232
+ resultIds.add(following.tool_call_id);
233
+ }
234
+ else if (TOOL_SCAN_BOUNDARY_ROLES.has(following.role)) {
235
+ break;
236
+ }
237
+ }
238
+ const missing = asst.tool_calls.filter((tc) => !resultIds.has(tc.id));
239
+ if (missing.length > 0) {
240
+ const syntheticResults = missing.map((tc) => ({
241
+ role: "tool",
242
+ tool_call_id: tc.id,
243
+ content: "error: tool call was interrupted (previous turn timed out or was aborted)",
244
+ }));
245
+ let insertAt = i + 1;
246
+ while (insertAt < messages.length && messages[insertAt].role === "tool")
247
+ insertAt++;
248
+ messages.splice(insertAt, 0, ...syntheticResults);
249
+ }
250
+ }
251
+ }
163
252
  // Detect context overflow errors from Azure or MiniMax
164
253
  function isContextOverflow(err) {
165
254
  if (!(err instanceof Error))
@@ -265,20 +354,21 @@ async function runAgent(messages, callbacks, channel, signal, options) {
265
354
  }
266
355
  }
267
356
  await (0, associative_recall_1.injectAssociativeRecall)(messages);
268
- // kickCount and lastKickReason preserved but unused while kick detection is disabled.
269
- // let kickCount = 0;
270
- // let lastKickReason: KickReason | null = null;
271
357
  let done = false;
272
358
  let lastUsage;
273
359
  let overflowRetried = false;
274
360
  let retryCount = 0;
361
+ let outcome = "complete";
362
+ let completion;
363
+ let sawSteeringFollowUp = false;
364
+ let mustResolveBeforeHandoffActive = options?.mustResolveBeforeHandoff === true;
275
365
  // Prevent MaxListenersExceeded warning — each iteration adds a listener
276
366
  try {
277
367
  require("events").setMaxListeners(50, signal);
278
368
  }
279
369
  catch { /* unsupported */ }
280
370
  const toolPreferences = currentContext?.friend?.toolPreferences;
281
- const baseTools = (0, tools_1.getToolsForChannel)(channel ? (0, channel_1.getChannelCapabilities)(channel) : undefined, toolPreferences && Object.keys(toolPreferences).length > 0 ? toolPreferences : undefined);
371
+ const baseTools = options?.tools ?? (0, tools_1.getToolsForChannel)(channel ? (0, channel_1.getChannelCapabilities)(channel) : undefined, toolPreferences && Object.keys(toolPreferences).length > 0 ? toolPreferences : undefined, currentContext);
282
372
  // Rebase provider-owned turn state from canonical messages at user-turn start.
283
373
  // This prevents stale provider caches from replaying prior-turn context.
284
374
  providerRuntime.resetTurnState(messages);
@@ -290,6 +380,18 @@ async function runAgent(messages, callbacks, channel, signal, options) {
290
380
  const activeTools = toolChoiceRequired ? [...baseTools, tools_1.finalAnswerTool] : baseTools;
291
381
  const steeringFollowUps = options?.drainSteeringFollowUps?.() ?? [];
292
382
  if (steeringFollowUps.length > 0) {
383
+ const hasSupersedingFollowUp = steeringFollowUps.some((followUp) => followUp.effect === "clear_and_supersede");
384
+ if (hasSupersedingFollowUp) {
385
+ mustResolveBeforeHandoffActive = false;
386
+ options?.setMustResolveBeforeHandoff?.(false);
387
+ outcome = "superseded";
388
+ break;
389
+ }
390
+ if (steeringFollowUps.some((followUp) => followUp.effect === "set_no_handoff")) {
391
+ mustResolveBeforeHandoffActive = true;
392
+ options?.setMustResolveBeforeHandoff?.(true);
393
+ }
394
+ sawSteeringFollowUp = true;
293
395
  for (const followUp of steeringFollowUps) {
294
396
  messages.push({ role: "user", content: followUp.text });
295
397
  }
@@ -297,8 +399,10 @@ async function runAgent(messages, callbacks, channel, signal, options) {
297
399
  }
298
400
  // Yield so pending I/O (stdin Ctrl-C) can be processed between iterations
299
401
  await new Promise((r) => setImmediate(r));
300
- if (signal?.aborted)
402
+ if (signal?.aborted) {
403
+ outcome = "aborted";
301
404
  break;
405
+ }
302
406
  try {
303
407
  callbacks.onModelStart();
304
408
  const result = await providerRuntime.streamTurn({
@@ -332,24 +436,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
332
436
  msg._reasoning_items = reasoningItems;
333
437
  }
334
438
  if (!result.toolCalls.length) {
335
- // Kick detection is disabled while tool_choice: required + final_answer
336
- // is the primary loop control mechanism. The model should never reach
337
- // this path (tool_choice: required forces a tool call), but if it does,
338
- // accept the response as-is rather than risk false-positive kicks.
339
- //
340
- // Preserved for future use — re-enable by uncommenting:
341
- // const kick = detectKick(result.content, options);
342
- // if (kick) {
343
- // kickCount++;
344
- // lastKickReason = kick.reason;
345
- // callbacks.onKick?.();
346
- // const kickContent = result.content
347
- // ? result.content + "\n\n" + kick.message
348
- // : kick.message;
349
- // messages.push({ role: "assistant", content: kickContent });
350
- // providerRuntime.resetTurnState(messages);
351
- // continue;
352
- // }
439
+ // No tool calls accept response as-is.
440
+ // (Kick detection disabled; tool_choice: required + final_answer
441
+ // is the primary loop control. See src/heart/kicks.ts to re-enable.)
353
442
  messages.push(msg);
354
443
  done = true;
355
444
  }
@@ -358,22 +447,17 @@ async function runAgent(messages, callbacks, channel, signal, options) {
358
447
  const isSoleFinalAnswer = result.toolCalls.length === 1 && result.toolCalls[0].name === "final_answer";
359
448
  if (isSoleFinalAnswer) {
360
449
  // Extract answer from the tool call arguments.
361
- // Supports: {"answer":"text"}, "text" (JSON string), retry on failure.
362
- let answer;
363
- try {
364
- const parsed = JSON.parse(result.toolCalls[0].arguments);
365
- if (typeof parsed === "string") {
366
- answer = parsed;
367
- }
368
- else if (parsed.answer != null) {
369
- answer = parsed.answer;
370
- }
371
- // else: valid JSON but no answer field — answer stays undefined (retry)
372
- }
373
- catch {
374
- // JSON parsing failed (e.g. truncated output) — answer stays undefined (retry)
375
- }
376
- if (answer != null) {
450
+ // Supports: {"answer":"text","intent":"..."} or "text" (JSON string).
451
+ const { answer, intent } = parseFinalAnswerPayload(result.toolCalls[0].arguments);
452
+ const validDirectReply = mustResolveBeforeHandoffActive && intent === "direct_reply" && sawSteeringFollowUp;
453
+ const validTerminalIntent = intent === "complete" || intent === "blocked";
454
+ const validClosure = answer != null
455
+ && (!mustResolveBeforeHandoffActive || validDirectReply || validTerminalIntent);
456
+ if (validClosure) {
457
+ completion = {
458
+ answer,
459
+ intent: validDirectReply ? "direct_reply" : intent === "blocked" ? "blocked" : "complete",
460
+ };
377
461
  if (result.finalAnswerStreamed) {
378
462
  // The streaming layer already parsed and emitted the answer
379
463
  // progressively via FinalAnswerParser. Skip clearing and
@@ -386,19 +470,26 @@ async function runAgent(messages, callbacks, channel, signal, options) {
386
470
  // Never truncate -- channel adapters handle splitting long messages.
387
471
  callbacks.onTextChunk(answer);
388
472
  }
389
- // Keep the full assistant message (with tool_calls) for debuggability,
390
- // plus a synthetic tool response so the conversation stays valid on resume.
391
473
  messages.push(msg);
392
- messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: "(delivered)" });
393
- providerRuntime.appendToolOutput(result.toolCalls[0].id, "(delivered)");
394
- done = true;
474
+ if (validDirectReply) {
475
+ const resumeWork = "direct reply delivered. resume the unresolved obligation now and keep working until you can finish or clearly report that you are blocked.";
476
+ messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: resumeWork });
477
+ providerRuntime.appendToolOutput(result.toolCalls[0].id, resumeWork);
478
+ }
479
+ else {
480
+ const delivered = "(delivered)";
481
+ messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: delivered });
482
+ providerRuntime.appendToolOutput(result.toolCalls[0].id, delivered);
483
+ outcome = intent === "blocked" ? "blocked" : "complete";
484
+ done = true;
485
+ }
395
486
  }
396
487
  else {
397
488
  // Answer is undefined -- the model's final_answer was incomplete or
398
489
  // malformed. Clear any partial streamed text or noise, then push the
399
490
  // assistant msg + error tool result and let the model try again.
400
491
  callbacks.onClearText?.();
401
- const retryError = "your final_answer was incomplete or malformed. call final_answer again with your complete response.";
492
+ const retryError = getFinalAnswerRetryError(mustResolveBeforeHandoffActive, intent, sawSteeringFollowUp);
402
493
  messages.push(msg);
403
494
  messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: retryError });
404
495
  providerRuntime.appendToolOutput(result.toolCalls[0].id, retryError);
@@ -444,7 +535,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
444
535
  let toolResult;
445
536
  let success;
446
537
  try {
447
- toolResult = await (0, tools_1.execTool)(tc.name, args, options?.toolContext);
538
+ const execToolFn = options?.execTool ?? tools_1.execTool;
539
+ toolResult = await execToolFn(tc.name, args, options?.toolContext);
448
540
  success = true;
449
541
  }
450
542
  catch (e) {
@@ -461,6 +553,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
461
553
  // Abort is not an error — just stop cleanly
462
554
  if (signal?.aborted) {
463
555
  stripLastToolCalls(messages);
556
+ outcome = "aborted";
464
557
  break;
465
558
  }
466
559
  // Context overflow: trim aggressively and retry once
@@ -495,6 +588,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
495
588
  });
496
589
  if (aborted) {
497
590
  stripLastToolCalls(messages);
591
+ outcome = "aborted";
498
592
  break;
499
593
  }
500
594
  providerRuntime.resetTurnState(messages);
@@ -510,6 +604,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
510
604
  meta: {},
511
605
  });
512
606
  stripLastToolCalls(messages);
607
+ outcome = "errored";
513
608
  done = true;
514
609
  }
515
610
  }
@@ -520,5 +615,5 @@ async function runAgent(messages, callbacks, channel, signal, options) {
520
615
  message: "runAgent turn completed",
521
616
  meta: { done },
522
617
  });
523
- return { usage: lastUsage };
618
+ return { usage: lastUsage, outcome, completion };
524
619
  }
@@ -0,0 +1,81 @@
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.listEnabledBundleAgents = listEnabledBundleAgents;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const identity_1 = require("../identity");
40
+ const runtime_1 = require("../../nerves/runtime");
41
+ function listEnabledBundleAgents(options = {}) {
42
+ const bundlesRoot = options.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
43
+ const readdirSync = options.readdirSync ?? fs.readdirSync;
44
+ const readFileSync = options.readFileSync ?? fs.readFileSync;
45
+ let entries;
46
+ try {
47
+ entries = readdirSync(bundlesRoot, { withFileTypes: true });
48
+ }
49
+ catch {
50
+ (0, runtime_1.emitNervesEvent)({
51
+ level: "warn",
52
+ component: "daemon",
53
+ event: "daemon.agent_discovery_failed",
54
+ message: "failed to read bundle root for daemon agent discovery",
55
+ meta: { bundlesRoot },
56
+ });
57
+ return [];
58
+ }
59
+ const discovered = [];
60
+ for (const entry of entries) {
61
+ if (!entry.isDirectory() || !entry.name.endsWith(".ouro"))
62
+ continue;
63
+ const agentName = entry.name.slice(0, -5);
64
+ const configPath = path.join(bundlesRoot, entry.name, "agent.json");
65
+ let enabled = true;
66
+ try {
67
+ const raw = readFileSync(configPath, "utf-8");
68
+ const parsed = JSON.parse(raw);
69
+ if (typeof parsed.enabled === "boolean") {
70
+ enabled = parsed.enabled;
71
+ }
72
+ }
73
+ catch {
74
+ continue;
75
+ }
76
+ if (enabled) {
77
+ discovered.push(agentName);
78
+ }
79
+ }
80
+ return discovered.sort((left, right) => left.localeCompare(right));
81
+ }