@ouro.bot/cli 0.1.0-alpha.7 → 0.1.0-alpha.71

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 (123) 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 +147 -205
  5. package/assets/ouroboros.png +0 -0
  6. package/changelog.json +395 -0
  7. package/dist/heart/active-work.js +178 -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 +68 -23
  12. package/dist/heart/core.js +282 -92
  13. package/dist/heart/cross-chat-delivery.js +146 -0
  14. package/dist/heart/daemon/agent-discovery.js +81 -0
  15. package/dist/heart/daemon/auth-flow.js +409 -0
  16. package/dist/heart/daemon/daemon-cli.js +1408 -248
  17. package/dist/heart/daemon/daemon-entry.js +55 -6
  18. package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
  19. package/dist/heart/daemon/daemon.js +216 -10
  20. package/dist/heart/daemon/hatch-animation.js +10 -3
  21. package/dist/heart/daemon/hatch-flow.js +7 -82
  22. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  23. package/dist/heart/daemon/launchd.js +159 -0
  24. package/dist/heart/daemon/log-tailer.js +4 -3
  25. package/dist/heart/daemon/message-router.js +17 -8
  26. package/dist/heart/daemon/ouro-bot-entry.js +0 -0
  27. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  28. package/dist/heart/daemon/ouro-entry.js +0 -0
  29. package/dist/heart/daemon/ouro-path-installer.js +178 -0
  30. package/dist/heart/daemon/ouro-uti.js +11 -2
  31. package/dist/heart/daemon/process-manager.js +14 -1
  32. package/dist/heart/daemon/run-hooks.js +37 -0
  33. package/dist/heart/daemon/runtime-logging.js +58 -15
  34. package/dist/heart/daemon/runtime-metadata.js +219 -0
  35. package/dist/heart/daemon/runtime-mode.js +67 -0
  36. package/dist/heart/daemon/sense-manager.js +307 -0
  37. package/dist/heart/daemon/skill-management-installer.js +94 -0
  38. package/dist/heart/daemon/socket-client.js +202 -0
  39. package/dist/heart/daemon/specialist-orchestrator.js +53 -84
  40. package/dist/heart/daemon/specialist-prompt.js +64 -5
  41. package/dist/heart/daemon/specialist-tools.js +213 -58
  42. package/dist/heart/daemon/staged-restart.js +114 -0
  43. package/dist/heart/daemon/thoughts.js +379 -0
  44. package/dist/heart/daemon/update-checker.js +111 -0
  45. package/dist/heart/daemon/update-hooks.js +138 -0
  46. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  47. package/dist/heart/delegation.js +62 -0
  48. package/dist/heart/identity.js +126 -21
  49. package/dist/heart/kicks.js +1 -19
  50. package/dist/heart/model-capabilities.js +48 -0
  51. package/dist/heart/progress-story.js +42 -0
  52. package/dist/heart/providers/anthropic.js +74 -9
  53. package/dist/heart/providers/azure.js +86 -7
  54. package/dist/heart/providers/github-copilot.js +149 -0
  55. package/dist/heart/providers/minimax.js +4 -0
  56. package/dist/heart/providers/openai-codex.js +12 -3
  57. package/dist/heart/safe-workspace.js +228 -0
  58. package/dist/heart/sense-truth.js +61 -0
  59. package/dist/heart/session-activity.js +169 -0
  60. package/dist/heart/session-recall.js +116 -0
  61. package/dist/heart/streaming.js +100 -22
  62. package/dist/heart/target-resolution.js +123 -0
  63. package/dist/heart/turn-coordinator.js +28 -0
  64. package/dist/mind/associative-recall.js +14 -2
  65. package/dist/mind/bundle-manifest.js +70 -0
  66. package/dist/mind/context.js +27 -11
  67. package/dist/mind/first-impressions.js +16 -2
  68. package/dist/mind/friends/channel.js +35 -0
  69. package/dist/mind/friends/group-context.js +144 -0
  70. package/dist/mind/friends/store-file.js +19 -0
  71. package/dist/mind/friends/trust-explanation.js +74 -0
  72. package/dist/mind/friends/types.js +8 -0
  73. package/dist/mind/memory.js +27 -26
  74. package/dist/mind/pending.js +72 -9
  75. package/dist/mind/phrases.js +1 -0
  76. package/dist/mind/prompt.js +358 -77
  77. package/dist/mind/token-estimate.js +8 -12
  78. package/dist/nerves/cli-logging.js +15 -2
  79. package/dist/nerves/coverage/run-artifacts.js +1 -1
  80. package/dist/repertoire/ado-client.js +4 -2
  81. package/dist/repertoire/coding/feedback.js +134 -0
  82. package/dist/repertoire/coding/index.js +4 -1
  83. package/dist/repertoire/coding/manager.js +62 -4
  84. package/dist/repertoire/coding/spawner.js +3 -3
  85. package/dist/repertoire/coding/tools.js +41 -2
  86. package/dist/repertoire/data/ado-endpoints.json +188 -0
  87. package/dist/repertoire/guardrails.js +279 -0
  88. package/dist/repertoire/mcp-client.js +254 -0
  89. package/dist/repertoire/mcp-manager.js +195 -0
  90. package/dist/repertoire/skills.js +3 -26
  91. package/dist/repertoire/tasks/board.js +12 -0
  92. package/dist/repertoire/tasks/index.js +23 -9
  93. package/dist/repertoire/tasks/transitions.js +1 -2
  94. package/dist/repertoire/tools-base.js +642 -251
  95. package/dist/repertoire/tools-bluebubbles.js +93 -0
  96. package/dist/repertoire/tools-teams.js +58 -25
  97. package/dist/repertoire/tools.js +93 -52
  98. package/dist/senses/bluebubbles-client.js +210 -5
  99. package/dist/senses/bluebubbles-entry.js +2 -0
  100. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  101. package/dist/senses/bluebubbles-media.js +339 -0
  102. package/dist/senses/bluebubbles-model.js +12 -4
  103. package/dist/senses/bluebubbles-mutation-log.js +45 -5
  104. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  105. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  106. package/dist/senses/bluebubbles.js +893 -45
  107. package/dist/senses/cli-layout.js +87 -0
  108. package/dist/senses/cli.js +348 -144
  109. package/dist/senses/continuity.js +94 -0
  110. package/dist/senses/debug-activity.js +148 -0
  111. package/dist/senses/inner-dialog-worker.js +47 -18
  112. package/dist/senses/inner-dialog.js +333 -84
  113. package/dist/senses/pipeline.js +278 -0
  114. package/dist/senses/teams.js +573 -129
  115. package/dist/senses/trust-gate.js +112 -2
  116. package/package.json +14 -3
  117. package/subagents/README.md +4 -70
  118. package/dist/heart/daemon/specialist-session.js +0 -142
  119. package/dist/heart/daemon/subagent-installer.js +0 -125
  120. package/dist/inner-worker-entry.js +0 -4
  121. package/subagents/work-doer.md +0 -233
  122. package/subagents/work-merger.md +0 -624
  123. package/subagents/work-planner.md +0 -373
