@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.
- package/AdoptionSpecialist.ouro/agent.json +70 -9
- package/AdoptionSpecialist.ouro/psyche/SOUL.md +5 -2
- package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
- package/README.md +147 -205
- package/assets/ouroboros.png +0 -0
- package/changelog.json +395 -0
- package/dist/heart/active-work.js +178 -0
- package/dist/heart/bridges/manager.js +358 -0
- package/dist/heart/bridges/state-machine.js +135 -0
- package/dist/heart/bridges/store.js +123 -0
- package/dist/heart/config.js +68 -23
- package/dist/heart/core.js +282 -92
- package/dist/heart/cross-chat-delivery.js +146 -0
- package/dist/heart/daemon/agent-discovery.js +81 -0
- package/dist/heart/daemon/auth-flow.js +409 -0
- package/dist/heart/daemon/daemon-cli.js +1408 -248
- package/dist/heart/daemon/daemon-entry.js +55 -6
- package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
- package/dist/heart/daemon/daemon.js +216 -10
- package/dist/heart/daemon/hatch-animation.js +10 -3
- package/dist/heart/daemon/hatch-flow.js +7 -82
- package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
- package/dist/heart/daemon/launchd.js +159 -0
- package/dist/heart/daemon/log-tailer.js +4 -3
- package/dist/heart/daemon/message-router.js +17 -8
- package/dist/heart/daemon/ouro-bot-entry.js +0 -0
- package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
- package/dist/heart/daemon/ouro-entry.js +0 -0
- package/dist/heart/daemon/ouro-path-installer.js +178 -0
- package/dist/heart/daemon/ouro-uti.js +11 -2
- package/dist/heart/daemon/process-manager.js +14 -1
- package/dist/heart/daemon/run-hooks.js +37 -0
- package/dist/heart/daemon/runtime-logging.js +58 -15
- package/dist/heart/daemon/runtime-metadata.js +219 -0
- package/dist/heart/daemon/runtime-mode.js +67 -0
- package/dist/heart/daemon/sense-manager.js +307 -0
- package/dist/heart/daemon/skill-management-installer.js +94 -0
- package/dist/heart/daemon/socket-client.js +202 -0
- package/dist/heart/daemon/specialist-orchestrator.js +53 -84
- package/dist/heart/daemon/specialist-prompt.js +64 -5
- package/dist/heart/daemon/specialist-tools.js +213 -58
- package/dist/heart/daemon/staged-restart.js +114 -0
- package/dist/heart/daemon/thoughts.js +379 -0
- package/dist/heart/daemon/update-checker.js +111 -0
- package/dist/heart/daemon/update-hooks.js +138 -0
- package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
- package/dist/heart/delegation.js +62 -0
- package/dist/heart/identity.js +126 -21
- package/dist/heart/kicks.js +1 -19
- package/dist/heart/model-capabilities.js +48 -0
- package/dist/heart/progress-story.js +42 -0
- package/dist/heart/providers/anthropic.js +74 -9
- package/dist/heart/providers/azure.js +86 -7
- package/dist/heart/providers/github-copilot.js +149 -0
- package/dist/heart/providers/minimax.js +4 -0
- package/dist/heart/providers/openai-codex.js +12 -3
- package/dist/heart/safe-workspace.js +228 -0
- package/dist/heart/sense-truth.js +61 -0
- package/dist/heart/session-activity.js +169 -0
- package/dist/heart/session-recall.js +116 -0
- package/dist/heart/streaming.js +100 -22
- package/dist/heart/target-resolution.js +123 -0
- package/dist/heart/turn-coordinator.js +28 -0
- package/dist/mind/associative-recall.js +14 -2
- package/dist/mind/bundle-manifest.js +70 -0
- package/dist/mind/context.js +27 -11
- package/dist/mind/first-impressions.js +16 -2
- package/dist/mind/friends/channel.js +35 -0
- package/dist/mind/friends/group-context.js +144 -0
- package/dist/mind/friends/store-file.js +19 -0
- package/dist/mind/friends/trust-explanation.js +74 -0
- package/dist/mind/friends/types.js +8 -0
- package/dist/mind/memory.js +27 -26
- package/dist/mind/pending.js +72 -9
- package/dist/mind/phrases.js +1 -0
- package/dist/mind/prompt.js +358 -77
- package/dist/mind/token-estimate.js +8 -12
- package/dist/nerves/cli-logging.js +15 -2
- package/dist/nerves/coverage/run-artifacts.js +1 -1
- package/dist/repertoire/ado-client.js +4 -2
- package/dist/repertoire/coding/feedback.js +134 -0
- package/dist/repertoire/coding/index.js +4 -1
- package/dist/repertoire/coding/manager.js +62 -4
- package/dist/repertoire/coding/spawner.js +3 -3
- package/dist/repertoire/coding/tools.js +41 -2
- package/dist/repertoire/data/ado-endpoints.json +188 -0
- package/dist/repertoire/guardrails.js +279 -0
- package/dist/repertoire/mcp-client.js +254 -0
- package/dist/repertoire/mcp-manager.js +195 -0
- package/dist/repertoire/skills.js +3 -26
- package/dist/repertoire/tasks/board.js +12 -0
- package/dist/repertoire/tasks/index.js +23 -9
- package/dist/repertoire/tasks/transitions.js +1 -2
- package/dist/repertoire/tools-base.js +642 -251
- package/dist/repertoire/tools-bluebubbles.js +93 -0
- package/dist/repertoire/tools-teams.js +58 -25
- package/dist/repertoire/tools.js +93 -52
- package/dist/senses/bluebubbles-client.js +210 -5
- package/dist/senses/bluebubbles-entry.js +2 -0
- package/dist/senses/bluebubbles-inbound-log.js +109 -0
- package/dist/senses/bluebubbles-media.js +339 -0
- package/dist/senses/bluebubbles-model.js +12 -4
- package/dist/senses/bluebubbles-mutation-log.js +45 -5
- package/dist/senses/bluebubbles-runtime-state.js +109 -0
- package/dist/senses/bluebubbles-session-cleanup.js +72 -0
- package/dist/senses/bluebubbles.js +893 -45
- package/dist/senses/cli-layout.js +87 -0
- package/dist/senses/cli.js +348 -144
- package/dist/senses/continuity.js +94 -0
- package/dist/senses/debug-activity.js +148 -0
- package/dist/senses/inner-dialog-worker.js +47 -18
- package/dist/senses/inner-dialog.js +333 -84
- package/dist/senses/pipeline.js +278 -0
- package/dist/senses/teams.js +573 -129
- package/dist/senses/trust-gate.js +112 -2
- package/package.json +14 -3
- package/subagents/README.md +4 -70
- package/dist/heart/daemon/specialist-session.js +0 -142
- package/dist/heart/daemon/subagent-installer.js +0 -125
- package/dist/inner-worker-entry.js +0 -4
- package/subagents/work-doer.md +0 -233
- package/subagents/work-merger.md +0 -624
- package/subagents/work-planner.md +0 -373
package/dist/heart/core.js
CHANGED
|
@@ -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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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
|
|
137
|
+
const provider = (0, identity_1.loadAgentConfig)().provider;
|
|
108
138
|
const providerLabelBuilders = {
|
|
109
|
-
azure: () =>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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[
|
|
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
|
|
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
|
|
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
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
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"}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
answer
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
}
|