@@ -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");
@@ -26,13 +24,43 @@ const anthropic_1 = require("./providers/anthropic");
26
24
  const azure_1 = require("./providers/azure");
27
25
  const minimax_1 = require("./providers/minimax");
28
26
  const openai_codex_1 = require("./providers/openai-codex");
27
+ const github_copilot_1 = require("./providers/github-copilot");
29
28
  let _providerRuntime = null;
29
+ function getProviderRuntimeFingerprint() {
30
+ const provider = (0, identity_1.loadAgentConfig)().provider;
31
+ /* v8 ignore next -- switch: not all provider branches exercised in CI @preserve */
32
+ switch (provider) {
33
+ case "azure": {
34
+ const { apiKey, endpoint, deployment, modelName, apiVersion, managedIdentityClientId } = (0, config_1.getAzureConfig)();
35
+ return JSON.stringify({ provider, apiKey, endpoint, deployment, modelName, apiVersion, managedIdentityClientId });
36
+ }
37
+ case "anthropic": {
38
+ const { model, setupToken } = (0, config_1.getAnthropicConfig)();
39
+ return JSON.stringify({ provider, model, setupToken });
40
+ }
41
+ case "minimax": {
42
+ const { apiKey, model } = (0, config_1.getMinimaxConfig)();
43
+ return JSON.stringify({ provider, apiKey, model });
44
+ }
45
+ case "openai-codex": {
46
+ const { model, oauthAccessToken } = (0, config_1.getOpenAICodexConfig)();
47
+ return JSON.stringify({ provider, model, oauthAccessToken });
48
+ }
49
+ /* v8 ignore start -- fingerprint: tested via provider init tests @preserve */
50
+ case "github-copilot": {
51
+ const { model, githubToken, baseUrl } = (0, config_1.getGithubCopilotConfig)();
52
+ return JSON.stringify({ provider, model, githubToken, baseUrl });
53
+ }
54
+ /* v8 ignore stop */
55
+ }
56
+ }
30
57
  function createProviderRegistry() {
31
58
  const factories = {
32
59
  azure: azure_1.createAzureProviderRuntime,
33
60
  anthropic: anthropic_1.createAnthropicProviderRuntime,
34
61
  minimax: minimax_1.createMinimaxProviderRuntime,
35
62
  "openai-codex": openai_codex_1.createOpenAICodexProviderRuntime,
63
+ "github-copilot": github_copilot_1.createGithubCopilotProviderRuntime,
36
64
  };
37
65
  return {
38
66
  resolve() {
@@ -42,42 +70,44 @@ function createProviderRegistry() {
42
70
  };
43
71
  }
44
72
  function getProviderRuntime() {
45
- if (!_providerRuntime) {
46
- try {
47
- _providerRuntime = createProviderRegistry().resolve();
48
- }
49
- catch (error) {
50
- const msg = error instanceof Error ? error.message : String(error);
51
- (0, runtime_1.emitNervesEvent)({
52
- level: "error",
53
- event: "engine.provider_init_error",
54
- component: "engine",
55
- message: msg,
56
- meta: {},
57
- });
58
- // eslint-disable-next-line no-console -- pre-boot guard: provider init failure
59
- console.error(`\n[fatal] ${msg}\n`);
60
- process.exit(1);
61
- throw new Error("unreachable");
62
- }
63
- if (!_providerRuntime) {
64
- (0, runtime_1.emitNervesEvent)({
65
- level: "error",
66
- event: "engine.provider_init_error",
67
- component: "engine",
68
- message: "provider runtime could not be initialized.",
69
- meta: {},
70
- });
71
- process.exit(1);
72
- throw new Error("unreachable");
73
+ try {
74
+ const fingerprint = getProviderRuntimeFingerprint();
75
+ if (!_providerRuntime || _providerRuntime.fingerprint !== fingerprint) {
76
+ const runtime = createProviderRegistry().resolve();
77
+ _providerRuntime = runtime ? { fingerprint, runtime } : null;
73
78
  }
74
79
  }
75
- return _providerRuntime;
80
+ catch (error) {
81
+ const msg = error instanceof Error ? error.message : String(error);
82
+ (0, runtime_1.emitNervesEvent)({
83
+ level: "error",
84
+ event: "engine.provider_init_error",
85
+ component: "engine",
86
+ message: msg,
87
+ meta: {},
88
+ });
89
+ // eslint-disable-next-line no-console -- pre-boot guard: provider init failure
90
+ console.error(`\n[fatal] ${msg}\n`);
91
+ process.exit(1);
92
+ throw new Error("unreachable");
93
+ }
94
+ if (!_providerRuntime) {
95
+ (0, runtime_1.emitNervesEvent)({
96
+ level: "error",
97
+ event: "engine.provider_init_error",
98
+ component: "engine",
99
+ message: "provider runtime could not be initialized.",
100
+ meta: {},
101
+ });
102
+ process.exit(1);
103
+ throw new Error("unreachable");
104
+ }
105
+ return _providerRuntime.runtime;
76
106
  }
77
107
  /**
78
- * Clear the cached provider runtime so the next call to getProviderRuntime()
79
- * re-creates it from current config. Used by the adoption specialist to
80
- * switch provider context without restarting the process.
108
+ * Clear the cached provider runtime so the next access re-creates it from
109
+ * current config. Runtime access also auto-refreshes when the selected
110
+ * provider fingerprint changes on disk.
81
111
  */
82
112
  function resetProviderRuntime() {
83
113
  _providerRuntime = null;
@@ -104,14 +134,19 @@ function createSummarize() {
104
134
  };
105
135
  }
106
136
  function getProviderDisplayLabel() {
107
- const model = getModel();
137
+ const provider = (0, identity_1.loadAgentConfig)().provider;
108
138
  const providerLabelBuilders = {
109
- azure: () => `azure openai (${(0, config_1.getAzureConfig)().deployment || "default"}, model: ${model})`,
110
- anthropic: () => `anthropic (${model})`,
111
- minimax: () => `minimax (${model})`,
112
- "openai-codex": () => `openai codex (${model})`,
139
+ azure: () => {
140
+ const config = (0, config_1.getAzureConfig)();
141
+ return `azure openai (${config.deployment || "default"}, model: ${config.modelName || "unknown"})`;
142
+ },
143
+ anthropic: () => `anthropic (${(0, config_1.getAnthropicConfig)().model || "unknown"})`,
144
+ minimax: () => `minimax (${(0, config_1.getMinimaxConfig)().model || "unknown"})`,
145
+ "openai-codex": () => `openai codex (${(0, config_1.getOpenAICodexConfig)().model || "unknown"})`,
146
+ /* v8 ignore next -- branch: tested via display label unit test @preserve */
147
+ "github-copilot": () => `github copilot (${(0, config_1.getGithubCopilotConfig)().model || "unknown"})`,
113
148
  };
114
- return providerLabelBuilders[getProvider()]();
149
+ return providerLabelBuilders[provider]();
115
150
  }
116
151
  // Re-export tools, execTool, summarizeArgs from ./tools for backward compat
117
152
  var tools_2 = require("../repertoire/tools");
@@ -128,6 +163,35 @@ Object.defineProperty(exports, "toResponsesTools", { enumerable: true, get: func
128
163
  // Re-export prompt functions for backward compat
129
164
  var prompt_2 = require("../mind/prompt");
130
165
  Object.defineProperty(exports, "buildSystem", { enumerable: true, get: function () { return prompt_2.buildSystem; } });
166
+ function parseFinalAnswerPayload(argumentsText) {
167
+ try {
168
+ const parsed = JSON.parse(argumentsText);
169
+ if (typeof parsed === "string") {
170
+ return { answer: parsed };
171
+ }
172
+ if (!parsed || typeof parsed !== "object") {
173
+ return {};
174
+ }
175
+ const answer = typeof parsed.answer === "string" ? parsed.answer : undefined;
176
+ const rawIntent = parsed.intent;
177
+ const intent = rawIntent === "complete" || rawIntent === "blocked" || rawIntent === "direct_reply"
178
+ ? rawIntent
179
+ : undefined;
180
+ return { answer, intent };
181
+ }
182
+ catch {
183
+ return {};
184
+ }
185
+ }
186
+ function getFinalAnswerRetryError(mustResolveBeforeHandoff, intent, sawSteeringFollowUp) {
187
+ if (mustResolveBeforeHandoff && !intent) {
188
+ 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.";
189
+ }
190
+ if (mustResolveBeforeHandoff && intent === "direct_reply" && !sawSteeringFollowUp) {
191
+ 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.";
192
+ }
193
+ return "your final_answer was incomplete or malformed. call final_answer again with your complete response.";
194
+ }
131
195
  // Re-export kick utilities for backward compat
132
196
  var kicks_1 = require("./kicks");
133
197
  Object.defineProperty(exports, "hasToolIntent", { enumerable: true, get: function () { return kicks_1.hasToolIntent; } });
@@ -160,6 +224,68 @@ function stripLastToolCalls(messages) {
160
224
  }
161
225
  }
162
226
  }
227
+ // Roles that end a tool-result scan. When scanning forward from an assistant
228
+ // message, stop at the next assistant or user message (tool results must be
229
+ // adjacent to their originating assistant message).
230
+ const TOOL_SCAN_BOUNDARY_ROLES = new Set(["assistant", "user"]);
231
+ // Repair orphaned tool_calls and tool results anywhere in the message history.
232
+ // 1. If an assistant message has tool_calls but missing tool results, inject synthetic error results.
233
+ // 2. If a tool result's tool_call_id doesn't match any tool_calls in a preceding assistant message, remove it.
234
+ // This prevents 400 errors from the API after an aborted turn.
235
+ function repairOrphanedToolCalls(messages) {
236
+ // Pass 1: collect all valid tool_call IDs from assistant messages
237
+ const validCallIds = new Set();
238
+ for (const msg of messages) {
239
+ if (msg.role === "assistant") {
240
+ const asst = msg;
241
+ if (asst.tool_calls) {
242
+ for (const tc of asst.tool_calls)
243
+ validCallIds.add(tc.id);
244
+ }
245
+ }
246
+ }
247
+ // Pass 2: remove orphaned tool results (tool_call_id not in any assistant's tool_calls)
248
+ for (let i = messages.length - 1; i >= 0; i--) {
249
+ if (messages[i].role === "tool") {
250
+ const toolMsg = messages[i];
251
+ if (!validCallIds.has(toolMsg.tool_call_id)) {
252
+ messages.splice(i, 1);
253
+ }
254
+ }
255
+ }
256
+ // Pass 3: inject synthetic results for tool_calls missing their tool results
257
+ for (let i = 0; i < messages.length; i++) {
258
+ const msg = messages[i];
259
+ if (msg.role !== "assistant")
260
+ continue;
261
+ const asst = msg;
262
+ if (!asst.tool_calls || asst.tool_calls.length === 0)
263
+ continue;
264
+ // Collect tool result IDs that follow this assistant message
265
+ const resultIds = new Set();
266
+ for (let j = i + 1; j < messages.length; j++) {
267
+ const following = messages[j];
268
+ if (following.role === "tool") {
269
+ resultIds.add(following.tool_call_id);
270
+ }
271
+ else if (TOOL_SCAN_BOUNDARY_ROLES.has(following.role)) {
272
+ break;
273
+ }
274
+ }
275
+ const missing = asst.tool_calls.filter((tc) => !resultIds.has(tc.id));
276
+ if (missing.length > 0) {
277
+ const syntheticResults = missing.map((tc) => ({
278
+ role: "tool",
279
+ tool_call_id: tc.id,
280
+ content: "error: tool call was interrupted (previous turn timed out or was aborted)",
281
+ }));
282
+ let insertAt = i + 1;
283
+ while (insertAt < messages.length && messages[insertAt].role === "tool")
284
+ insertAt++;
285
+ messages.splice(insertAt, 0, ...syntheticResults);
286
+ }
287
+ }
288
+ }
163
289
  // Detect context overflow errors from Azure or MiniMax
164
290
  function isContextOverflow(err) {
165
291
  if (!(err instanceof Error))
@@ -242,7 +368,12 @@ async function runAgent(messages, callbacks, channel, signal, options) {
242
368
  // so turn execution remains consistent and non-fatal.
243
369
  if (channel) {
244
370
  try {
245
- const refreshed = await (0, prompt_1.buildSystem)(channel, options, currentContext);
371
+ const buildSystemOptions = {
372
+ ...options,
373
+ providerCapabilities: providerRuntime.capabilities,
374
+ supportedReasoningEfforts: providerRuntime.supportedReasoningEfforts,
375
+ };
376
+ const refreshed = await (0, prompt_1.buildSystem)(channel, buildSystemOptions, currentContext);
246
377
  upsertSystemPrompt(messages, refreshed);
247
378
  }
248
379
  catch (error) {
@@ -265,20 +396,30 @@ async function runAgent(messages, callbacks, channel, signal, options) {
265
396
  }
266
397
  }
267
398
  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
399
  let done = false;
272
400
  let lastUsage;
273
401
  let overflowRetried = false;
274
402
  let retryCount = 0;
403
+ let outcome = "complete";
404
+ let completion;
405
+ let sawSteeringFollowUp = false;
406
+ let mustResolveBeforeHandoffActive = options?.mustResolveBeforeHandoff === true;
407
+ let currentReasoningEffort = "medium";
275
408
  // Prevent MaxListenersExceeded warning — each iteration adds a listener
276
409
  try {
277
410
  require("events").setMaxListeners(50, signal);
278
411
  }
279
412
  catch { /* unsupported */ }
280
413
  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);
414
+ const baseTools = options?.tools ?? (0, tools_1.getToolsForChannel)(channel ? (0, channel_1.getChannelCapabilities)(channel) : undefined, toolPreferences && Object.keys(toolPreferences).length > 0 ? toolPreferences : undefined, currentContext, providerRuntime.capabilities);
415
+ // Augment tool context with reasoning effort controls from provider
416
+ const augmentedToolContext = options?.toolContext
417
+ ? {
418
+ ...options.toolContext,
419
+ supportedReasoningEfforts: providerRuntime.supportedReasoningEfforts,
420
+ setReasoningEffort: (level) => { currentReasoningEffort = level; },
421
+ }
422
+ : undefined;
282
423
  // Rebase provider-owned turn state from canonical messages at user-turn start.
283
424
  // This prevents stale provider caches from replaying prior-turn context.
284
425
  providerRuntime.resetTurnState(messages);
@@ -287,9 +428,23 @@ async function runAgent(messages, callbacks, channel, signal, options) {
287
428
  // so the model can signal completion. With tool_choice: required, the
288
429
  // model must call a tool every turn — final_answer is how it exits.
289
430
  // Overridable via options.toolChoiceRequired = false (e.g. CLI).
290
- const activeTools = toolChoiceRequired ? [...baseTools, tools_1.finalAnswerTool] : baseTools;
431
+ const activeTools = toolChoiceRequired
432
+ ? [...baseTools, ...(currentContext?.isGroupChat ? [tools_1.noResponseTool] : []), tools_1.finalAnswerTool]
433
+ : baseTools;
291
434
  const steeringFollowUps = options?.drainSteeringFollowUps?.() ?? [];
292
435
  if (steeringFollowUps.length > 0) {
436
+ const hasSupersedingFollowUp = steeringFollowUps.some((followUp) => followUp.effect === "clear_and_supersede");
437
+ if (hasSupersedingFollowUp) {
438
+ mustResolveBeforeHandoffActive = false;
439
+ options?.setMustResolveBeforeHandoff?.(false);
440
+ outcome = "superseded";
441
+ break;
442
+ }
443
+ if (steeringFollowUps.some((followUp) => followUp.effect === "set_no_handoff")) {
444
+ mustResolveBeforeHandoffActive = true;
445
+ options?.setMustResolveBeforeHandoff?.(true);
446
+ }
447
+ sawSteeringFollowUp = true;
293
448
  for (const followUp of steeringFollowUps) {
294
449
  messages.push({ role: "user", content: followUp.text });
295
450
  }
@@ -297,8 +452,10 @@ async function runAgent(messages, callbacks, channel, signal, options) {
297
452
  }
298
453
  // Yield so pending I/O (stdin Ctrl-C) can be processed between iterations
299
454
  await new Promise((r) => setImmediate(r));
300
- if (signal?.aborted)
455
+ if (signal?.aborted) {
456
+ outcome = "aborted";
301
457
  break;
458
+ }
302
459
  try {
303
460
  callbacks.onModelStart();
304
461
  const result = await providerRuntime.streamTurn({
@@ -308,6 +465,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
308
465
  signal,
309
466
  traceId,
310
467
  toolChoiceRequired,
468
+ reasoningEffort: currentReasoningEffort,
311
469
  });
312
470
  // Track usage from the latest API call
313
471
  if (result.usage)
@@ -331,49 +489,39 @@ async function runAgent(messages, callbacks, channel, signal, options) {
331
489
  if (reasoningItems.length > 0) {
332
490
  msg._reasoning_items = reasoningItems;
333
491
  }
492
+ // Store thinking blocks (Anthropic) on the assistant message for round-tripping
493
+ const thinkingItems = result.outputItems.filter((item) => "type" in item && (item.type === "thinking" || item.type === "redacted_thinking"));
494
+ if (thinkingItems.length > 0) {
495
+ msg._thinking_blocks = thinkingItems;
496
+ }
497
+ // Phase annotation for Codex provider
498
+ const hasPhaseAnnotation = providerRuntime.capabilities.has("phase-annotation");
499
+ const isSoleFinalAnswer = result.toolCalls.length === 1 && result.toolCalls[0].name === "final_answer";
500
+ if (hasPhaseAnnotation) {
501
+ msg.phase = isSoleFinalAnswer ? "final_answer" : "commentary";
502
+ }
334
503
  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
- // }
504
+ // No tool calls accept response as-is.
505
+ // (Kick detection disabled; tool_choice: required + final_answer
506
+ // is the primary loop control. See src/heart/kicks.ts to re-enable.)
353
507
  messages.push(msg);
354
508
  done = true;
355
509
  }
356
510
  else {
357
511
  // Check for final_answer sole call: intercept before tool execution
358
- const isSoleFinalAnswer = result.toolCalls.length === 1 && result.toolCalls[0].name === "final_answer";
359
512
  if (isSoleFinalAnswer) {
360
513
  // 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) {
514
+ // Supports: {"answer":"text","intent":"..."} or "text" (JSON string).
515
+ const { answer, intent } = parseFinalAnswerPayload(result.toolCalls[0].arguments);
516
+ const validDirectReply = mustResolveBeforeHandoffActive && intent === "direct_reply" && sawSteeringFollowUp;
517
+ const validTerminalIntent = intent === "complete" || intent === "blocked";
518
+ const validClosure = answer != null
519
+ && (!mustResolveBeforeHandoffActive || validDirectReply || validTerminalIntent);
520
+ if (validClosure) {
521
+ completion = {
522
+ answer,
523
+ intent: validDirectReply ? "direct_reply" : intent === "blocked" ? "blocked" : "complete",
524
+ };
377
525
  if (result.finalAnswerStreamed) {
378
526
  // The streaming layer already parsed and emitted the answer
379
527
  // progressively via FinalAnswerParser. Skip clearing and
@@ -386,27 +534,58 @@ async function runAgent(messages, callbacks, channel, signal, options) {
386
534
  // Never truncate -- channel adapters handle splitting long messages.
387
535
  callbacks.onTextChunk(answer);
388
536
  }
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
537
  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;
538
+ if (validDirectReply) {
539
+ const resumeWork = "direct reply delivered. resume the unresolved obligation now and keep working until you can finish or clearly report that you are blocked.";
540
+ messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: resumeWork });
541
+ providerRuntime.appendToolOutput(result.toolCalls[0].id, resumeWork);
542
+ }
543
+ else {
544
+ const delivered = "(delivered)";
545
+ messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: delivered });
546
+ providerRuntime.appendToolOutput(result.toolCalls[0].id, delivered);
547
+ outcome = intent === "blocked" ? "blocked" : "complete";
548
+ done = true;
549
+ }
395
550
  }
396
551
  else {
397
552
  // Answer is undefined -- the model's final_answer was incomplete or
398
553
  // malformed. Clear any partial streamed text or noise, then push the
399
554
  // assistant msg + error tool result and let the model try again.
400
555
  callbacks.onClearText?.();
401
- const retryError = "your final_answer was incomplete or malformed. call final_answer again with your complete response.";
556
+ const retryError = getFinalAnswerRetryError(mustResolveBeforeHandoffActive, intent, sawSteeringFollowUp);
402
557
  messages.push(msg);
403
558
  messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: retryError });
404
559
  providerRuntime.appendToolOutput(result.toolCalls[0].id, retryError);
405
560
  }
406
561
  continue;
407
562
  }
563
+ // Check for no_response sole call: intercept before tool execution
564
+ const isSoleNoResponse = result.toolCalls.length === 1 && result.toolCalls[0].name === "no_response";
565
+ if (isSoleNoResponse) {
566
+ let reason;
567
+ try {
568
+ const parsed = JSON.parse(result.toolCalls[0].arguments);
569
+ if (typeof parsed?.reason === "string")
570
+ reason = parsed.reason;
571
+ }
572
+ catch { /* ignore */ }
573
+ (0, runtime_1.emitNervesEvent)({
574
+ component: "engine",
575
+ event: "engine.no_response",
576
+ message: "agent declined to respond in group chat",
577
+ meta: { ...(reason ? { reason } : {}) },
578
+ });
579
+ messages.push(msg);
580
+ const silenced = "(silenced)";
581
+ messages.push({ role: "tool", tool_call_id: result.toolCalls[0].id, content: silenced });
582
+ providerRuntime.appendToolOutput(result.toolCalls[0].id, silenced);
583
+ outcome = "no_response";
584
+ done = true;
585
+ continue;
586
+ }
408
587
  messages.push(msg);
409
- // SHARED: execute tools (final_answer in mixed calls is rejected inline)
588
+ // SHARED: execute tools (final_answer and no_response in mixed calls are rejected inline)
410
589
  for (const tc of result.toolCalls) {
411
590
  if (signal?.aborted)
412
591
  break;
@@ -417,6 +596,13 @@ async function runAgent(messages, callbacks, channel, signal, options) {
417
596
  providerRuntime.appendToolOutput(tc.id, rejection);
418
597
  continue;
419
598
  }
599
+ // Intercept no_response in mixed call: reject it
600
+ if (tc.name === "no_response") {
601
+ const rejection = "rejected: no_response must be the only tool call. call no_response alone when you want to stay silent.";
602
+ messages.push({ role: "tool", tool_call_id: tc.id, content: rejection });
603
+ providerRuntime.appendToolOutput(tc.id, rejection);
604
+ continue;
605
+ }
420
606
  let args = {};
421
607
  try {
422
608
  args = JSON.parse(tc.arguments);
@@ -444,7 +630,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
444
630
  let toolResult;
445
631
  let success;
446
632
  try {
447
- toolResult = await (0, tools_1.execTool)(tc.name, args, options?.toolContext);
633
+ const execToolFn = options?.execTool ?? tools_1.execTool;
634
+ toolResult = await execToolFn(tc.name, args, augmentedToolContext ?? options?.toolContext);
448
635
  success = true;
449
636
  }
450
637
  catch (e) {
@@ -461,6 +648,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
461
648
  // Abort is not an error — just stop cleanly
462
649
  if (signal?.aborted) {
463
650
  stripLastToolCalls(messages);
651
+ outcome = "aborted";
464
652
  break;
465
653
  }
466
654
  // Context overflow: trim aggressively and retry once
@@ -495,6 +683,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
495
683
  });
496
684
  if (aborted) {
497
685
  stripLastToolCalls(messages);
686
+ outcome = "aborted";
498
687
  break;
499
688
  }
500
689
  providerRuntime.resetTurnState(messages);
@@ -510,6 +699,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
510
699
  meta: {},
511
700
  });
512
701
  stripLastToolCalls(messages);
702
+ outcome = "errored";
513
703
  done = true;
514
704
  }
515
705
  }
@@ -520,5 +710,5 @@ async function runAgent(messages, callbacks, channel, signal, options) {
520
710
  message: "runAgent turn completed",
521
711
  meta: { done },
522
712
  });
523
- return { usage: lastUsage };
713
+ return { usage: lastUsage, outcome, completion };
524
714
  }