funolio-agent 1.0.75 → 1.1.65
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/dist/auth/credential-reader.d.ts.map +1 -1
- package/dist/auth/credential-reader.js +4 -3
- package/dist/auth/credential-reader.js.map +1 -1
- package/dist/auth/token-refresh.d.ts +8 -0
- package/dist/auth/token-refresh.d.ts.map +1 -1
- package/dist/auth/token-refresh.js +82 -52
- package/dist/auth/token-refresh.js.map +1 -1
- package/dist/auto-organizer.d.ts.map +1 -1
- package/dist/auto-organizer.js +6 -7
- package/dist/auto-organizer.js.map +1 -1
- package/dist/bench-prefix.d.ts +16 -0
- package/dist/bench-prefix.d.ts.map +1 -0
- package/dist/bench-prefix.js +25 -0
- package/dist/bench-prefix.js.map +1 -0
- package/dist/bot-manager.d.ts.map +1 -1
- package/dist/bot-manager.js +23 -14
- package/dist/bot-manager.js.map +1 -1
- package/dist/chat-sync.d.ts +42 -0
- package/dist/chat-sync.d.ts.map +1 -0
- package/dist/chat-sync.js +95 -0
- package/dist/chat-sync.js.map +1 -0
- package/dist/clerk-model.d.ts +7 -0
- package/dist/clerk-model.d.ts.map +1 -1
- package/dist/clerk-model.js +42 -8
- package/dist/clerk-model.js.map +1 -1
- package/dist/cli-bootstrap-history.d.ts +10 -0
- package/dist/cli-bootstrap-history.d.ts.map +1 -0
- package/dist/cli-bootstrap-history.js +112 -0
- package/dist/cli-bootstrap-history.js.map +1 -0
- package/dist/cli-models.d.ts +8 -0
- package/dist/cli-models.d.ts.map +1 -0
- package/dist/cli-models.js +91 -0
- package/dist/cli-models.js.map +1 -0
- package/dist/cli-session-epoch.d.ts +13 -3
- package/dist/cli-session-epoch.d.ts.map +1 -1
- package/dist/cli-session-epoch.js +53 -4
- package/dist/cli-session-epoch.js.map +1 -1
- package/dist/codex-app-server-manager.d.ts +64 -4
- package/dist/codex-app-server-manager.d.ts.map +1 -1
- package/dist/codex-app-server-manager.js +755 -55
- package/dist/codex-app-server-manager.js.map +1 -1
- package/dist/commands/pool.d.ts +32 -0
- package/dist/commands/pool.d.ts.map +1 -1
- package/dist/commands/pool.js +145 -66
- package/dist/commands/pool.js.map +1 -1
- package/dist/commands/start.d.ts +21 -0
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +484 -63
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +5 -2
- package/dist/commands/status.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +170 -58
- package/dist/config.js.map +1 -1
- package/dist/context-window.d.ts +37 -1
- package/dist/context-window.d.ts.map +1 -1
- package/dist/context-window.js +202 -16
- package/dist/context-window.js.map +1 -1
- package/dist/live-activity.d.ts +3 -1
- package/dist/live-activity.d.ts.map +1 -1
- package/dist/live-activity.js.map +1 -1
- package/dist/local-chat-execution.d.ts +114 -0
- package/dist/local-chat-execution.d.ts.map +1 -0
- package/dist/local-chat-execution.js +349 -0
- package/dist/local-chat-execution.js.map +1 -0
- package/dist/local-cli-pty-manager.d.ts +138 -3
- package/dist/local-cli-pty-manager.d.ts.map +1 -1
- package/dist/local-cli-pty-manager.js +1415 -111
- package/dist/local-cli-pty-manager.js.map +1 -1
- package/dist/local-conversation-gateway.d.ts +110 -0
- package/dist/local-conversation-gateway.d.ts.map +1 -0
- package/dist/local-conversation-gateway.js +175 -0
- package/dist/local-conversation-gateway.js.map +1 -0
- package/dist/local-data.d.ts +235 -5
- package/dist/local-data.d.ts.map +1 -1
- package/dist/local-data.js +1066 -87
- package/dist/local-data.js.map +1 -1
- package/dist/local-db.d.ts +6 -0
- package/dist/local-db.d.ts.map +1 -1
- package/dist/local-db.js +376 -4
- package/dist/local-db.js.map +1 -1
- package/dist/local-funnel.d.ts.map +1 -1
- package/dist/local-funnel.js +6 -5
- package/dist/local-funnel.js.map +1 -1
- package/dist/local-server.d.ts +30 -0
- package/dist/local-server.d.ts.map +1 -1
- package/dist/local-server.js +2898 -319
- package/dist/local-server.js.map +1 -1
- package/dist/managed-process-registry.d.ts +59 -0
- package/dist/managed-process-registry.d.ts.map +1 -0
- package/dist/managed-process-registry.js +390 -0
- package/dist/managed-process-registry.js.map +1 -0
- package/dist/mcp/claude-config-writer.d.ts +5 -5
- package/dist/mcp/claude-config-writer.d.ts.map +1 -1
- package/dist/mcp/claude-config-writer.js +19 -11
- package/dist/mcp/claude-config-writer.js.map +1 -1
- package/dist/mcp/index.d.ts +4 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/sync-cli-config.d.ts +42 -4
- package/dist/mcp/sync-cli-config.d.ts.map +1 -1
- package/dist/mcp/sync-cli-config.js +497 -17
- package/dist/mcp/sync-cli-config.js.map +1 -1
- package/dist/message-loop.d.ts.map +1 -1
- package/dist/message-loop.js +43 -1
- package/dist/message-loop.js.map +1 -1
- package/dist/mqtt-client.d.ts +34 -0
- package/dist/mqtt-client.d.ts.map +1 -1
- package/dist/mqtt-client.js +270 -45
- package/dist/mqtt-client.js.map +1 -1
- package/dist/mqtt-data-relay.d.ts +44 -0
- package/dist/mqtt-data-relay.d.ts.map +1 -0
- package/dist/mqtt-data-relay.js +106 -0
- package/dist/mqtt-data-relay.js.map +1 -0
- package/dist/orchestration/capabilities.d.ts +13 -0
- package/dist/orchestration/capabilities.d.ts.map +1 -0
- package/dist/orchestration/capabilities.js +152 -0
- package/dist/orchestration/capabilities.js.map +1 -0
- package/dist/orchestration/dispatch-executor.d.ts +83 -0
- package/dist/orchestration/dispatch-executor.d.ts.map +1 -0
- package/dist/orchestration/dispatch-executor.js +266 -0
- package/dist/orchestration/dispatch-executor.js.map +1 -0
- package/dist/orchestration/dispatch-hint.d.ts +134 -0
- package/dist/orchestration/dispatch-hint.d.ts.map +1 -0
- package/dist/orchestration/dispatch-hint.js +247 -0
- package/dist/orchestration/dispatch-hint.js.map +1 -0
- package/dist/orchestration/dispatch-runner.d.ts +106 -0
- package/dist/orchestration/dispatch-runner.d.ts.map +1 -0
- package/dist/orchestration/dispatch-runner.js +604 -0
- package/dist/orchestration/dispatch-runner.js.map +1 -0
- package/dist/orchestration/dispatch-tools.d.ts +167 -0
- package/dist/orchestration/dispatch-tools.d.ts.map +1 -0
- package/dist/orchestration/dispatch-tools.js +328 -0
- package/dist/orchestration/dispatch-tools.js.map +1 -0
- package/dist/orchestration/front-door-policy.d.ts +35 -10
- package/dist/orchestration/front-door-policy.d.ts.map +1 -1
- package/dist/orchestration/front-door-policy.js +30 -267
- package/dist/orchestration/front-door-policy.js.map +1 -1
- package/dist/orchestration/orchestrator-dispatch-prompt.d.ts +43 -0
- package/dist/orchestration/orchestrator-dispatch-prompt.d.ts.map +1 -0
- package/dist/orchestration/orchestrator-dispatch-prompt.js +267 -0
- package/dist/orchestration/orchestrator-dispatch-prompt.js.map +1 -0
- package/dist/orchestration/orchestrator-operating-prompt.d.ts +14 -0
- package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
- package/dist/orchestration/orchestrator-operating-prompt.js +157 -31
- package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
- package/dist/orchestration/plan-import.d.ts +39 -0
- package/dist/orchestration/plan-import.d.ts.map +1 -0
- package/dist/orchestration/plan-import.js +547 -0
- package/dist/orchestration/plan-import.js.map +1 -0
- package/dist/orchestration/worker-operating-prompt.d.ts +2 -0
- package/dist/orchestration/worker-operating-prompt.d.ts.map +1 -1
- package/dist/orchestration/worker-operating-prompt.js +36 -46
- package/dist/orchestration/worker-operating-prompt.js.map +1 -1
- package/dist/orchestrator.d.ts +195 -3
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +1970 -432
- package/dist/orchestrator.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +8 -4
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/claude-cli.d.ts.map +1 -1
- package/dist/providers/claude-cli.js +28 -3
- package/dist/providers/claude-cli.js.map +1 -1
- package/dist/providers/codex-cli.d.ts +10 -6
- package/dist/providers/codex-cli.d.ts.map +1 -1
- package/dist/providers/codex-cli.js +190 -17
- package/dist/providers/codex-cli.js.map +1 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +15 -5
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/index.d.ts +15 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +13 -5
- package/dist/providers/openai.js.map +1 -1
- package/dist/server-adapter.d.ts +8 -0
- package/dist/server-adapter.d.ts.map +1 -1
- package/dist/server-adapter.js +7 -0
- package/dist/server-adapter.js.map +1 -1
- package/dist/service-mode.d.ts +1 -1
- package/dist/service-mode.d.ts.map +1 -1
- package/dist/service-mode.js +64 -1
- package/dist/service-mode.js.map +1 -1
- package/dist/service-setup-only.d.ts +8 -0
- package/dist/service-setup-only.d.ts.map +1 -0
- package/dist/service-setup-only.js +37 -0
- package/dist/service-setup-only.js.map +1 -0
- package/dist/slash-commands.d.ts +21 -0
- package/dist/slash-commands.d.ts.map +1 -0
- package/dist/slash-commands.js +99 -0
- package/dist/slash-commands.js.map +1 -0
- package/dist/subagent/index.d.ts +4 -2
- package/dist/subagent/index.d.ts.map +1 -1
- package/dist/subagent/index.js.map +1 -1
- package/dist/summarization-pipeline.d.ts.map +1 -1
- package/dist/summarization-pipeline.js +1 -9
- package/dist/summarization-pipeline.js.map +1 -1
- package/dist/token-counter.d.ts.map +1 -1
- package/dist/token-counter.js +11 -4
- package/dist/token-counter.js.map +1 -1
- package/dist/tool-filter.d.ts.map +1 -1
- package/dist/tool-filter.js +10 -6
- package/dist/tool-filter.js.map +1 -1
- package/dist/tools/admin-tools.d.ts.map +1 -1
- package/dist/tools/admin-tools.js +13 -4
- package/dist/tools/admin-tools.js.map +1 -1
- package/dist/tools/run-command.d.ts.map +1 -1
- package/dist/tools/run-command.js +5 -1
- package/dist/tools/run-command.js.map +1 -1
- package/dist/tools/search-conversation-history.d.ts.map +1 -1
- package/dist/tools/search-conversation-history.js +12 -2
- package/dist/tools/search-conversation-history.js.map +1 -1
- package/dist/tools/todo-tasks.d.ts.map +1 -1
- package/dist/tools/todo-tasks.js +77 -5
- package/dist/tools/todo-tasks.js.map +1 -1
- package/dist/usage-log.d.ts +62 -0
- package/dist/usage-log.d.ts.map +1 -0
- package/dist/usage-log.js +98 -0
- package/dist/usage-log.js.map +1 -0
- package/dist/wizard-state.d.ts +13 -0
- package/dist/wizard-state.d.ts.map +1 -1
- package/dist/wizard-state.js +61 -3
- package/dist/wizard-state.js.map +1 -1
- package/dist/wizard-support.d.ts.map +1 -1
- package/dist/wizard-support.js +27 -1
- package/dist/wizard-support.js.map +1 -1
- package/dist/workflow-engine.d.ts +40 -1
- package/dist/workflow-engine.d.ts.map +1 -1
- package/dist/workflow-engine.js +753 -93
- package/dist/workflow-engine.js.map +1 -1
- package/package.json +2 -2
package/dist/orchestrator.js
CHANGED
|
@@ -48,28 +48,36 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
48
48
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
49
|
exports.OrchestratorAgent = void 0;
|
|
50
50
|
exports.buildLocalDesktopOrchestratorRuntime = buildLocalDesktopOrchestratorRuntime;
|
|
51
|
+
exports.buildNarrationStreamGate = buildNarrationStreamGate;
|
|
51
52
|
const index_1 = require("./providers/index");
|
|
53
|
+
const approval_1 = require("./approval");
|
|
52
54
|
const status_parser_1 = require("./orchestration/status-parser");
|
|
53
55
|
const validation_1 = require("./orchestration/validation");
|
|
54
56
|
const front_door_policy_1 = require("./orchestration/front-door-policy");
|
|
55
57
|
const deterministic_path_1 = require("./orchestration/deterministic-path");
|
|
56
58
|
const orchestrator_operating_prompt_1 = require("./orchestration/orchestrator-operating-prompt");
|
|
57
59
|
const policy_prompt_1 = require("./orchestration/policy-prompt");
|
|
60
|
+
const safeguards_1 = require("./orchestration/safeguards");
|
|
58
61
|
const orchestrator_blocked_prompt_1 = require("./orchestration/orchestrator-blocked-prompt");
|
|
59
62
|
const orchestrator_final_response_prompt_1 = require("./orchestration/orchestrator-final-response-prompt");
|
|
60
63
|
const worker_operating_prompt_1 = require("./orchestration/worker-operating-prompt");
|
|
64
|
+
const capabilities_1 = require("./orchestration/capabilities");
|
|
61
65
|
const policy_detection_1 = require("./policy-detection");
|
|
62
66
|
const execution_contract_1 = require("./execution-contract");
|
|
63
67
|
const state_1 = require("./orchestration/state");
|
|
64
68
|
const orchestrator_profile_1 = require("./orchestrator-profile");
|
|
65
69
|
const context_window_1 = require("./context-window");
|
|
70
|
+
const cli_bootstrap_history_1 = require("./cli-bootstrap-history");
|
|
71
|
+
const plan_import_1 = require("./orchestration/plan-import");
|
|
72
|
+
const index_2 = require("./index");
|
|
66
73
|
const data = __importStar(require("./local-data"));
|
|
67
74
|
const cli_session_epoch_1 = require("./cli-session-epoch");
|
|
68
75
|
const storage_mode_1 = require("./storage-mode");
|
|
76
|
+
const local_cli_pty_manager_1 = require("./local-cli-pty-manager");
|
|
77
|
+
const codex_app_server_manager_1 = require("./codex-app-server-manager");
|
|
69
78
|
const fs = __importStar(require("fs"));
|
|
70
79
|
const os = __importStar(require("os"));
|
|
71
80
|
const path = __importStar(require("path"));
|
|
72
|
-
const ORCHESTRATOR_ROLE_SETTING_KEY = 'orchestrator_role_assignments';
|
|
73
81
|
const ORCHESTRATOR_WORKFLOW_SETTING_KEY = 'orchestrator_preferred_workflow';
|
|
74
82
|
const ORCHESTRATOR_TEMPLATE_SETTING_KEY = 'orchestrator_default_workflow_template_id';
|
|
75
83
|
const HEARTBEAT_MS = 30_000;
|
|
@@ -84,11 +92,22 @@ const ORCHESTRATION_NODE_RETRY_LIMITS = {
|
|
|
84
92
|
finalize_response: 0,
|
|
85
93
|
require_confirmation: 0,
|
|
86
94
|
};
|
|
95
|
+
function isAbortLikeError(err) {
|
|
96
|
+
return err?.name === 'AbortError'
|
|
97
|
+
|| err?.code === 'ABORT_ERR'
|
|
98
|
+
|| /\baborted\b/i.test(String(err?.message || err || ''));
|
|
99
|
+
}
|
|
87
100
|
// ─── Orchestrator Agent ──────────────────────────────────────────
|
|
88
|
-
function resolveLocalProviderCredentials(providerName, profile) {
|
|
101
|
+
function resolveLocalProviderCredentials(providerName, profile, explicitConnection) {
|
|
89
102
|
if (index_1.CLI_PROVIDERS.has(providerName)) {
|
|
90
103
|
return { apiKey: 'cli-auth' };
|
|
91
104
|
}
|
|
105
|
+
if (explicitConnection?.oauth_token) {
|
|
106
|
+
return { apiKey: explicitConnection.oauth_token, authMode: 'oauth-bearer' };
|
|
107
|
+
}
|
|
108
|
+
if (explicitConnection?.api_key_enc) {
|
|
109
|
+
return { apiKey: explicitConnection.api_key_enc };
|
|
110
|
+
}
|
|
92
111
|
const directConnection = profile?.provider_connection_id
|
|
93
112
|
? data.getProviderConnection(profile.provider_connection_id)
|
|
94
113
|
: undefined;
|
|
@@ -126,12 +145,16 @@ function resolveLocalProviderCredentials(providerName, profile) {
|
|
|
126
145
|
}
|
|
127
146
|
function buildLocalDesktopOrchestratorRuntime(selectedBot) {
|
|
128
147
|
if (data.isClerkOrchestratorEnabled()) {
|
|
129
|
-
const
|
|
130
|
-
const
|
|
148
|
+
const clerkConfig = data.getResolvedClerkConfigInfo();
|
|
149
|
+
const clerkConnection = clerkConfig.providerConnectionId
|
|
150
|
+
? data.getProviderConnection(clerkConfig.providerConnectionId)
|
|
151
|
+
: undefined;
|
|
152
|
+
const providerName = (clerkConfig.provider || '').trim();
|
|
153
|
+
const model = (clerkConfig.model || '').trim() || 'default';
|
|
131
154
|
if (!providerName) {
|
|
132
155
|
throw new Error('Clerk is selected as orchestrator, but no clerk provider is configured.');
|
|
133
156
|
}
|
|
134
|
-
const credentials = resolveLocalProviderCredentials(providerName);
|
|
157
|
+
const credentials = resolveLocalProviderCredentials(providerName, null, clerkConnection);
|
|
135
158
|
const llm = (0, index_1.createProvider)(providerName, {
|
|
136
159
|
apiKey: credentials.apiKey,
|
|
137
160
|
model,
|
|
@@ -143,15 +166,23 @@ function buildLocalDesktopOrchestratorRuntime(selectedBot) {
|
|
|
143
166
|
llm,
|
|
144
167
|
providerName,
|
|
145
168
|
model,
|
|
146
|
-
agentName: '
|
|
169
|
+
agentName: 'Orchestrator',
|
|
147
170
|
botId: null,
|
|
148
|
-
modelLabel: model
|
|
171
|
+
modelLabel: model || null,
|
|
149
172
|
};
|
|
150
173
|
}
|
|
151
|
-
|
|
174
|
+
// Orchestrator must be designated explicitly (is_orchestrator=1 or selectedBot).
|
|
175
|
+
// No fallback to the default worker bot — that's what created the "Ben is both
|
|
176
|
+
// orchestrator and worker" bug. If neither Clerk nor an orchestrator bot is
|
|
177
|
+
// configured, the runtime should refuse to start rather than silently reuse
|
|
178
|
+
// whichever bot happens to be is_default=1.
|
|
179
|
+
const profile = selectedBot || data.getOrchestratorBot();
|
|
152
180
|
if (!profile) {
|
|
153
|
-
throw new Error('No orchestrator
|
|
181
|
+
throw new Error('No orchestrator bot is configured. Mark a bot with is_orchestrator=1 or enable Clerk orchestration.');
|
|
154
182
|
}
|
|
183
|
+
return buildLocalDesktopRuntimeFromProfile(profile);
|
|
184
|
+
}
|
|
185
|
+
function buildLocalDesktopRuntimeFromProfile(profile) {
|
|
155
186
|
const credentials = resolveLocalProviderCredentials(profile.provider, profile);
|
|
156
187
|
const llm = (0, index_1.createProvider)(profile.provider, {
|
|
157
188
|
apiKey: credentials.apiKey,
|
|
@@ -169,6 +200,135 @@ function buildLocalDesktopOrchestratorRuntime(selectedBot) {
|
|
|
169
200
|
modelLabel: profile.model || profile.name,
|
|
170
201
|
};
|
|
171
202
|
}
|
|
203
|
+
function scorePlannerModel(profile) {
|
|
204
|
+
const model = String(profile.model || '').trim().toLowerCase();
|
|
205
|
+
const provider = String(profile.provider || '').trim().toLowerCase();
|
|
206
|
+
let score = 50;
|
|
207
|
+
if (/gpt-5\.4/.test(model))
|
|
208
|
+
score += 60;
|
|
209
|
+
else if (/\bopus\b/.test(model))
|
|
210
|
+
score += 58;
|
|
211
|
+
else if (/gpt-5\.2/.test(model))
|
|
212
|
+
score += 54;
|
|
213
|
+
else if (/\bsonnet\b/.test(model))
|
|
214
|
+
score += 48;
|
|
215
|
+
else if (/gpt-5/.test(model))
|
|
216
|
+
score += 46;
|
|
217
|
+
else if (/\bpro\b/.test(model))
|
|
218
|
+
score += 44;
|
|
219
|
+
else if (/\bcodex\b/.test(model))
|
|
220
|
+
score += 38;
|
|
221
|
+
else if (/\bflash\b/.test(model))
|
|
222
|
+
score += 28;
|
|
223
|
+
else if (/\bmini\b/.test(model))
|
|
224
|
+
score += 24;
|
|
225
|
+
else if (/\bhaiku\b/.test(model))
|
|
226
|
+
score += 22;
|
|
227
|
+
else if (model)
|
|
228
|
+
score += 30;
|
|
229
|
+
if (provider === 'openai')
|
|
230
|
+
score += 8;
|
|
231
|
+
else if (provider === 'anthropic' || provider === 'claude-cli')
|
|
232
|
+
score += 7;
|
|
233
|
+
else if (provider === 'google')
|
|
234
|
+
score += 6;
|
|
235
|
+
else if (provider === 'codex-cli')
|
|
236
|
+
score += 5;
|
|
237
|
+
return score;
|
|
238
|
+
}
|
|
239
|
+
function selectStrongestPlannerBot(bots) {
|
|
240
|
+
if (bots.length === 0)
|
|
241
|
+
return null;
|
|
242
|
+
return [...bots]
|
|
243
|
+
.sort((a, b) => {
|
|
244
|
+
const scoreDiff = scorePlannerModel(b) - scorePlannerModel(a);
|
|
245
|
+
if (scoreDiff !== 0)
|
|
246
|
+
return scoreDiff;
|
|
247
|
+
return a.name.localeCompare(b.name);
|
|
248
|
+
})[0] || null;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Builds a chunk-forwarding gate used to stream orchestrator narration to
|
|
252
|
+
* the user while preserving the structured-decision contract:
|
|
253
|
+
*
|
|
254
|
+
* - Forwards pre-sentinel text to onNarrationChunk
|
|
255
|
+
* - Stops as soon as ===DECISION=== sentinel is seen (the rest is JSON)
|
|
256
|
+
* - Detects "looks like raw JSON, no sentinel coming" early and suppresses
|
|
257
|
+
* forwarding entirely so JSON does not leak to the user
|
|
258
|
+
* - Holds the first 16 chars to make the narration-vs-JSON decision
|
|
259
|
+
* - Handles sentinel arriving before the sniff length is reached
|
|
260
|
+
*
|
|
261
|
+
* Exported separately so unit tests can exercise the gate logic without
|
|
262
|
+
* standing up a full orchestrator instance.
|
|
263
|
+
*/
|
|
264
|
+
function buildNarrationStreamGate(onNarrationChunk) {
|
|
265
|
+
const SNIFF_LEN = 16;
|
|
266
|
+
let buffer = '';
|
|
267
|
+
let suppressed = false;
|
|
268
|
+
let decided = false;
|
|
269
|
+
let forwardedLen = 0;
|
|
270
|
+
const isLikelyRawJsonPrefix = (text) => {
|
|
271
|
+
const trimmed = text.replace(/^\s+/, '');
|
|
272
|
+
if (!trimmed)
|
|
273
|
+
return false;
|
|
274
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('```json') || trimmed.startsWith('```'))
|
|
275
|
+
return true;
|
|
276
|
+
return false;
|
|
277
|
+
};
|
|
278
|
+
return async (chunk) => {
|
|
279
|
+
if (!chunk || suppressed)
|
|
280
|
+
return;
|
|
281
|
+
buffer += chunk;
|
|
282
|
+
const sentinelIdx = buffer.indexOf(orchestrator_operating_prompt_1.ORCHESTRATOR_DECISION_SENTINEL);
|
|
283
|
+
if (sentinelIdx >= 0) {
|
|
284
|
+
const preSentinel = buffer.slice(0, sentinelIdx);
|
|
285
|
+
if (!decided) {
|
|
286
|
+
if (preSentinel.length > 0 && !isLikelyRawJsonPrefix(preSentinel)) {
|
|
287
|
+
try {
|
|
288
|
+
await onNarrationChunk(preSentinel);
|
|
289
|
+
}
|
|
290
|
+
catch { /* swallow */ }
|
|
291
|
+
}
|
|
292
|
+
decided = true;
|
|
293
|
+
}
|
|
294
|
+
else if (sentinelIdx > forwardedLen) {
|
|
295
|
+
const tail = buffer.slice(forwardedLen, sentinelIdx);
|
|
296
|
+
if (tail) {
|
|
297
|
+
try {
|
|
298
|
+
await onNarrationChunk(tail);
|
|
299
|
+
}
|
|
300
|
+
catch { /* swallow */ }
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
forwardedLen = sentinelIdx;
|
|
304
|
+
suppressed = true;
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (!decided) {
|
|
308
|
+
if (buffer.length < SNIFF_LEN)
|
|
309
|
+
return;
|
|
310
|
+
if (isLikelyRawJsonPrefix(buffer)) {
|
|
311
|
+
suppressed = true;
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
decided = true;
|
|
315
|
+
forwardedLen = buffer.length;
|
|
316
|
+
try {
|
|
317
|
+
await onNarrationChunk(buffer);
|
|
318
|
+
}
|
|
319
|
+
catch { /* swallow */ }
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const tail = buffer.slice(forwardedLen);
|
|
323
|
+
if (tail) {
|
|
324
|
+
forwardedLen = buffer.length;
|
|
325
|
+
try {
|
|
326
|
+
await onNarrationChunk(tail);
|
|
327
|
+
}
|
|
328
|
+
catch { /* swallow */ }
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
}
|
|
172
332
|
class OrchestratorAgent {
|
|
173
333
|
orchestratorRuntime;
|
|
174
334
|
workflowEngine;
|
|
@@ -191,9 +351,9 @@ class OrchestratorAgent {
|
|
|
191
351
|
},
|
|
192
352
|
providerName: runtimeInfo?.provider || 'openai',
|
|
193
353
|
model: runtimeInfo?.model || null,
|
|
194
|
-
agentName: '
|
|
354
|
+
agentName: 'Orchestrator',
|
|
195
355
|
botId: null,
|
|
196
|
-
modelLabel: runtimeInfo?.model
|
|
356
|
+
modelLabel: runtimeInfo?.model || null,
|
|
197
357
|
};
|
|
198
358
|
}
|
|
199
359
|
else {
|
|
@@ -228,6 +388,8 @@ class OrchestratorAgent {
|
|
|
228
388
|
return await work(attempt);
|
|
229
389
|
}
|
|
230
390
|
catch (error) {
|
|
391
|
+
if (isAbortLikeError(error))
|
|
392
|
+
throw error;
|
|
231
393
|
if (attempt >= maxRetries)
|
|
232
394
|
throw error;
|
|
233
395
|
attempt += 1;
|
|
@@ -254,11 +416,52 @@ class OrchestratorAgent {
|
|
|
254
416
|
add('GPT');
|
|
255
417
|
return Array.from(refs);
|
|
256
418
|
}
|
|
419
|
+
getOrchestrationRoleLabel(agent) {
|
|
420
|
+
return data.getAgentOrchestrationRoleLabel(agent) || 'general';
|
|
421
|
+
}
|
|
422
|
+
getOrchestrationRoleClass(agent) {
|
|
423
|
+
return String(data.getAgentOrchestrationRoleClass(agent) || '').trim().toLowerCase();
|
|
424
|
+
}
|
|
425
|
+
getOrchestrationRolePriorities(agent) {
|
|
426
|
+
if (!agent)
|
|
427
|
+
return [];
|
|
428
|
+
return data.getAgentRolePriorities(agent).map((role) => role.trim().toLowerCase()).filter(Boolean);
|
|
429
|
+
}
|
|
430
|
+
agentHasAnyRole(agent, roles) {
|
|
431
|
+
if (!agent)
|
|
432
|
+
return false;
|
|
433
|
+
const wanted = new Set(roles.map((role) => role.trim().toLowerCase()).filter(Boolean));
|
|
434
|
+
if (wanted.size === 0)
|
|
435
|
+
return false;
|
|
436
|
+
return this.getOrchestrationRolePriorities(agent).some((role) => wanted.has(role))
|
|
437
|
+
|| wanted.has(this.getOrchestrationRoleClass(agent));
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* The worker's `orchestration_include_user_prompt` setting controls
|
|
441
|
+
* whether their task prompt is appended with the original user
|
|
442
|
+
* request as a reference block. Position-aware (2026-04-19): the
|
|
443
|
+
* FIRST worker in a chain ALWAYS gets the user prompt, regardless
|
|
444
|
+
* of the flag — they have no predecessor handoff to stand in for
|
|
445
|
+
* it, and without the prompt they'd have no concrete context at
|
|
446
|
+
* all. Downstream workers honor the explicit flag. This matches
|
|
447
|
+
* Steven's intent: "turn it off for Ben/John so they just read
|
|
448
|
+
* Brain's handoff, but Brain (the first bot) always gets it."
|
|
449
|
+
* If the user configures a different chain where Ben is first,
|
|
450
|
+
* Ben gets the prompt automatically without the user needing to
|
|
451
|
+
* remember to toggle the flag.
|
|
452
|
+
*/
|
|
453
|
+
shouldIncludeOriginalPromptForWorker(agent, isFirstInChain) {
|
|
454
|
+
if (isFirstInChain)
|
|
455
|
+
return true;
|
|
456
|
+
return agent.orchestration_include_user_prompt === 1;
|
|
457
|
+
}
|
|
257
458
|
describeAgentResponsibilities(agent) {
|
|
258
459
|
const candidates = [
|
|
259
460
|
agent.purpose_md,
|
|
260
461
|
agent.identity_summary,
|
|
261
462
|
agent.skills_md,
|
|
463
|
+
agent.orchestration_role_label,
|
|
464
|
+
agent.orchestration_role_class,
|
|
262
465
|
agent.role_label,
|
|
263
466
|
agent.role_class,
|
|
264
467
|
].map((value) => String(value || '').replace(/\s+/g, ' ').trim()).filter(Boolean);
|
|
@@ -273,7 +476,7 @@ class OrchestratorAgent {
|
|
|
273
476
|
return cleaned.length > 180 ? `${cleaned.slice(0, 177).trim()}...` : cleaned;
|
|
274
477
|
}
|
|
275
478
|
}
|
|
276
|
-
const normalizedRole =
|
|
479
|
+
const normalizedRole = this.getOrchestrationRoleClass(agent);
|
|
277
480
|
if (normalizedRole === 'code' || normalizedRole === 'coding') {
|
|
278
481
|
return 'implementation, code changes, fixes, and app changes';
|
|
279
482
|
}
|
|
@@ -299,7 +502,7 @@ class OrchestratorAgent {
|
|
|
299
502
|
const orchestrationState = (0, state_1.createOrchestrationState)({
|
|
300
503
|
conversationId: conversationId || null,
|
|
301
504
|
projectId: conversationProjectId || null,
|
|
302
|
-
orchestratorBotId:
|
|
505
|
+
orchestratorBotId: opts.orchestratorBotIdHint || null,
|
|
303
506
|
userPromptRaw: prompt,
|
|
304
507
|
});
|
|
305
508
|
orchestrationState.requestedArtifactTargets = this.extractRequestedArtifactTargets(prompt);
|
|
@@ -318,24 +521,36 @@ class OrchestratorAgent {
|
|
|
318
521
|
return this.applyPendingPolicy(conversationId, pendingPolicy);
|
|
319
522
|
}
|
|
320
523
|
const promptAssignments = this.parseRoleAssignments(prompt);
|
|
321
|
-
const selectedOrchestrator = this.
|
|
322
|
-
|
|
524
|
+
const selectedOrchestrator = this.resolveOrchestratorRuntimeBot(opts.orchestratorBotIdHint);
|
|
525
|
+
const dispatchOrchestratorBot = this.orchestratorRuntime.kind === 'clerk'
|
|
526
|
+
? null
|
|
527
|
+
: selectedOrchestrator || null;
|
|
528
|
+
const hasConfiguredOrchestratorRuntime = this.orchestratorRuntime.kind === 'clerk' || !!selectedOrchestrator;
|
|
529
|
+
const selectedOrchestratorActor = this.getOrchestratorDispatchActor(dispatchOrchestratorBot);
|
|
530
|
+
if (this.orchestratorRuntime.kind === 'clerk') {
|
|
531
|
+
this.setLastResponseMeta({
|
|
532
|
+
agentName: 'Orchestrator',
|
|
533
|
+
botId: null,
|
|
534
|
+
modelLabel: this.orchestratorRuntime.modelLabel,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
else if (selectedOrchestrator) {
|
|
323
538
|
this.setLastResponseMeta({
|
|
324
539
|
agentName: 'Orchestrator',
|
|
325
540
|
botId: selectedOrchestrator.id,
|
|
326
541
|
modelLabel: null,
|
|
327
542
|
});
|
|
328
543
|
}
|
|
329
|
-
if (
|
|
330
|
-
orchestrationState.orchestratorBotId =
|
|
544
|
+
if (dispatchOrchestratorBot) {
|
|
545
|
+
orchestrationState.orchestratorBotId = dispatchOrchestratorBot.id;
|
|
331
546
|
}
|
|
332
547
|
const initialProject = conversationProjectId ? data.getProject(conversationProjectId) : undefined;
|
|
333
548
|
const initialPolicy = data.getEffectiveOrchestrationPolicy(conversationProjectId || undefined);
|
|
334
|
-
const initialAssignments = this.mergeRoleAssignments(this.policyRoleAssignments(initialPolicy),
|
|
549
|
+
const initialAssignments = this.mergeRoleAssignments(this.policyRoleAssignments(initialPolicy), this.deriveRoleAssignmentsFromProject(conversationProjectId, dispatchOrchestratorBot), promptAssignments);
|
|
335
550
|
const initialOverview = conversationProjectId ? data.getProjectOverview(conversationProjectId) : undefined;
|
|
336
551
|
// If a specific workflow template was selected by the user, skip front-door classification
|
|
337
552
|
// and go straight to workflow execution.
|
|
338
|
-
if (opts.workflowTemplateId &&
|
|
553
|
+
if (opts.workflowTemplateId && hasConfiguredOrchestratorRuntime) {
|
|
339
554
|
const template = data.getWorkflowTemplate(opts.workflowTemplateId);
|
|
340
555
|
if (template) {
|
|
341
556
|
orchestrationState.intent = 'workflow';
|
|
@@ -354,14 +569,21 @@ class OrchestratorAgent {
|
|
|
354
569
|
const validation = this.validateIntentExecution(prompt, workflowIntent, initialPolicy);
|
|
355
570
|
if (validation.ok) {
|
|
356
571
|
this.applyExecutionSpecToState(orchestrationState, validation.executionSpec);
|
|
357
|
-
|
|
572
|
+
if (this.isLocalDesktopRuntime()) {
|
|
573
|
+
const toolDispatchResult = await this.handleUserMessageViaToolDispatch(prompt, conversationId, opts, conversation, conversationProjectId, dispatchOrchestratorBot, orchestrationState, initialProject, template, 'selected_workflow_template');
|
|
574
|
+
if (toolDispatchResult !== null) {
|
|
575
|
+
return toolDispatchResult;
|
|
576
|
+
}
|
|
577
|
+
return 'Orchestration could not run the selected workflow: no LLM runtime is attached to the configured orchestrator.';
|
|
578
|
+
}
|
|
579
|
+
return await this.queueWorkflowTodoPlan(prompt, conversationId, workflowIntent, validation.executionSpec, opts, initialAssignments, initialProject, orchestrationState, selectedOrchestratorActor, template);
|
|
358
580
|
}
|
|
359
581
|
}
|
|
360
582
|
}
|
|
361
|
-
const namedWorkflowTemplate =
|
|
583
|
+
const namedWorkflowTemplate = hasConfiguredOrchestratorRuntime
|
|
362
584
|
? this.resolveWorkflowTemplateByPrompt(conversationProjectId, prompt)
|
|
363
585
|
: undefined;
|
|
364
|
-
if (namedWorkflowTemplate &&
|
|
586
|
+
if (namedWorkflowTemplate && hasConfiguredOrchestratorRuntime) {
|
|
365
587
|
orchestrationState.intent = 'workflow';
|
|
366
588
|
this.recordOrchestrationAudit(orchestrationState, 'choose_path', 'path_selected', `User explicitly named workflow template: ${namedWorkflowTemplate.name}`, { workflowTemplateId: namedWorkflowTemplate.id });
|
|
367
589
|
const workflowIntent = {
|
|
@@ -378,8 +600,34 @@ class OrchestratorAgent {
|
|
|
378
600
|
const validation = this.validateIntentExecution(prompt, workflowIntent, initialPolicy);
|
|
379
601
|
if (validation.ok) {
|
|
380
602
|
this.applyExecutionSpecToState(orchestrationState, validation.executionSpec);
|
|
381
|
-
|
|
603
|
+
if (this.isLocalDesktopRuntime()) {
|
|
604
|
+
const toolDispatchResult = await this.handleUserMessageViaToolDispatch(prompt, conversationId, opts, conversation, conversationProjectId, dispatchOrchestratorBot, orchestrationState, initialProject, namedWorkflowTemplate, 'named_workflow_template');
|
|
605
|
+
if (toolDispatchResult !== null) {
|
|
606
|
+
return toolDispatchResult;
|
|
607
|
+
}
|
|
608
|
+
return 'Orchestration could not run the named workflow: no LLM runtime is attached to the configured orchestrator.';
|
|
609
|
+
}
|
|
610
|
+
return await this.queueWorkflowTodoPlan(prompt, conversationId, workflowIntent, validation.executionSpec, opts, initialAssignments, initialProject, orchestrationState, selectedOrchestratorActor, namedWorkflowTemplate);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (this.isLocalDesktopRuntime() && hasConfiguredOrchestratorRuntime) {
|
|
614
|
+
// Phase B of orchestration-plan.txt. For the local_desktop runtime, the
|
|
615
|
+
// LLM-tool-dispatch path replaces the regex-based front door. All
|
|
616
|
+
// preflight flows (pendingCheckpoint, pendingPolicy, workflowTemplateId,
|
|
617
|
+
// namedWorkflowTemplate) have already been handled above — by this
|
|
618
|
+
// point we are choosing between reply_directly / delegate_single /
|
|
619
|
+
// create_workflow via the orchestrator LLM. Other runtimes
|
|
620
|
+
// (connected/server) still use the legacy front door.
|
|
621
|
+
const toolDispatchResult = await this.handleUserMessageViaToolDispatch(prompt, conversationId, opts, conversation, conversationProjectId, dispatchOrchestratorBot, orchestrationState, initialProject);
|
|
622
|
+
if (toolDispatchResult !== null) {
|
|
623
|
+
return toolDispatchResult;
|
|
382
624
|
}
|
|
625
|
+
// Preconditions unmet in local_desktop. Surface a clear error rather
|
|
626
|
+
// than fall through to the deprecated regex front door.
|
|
627
|
+
return 'Orchestration could not run: no LLM runtime is attached to the configured orchestrator. Check Settings -> Orchestration or Settings -> Bots.';
|
|
628
|
+
}
|
|
629
|
+
if (this.isLocalDesktopRuntime()) {
|
|
630
|
+
return 'Orchestration could not run: no orchestrator runtime is configured. Check Settings -> Orchestration or mark a bot as orchestrator in Settings -> Bots.';
|
|
383
631
|
}
|
|
384
632
|
if (selectedOrchestrator) {
|
|
385
633
|
const frontDoorResult = await this.handleOrchestratorFrontDoor(prompt, conversationId, opts, selectedOrchestrator, conversationProjectId, initialProject, initialPolicy, promptAssignments, initialAssignments, initialOverview, orchestrationState);
|
|
@@ -414,13 +662,14 @@ class OrchestratorAgent {
|
|
|
414
662
|
const effectiveProjectId = refreshedConversation?.project_id || resolvedProjectId;
|
|
415
663
|
const effectiveProject = effectiveProjectId ? data.getProject(effectiveProjectId) : undefined;
|
|
416
664
|
const effectivePolicy = data.getEffectiveOrchestrationPolicy(effectiveProjectId || conversationProjectId || undefined);
|
|
417
|
-
const effectiveOrchestrator =
|
|
418
|
-
const effectiveAssignments = this.mergeRoleAssignments(this.policyRoleAssignments(effectivePolicy),
|
|
665
|
+
const effectiveOrchestrator = this.resolveOrchestratorRuntimeBot(opts.orchestratorBotIdHint) || selectedOrchestrator;
|
|
666
|
+
const effectiveAssignments = this.mergeRoleAssignments(this.policyRoleAssignments(effectivePolicy), this.deriveRoleAssignmentsFromProject(effectiveProjectId, effectiveOrchestrator), promptAssignments);
|
|
419
667
|
if (effectiveProjectId &&
|
|
420
668
|
this.hasRoleAssignments(promptAssignments) &&
|
|
421
669
|
!this.hasMode(intent, 'POLICY_UPDATE')) {
|
|
422
|
-
this.
|
|
423
|
-
|
|
670
|
+
this.persistPreferredWorkflow(effectiveProjectId, conversationId, this.orchestratorRuntime.kind === 'clerk'
|
|
671
|
+
? 'clerk-orchestrator'
|
|
672
|
+
: (effectiveOrchestrator?.id || opts.orchestratorBotIdHint || 'orchestrator'), effectiveAssignments);
|
|
424
673
|
}
|
|
425
674
|
const projectOverview = effectiveProjectId ? data.getProjectOverview(effectiveProjectId) : undefined;
|
|
426
675
|
if (this.isOrchestratorControlMessage(prompt)) {
|
|
@@ -564,22 +813,15 @@ class OrchestratorAgent {
|
|
|
564
813
|
buildFrontDoorEffectivePolicy(policy, orchestratorBot, roleAssignments, taskType) {
|
|
565
814
|
const normalize = (value) => String(value || '').trim().toLowerCase();
|
|
566
815
|
const orchestratorName = normalize(orchestratorBot.name);
|
|
567
|
-
const roleClass = normalize(orchestratorBot.role_class);
|
|
568
816
|
const codingOwner = normalize(roleAssignments.coding);
|
|
569
817
|
const qaOwner = normalize(roleAssignments.qa);
|
|
570
818
|
const next = { ...policy };
|
|
571
819
|
const orchestratorOwnsCoding = orchestratorName.length > 0
|
|
572
820
|
&& (orchestratorName === codingOwner
|
|
573
|
-
||
|
|
574
|
-
|| roleClass === 'coding'
|
|
575
|
-
|| roleClass === 'coder'
|
|
576
|
-
|| roleClass === 'builder'
|
|
577
|
-
|| roleClass === 'developer');
|
|
821
|
+
|| this.agentHasAnyRole(orchestratorBot, ['code', 'coding', 'coder', 'builder', 'developer']));
|
|
578
822
|
const orchestratorOwnsQa = orchestratorName.length > 0
|
|
579
823
|
&& (orchestratorName === qaOwner
|
|
580
|
-
||
|
|
581
|
-
|| roleClass === 'review'
|
|
582
|
-
|| roleClass === 'reviewer');
|
|
824
|
+
|| this.agentHasAnyRole(orchestratorBot, ['qa', 'review', 'reviewer']));
|
|
583
825
|
if (taskType === 'coding' && orchestratorOwnsCoding) {
|
|
584
826
|
next.allowOrchestratorCode = true;
|
|
585
827
|
}
|
|
@@ -601,67 +843,16 @@ class OrchestratorAgent {
|
|
|
601
843
|
const mentionedAgents = this.countMentionedAgents(prompt);
|
|
602
844
|
return mentionedAgents === 1 && /\b(ask|let|have|talk to|discuss with|check with)\b/i.test(prompt);
|
|
603
845
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
const explicitWorkflowRoles = this.inferRequestedWorkflowRoles(prompt, {});
|
|
615
|
-
const explicitWorkflowRequest = explicitWorkflowRoles.length >= 2 && this.hasConcreteArtifactDetails(prompt);
|
|
616
|
-
if (targetAgent && !intent.targetAgent) {
|
|
617
|
-
intent.targetAgent = targetAgent;
|
|
618
|
-
}
|
|
619
|
-
if (explicitWorkflowRequest) {
|
|
620
|
-
intent.primaryMode = 'WORKFLOW_MODE';
|
|
621
|
-
intent.userFacingMode = 'FULL_WORKFLOW';
|
|
622
|
-
intent.targetScope = 'MULTI_WORKER';
|
|
623
|
-
intent.isMultiStep = true;
|
|
624
|
-
intent.intent = 'build';
|
|
625
|
-
intent.targetAgent = undefined;
|
|
626
|
-
intent.needsClarification = false;
|
|
627
|
-
intent.clarificationQuestions = undefined;
|
|
628
|
-
intent.secondaryModes = intent.secondaryModes.filter((mode) => mode !== 'PROXY_MODE');
|
|
629
|
-
intent.executionOrder = ['WORKFLOW_MODE'];
|
|
630
|
-
}
|
|
631
|
-
if (intent.primaryMode === 'PROXY_MODE' && !intent.targetAgent) {
|
|
632
|
-
intent.needsClarification = true;
|
|
633
|
-
intent.clarificationQuestions = Array.from(new Set([
|
|
634
|
-
...(intent.clarificationQuestions || []),
|
|
635
|
-
'Which worker should handle this request?',
|
|
636
|
-
]));
|
|
637
|
-
}
|
|
638
|
-
if (needsConcreteArtifact &&
|
|
639
|
-
(intent.primaryMode === 'WORKFLOW_MODE' || intent.primaryMode === 'PROXY_MODE')) {
|
|
640
|
-
intent.needsClarification = true;
|
|
641
|
-
intent.clarificationQuestions = Array.from(new Set([
|
|
642
|
-
...(intent.clarificationQuestions || []),
|
|
643
|
-
'What specific idea, artifact, or content should the team work on?',
|
|
644
|
-
]));
|
|
645
|
-
}
|
|
646
|
-
if (intent.primaryMode === 'WORKFLOW_MODE' && !intent.isMultiStep && intent.targetScope !== 'MULTI_WORKER') {
|
|
647
|
-
intent.needsClarification = true;
|
|
648
|
-
intent.clarificationQuestions = Array.from(new Set([
|
|
649
|
-
...(intent.clarificationQuestions || []),
|
|
650
|
-
'What multi-step workflow do you want me to coordinate?',
|
|
651
|
-
]));
|
|
652
|
-
}
|
|
653
|
-
if (intent.needsClarification && this.hasConcreteArtifactDetails(prompt)) {
|
|
654
|
-
const remaining = (intent.clarificationQuestions || []).filter((question) => !/specific idea, artifact, or content/i.test(question));
|
|
655
|
-
intent.clarificationQuestions = remaining.length > 0 ? remaining : undefined;
|
|
656
|
-
intent.needsClarification = Boolean(intent.clarificationQuestions?.length);
|
|
657
|
-
}
|
|
658
|
-
const after = JSON.stringify({
|
|
659
|
-
primaryMode: intent.primaryMode,
|
|
660
|
-
targetAgent: intent.targetAgent || null,
|
|
661
|
-
needsClarification: !!intent.needsClarification,
|
|
662
|
-
clarificationQuestions: intent.clarificationQuestions || [],
|
|
663
|
-
});
|
|
664
|
-
return before !== after;
|
|
846
|
+
/**
|
|
847
|
+
* DEPRECATED — returns false. Previously 70+ lines of regex that
|
|
848
|
+
* post-mutated an IntentAnalysis based on prose signals (bot names,
|
|
849
|
+
* role keywords, workflow language, concrete-artifact heuristics).
|
|
850
|
+
* Codex audit HIGH #5 (2026-04-19): runtime must not override mode/
|
|
851
|
+
* route via regex. The orchestrator LLM decides via structured tool
|
|
852
|
+
* calls (delegate_single / create_workflow / reply_directly).
|
|
853
|
+
*/
|
|
854
|
+
applyClassificationGuards(_prompt, _intent, _routingMode) {
|
|
855
|
+
return false;
|
|
665
856
|
}
|
|
666
857
|
formatOrchestratorControlResponse(prompt, assignments, overview) {
|
|
667
858
|
const lines = [];
|
|
@@ -714,82 +905,33 @@ class OrchestratorAgent {
|
|
|
714
905
|
// Aliases like "claude" → "Ben" cause false matches on every prompt.
|
|
715
906
|
return [agent.name.trim()].filter(Boolean);
|
|
716
907
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
const secondaryModes = [];
|
|
730
|
-
let intent = 'simple';
|
|
731
|
-
let isMultiStep = false;
|
|
732
|
-
if (hasPolicyLanguage) {
|
|
733
|
-
primaryMode = 'POLICY_UPDATE';
|
|
734
|
-
intent = 'plan';
|
|
735
|
-
secondaryModes.push('DIRECT_CONVERSATION');
|
|
736
|
-
}
|
|
737
|
-
else if (hasMemoryLanguage) {
|
|
738
|
-
primaryMode = 'MEMORY_CAPTURE';
|
|
739
|
-
}
|
|
740
|
-
else if (isStatus) {
|
|
741
|
-
primaryMode = 'STATUS_INQUIRY';
|
|
742
|
-
intent = 'question';
|
|
743
|
-
}
|
|
744
|
-
else if (needsWorkflow) {
|
|
745
|
-
primaryMode = 'WORKFLOW_MODE';
|
|
746
|
-
intent = 'build';
|
|
747
|
-
isMultiStep = true;
|
|
748
|
-
}
|
|
749
|
-
else if (isProxy) {
|
|
750
|
-
primaryMode = 'PROXY_MODE';
|
|
751
|
-
intent = 'discuss';
|
|
752
|
-
}
|
|
753
|
-
else if (isGreeting || this.isProjectKnowledgeRequest(prompt) || this.isOrchestratorControlMessage(prompt)) {
|
|
754
|
-
primaryMode = 'DIRECT_CONVERSATION';
|
|
755
|
-
intent = 'question';
|
|
756
|
-
}
|
|
757
|
-
const executionOrder = [primaryMode];
|
|
758
|
-
if (hasPolicyLanguage && needsWorkflow && !secondaryModes.includes('WORKFLOW_MODE')) {
|
|
759
|
-
secondaryModes.push('WORKFLOW_MODE');
|
|
760
|
-
executionOrder.push('WORKFLOW_MODE');
|
|
761
|
-
}
|
|
908
|
+
/**
|
|
909
|
+
* DEPRECATED — returns a neutral DIRECT_CONVERSATION intent. Previously
|
|
910
|
+
* 80+ lines of regex on the user prompt classifying into
|
|
911
|
+
* POLICY_UPDATE / MEMORY_CAPTURE / STATUS_INQUIRY / WORKFLOW_MODE /
|
|
912
|
+
* PROXY_MODE / DIRECT_CONVERSATION based on bot names and role
|
|
913
|
+
* keywords. Codex audit HIGH #5 (2026-04-19): runtime must not
|
|
914
|
+
* infer mode/route from prose. This fallback only runs when the
|
|
915
|
+
* primary LLM classifier is unavailable (non-local_desktop paths);
|
|
916
|
+
* returning a neutral default routes to the orchestrator's direct
|
|
917
|
+
* handling rather than miscategorizing via regex.
|
|
918
|
+
*/
|
|
919
|
+
fallbackClassifyUserMessage(_prompt, _routingMode) {
|
|
762
920
|
return {
|
|
763
|
-
primaryMode,
|
|
764
|
-
secondaryModes,
|
|
765
|
-
executionOrder,
|
|
766
|
-
userFacingMode:
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
: primaryMode === 'PROXY_MODE'
|
|
771
|
-
? 'ASK_WORKER'
|
|
772
|
-
: primaryMode === 'WORKFLOW_MODE'
|
|
773
|
-
? 'FULL_WORKFLOW'
|
|
774
|
-
: intent === 'plan'
|
|
775
|
-
? 'PLANNING'
|
|
776
|
-
: 'DIRECT_RESPONSE',
|
|
777
|
-
targetScope: primaryMode === 'PROXY_MODE'
|
|
778
|
-
? 'ONE_WORKER'
|
|
779
|
-
: primaryMode === 'WORKFLOW_MODE'
|
|
780
|
-
? 'MULTI_WORKER'
|
|
781
|
-
: 'SELF',
|
|
782
|
-
confidence: hasPolicyLanguage || isStatus || needsWorkflow || isProxy ? 'MEDIUM' : 'LOW',
|
|
783
|
-
intent,
|
|
921
|
+
primaryMode: 'DIRECT_CONVERSATION',
|
|
922
|
+
secondaryModes: [],
|
|
923
|
+
executionOrder: ['DIRECT_CONVERSATION'],
|
|
924
|
+
userFacingMode: 'DIRECT_RESPONSE',
|
|
925
|
+
targetScope: 'SELF',
|
|
926
|
+
confidence: 'LOW',
|
|
927
|
+
intent: 'simple',
|
|
784
928
|
projectMatch: undefined,
|
|
785
929
|
topicMatch: undefined,
|
|
786
|
-
targetAgent,
|
|
787
|
-
isMultiStep,
|
|
788
|
-
reasoning: 'Fallback
|
|
789
|
-
needsClarification:
|
|
790
|
-
clarificationQuestions:
|
|
791
|
-
? [`What specific idea, artifact, or content should ${targetAgent || 'the specialist'} review?`]
|
|
792
|
-
: undefined,
|
|
930
|
+
targetAgent: undefined,
|
|
931
|
+
isMultiStep: false,
|
|
932
|
+
reasoning: 'Fallback classifier deprecated — neutral direct-conversation default (Codex audit HIGH #5).',
|
|
933
|
+
needsClarification: false,
|
|
934
|
+
clarificationQuestions: undefined,
|
|
793
935
|
};
|
|
794
936
|
}
|
|
795
937
|
isExplicitPolicyMessage(prompt) {
|
|
@@ -895,6 +1037,7 @@ class OrchestratorAgent {
|
|
|
895
1037
|
this.reportHiddenRoleProgress(opts, 'dispatch_controller', `Routing readiness ping to ${agent.name}`);
|
|
896
1038
|
const result = await this.workflowEngine.execute(readinessPrompt, conversationId, agent.id, {
|
|
897
1039
|
apiKey: this.resolveApiKey(agent.provider),
|
|
1040
|
+
abortSignal: opts.abortSignal,
|
|
898
1041
|
disableDecomposition: true,
|
|
899
1042
|
});
|
|
900
1043
|
if (result.status === 'completed' || result.status === 'partial') {
|
|
@@ -1108,14 +1251,686 @@ class OrchestratorAgent {
|
|
|
1108
1251
|
.replace(/\n?STATUS:\s*(PASS|FAIL|COMPLETE|BLOCKED|UNKNOWN)\s*$/i, '')
|
|
1109
1252
|
.trim() || rawText.trim();
|
|
1110
1253
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1254
|
+
/** Summarize a TodoTaskRow.status map ({participant: completed}) into a single label. */
|
|
1255
|
+
// NOTE: module-level helper hoisted below the class; declared here as a forward reference only.
|
|
1256
|
+
summarizeTodoStatus(status) {
|
|
1257
|
+
if (!status || typeof status !== 'object')
|
|
1258
|
+
return 'queued';
|
|
1259
|
+
const values = Object.values(status);
|
|
1260
|
+
if (values.length === 0)
|
|
1261
|
+
return 'queued';
|
|
1262
|
+
if (values.every((v) => v === true))
|
|
1263
|
+
return 'completed';
|
|
1264
|
+
if (values.some((v) => v === true))
|
|
1265
|
+
return 'in_progress';
|
|
1266
|
+
return 'queued';
|
|
1267
|
+
}
|
|
1268
|
+
resolveOrchestratorRuntimeBot(orchestratorBotIdHint) {
|
|
1269
|
+
const hintedBotId = String(orchestratorBotIdHint || '').trim();
|
|
1270
|
+
if (hintedBotId) {
|
|
1271
|
+
const bot = data.getAgentProfile(hintedBotId);
|
|
1115
1272
|
if (bot)
|
|
1116
1273
|
return bot;
|
|
1117
1274
|
}
|
|
1118
|
-
|
|
1275
|
+
// Orchestrator must be explicitly designated via is_orchestrator=1.
|
|
1276
|
+
// No fallback to getDefaultAgentProfile() — that's the hardcoded-Ben trap that
|
|
1277
|
+
// made the default bot double as orchestrator and worker simultaneously.
|
|
1278
|
+
return data.getOrchestratorBot() || undefined;
|
|
1279
|
+
}
|
|
1280
|
+
getOrchestratorDispatchActor(orchestratorBot) {
|
|
1281
|
+
if (orchestratorBot) {
|
|
1282
|
+
return { name: orchestratorBot.name, actorId: orchestratorBot.id, bot: orchestratorBot };
|
|
1283
|
+
}
|
|
1284
|
+
return {
|
|
1285
|
+
name: this.orchestratorRuntime.agentName || 'Orchestrator',
|
|
1286
|
+
actorId: this.orchestratorRuntime.botId || 'clerk-orchestrator',
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Phase A wire-up for orchestration-plan.txt.
|
|
1291
|
+
* Called when `orchestration.tool_dispatch_enabled` flag is ON.
|
|
1292
|
+
* Builds the hint, calls the orchestrator LLM with the three dispatch
|
|
1293
|
+
* tools exposed, validates the tool call, and executes it via the
|
|
1294
|
+
* pure dispatch-executor (creates TODOs or returns a direct reply).
|
|
1295
|
+
*
|
|
1296
|
+
* Returns the user-facing reply string on success. Returns `null` if
|
|
1297
|
+
* preconditions are not met (no orchestrator bot configured, no LLM
|
|
1298
|
+
* runtime available) so the caller can fall back to the legacy path.
|
|
1299
|
+
*/
|
|
1300
|
+
async handleUserMessageViaToolDispatch(prompt, conversationId, opts, conversation, conversationProjectId, orchestratorBot, orchestrationState, initialProject, selectedWorkflowTemplate, selectedWorkflowSource = 'selected_workflow_template') {
|
|
1301
|
+
// Dynamic imports — keep dispatch modules isolated.
|
|
1302
|
+
const { runDispatchTurn } = await Promise.resolve().then(() => __importStar(require('./orchestration/dispatch-runner')));
|
|
1303
|
+
const { executeCreateWorkflow } = await Promise.resolve().then(() => __importStar(require('./orchestration/dispatch-executor')));
|
|
1304
|
+
const { computePlanExecutionIntent } = await Promise.resolve().then(() => __importStar(require('./orchestration/dispatch-hint')));
|
|
1305
|
+
// Get an LLM interface to call. For the local_desktop runtime, the
|
|
1306
|
+
// orchestratorRuntime's llm is what we need; for clerk runtime, same.
|
|
1307
|
+
const llm = this.orchestratorRuntime?.llm;
|
|
1308
|
+
if (!llm || typeof llm.chat !== 'function') {
|
|
1309
|
+
return null;
|
|
1310
|
+
}
|
|
1311
|
+
const project = conversationProjectId ? data.getProject(conversationProjectId) : undefined;
|
|
1312
|
+
const effectivePolicy = data.getEffectiveOrchestrationPolicy(conversationProjectId || undefined);
|
|
1313
|
+
const effectivePolicyText = (0, policy_prompt_1.buildEffectivePolicyPromptSection)(effectivePolicy, {
|
|
1314
|
+
heading: '',
|
|
1315
|
+
defaultLine: 'No confirmed special policy is set.',
|
|
1316
|
+
});
|
|
1317
|
+
// Candidate worker bots for dispatch: project-scoped and orchestrator-
|
|
1318
|
+
// profiles excluded (Codex QA 2026-04-18, finding #2). Using
|
|
1319
|
+
// listProjectScopedBots ensures role resolution cannot assign work to
|
|
1320
|
+
// a bot that isn't in the current project roster, and cannot pick
|
|
1321
|
+
// the orchestrator itself as a worker.
|
|
1322
|
+
const activeBots = this.listProjectScopedBots(conversationProjectId || undefined, { includeOrchestratorProfiles: !orchestratorBot })
|
|
1323
|
+
.filter((b) => b.is_active === 1 && (!orchestratorBot || b.id !== orchestratorBot.id));
|
|
1324
|
+
if (selectedWorkflowTemplate) {
|
|
1325
|
+
const existingIds = new Set(activeBots.map((bot) => bot.id));
|
|
1326
|
+
for (const step of selectedWorkflowTemplate.steps) {
|
|
1327
|
+
const bot = data.getAgentProfile(step.agent_id);
|
|
1328
|
+
if (bot && bot.is_active === 1 && !existingIds.has(bot.id)) {
|
|
1329
|
+
activeBots.push(bot);
|
|
1330
|
+
existingIds.add(bot.id);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
const dispatchHintBots = activeBots
|
|
1335
|
+
.filter((bot) => bot.is_active === 1)
|
|
1336
|
+
.map((bot) => ({
|
|
1337
|
+
name: bot.name,
|
|
1338
|
+
provider: bot.provider,
|
|
1339
|
+
model: bot.model || null,
|
|
1340
|
+
role_priorities: data.getAgentRolePriorities(bot),
|
|
1341
|
+
is_active: bot.is_active === 1,
|
|
1342
|
+
}));
|
|
1343
|
+
const planExecutionIntent = computePlanExecutionIntent(prompt, dispatchHintBots);
|
|
1344
|
+
const selectedWorkflowTemplateHint = selectedWorkflowTemplate
|
|
1345
|
+
? this.buildSelectedWorkflowTemplateHint(selectedWorkflowTemplate)
|
|
1346
|
+
: null;
|
|
1347
|
+
const explicitCandidatePlan = selectedWorkflowTemplateHint || planExecutionIntent.mode !== 'none'
|
|
1348
|
+
? null
|
|
1349
|
+
: this.buildExplicitBotSequenceCandidatePlan(prompt, activeBots);
|
|
1350
|
+
const candidateWorkflowPlan = planExecutionIntent.mode !== 'none'
|
|
1351
|
+
? null
|
|
1352
|
+
: selectedWorkflowTemplateHint
|
|
1353
|
+
? {
|
|
1354
|
+
source: selectedWorkflowSource,
|
|
1355
|
+
reason: selectedWorkflowSource === 'named_workflow_template'
|
|
1356
|
+
? `User named workflow "${selectedWorkflowTemplate.name}".`
|
|
1357
|
+
: `User selected workflow "${selectedWorkflowTemplate.name}".`,
|
|
1358
|
+
steps: selectedWorkflowTemplateHint.steps.map((step) => ({
|
|
1359
|
+
index: step.index,
|
|
1360
|
+
bot_name: step.bot_name,
|
|
1361
|
+
role_priorities: step.role_priorities,
|
|
1362
|
+
suggested_role: step.role_priorities[0] || null,
|
|
1363
|
+
confidence: 'high',
|
|
1364
|
+
reason: `Step comes from selected workflow "${selectedWorkflowTemplate.name}".`,
|
|
1365
|
+
})),
|
|
1366
|
+
}
|
|
1367
|
+
: explicitCandidatePlan;
|
|
1368
|
+
if (candidateWorkflowPlan) {
|
|
1369
|
+
this.reportCandidateWorkflowProgress(opts, candidateWorkflowPlan);
|
|
1370
|
+
}
|
|
1371
|
+
const orchestratorActor = this.getOrchestratorDispatchActor(orchestratorBot);
|
|
1372
|
+
const contextWindow = conversationId
|
|
1373
|
+
? (0, context_window_1.getPromptContextWindow)(conversationId, 5)
|
|
1374
|
+
: { summary: null, turns: [], carriedForward: false };
|
|
1375
|
+
const recentTurnsText = contextWindow.turns.length > 0
|
|
1376
|
+
? (0, context_window_1.formatTurnsForPrompt)(contextWindow.turns)
|
|
1377
|
+
: '';
|
|
1378
|
+
const recentContext = conversationId
|
|
1379
|
+
? this.buildRecentConversationContextForWorker(conversationId)
|
|
1380
|
+
: '';
|
|
1381
|
+
const isCliDispatchOrchestrator = !!(orchestratorBot && index_1.CLI_PROVIDERS.has(orchestratorBot.provider));
|
|
1382
|
+
const orchestratorCliSessionPlan = isCliDispatchOrchestrator
|
|
1383
|
+
? (0, cli_session_epoch_1.selectCliSessionEpoch)(conversationId, orchestratorBot.id, orchestratorBot.provider)
|
|
1384
|
+
: null;
|
|
1385
|
+
const orchestratorBootstrapHistoryFilePath = isCliDispatchOrchestrator
|
|
1386
|
+
&& !orchestratorCliSessionPlan?.resumeSessionId
|
|
1387
|
+
? this.prepareBootstrapHistoryFile(conversationId)
|
|
1388
|
+
: null;
|
|
1389
|
+
const recentSummary = contextWindow.summary?.summary_text?.trim() || null;
|
|
1390
|
+
const shouldSendFreshCliFallbackContext = isCliDispatchOrchestrator
|
|
1391
|
+
&& !orchestratorCliSessionPlan?.resumeSessionId
|
|
1392
|
+
&& !orchestratorBootstrapHistoryFilePath;
|
|
1393
|
+
const dispatchRecentSummary = !isCliDispatchOrchestrator || shouldSendFreshCliFallbackContext
|
|
1394
|
+
? recentSummary
|
|
1395
|
+
: null;
|
|
1396
|
+
const dispatchRecentTurns = !isCliDispatchOrchestrator || shouldSendFreshCliFallbackContext
|
|
1397
|
+
? (recentTurnsText || null)
|
|
1398
|
+
: null;
|
|
1399
|
+
if (isCliDispatchOrchestrator && orchestratorBot) {
|
|
1400
|
+
const sessionLaunchReason = orchestratorCliSessionPlan?.resumeSessionId
|
|
1401
|
+
? 'resumed'
|
|
1402
|
+
: ('mode' in contextWindow && contextWindow.mode === 'new_topic')
|
|
1403
|
+
? 'new_topic/no_context'
|
|
1404
|
+
: 'fresh_with_bootstrap';
|
|
1405
|
+
console.info(`[orchestrator] session_launch_reason=${sessionLaunchReason} botId=${orchestratorBot.id} provider=${orchestratorBot.provider} bootstrap=${!!orchestratorBootstrapHistoryFilePath}`);
|
|
1406
|
+
console.info(`[orchestrator] cli_session_selected conversationId=${conversationId} botId=${orchestratorBot.id} provider=${orchestratorBot.provider} selectedSessionId=${orchestratorCliSessionPlan?.resumeSessionId || '(none)'} resetReason=${orchestratorCliSessionPlan?.resetReason || '(none)'}`);
|
|
1407
|
+
if (orchestratorCliSessionPlan?.resumeSessionId) {
|
|
1408
|
+
console.info(`[orchestrator] cli_session_resuming conversationId=${conversationId} botId=${orchestratorBot.id} provider=${orchestratorBot.provider} sessionId=${orchestratorCliSessionPlan.resumeSessionId}`);
|
|
1409
|
+
}
|
|
1410
|
+
else if ('mode' in contextWindow && contextWindow.mode === 'new_topic') {
|
|
1411
|
+
console.info(`[orchestrator] cli_session_fresh_without_bootstrap_new_topic conversationId=${conversationId} botId=${orchestratorBot.id} provider=${orchestratorBot.provider}`);
|
|
1412
|
+
}
|
|
1413
|
+
else if (orchestratorBootstrapHistoryFilePath) {
|
|
1414
|
+
console.info(`[orchestrator] cli_session_fresh_bootstrap_applied conversationId=${conversationId} botId=${orchestratorBot.id} provider=${orchestratorBot.provider}`);
|
|
1415
|
+
}
|
|
1416
|
+
else {
|
|
1417
|
+
console.warn(`[orchestrator] cli_session_fresh_without_bootstrap_unexpected conversationId=${conversationId} botId=${orchestratorBot.id} provider=${orchestratorBot.provider} mode=${'mode' in contextWindow ? contextWindow.mode : 'unknown'}`);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
const precreatedCandidateTaskIds = candidateWorkflowPlan
|
|
1421
|
+
? this.createPendingWorkflowProposal({
|
|
1422
|
+
prompt,
|
|
1423
|
+
conversationId,
|
|
1424
|
+
projectId: conversationProjectId || null,
|
|
1425
|
+
orchestratorActor,
|
|
1426
|
+
activeBots,
|
|
1427
|
+
selectedWorkflowTemplateHint,
|
|
1428
|
+
candidateWorkflowPlan,
|
|
1429
|
+
executeCreateWorkflow,
|
|
1430
|
+
recentContext,
|
|
1431
|
+
})
|
|
1432
|
+
: [];
|
|
1433
|
+
const existingTodoRows = data.listActiveTodoTasksForConversation(conversationId);
|
|
1434
|
+
const existingTodos = existingTodoRows.map((t) => ({
|
|
1435
|
+
id: t.id,
|
|
1436
|
+
owner_name: t.owner_name,
|
|
1437
|
+
title: t.title,
|
|
1438
|
+
// TodoTaskRow.status is a participant→boolean map ({"Ben":true,"John":false}).
|
|
1439
|
+
// Derive a human-readable status label for the hint block.
|
|
1440
|
+
status: this.summarizeTodoStatus(t.status),
|
|
1441
|
+
}));
|
|
1442
|
+
const turnCount = conversation?.turn_count || 0;
|
|
1443
|
+
// Read-side tools for the orchestrator. Steven's rule (april19fixes.txt
|
|
1444
|
+
// item 8): the orchestrator handles activities directly when no role-
|
|
1445
|
+
// specific bot applies. These tools let it read files, search the
|
|
1446
|
+
// codebase, look up TODOs, and fetch web content without delegating
|
|
1447
|
+
// trivial reads to worker bots. Write-side tools (write_file,
|
|
1448
|
+
// edit_file, run_command) are deliberately excluded — role
|
|
1449
|
+
// separation still sends those to workers via delegate_single.
|
|
1450
|
+
const { readFileTool } = await Promise.resolve().then(() => __importStar(require('./tools/read-file')));
|
|
1451
|
+
const { listDirectoryTool } = await Promise.resolve().then(() => __importStar(require('./tools/list-directory')));
|
|
1452
|
+
const { searchCodebaseTool } = await Promise.resolve().then(() => __importStar(require('./tools/search-codebase')));
|
|
1453
|
+
const { searchLocalMemoryTool } = await Promise.resolve().then(() => __importStar(require('./tools/search-local-memory')));
|
|
1454
|
+
const { webSearchTool } = await Promise.resolve().then(() => __importStar(require('./tools/web-search')));
|
|
1455
|
+
const { webFetchTool } = await Promise.resolve().then(() => __importStar(require('./tools/web-fetch')));
|
|
1456
|
+
const { listTasksTool } = await Promise.resolve().then(() => __importStar(require('./tools/todo-tasks')));
|
|
1457
|
+
const orchestratorTools = [
|
|
1458
|
+
readFileTool,
|
|
1459
|
+
listDirectoryTool,
|
|
1460
|
+
searchCodebaseTool,
|
|
1461
|
+
searchLocalMemoryTool,
|
|
1462
|
+
webSearchTool,
|
|
1463
|
+
webFetchTool,
|
|
1464
|
+
listTasksTool,
|
|
1465
|
+
];
|
|
1466
|
+
const workspacePath = project?.folder?.trim() || opts.projectDir;
|
|
1467
|
+
const orchestratorToolContext = (0, index_2.createToolContext)(workspacePath, {
|
|
1468
|
+
runtimeMode: 'local_desktop',
|
|
1469
|
+
projectId: conversationProjectId || null,
|
|
1470
|
+
actorType: 'orchestrator',
|
|
1471
|
+
actorId: orchestratorBot?.id || orchestratorActor.actorId,
|
|
1472
|
+
botName: orchestratorActor.name,
|
|
1473
|
+
llmProvider: this.orchestratorRuntime.providerName,
|
|
1474
|
+
llmModel: this.orchestratorRuntime.model || undefined,
|
|
1475
|
+
});
|
|
1476
|
+
const result = await runDispatchTurn({
|
|
1477
|
+
userPrompt: prompt,
|
|
1478
|
+
orchestratorBot,
|
|
1479
|
+
orchestratorActor,
|
|
1480
|
+
llm,
|
|
1481
|
+
conversationId,
|
|
1482
|
+
projectId: conversationProjectId || null,
|
|
1483
|
+
conversationTitle: conversation?.title || null,
|
|
1484
|
+
topicId: null, // topic_id is not directly on conversation; callers that need it can extend later
|
|
1485
|
+
projectName: project?.name || null,
|
|
1486
|
+
projectFolder: project?.folder || null,
|
|
1487
|
+
turnCount,
|
|
1488
|
+
recentSummary: dispatchRecentSummary,
|
|
1489
|
+
recentTurnsForHint: dispatchRecentTurns,
|
|
1490
|
+
bootstrapHistoryFilePath: orchestratorBootstrapHistoryFilePath,
|
|
1491
|
+
recentContext,
|
|
1492
|
+
activeBots,
|
|
1493
|
+
selectedWorkflowTemplate: selectedWorkflowTemplateHint,
|
|
1494
|
+
candidateWorkflowPlan,
|
|
1495
|
+
precreatedCandidateTaskIds,
|
|
1496
|
+
existingTodos,
|
|
1497
|
+
effectivePolicyText,
|
|
1498
|
+
additionalGuidance: orchestratorBot?.soul_md || null,
|
|
1499
|
+
orchestratorTools,
|
|
1500
|
+
toolContext: orchestratorToolContext,
|
|
1501
|
+
});
|
|
1502
|
+
// Record dispatch outcome in the orchestration audit trail.
|
|
1503
|
+
try {
|
|
1504
|
+
orchestrationState.orchestratorBotId = orchestratorBot?.id || null;
|
|
1505
|
+
this.recordOrchestrationAudit(orchestrationState, 'choose_path', result.outcome.kind === 'error' ? 'blocked' : 'completed', `Tool-dispatch decision: ${result.outcome.kind}`, { outcomeKind: result.outcome.kind });
|
|
1506
|
+
}
|
|
1507
|
+
catch {
|
|
1508
|
+
// audit best-effort
|
|
1509
|
+
}
|
|
1510
|
+
this.setLastResponseMeta({
|
|
1511
|
+
agentName: 'Orchestrator',
|
|
1512
|
+
botId: orchestratorBot?.id || null,
|
|
1513
|
+
modelLabel: this.orchestratorRuntime.modelLabel || orchestratorBot?.model || null,
|
|
1514
|
+
});
|
|
1515
|
+
// reply_directly and error cases have no worker chain to run — return
|
|
1516
|
+
// immediately with the LLM's (or error) text.
|
|
1517
|
+
if (result.outcome.kind === 'reply_directly' || result.outcome.kind === 'error') {
|
|
1518
|
+
return result.userReplyText;
|
|
1519
|
+
}
|
|
1520
|
+
// delegate_single / create_workflow created TODOs. Fix for Codex QA
|
|
1521
|
+
// 2026-04-18 finding #1: the new path must EXECUTE the queued chain,
|
|
1522
|
+
// not just return a "queued" summary. Reuse the existing
|
|
1523
|
+
// dispatchQueuedTodoChain which walks active TODOs for this conversation
|
|
1524
|
+
// and runs each through workflowEngine.execute(). Without this, TODOs
|
|
1525
|
+
// would be orphaned in the queue with no worker ever picking them up.
|
|
1526
|
+
const createdTaskIds = [];
|
|
1527
|
+
if (result.outcome.kind === 'delegate_single') {
|
|
1528
|
+
const id = Number(result.outcome.taskId);
|
|
1529
|
+
if (Number.isFinite(id))
|
|
1530
|
+
createdTaskIds.push(id);
|
|
1531
|
+
}
|
|
1532
|
+
else if (result.outcome.kind === 'create_workflow') {
|
|
1533
|
+
for (const tid of result.outcome.taskIds) {
|
|
1534
|
+
const id = Number(tid);
|
|
1535
|
+
if (Number.isFinite(id))
|
|
1536
|
+
createdTaskIds.push(id);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
if (createdTaskIds.length > 0) {
|
|
1540
|
+
this.markConversationOrchestrated(conversationId);
|
|
1541
|
+
}
|
|
1542
|
+
if (opts.autoDispatchTodos === false) {
|
|
1543
|
+
const replyText = selectedWorkflowTemplate && result.outcome.kind === 'create_workflow'
|
|
1544
|
+
? `Queued workflow from "${selectedWorkflowTemplate.name}".\n${result.userReplyText}`
|
|
1545
|
+
: result.userReplyText;
|
|
1546
|
+
orchestrationState.finalResponseDraft = replyText;
|
|
1547
|
+
return replyText;
|
|
1548
|
+
}
|
|
1549
|
+
return await this.dispatchQueuedTodoChain(conversationId, opts, orchestrationState, orchestratorActor, initialProject, {
|
|
1550
|
+
taskIds: createdTaskIds,
|
|
1551
|
+
workflowName: result.outcome.kind === 'create_workflow' ? (selectedWorkflowTemplate?.name || 'tool_dispatch_workflow') : null,
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
async importPlanAsTodos(input) {
|
|
1555
|
+
const { executeCreateWorkflow } = await Promise.resolve().then(() => __importStar(require('./orchestration/dispatch-executor')));
|
|
1556
|
+
const { buildDispatchValidationContext, validateCreateWorkflow } = await Promise.resolve().then(() => __importStar(require('./orchestration/dispatch-tools')));
|
|
1557
|
+
const project = data.getProject(input.projectId);
|
|
1558
|
+
const activeBots = this.listProjectScopedBots(input.projectId || undefined, {
|
|
1559
|
+
includeOrchestratorProfiles: true,
|
|
1560
|
+
}).filter((bot) => bot.is_active === 1);
|
|
1561
|
+
if (activeBots.length === 0) {
|
|
1562
|
+
throw new Error('No active bots are assigned to this project.');
|
|
1563
|
+
}
|
|
1564
|
+
const selectedStages = (input.stages || [])
|
|
1565
|
+
.map((stage) => ({
|
|
1566
|
+
role: String(stage?.role || '').trim(),
|
|
1567
|
+
botId: String(stage?.botId || '').trim(),
|
|
1568
|
+
}))
|
|
1569
|
+
.filter((stage) => stage.role && stage.botId);
|
|
1570
|
+
if (selectedStages.length === 0) {
|
|
1571
|
+
throw new Error('Choose at least one stage and bot for the imported workflow.');
|
|
1572
|
+
}
|
|
1573
|
+
const plannerBot = this.resolvePlanImportBot(input.plannerBotId, input.projectId, input.orchestratorBot || null);
|
|
1574
|
+
const plannerRuntime = plannerBot && input.orchestratorBot && plannerBot.id === input.orchestratorBot.id
|
|
1575
|
+
? this.orchestratorRuntime
|
|
1576
|
+
: buildLocalDesktopRuntimeFromProfile(plannerBot);
|
|
1577
|
+
const effectiveExecutionOrder = input.executionOrder === 'batch_by_stage' || input.executionOrder === 'per_item_pipeline'
|
|
1578
|
+
? input.executionOrder
|
|
1579
|
+
: (0, plan_import_1.detectExecutionOrder)(input.plannerInstructions || null);
|
|
1580
|
+
const stageHints = selectedStages.map((stage) => {
|
|
1581
|
+
const stageBot = activeBots.find((bot) => bot.id === stage.botId);
|
|
1582
|
+
if (!stageBot) {
|
|
1583
|
+
throw new Error(`Plan import stage bot is not active in this project: ${stage.botId}`);
|
|
1584
|
+
}
|
|
1585
|
+
return {
|
|
1586
|
+
role: stage.role,
|
|
1587
|
+
botName: stageBot.name,
|
|
1588
|
+
};
|
|
1589
|
+
});
|
|
1590
|
+
const promptInput = {
|
|
1591
|
+
filePath: input.filePath,
|
|
1592
|
+
fileContent: input.fileContent,
|
|
1593
|
+
mode: input.mode,
|
|
1594
|
+
stages: stageHints,
|
|
1595
|
+
plannerInstructions: input.plannerInstructions || null,
|
|
1596
|
+
executionOrder: effectiveExecutionOrder,
|
|
1597
|
+
availableBots: activeBots.map((bot) => ({
|
|
1598
|
+
name: bot.name,
|
|
1599
|
+
provider: bot.provider,
|
|
1600
|
+
model: bot.model || null,
|
|
1601
|
+
rolePriorities: data.getAgentRolePriorities(bot),
|
|
1602
|
+
})),
|
|
1603
|
+
projectName: project?.name || null,
|
|
1604
|
+
projectFolder: project?.folder || input.opts.projectDir,
|
|
1605
|
+
};
|
|
1606
|
+
const systemPrompt = (0, plan_import_1.buildPlanImportSystemPrompt)(promptInput);
|
|
1607
|
+
const messages = [
|
|
1608
|
+
{ role: 'user', content: (0, plan_import_1.buildPlanImportUserPrompt)(promptInput) },
|
|
1609
|
+
];
|
|
1610
|
+
const validationContext = buildDispatchValidationContext(activeBots);
|
|
1611
|
+
let workflowArgs = null;
|
|
1612
|
+
let lastError = '';
|
|
1613
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1614
|
+
const rawContent = await this.runPlanImportPlannerTurn({
|
|
1615
|
+
plannerRuntime,
|
|
1616
|
+
plannerBot,
|
|
1617
|
+
conversationId: input.conversationId,
|
|
1618
|
+
projectId: input.projectId,
|
|
1619
|
+
workspacePath: project?.folder || input.opts.projectDir,
|
|
1620
|
+
systemPrompt,
|
|
1621
|
+
messages,
|
|
1622
|
+
abortSignal: input.opts.abortSignal,
|
|
1623
|
+
});
|
|
1624
|
+
const parsed = (0, plan_import_1.extractPlanImportResponse)(rawContent);
|
|
1625
|
+
if (parsed.error) {
|
|
1626
|
+
if (parsed.error === 'Planner authentication failed.') {
|
|
1627
|
+
lastError = `${plannerRuntime.agentName} could not authenticate. Check ${plannerRuntime.agentName}'s connection in Settings and retry.`;
|
|
1628
|
+
}
|
|
1629
|
+
else {
|
|
1630
|
+
lastError = parsed.error;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
else if (parsed.workflow) {
|
|
1634
|
+
const validation = validateCreateWorkflow(parsed.workflow, validationContext);
|
|
1635
|
+
if (validation.ok) {
|
|
1636
|
+
const detailValidation = (0, plan_import_1.validatePlanImportWorkflow)(parsed.workflow);
|
|
1637
|
+
if (!detailValidation.ok) {
|
|
1638
|
+
lastError = detailValidation.error || 'Imported workflow steps were too sparse.';
|
|
1639
|
+
}
|
|
1640
|
+
else if (effectiveExecutionOrder === 'batch_by_stage') {
|
|
1641
|
+
const orderValidation = (0, plan_import_1.validateBatchByStageOrdering)(parsed.workflow, stageHints);
|
|
1642
|
+
if (!orderValidation.ok) {
|
|
1643
|
+
lastError = orderValidation.error || 'Planner output interleaves stages instead of batching them.';
|
|
1644
|
+
}
|
|
1645
|
+
else {
|
|
1646
|
+
workflowArgs = parsed.workflow;
|
|
1647
|
+
break;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
else {
|
|
1651
|
+
workflowArgs = parsed.workflow;
|
|
1652
|
+
break;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
else {
|
|
1656
|
+
lastError = validation.error;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
else {
|
|
1660
|
+
lastError = 'Planner did not return a workflow.';
|
|
1661
|
+
}
|
|
1662
|
+
const plannerSnippet = rawContent
|
|
1663
|
+
.replace(/\s+/g, ' ')
|
|
1664
|
+
.slice(0, 300);
|
|
1665
|
+
console.warn(`[plan-import] invalid planner response attempt=${attempt + 1}/2 planner=${plannerRuntime.agentName} mode=${input.mode} error=${lastError} snippet="${plannerSnippet}"`);
|
|
1666
|
+
if (parsed.error === 'Planner authentication failed.') {
|
|
1667
|
+
break;
|
|
1668
|
+
}
|
|
1669
|
+
messages.push({ role: 'assistant', content: rawContent });
|
|
1670
|
+
messages.push({
|
|
1671
|
+
role: 'user',
|
|
1672
|
+
content: `The previous output was invalid: ${lastError}\nReturn corrected workflow step blocks only. Do not add prose before or after the workflow. Fully materialize every task with Files:, Do:/Verify:, and success criteria.`,
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
if (!workflowArgs) {
|
|
1676
|
+
throw new Error(lastError || 'Planner could not produce a valid workflow import.');
|
|
1677
|
+
}
|
|
1678
|
+
const orchestratorActor = this.getOrchestratorDispatchActor(input.orchestratorBot || null);
|
|
1679
|
+
const existingTodoRows = data.listActiveTodoTasksForConversation(input.conversationId);
|
|
1680
|
+
const executionContext = {
|
|
1681
|
+
conversationId: input.conversationId,
|
|
1682
|
+
projectId: input.projectId,
|
|
1683
|
+
orchestratorActor,
|
|
1684
|
+
resolution: {
|
|
1685
|
+
activeBots,
|
|
1686
|
+
botsWithActiveTodos: new Set(existingTodoRows.map((row) => row.owner_name).filter(Boolean)),
|
|
1687
|
+
},
|
|
1688
|
+
userPrompt: `Imported plan from ${path.basename(input.filePath)} using ${plannerRuntime.agentName}.`,
|
|
1689
|
+
recentContext: null,
|
|
1690
|
+
workflowSourceLabel: `Imported plan ${path.basename(input.filePath)}`,
|
|
1691
|
+
};
|
|
1692
|
+
const result = executeCreateWorkflow(workflowArgs, executionContext);
|
|
1693
|
+
if (result.kind === 'error') {
|
|
1694
|
+
throw new Error(result.error);
|
|
1695
|
+
}
|
|
1696
|
+
const taskIds = result.tasks.map((task) => Number(task.id)).filter((id) => Number.isFinite(id));
|
|
1697
|
+
const ownerNames = Array.from(new Set(result.tasks.map((task) => task.owner_name).filter(Boolean)));
|
|
1698
|
+
const planRun = data.createImportedPlanRun({
|
|
1699
|
+
conversationId: input.conversationId,
|
|
1700
|
+
projectId: input.projectId,
|
|
1701
|
+
sourceFilePath: input.filePath,
|
|
1702
|
+
sourceFileName: path.basename(input.filePath),
|
|
1703
|
+
plannerBotId: plannerBot.id,
|
|
1704
|
+
plannerAgentName: plannerRuntime.agentName,
|
|
1705
|
+
plannerProvider: plannerRuntime.providerName,
|
|
1706
|
+
plannerModel: plannerRuntime.model,
|
|
1707
|
+
requestedStages: selectedStages,
|
|
1708
|
+
requestedOptions: {
|
|
1709
|
+
plannerInstructions: input.plannerInstructions || null,
|
|
1710
|
+
executionOrder: effectiveExecutionOrder,
|
|
1711
|
+
},
|
|
1712
|
+
taskIds,
|
|
1713
|
+
status: input.autoDispatchTodos ? 'running' : 'paused',
|
|
1714
|
+
});
|
|
1715
|
+
if (input.autoDispatchTodos) {
|
|
1716
|
+
const state = (0, state_1.createOrchestrationState)({
|
|
1717
|
+
conversationId: input.conversationId,
|
|
1718
|
+
projectId: input.projectId,
|
|
1719
|
+
orchestratorBotId: input.orchestratorBot?.id || this.orchestratorRuntime.botId || null,
|
|
1720
|
+
userPromptRaw: `Imported plan ${path.basename(input.filePath)}`,
|
|
1721
|
+
});
|
|
1722
|
+
state.selectedPath = 'delegate';
|
|
1723
|
+
await this.dispatchQueuedTodoChain(input.conversationId, input.opts, state, orchestratorActor, project, {
|
|
1724
|
+
taskIds,
|
|
1725
|
+
workflowName: path.basename(input.filePath),
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
const fileLabel = path.basename(input.filePath);
|
|
1729
|
+
const pausedText = input.autoDispatchTodos ? 'and started the workflow' : 'and left the workflow paused for review';
|
|
1730
|
+
return {
|
|
1731
|
+
planRunId: planRun.id,
|
|
1732
|
+
replyText: `Imported ${fileLabel} into ${result.tasks.length} TODOs ${pausedText}. Planner: ${plannerRuntime.agentName}${plannerRuntime.model ? ` (${plannerRuntime.model})` : ''}.`,
|
|
1733
|
+
taskIds,
|
|
1734
|
+
ownerNames,
|
|
1735
|
+
planner: {
|
|
1736
|
+
botId: plannerBot.id,
|
|
1737
|
+
provider: plannerRuntime.providerName,
|
|
1738
|
+
model: plannerRuntime.model,
|
|
1739
|
+
agentName: plannerRuntime.agentName,
|
|
1740
|
+
},
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
async startImportedPlanRun(input) {
|
|
1744
|
+
const run = data.getImportedPlanRun(input.runId);
|
|
1745
|
+
if (!run) {
|
|
1746
|
+
throw new Error(`Imported plan run not found: ${input.runId}`);
|
|
1747
|
+
}
|
|
1748
|
+
if (run.status === 'discarded') {
|
|
1749
|
+
throw new Error('This imported plan was discarded and cannot be started.');
|
|
1750
|
+
}
|
|
1751
|
+
const project = run.project_id ? data.getProject(run.project_id) : undefined;
|
|
1752
|
+
const remainingTaskIds = data.getImportedPlanRunRemainingTaskIds(run.id);
|
|
1753
|
+
if (remainingTaskIds.length === 0) {
|
|
1754
|
+
const completedRun = data.updateImportedPlanRun(run.id, {
|
|
1755
|
+
status: 'completed',
|
|
1756
|
+
completedAt: run.completed_at || new Date().toISOString(),
|
|
1757
|
+
lastError: null,
|
|
1758
|
+
}) || run;
|
|
1759
|
+
return {
|
|
1760
|
+
run: completedRun,
|
|
1761
|
+
replyText: `Imported plan ${run.source_file_name} is already complete.`,
|
|
1762
|
+
remainingTaskIds: [],
|
|
1763
|
+
completed: true,
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
const orchestratorActor = this.getOrchestratorDispatchActor(input.orchestratorBot || null);
|
|
1767
|
+
const state = (0, state_1.createOrchestrationState)({
|
|
1768
|
+
conversationId: run.conversation_id,
|
|
1769
|
+
projectId: run.project_id || null,
|
|
1770
|
+
orchestratorBotId: input.orchestratorBot?.id || this.orchestratorRuntime.botId || null,
|
|
1771
|
+
userPromptRaw: `Resume imported plan ${run.source_file_name}`,
|
|
1772
|
+
});
|
|
1773
|
+
state.selectedPath = 'delegate';
|
|
1774
|
+
data.updateImportedPlanRun(run.id, {
|
|
1775
|
+
status: 'running',
|
|
1776
|
+
startedAt: run.started_at || new Date().toISOString(),
|
|
1777
|
+
completedAt: null,
|
|
1778
|
+
discardedAt: null,
|
|
1779
|
+
lastError: null,
|
|
1780
|
+
});
|
|
1781
|
+
let replyText = '';
|
|
1782
|
+
try {
|
|
1783
|
+
replyText = await this.dispatchQueuedTodoChain(run.conversation_id, input.opts, state, orchestratorActor, project, {
|
|
1784
|
+
taskIds: remainingTaskIds,
|
|
1785
|
+
workflowName: run.source_file_name,
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
catch (err) {
|
|
1789
|
+
const pausedRun = data.updateImportedPlanRun(run.id, {
|
|
1790
|
+
status: 'paused',
|
|
1791
|
+
lastError: err?.message || 'Imported plan execution failed.',
|
|
1792
|
+
}) || run;
|
|
1793
|
+
return {
|
|
1794
|
+
run: pausedRun,
|
|
1795
|
+
replyText: err?.message || 'Imported plan execution failed.',
|
|
1796
|
+
remainingTaskIds: data.getImportedPlanRunRemainingTaskIds(run.id),
|
|
1797
|
+
completed: false,
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
const remainingAfter = data.getImportedPlanRunRemainingTaskIds(run.id);
|
|
1801
|
+
const completed = remainingAfter.length === 0;
|
|
1802
|
+
const updatedRun = data.updateImportedPlanRun(run.id, {
|
|
1803
|
+
status: completed ? 'completed' : 'paused',
|
|
1804
|
+
completedAt: completed ? new Date().toISOString() : null,
|
|
1805
|
+
lastError: completed ? null : (replyText || state.finalResponseDraft || null),
|
|
1806
|
+
}) || run;
|
|
1807
|
+
return {
|
|
1808
|
+
run: updatedRun,
|
|
1809
|
+
replyText,
|
|
1810
|
+
remainingTaskIds: remainingAfter,
|
|
1811
|
+
completed,
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
buildDirectExecutionSystemPrompt(prompt, conversationId, project) {
|
|
1815
|
+
const contextWindow = conversationId
|
|
1816
|
+
? (0, context_window_1.getPromptContextWindow)(conversationId, 5)
|
|
1817
|
+
: { summary: null, turns: [], carriedForward: false };
|
|
1818
|
+
const recentTurnsText = (0, context_window_1.formatTurnsForPrompt)(contextWindow.turns) || '(none)';
|
|
1819
|
+
const recentSummaryText = contextWindow.summary?.summary_text?.trim()
|
|
1820
|
+
? `${contextWindow.carriedForward ? '(carried forward from the previous conversation in this topic)\n' : ''}${contextWindow.summary.summary_text.trim()}`
|
|
1821
|
+
: '(none)';
|
|
1822
|
+
return [
|
|
1823
|
+
'You are the user-facing Orchestrator in Funolio local desktop mode.',
|
|
1824
|
+
'You are handling this request directly.',
|
|
1825
|
+
'Use the available tools whenever they are helpful.',
|
|
1826
|
+
'Do not delegate, do not create workflow TODOs, and do not claim another bot is doing the work.',
|
|
1827
|
+
'If the user asked to read, inspect, summarize, or show a file, do that directly.',
|
|
1828
|
+
'If the user asked to update plain-text notes, plans, summaries, or TODO-like content, do that directly.',
|
|
1829
|
+
'If you cannot complete the request directly, explain the concrete blocker briefly.',
|
|
1830
|
+
'',
|
|
1831
|
+
'[Project Context]',
|
|
1832
|
+
`Project: ${project?.name || '(none)'}`,
|
|
1833
|
+
`Workspace: ${project?.folder || '(none)'}`,
|
|
1834
|
+
'',
|
|
1835
|
+
'[Rolling Summary]',
|
|
1836
|
+
recentSummaryText,
|
|
1837
|
+
'',
|
|
1838
|
+
'[Recent Turns]',
|
|
1839
|
+
recentTurnsText,
|
|
1840
|
+
'',
|
|
1841
|
+
'[Current Request]',
|
|
1842
|
+
prompt,
|
|
1843
|
+
].join('\n');
|
|
1844
|
+
}
|
|
1845
|
+
async executeClerkOwnedWorkDirect(prompt, conversationId, orchestratorBot, project, opts) {
|
|
1846
|
+
const systemPrompt = this.buildDirectExecutionSystemPrompt(prompt, conversationId, project);
|
|
1847
|
+
const workspacePath = project?.folder?.trim() || opts.projectDir;
|
|
1848
|
+
const toolContext = (0, index_2.createToolContext)(workspacePath, {
|
|
1849
|
+
runtimeMode: 'local_desktop',
|
|
1850
|
+
projectId: project?.id || opts.projectId || null,
|
|
1851
|
+
actorType: 'orchestrator',
|
|
1852
|
+
actorId: 'Orchestrator',
|
|
1853
|
+
botName: 'Orchestrator',
|
|
1854
|
+
llmProvider: this.orchestratorRuntime.providerName,
|
|
1855
|
+
llmModel: this.orchestratorRuntime.model || undefined,
|
|
1856
|
+
});
|
|
1857
|
+
const toolDefinitions = (0, index_2.getAllToolDefinitions)('local_desktop');
|
|
1858
|
+
const messages = [{ role: 'user', content: prompt }];
|
|
1859
|
+
const permissionMode = (orchestratorBot.permission_mode || 'autopilot');
|
|
1860
|
+
for (let iteration = 0; iteration < 8; iteration++) {
|
|
1861
|
+
const response = await this.orchestratorRuntime.llm.chat({
|
|
1862
|
+
messages,
|
|
1863
|
+
system: systemPrompt,
|
|
1864
|
+
stream: true,
|
|
1865
|
+
tools: toolDefinitions,
|
|
1866
|
+
toolChoice: 'auto',
|
|
1867
|
+
runtimeMode: 'local_desktop',
|
|
1868
|
+
onChunk: opts.onWorkerChunk
|
|
1869
|
+
? async (chunk) => {
|
|
1870
|
+
if (!chunk)
|
|
1871
|
+
return;
|
|
1872
|
+
opts.onWorkerChunk?.({
|
|
1873
|
+
type: 'chunk',
|
|
1874
|
+
text: chunk,
|
|
1875
|
+
stepId: 'orchestrator-direct',
|
|
1876
|
+
agentName: 'Orchestrator',
|
|
1877
|
+
description: 'Direct orchestrator work',
|
|
1878
|
+
stepIndex: 0,
|
|
1879
|
+
totalSteps: 1,
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
: undefined,
|
|
1883
|
+
});
|
|
1884
|
+
if (response.toolCalls?.length) {
|
|
1885
|
+
messages.push({
|
|
1886
|
+
role: 'assistant',
|
|
1887
|
+
content: response.content || '',
|
|
1888
|
+
toolCalls: response.toolCalls,
|
|
1889
|
+
});
|
|
1890
|
+
for (const tc of response.toolCalls) {
|
|
1891
|
+
const approval = (0, approval_1.checkPermission)(tc.name, permissionMode);
|
|
1892
|
+
if (!approval.approved) {
|
|
1893
|
+
throw new Error(`APPROVAL_REQUIRED: ${approval.reason}`);
|
|
1894
|
+
}
|
|
1895
|
+
opts.onWorkerChunk?.({
|
|
1896
|
+
type: 'tool_call',
|
|
1897
|
+
text: '',
|
|
1898
|
+
stepId: 'orchestrator-direct',
|
|
1899
|
+
agentName: 'Orchestrator',
|
|
1900
|
+
description: 'Direct orchestrator work',
|
|
1901
|
+
stepIndex: 0,
|
|
1902
|
+
totalSteps: 1,
|
|
1903
|
+
toolCallId: tc.id,
|
|
1904
|
+
toolName: tc.name,
|
|
1905
|
+
toolArguments: tc.arguments,
|
|
1906
|
+
});
|
|
1907
|
+
const result = await (0, index_2.executeAndVerify)({ id: tc.id, name: tc.name, arguments: tc.arguments }, toolContext);
|
|
1908
|
+
const output = result.success ? result.output : `ERROR: ${result.error || 'Unknown'}`;
|
|
1909
|
+
messages.push({
|
|
1910
|
+
role: 'tool',
|
|
1911
|
+
content: output,
|
|
1912
|
+
toolCallId: tc.id,
|
|
1913
|
+
toolName: tc.name,
|
|
1914
|
+
});
|
|
1915
|
+
opts.onWorkerChunk?.({
|
|
1916
|
+
type: 'tool_result',
|
|
1917
|
+
text: '',
|
|
1918
|
+
stepId: 'orchestrator-direct',
|
|
1919
|
+
agentName: 'Orchestrator',
|
|
1920
|
+
description: 'Direct orchestrator work',
|
|
1921
|
+
stepIndex: 0,
|
|
1922
|
+
totalSteps: 1,
|
|
1923
|
+
toolCallId: tc.id,
|
|
1924
|
+
toolName: tc.name,
|
|
1925
|
+
toolOutput: output.length > 500 ? `${output.slice(0, 500)}...` : output,
|
|
1926
|
+
toolIsError: !result.success,
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
continue;
|
|
1930
|
+
}
|
|
1931
|
+
return String(response.content || '').trim();
|
|
1932
|
+
}
|
|
1933
|
+
throw new Error('Orchestrator direct execution exceeded the maximum tool iterations.');
|
|
1119
1934
|
}
|
|
1120
1935
|
async handleOrchestratorFrontDoor(prompt, conversationId, opts, orchestratorBot, projectId, project, policy, promptAssignments, roleAssignments, overview, state) {
|
|
1121
1936
|
(0, state_1.addHelperRoleUsage)(state, 'orchestrator_front_door');
|
|
@@ -1133,7 +1948,7 @@ class OrchestratorAgent {
|
|
|
1133
1948
|
const PURE_ORCHESTRATOR_ROLES = new Set(['orchestrator', 'project manager']);
|
|
1134
1949
|
const specialists = allProfiles
|
|
1135
1950
|
.filter((agent) => {
|
|
1136
|
-
const rc =
|
|
1951
|
+
const rc = this.getOrchestrationRoleClass(agent);
|
|
1137
1952
|
if (agent.is_orchestrator === 1 && PURE_ORCHESTRATOR_ROLES.has(rc))
|
|
1138
1953
|
return false;
|
|
1139
1954
|
if (!agent.is_orchestrator && (0, orchestrator_profile_1.isOrchestratorProfile)(agent))
|
|
@@ -1142,14 +1957,14 @@ class OrchestratorAgent {
|
|
|
1142
1957
|
})
|
|
1143
1958
|
.map((agent) => ({
|
|
1144
1959
|
name: agent.name,
|
|
1145
|
-
roleLabel:
|
|
1960
|
+
roleLabel: this.getOrchestrationRoleLabel(agent),
|
|
1146
1961
|
references: this.getAgentRoutingReferences(agent),
|
|
1147
1962
|
responsibilities: this.describeAgentResponsibilities(agent),
|
|
1148
1963
|
}));
|
|
1149
1964
|
const signalSpecialists = allProfiles
|
|
1150
1965
|
.map((agent) => ({
|
|
1151
1966
|
name: agent.name,
|
|
1152
|
-
roleLabel:
|
|
1967
|
+
roleLabel: this.getOrchestrationRoleLabel(agent),
|
|
1153
1968
|
references: this.getAgentRoutingReferences(agent),
|
|
1154
1969
|
responsibilities: this.describeAgentResponsibilities(agent),
|
|
1155
1970
|
}));
|
|
@@ -1181,16 +1996,52 @@ class OrchestratorAgent {
|
|
|
1181
1996
|
});
|
|
1182
1997
|
}
|
|
1183
1998
|
const useSinglePassOrchestrator = this.isLocalDesktopRuntime() && this.orchestratorRuntime.kind === 'bot';
|
|
1999
|
+
const orchestratorCliSessionPlan = useSinglePassOrchestrator
|
|
2000
|
+
&& conversationId
|
|
2001
|
+
&& index_1.CLI_PROVIDERS.has(orchestratorBot.provider)
|
|
2002
|
+
? (0, cli_session_epoch_1.selectCliSessionEpoch)(conversationId, orchestratorBot.id, orchestratorBot.provider)
|
|
2003
|
+
: null;
|
|
2004
|
+
const orchestratorBootstrapHistoryFilePath = this.isLocalDesktopRuntime()
|
|
2005
|
+
&& conversationId
|
|
2006
|
+
&& index_1.CLI_PROVIDERS.has(orchestratorBot.provider)
|
|
2007
|
+
&& !orchestratorCliSessionPlan?.resumeSessionId
|
|
2008
|
+
? this.prepareBootstrapHistoryFile(conversationId)
|
|
2009
|
+
: null;
|
|
1184
2010
|
const localPromptContract = useSinglePassOrchestrator
|
|
1185
2011
|
? (index_1.CLI_PROVIDERS.has(orchestratorBot.provider)
|
|
1186
2012
|
&& conversationId
|
|
1187
|
-
&& !!
|
|
2013
|
+
&& !!orchestratorCliSessionPlan?.resumeSessionId
|
|
1188
2014
|
? 'cli_recurring'
|
|
1189
2015
|
: 'api_or_fresh_cli')
|
|
1190
2016
|
: 'legacy';
|
|
2017
|
+
if (useSinglePassOrchestrator && index_1.CLI_PROVIDERS.has(orchestratorBot.provider)) {
|
|
2018
|
+
const sessionLaunchReason = orchestratorCliSessionPlan?.resumeSessionId
|
|
2019
|
+
? 'resumed'
|
|
2020
|
+
: ('mode' in contextWindow && contextWindow.mode === 'new_topic')
|
|
2021
|
+
? 'new_topic/no_context'
|
|
2022
|
+
: 'fresh_with_bootstrap';
|
|
2023
|
+
console.info(`[orchestrator-front-door] session_launch_reason=${sessionLaunchReason} botId=${orchestratorBot.id} provider=${orchestratorBot.provider} bootstrap=${!!orchestratorBootstrapHistoryFilePath}`);
|
|
2024
|
+
console.info(`[orchestrator-front-door] cli_session_selected conversationId=${conversationId} botId=${orchestratorBot.id} provider=${orchestratorBot.provider} selectedSessionId=${orchestratorCliSessionPlan?.resumeSessionId || '(none)'} resetReason=${orchestratorCliSessionPlan?.resetReason || '(none)'}`);
|
|
2025
|
+
if (orchestratorCliSessionPlan?.resumeSessionId) {
|
|
2026
|
+
console.info(`[orchestrator-front-door] cli_session_resuming conversationId=${conversationId} botId=${orchestratorBot.id} provider=${orchestratorBot.provider} sessionId=${orchestratorCliSessionPlan.resumeSessionId}`);
|
|
2027
|
+
}
|
|
2028
|
+
else if ('mode' in contextWindow && contextWindow.mode === 'new_topic') {
|
|
2029
|
+
console.info(`[orchestrator-front-door] cli_session_fresh_without_bootstrap_new_topic conversationId=${conversationId} botId=${orchestratorBot.id} provider=${orchestratorBot.provider}`);
|
|
2030
|
+
}
|
|
2031
|
+
else if (orchestratorBootstrapHistoryFilePath) {
|
|
2032
|
+
console.info(`[orchestrator-front-door] cli_session_fresh_bootstrap_applied conversationId=${conversationId} botId=${orchestratorBot.id} provider=${orchestratorBot.provider}`);
|
|
2033
|
+
}
|
|
2034
|
+
else {
|
|
2035
|
+
console.warn(`[orchestrator-front-door] cli_session_fresh_without_bootstrap_unexpected conversationId=${conversationId} botId=${orchestratorBot.id} provider=${orchestratorBot.provider} mode=${'mode' in contextWindow ? contextWindow.mode : 'unknown'}`);
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
const isClerkRuntime = this.orchestratorRuntime.kind === 'clerk';
|
|
2039
|
+
const orchestratorDisplayName = isClerkRuntime && !orchestratorBot.is_orchestrator
|
|
2040
|
+
? 'Orchestrator'
|
|
2041
|
+
: orchestratorBot.name;
|
|
1191
2042
|
const operatingPrompt = (0, orchestrator_operating_prompt_1.buildOrchestratorOperatingPrompt)({
|
|
1192
|
-
orchestratorName:
|
|
1193
|
-
primaryRole:
|
|
2043
|
+
orchestratorName: orchestratorDisplayName,
|
|
2044
|
+
primaryRole: isClerkRuntime ? 'project manager' : data.getAgentOrchestrationRoleLabel(orchestratorBot),
|
|
1194
2045
|
specialists,
|
|
1195
2046
|
workflowNames,
|
|
1196
2047
|
localPromptContract,
|
|
@@ -1212,7 +2063,9 @@ class OrchestratorAgent {
|
|
|
1212
2063
|
}),
|
|
1213
2064
|
recentSummary: recentSummaryText,
|
|
1214
2065
|
lastFiveTurns: recentTurnsText,
|
|
2066
|
+
bootstrapHistoryFilePath: orchestratorBootstrapHistoryFilePath,
|
|
1215
2067
|
decisionOnly: !useSinglePassOrchestrator,
|
|
2068
|
+
defaultBotName: data.getDefaultAgentProfile()?.name || roleAssignments.coding || null,
|
|
1216
2069
|
});
|
|
1217
2070
|
const decisionPrompt = [
|
|
1218
2071
|
'This is a routing decision only. Do not perform the task.',
|
|
@@ -1223,19 +2076,21 @@ class OrchestratorAgent {
|
|
|
1223
2076
|
'Return strict JSON only.',
|
|
1224
2077
|
].join('\n');
|
|
1225
2078
|
let decision = null;
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
},
|
|
2079
|
+
// No robotic 'Understanding request' progress message — the streamed
|
|
2080
|
+
// narration from the LLM front-door call is what the user sees instead.
|
|
2081
|
+
// Heartbeat removed for the same reason.
|
|
2082
|
+
let understandingHeartbeat = setInterval(() => { }, 1_000_000);
|
|
1230
2083
|
try {
|
|
1231
2084
|
if (useSinglePassOrchestrator) {
|
|
1232
2085
|
const result = await this.runNodeWithRetry('understand_request', state, async () => this.workflowEngine.execute(prompt, conversationId, orchestratorBot.id, {
|
|
2086
|
+
abortSignal: opts.abortSignal,
|
|
1233
2087
|
disableDecomposition: true,
|
|
1234
2088
|
isOrchestrated: false,
|
|
1235
2089
|
projectId: projectId || null,
|
|
1236
2090
|
workspacePath: project?.folder?.trim() || undefined,
|
|
1237
2091
|
persistConversationMessages: false,
|
|
1238
2092
|
workerMode: false,
|
|
2093
|
+
storedAttachments: opts.storedAttachments,
|
|
1239
2094
|
systemPromptOverride: operatingPrompt,
|
|
1240
2095
|
onWorkerChunk: opts.onWorkerChunk,
|
|
1241
2096
|
onProgress: (progress) => {
|
|
@@ -1263,6 +2118,8 @@ class OrchestratorAgent {
|
|
|
1263
2118
|
projectId: projectId || null,
|
|
1264
2119
|
workspacePath: project?.folder?.trim() || undefined,
|
|
1265
2120
|
disableTools: true,
|
|
2121
|
+
abortSignal: opts.abortSignal,
|
|
2122
|
+
onNarrationChunk: this.makeOrchestratorNarrationForwarder(opts, orchestratorBot),
|
|
1266
2123
|
}));
|
|
1267
2124
|
decision = (0, orchestrator_operating_prompt_1.parseOrchestratorFrontDoorDecision)(rawDecision);
|
|
1268
2125
|
}
|
|
@@ -1322,7 +2179,20 @@ class OrchestratorAgent {
|
|
|
1322
2179
|
};
|
|
1323
2180
|
}
|
|
1324
2181
|
}
|
|
1325
|
-
|
|
2182
|
+
// Fallback narration: if the decision has no narration (LLM call failed
|
|
2183
|
+
// or returned a code-fallback decision), synthesize a short factual one
|
|
2184
|
+
// and emit it so the user sees something natural in the orchestrator card.
|
|
2185
|
+
// Pass roleAssignments so workflow fallbacks can use real worker names.
|
|
2186
|
+
if (decision && !decision.narration?.trim()) {
|
|
2187
|
+
const fallbackText = this.synthesizeFallbackNarration(decision, roleAssignments);
|
|
2188
|
+
if (fallbackText) {
|
|
2189
|
+
decision.narration = fallbackText;
|
|
2190
|
+
const forwarder = this.makeOrchestratorNarrationForwarder(opts, orchestratorBot);
|
|
2191
|
+
if (forwarder)
|
|
2192
|
+
forwarder(fallbackText);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
return this.handleFrontDoorDecision(decision, prompt, conversationId, opts, orchestratorBot, projectId, project, policy, promptAssignments, roleAssignments, overview, state, frontDoorSignals, signalSpecialists, workflowNames);
|
|
1326
2196
|
}
|
|
1327
2197
|
buildFrontDoorSystemSuggestion(prompt, frontDoorSignals, promptAssignments, roleAssignments) {
|
|
1328
2198
|
const normalizeName = (value) => String(value || '').trim().toLowerCase();
|
|
@@ -1367,7 +2237,7 @@ class OrchestratorAgent {
|
|
|
1367
2237
|
delegateRole,
|
|
1368
2238
|
};
|
|
1369
2239
|
}
|
|
1370
|
-
async handleFrontDoorDecision(decision, prompt, conversationId, opts, orchestratorBot, projectId, project, policy, promptAssignments, roleAssignments, overview, state, signalSpecialists, workflowNames) {
|
|
2240
|
+
async handleFrontDoorDecision(decision, prompt, conversationId, opts, orchestratorBot, projectId, project, policy, promptAssignments, roleAssignments, overview, state, frontDoorSignals, signalSpecialists, workflowNames) {
|
|
1371
2241
|
if (!decision) {
|
|
1372
2242
|
this.recordOrchestrationAudit(state, 'understand_request', 'blocked', 'Orchestrator-first decision phase did not return valid JSON. Falling back to legacy clerk chain.', { featureFlag: 'orchestrator_v2_enabled' });
|
|
1373
2243
|
return null;
|
|
@@ -1375,21 +2245,16 @@ class OrchestratorAgent {
|
|
|
1375
2245
|
const highRiskPrompt = this.isHighRiskPrompt(prompt);
|
|
1376
2246
|
state.riskLevel = highRiskPrompt ? 'high' : state.riskLevel;
|
|
1377
2247
|
this.recordOrchestrationAudit(state, 'risk_gate', 'risk_assessed', highRiskPrompt ? 'Prompt matched high-risk action patterns.' : 'No high-risk action patterns matched before execution.', { highRiskPrompt });
|
|
1378
|
-
const normalizedDecision =
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
});
|
|
1389
|
-
if (normalizedDecision.corrected) {
|
|
1390
|
-
(0, state_1.markMisrouteCorrected)(state);
|
|
1391
|
-
}
|
|
1392
|
-
decision = normalizedDecision.decision;
|
|
2248
|
+
const normalizedDecision = {
|
|
2249
|
+
decision,
|
|
2250
|
+
corrected: false,
|
|
2251
|
+
correctionReason: undefined,
|
|
2252
|
+
taskType: (0, front_door_policy_1.inferFrontDoorTaskType)(prompt),
|
|
2253
|
+
qaMode: 'none',
|
|
2254
|
+
preferredWorkflowRequested: frontDoorSignals.matchedWorkflowNames.length > 0,
|
|
2255
|
+
explicitWorkflowRequested: frontDoorSignals.explicitOrchestrationRequested,
|
|
2256
|
+
signals: frontDoorSignals,
|
|
2257
|
+
};
|
|
1393
2258
|
state.intent = decision.mode;
|
|
1394
2259
|
state.taskType = normalizedDecision.taskType;
|
|
1395
2260
|
this.recordOrchestrationAudit(state, 'choose_path', 'path_selected', normalizedDecision.correctionReason || decision.reason || `Orchestrator selected ${decision.mode}.`, {
|
|
@@ -1432,42 +2297,39 @@ class OrchestratorAgent {
|
|
|
1432
2297
|
return response;
|
|
1433
2298
|
}
|
|
1434
2299
|
if (decision.mode === 'execute_self') {
|
|
1435
|
-
if (this.orchestratorRuntime.kind === 'clerk') {
|
|
1436
|
-
const fallbackDelegateTarget = roleAssignments.coding
|
|
1437
|
-
|| data.getDefaultAgentProfile()?.name
|
|
1438
|
-
|| data.listAgentProfiles().find((agent) => !(0, orchestrator_profile_1.isOrchestratorProfile)(agent))?.name;
|
|
1439
|
-
if (fallbackDelegateTarget) {
|
|
1440
|
-
const delegateIntent = {
|
|
1441
|
-
primaryMode: 'PROXY_MODE',
|
|
1442
|
-
secondaryModes: [],
|
|
1443
|
-
executionOrder: ['PROXY_MODE'],
|
|
1444
|
-
userFacingMode: 'ASK_WORKER',
|
|
1445
|
-
targetScope: 'ONE_WORKER',
|
|
1446
|
-
confidence: 'MEDIUM',
|
|
1447
|
-
intent: normalizedDecision.taskType === 'qa'
|
|
1448
|
-
? 'review'
|
|
1449
|
-
: normalizedDecision.taskType === 'research'
|
|
1450
|
-
? 'brainstorm'
|
|
1451
|
-
: 'build',
|
|
1452
|
-
targetAgent: fallbackDelegateTarget,
|
|
1453
|
-
isMultiStep: false,
|
|
1454
|
-
reasoning: 'Clerk orchestrator cannot own implementation work directly, so the task falls back to the default worker.',
|
|
1455
|
-
};
|
|
1456
|
-
const delegatePrompt = prompt;
|
|
1457
|
-
const executionSpec = this.buildFrontDoorDelegateExecutionSpec(delegatePrompt, delegateIntent, fallbackDelegateTarget);
|
|
1458
|
-
const guardrailQuestions = this.buildDeterministicGuardrailQuestions(executionSpec, roleAssignments, delegateIntent);
|
|
1459
|
-
if (guardrailQuestions.length > 0) {
|
|
1460
|
-
return this.formatClarificationResponse({ ...delegateIntent, clarificationQuestions: guardrailQuestions }, overview);
|
|
1461
|
-
}
|
|
1462
|
-
this.applyExecutionSpecToState(state, executionSpec);
|
|
1463
|
-
return this.handleProxyRequest(delegatePrompt, conversationId, delegateIntent, opts, executionSpec, roleAssignments, state);
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
// No guardrails, no confirmation checkpoints, no validation blocking.
|
|
1467
|
-
// Ben just does the work — like Claude CLI.
|
|
1468
2300
|
(0, state_1.setOrchestrationPath)(state, 'direct', 'direct');
|
|
1469
2301
|
const directResult = await this.executeOrchestratorOwnedWork(prompt, conversationId, orchestratorBot, roleAssignments, project, opts, state);
|
|
1470
|
-
|
|
2302
|
+
if (directResult.ok) {
|
|
2303
|
+
return directResult.response;
|
|
2304
|
+
}
|
|
2305
|
+
const fallbackAgent = this.resolveDefaultFallbackAgent(project?.id || state.projectId || undefined, orchestratorBot, roleAssignments);
|
|
2306
|
+
if (!fallbackAgent) {
|
|
2307
|
+
return directResult.response;
|
|
2308
|
+
}
|
|
2309
|
+
const fallbackIntent = {
|
|
2310
|
+
primaryMode: 'PROXY_MODE',
|
|
2311
|
+
secondaryModes: [],
|
|
2312
|
+
executionOrder: ['PROXY_MODE'],
|
|
2313
|
+
userFacingMode: 'ASK_WORKER',
|
|
2314
|
+
targetScope: 'ONE_WORKER',
|
|
2315
|
+
confidence: 'MEDIUM',
|
|
2316
|
+
intent: normalizedDecision.taskType === 'qa'
|
|
2317
|
+
? 'review'
|
|
2318
|
+
: normalizedDecision.taskType === 'research'
|
|
2319
|
+
? 'brainstorm'
|
|
2320
|
+
: 'build',
|
|
2321
|
+
targetAgent: fallbackAgent.name,
|
|
2322
|
+
isMultiStep: false,
|
|
2323
|
+
reasoning: `${orchestratorBot.name} could not complete the kept task, so it is falling back to default bot ${fallbackAgent.name}.`,
|
|
2324
|
+
};
|
|
2325
|
+
const fallbackExecutionSpec = this.buildFrontDoorDelegateExecutionSpec(prompt, fallbackIntent, fallbackAgent.name);
|
|
2326
|
+
this.applyExecutionSpecToState(state, fallbackExecutionSpec);
|
|
2327
|
+
this.recordOrchestrationAudit(state, 'delegate_specialist', 'delegated', `${orchestratorBot.name} could not finish the kept task, so it is falling back to default bot ${fallbackAgent.name}.`, {
|
|
2328
|
+
targetAgentId: fallbackAgent.id,
|
|
2329
|
+
targetAgentName: fallbackAgent.name,
|
|
2330
|
+
fallbackReason: directResult.error || null,
|
|
2331
|
+
});
|
|
2332
|
+
return this.handleProxyRequest(prompt, conversationId, fallbackIntent, opts, fallbackExecutionSpec, roleAssignments, state);
|
|
1471
2333
|
}
|
|
1472
2334
|
if (decision.mode === 'delegate') {
|
|
1473
2335
|
const delegateTarget = this.resolveDelegatedAgentName(decision, roleAssignments);
|
|
@@ -1509,7 +2371,7 @@ class OrchestratorAgent {
|
|
|
1509
2371
|
if (executionSpec.requiresConfirmation) {
|
|
1510
2372
|
return this.createConfirmationCheckpoint(delegatePrompt, conversationId, delegateIntent, executionSpec, projectId, roleAssignments, state);
|
|
1511
2373
|
}
|
|
1512
|
-
return await this.queueDelegateTodo(delegatePrompt, conversationId, delegateIntent, opts, executionSpec, roleAssignments, state, orchestratorBot, delegateTarget);
|
|
2374
|
+
return await this.queueDelegateTodo(delegatePrompt, conversationId, delegateIntent, opts, executionSpec, roleAssignments, state, this.getOrchestratorDispatchActor(orchestratorBot), delegateTarget);
|
|
1513
2375
|
}
|
|
1514
2376
|
if (decision.mode === 'workflow') {
|
|
1515
2377
|
const workflowObjective = decision.workflow_request?.trim() || prompt;
|
|
@@ -1539,19 +2401,32 @@ class OrchestratorAgent {
|
|
|
1539
2401
|
return this.createConfirmationCheckpoint(prompt, conversationId, workflowIntent, validation.executionSpec, projectId, roleAssignments, state);
|
|
1540
2402
|
}
|
|
1541
2403
|
this.recordOrchestrationAudit(state, 'run_workflow', 'delegated', 'Workflow execution is using the original user prompt as the execution source of truth.', { workflowObjective });
|
|
1542
|
-
return await this.queueWorkflowTodoPlan(prompt, conversationId, workflowIntent, validation.executionSpec, opts, roleAssignments, project, state, orchestratorBot);
|
|
2404
|
+
return await this.queueWorkflowTodoPlan(prompt, conversationId, workflowIntent, validation.executionSpec, opts, roleAssignments, project, state, this.getOrchestratorDispatchActor(orchestratorBot));
|
|
1543
2405
|
}
|
|
1544
2406
|
return null;
|
|
1545
2407
|
}
|
|
1546
2408
|
resolveDelegatedAgentName(decision, roleAssignments) {
|
|
1547
2409
|
const target = decision.delegate_target?.trim();
|
|
2410
|
+
// Validate delegate_target matches delegate_role — if the LLM says role=qa
|
|
2411
|
+
// but target=Ben (the default/coding bot), override with the correct role assignment.
|
|
2412
|
+
// This prevents the LLM from always defaulting to the "default bot" for every role.
|
|
2413
|
+
const role = decision.delegate_role;
|
|
2414
|
+
if (role && target && target.toLowerCase() !== 'none') {
|
|
2415
|
+
const roleBot = role === 'qa' ? roleAssignments.qa
|
|
2416
|
+
: role === 'research' ? roleAssignments.research
|
|
2417
|
+
: role === 'coding' ? roleAssignments.coding
|
|
2418
|
+
: undefined;
|
|
2419
|
+
if (roleBot && roleBot.toLowerCase() !== target.toLowerCase()) {
|
|
2420
|
+
return roleBot;
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
1548
2423
|
if (target && target.toLowerCase() !== 'none')
|
|
1549
2424
|
return target;
|
|
1550
|
-
if (
|
|
2425
|
+
if (role === 'coding')
|
|
1551
2426
|
return roleAssignments.coding;
|
|
1552
|
-
if (
|
|
2427
|
+
if (role === 'qa')
|
|
1553
2428
|
return roleAssignments.qa;
|
|
1554
|
-
if (
|
|
2429
|
+
if (role === 'research')
|
|
1555
2430
|
return roleAssignments.research;
|
|
1556
2431
|
return undefined;
|
|
1557
2432
|
}
|
|
@@ -1615,7 +2490,7 @@ class OrchestratorAgent {
|
|
|
1615
2490
|
locked: true,
|
|
1616
2491
|
};
|
|
1617
2492
|
}
|
|
1618
|
-
async queueDelegateTodo(prompt, conversationId, intent, opts, executionSpec, roleAssignments, state,
|
|
2493
|
+
async queueDelegateTodo(prompt, conversationId, intent, opts, executionSpec, roleAssignments, state, orchestratorActor, delegateTarget) {
|
|
1619
2494
|
const projectId = opts.projectId || state.projectId || null;
|
|
1620
2495
|
const project = projectId ? data.getProject(projectId) : undefined;
|
|
1621
2496
|
const targetAgent = this.findAgentByName(delegateTarget, project?.id || undefined);
|
|
@@ -1626,7 +2501,7 @@ class OrchestratorAgent {
|
|
|
1626
2501
|
clarificationQuestions: [`I could not resolve the target bot "${delegateTarget}". Which worker should own this task?`],
|
|
1627
2502
|
}, projectId ? data.getProjectOverview(projectId) : undefined);
|
|
1628
2503
|
}
|
|
1629
|
-
const step = this.buildSingleDelegateTodoStep(prompt, targetAgent,
|
|
2504
|
+
const step = this.buildSingleDelegateTodoStep(prompt, conversationId, targetAgent, orchestratorActor, project?.name || null, project?.folder || null, data.getEffectiveOrchestrationPolicy(projectId || undefined));
|
|
1630
2505
|
const task = data.addTodoTask({
|
|
1631
2506
|
projectId,
|
|
1632
2507
|
conversationId,
|
|
@@ -1639,7 +2514,7 @@ class OrchestratorAgent {
|
|
|
1639
2514
|
taskType: step.taskType,
|
|
1640
2515
|
profileName: 'Programming',
|
|
1641
2516
|
taskPrompt: step.taskPrompt,
|
|
1642
|
-
actor: { actorType: 'orchestrator', actorId:
|
|
2517
|
+
actor: { actorType: 'orchestrator', actorId: orchestratorActor.actorId },
|
|
1643
2518
|
});
|
|
1644
2519
|
this.markConversationOrchestrated(conversationId);
|
|
1645
2520
|
this.recordOrchestrationAudit(state, 'delegate_specialist', 'delegated', `Queued delegated TODO for ${targetAgent.name}.`, {
|
|
@@ -1659,14 +2534,14 @@ class OrchestratorAgent {
|
|
|
1659
2534
|
].join('\n');
|
|
1660
2535
|
return state.finalResponseDraft;
|
|
1661
2536
|
}
|
|
1662
|
-
return await this.dispatchQueuedTodoChain(conversationId, opts, state,
|
|
2537
|
+
return await this.dispatchQueuedTodoChain(conversationId, opts, state, orchestratorActor, project, { taskIds: [task.id], workflowName: null });
|
|
1663
2538
|
}
|
|
1664
|
-
async queueWorkflowTodoPlan(prompt, conversationId, intent, executionSpec, opts, roleAssignments, project, state,
|
|
2539
|
+
async queueWorkflowTodoPlan(prompt, conversationId, intent, executionSpec, opts, roleAssignments, project, state, orchestratorActor, workflowTemplate) {
|
|
1665
2540
|
const projectId = project?.id || opts.projectId || state.projectId || null;
|
|
1666
2541
|
const effectivePolicy = data.getEffectiveOrchestrationPolicy(projectId || undefined);
|
|
1667
2542
|
const plannedSteps = workflowTemplate
|
|
1668
|
-
? this.buildTodoStepsFromTemplate(prompt, workflowTemplate,
|
|
1669
|
-
: this.buildTodoStepsFromAssignments(prompt, roleAssignments, intent,
|
|
2543
|
+
? this.buildTodoStepsFromTemplate(prompt, conversationId, workflowTemplate, orchestratorActor, project?.name || null, project?.folder || null, effectivePolicy)
|
|
2544
|
+
: this.buildTodoStepsFromAssignments(prompt, conversationId, roleAssignments, intent, orchestratorActor, projectId || undefined, project?.name || null, project?.folder || null, effectivePolicy);
|
|
1670
2545
|
if (plannedSteps.length === 0) {
|
|
1671
2546
|
const overview = projectId ? data.getProjectOverview(projectId) : undefined;
|
|
1672
2547
|
return this.formatClarificationResponse({
|
|
@@ -1692,9 +2567,9 @@ class OrchestratorAgent {
|
|
|
1692
2567
|
profileName: 'Programming',
|
|
1693
2568
|
nextWorkerBotId: next?.owner.id || null,
|
|
1694
2569
|
nextWorkerName: next?.owner.name || null,
|
|
1695
|
-
nextWorkerRole: next ? (next.owner
|
|
2570
|
+
nextWorkerRole: next ? data.getAgentOrchestrationRoleLabel(next.owner) : null,
|
|
1696
2571
|
taskPrompt: step.taskPrompt,
|
|
1697
|
-
actor: { actorType: 'orchestrator', actorId:
|
|
2572
|
+
actor: { actorType: 'orchestrator', actorId: orchestratorActor.actorId },
|
|
1698
2573
|
});
|
|
1699
2574
|
createdTasks.push(task);
|
|
1700
2575
|
(0, state_1.addDelegateTarget)(state, step.owner.name);
|
|
@@ -1723,18 +2598,72 @@ class OrchestratorAgent {
|
|
|
1723
2598
|
state.finalResponseDraft = lines.join('\n');
|
|
1724
2599
|
return state.finalResponseDraft;
|
|
1725
2600
|
}
|
|
1726
|
-
return await this.dispatchQueuedTodoChain(conversationId, opts, state,
|
|
2601
|
+
return await this.dispatchQueuedTodoChain(conversationId, opts, state, orchestratorActor, project, {
|
|
1727
2602
|
taskIds: createdTasks.map((task) => task.id),
|
|
1728
2603
|
workflowName: workflowTemplate?.name || null,
|
|
1729
2604
|
});
|
|
1730
2605
|
}
|
|
1731
|
-
async dispatchQueuedTodoChain(conversationId, opts, state,
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
2606
|
+
async dispatchQueuedTodoChain(conversationId, opts, state, orchestratorActor, project, context) {
|
|
2607
|
+
// Pending queue seeded from the orchestrator's delegation. These IDs are
|
|
2608
|
+
// the source of truth for the dispatch chain; we run them in the exact
|
|
2609
|
+
// order the orchestrator returned them. If a completion produces a new
|
|
2610
|
+
// TODO (qa_result='fail' auto-fix, insert_task, or re-QA companion),
|
|
2611
|
+
// the new TODO is appended to the front of pending so the inline fix
|
|
2612
|
+
// cycle runs before anything sitting later in the original sequence.
|
|
2613
|
+
// (Codex QA 2026-04-19 #16 / april19fixes.txt item 5.)
|
|
2614
|
+
const pending = context.taskIds.filter((id) => Number.isFinite(id)).map((id) => Number(id));
|
|
2615
|
+
const seen = new Set(pending);
|
|
2616
|
+
// Legacy fallback: if the caller didn't provide taskIds (older code
|
|
2617
|
+
// paths that haven't been migrated yet), seed from the oldest active
|
|
2618
|
+
// TODO. The new local_desktop dispatch path always supplies taskIds.
|
|
2619
|
+
const completedTaskIds = [];
|
|
2620
|
+
const initialTaskCount = pending.length;
|
|
2621
|
+
// Explicit/imported workflows must be allowed to run to completion even
|
|
2622
|
+
// when they legitimately contain dozens of ordered TODOs. Keep the
|
|
2623
|
+
// safety brake, but make it an emergency guard against runaway re-queue
|
|
2624
|
+
// loops rather than a low fixed workflow-length cap.
|
|
2625
|
+
const maxIterations = Math.max(500, (initialTaskCount * 10) + 100);
|
|
2626
|
+
const maxDynamicallyInsertedTasks = Math.max(200, (initialTaskCount * 4) + 50);
|
|
2627
|
+
let dynamicallyInsertedCount = 0;
|
|
2628
|
+
if (pending.length === 0) {
|
|
2629
|
+
const first = data.getNextActiveTodoForConversation(conversationId);
|
|
2630
|
+
if (first) {
|
|
2631
|
+
pending.push(first.id);
|
|
2632
|
+
seen.add(first.id);
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
for (let iteration = 0;; iteration += 1) {
|
|
2636
|
+
if (iteration >= maxIterations) {
|
|
2637
|
+
const message = 'The workflow created too many follow-up executions and was stopped to prevent an infinite loop.';
|
|
2638
|
+
state.finalResponseDraft = message;
|
|
2639
|
+
this.recordOrchestrationAudit(state, 'run_workflow', 'blocked', message, { maxIterations, initialTaskCount, dynamicallyInsertedCount });
|
|
2640
|
+
return message;
|
|
2641
|
+
}
|
|
2642
|
+
if (opts.abortSignal?.aborted) {
|
|
2643
|
+
state.finalResponseDraft = '';
|
|
2644
|
+
return '';
|
|
2645
|
+
}
|
|
2646
|
+
let task;
|
|
2647
|
+
while (pending.length > 0 && !task) {
|
|
2648
|
+
const candidateId = pending.shift();
|
|
2649
|
+
const active = data.getTodoTask(candidateId, 'active');
|
|
2650
|
+
if (active) {
|
|
2651
|
+
task = active;
|
|
2652
|
+
}
|
|
2653
|
+
else if (data.getTodoTask(candidateId, 'completed')) {
|
|
2654
|
+
this.recordOrchestrationAudit(state, 'run_workflow', 'completed', `resume_skipped_completed_task:${candidateId}`, { taskId: candidateId });
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
1735
2657
|
if (!task) {
|
|
1736
|
-
return await this.finalizeQueuedTodoChain(conversationId, opts, state,
|
|
2658
|
+
return await this.finalizeQueuedTodoChain(conversationId, opts, state, orchestratorActor, project, context.workflowName, completedTaskIds);
|
|
1737
2659
|
}
|
|
2660
|
+
// Snapshot active IDs BEFORE this iteration so the post-iteration
|
|
2661
|
+
// "new TODOs" scan only picks up tasks that were actually created
|
|
2662
|
+
// during this execute() call (fix cycles, insert_task, re-QA
|
|
2663
|
+
// companions). Stale/pre-existing active TODOs from prior turns
|
|
2664
|
+
// are NOT swept in — the dispatcher only executes what the
|
|
2665
|
+
// orchestrator delegated plus inline insertions.
|
|
2666
|
+
const beforeActiveIds = new Set(data.listActiveTodoTasksForConversation(conversationId).map((t) => t.id));
|
|
1738
2667
|
const owner = task.owner_bot_id
|
|
1739
2668
|
? data.getAgentProfile(task.owner_bot_id)
|
|
1740
2669
|
: this.findAgentByName(task.owner_name || undefined, project?.id || undefined);
|
|
@@ -1745,8 +2674,12 @@ class OrchestratorAgent {
|
|
|
1745
2674
|
return message;
|
|
1746
2675
|
}
|
|
1747
2676
|
const taskPrompt = task.task_prompt?.trim() || task.details || task.title;
|
|
2677
|
+
if (opts.importedPlanRunId) {
|
|
2678
|
+
data.touchImportedPlanRun(opts.importedPlanRunId);
|
|
2679
|
+
}
|
|
1748
2680
|
const result = await this.runNodeWithRetry('run_workflow', state, async () => this.workflowEngine.execute(taskPrompt, conversationId, owner.id, {
|
|
1749
2681
|
apiKey: this.resolveApiKey(owner.provider),
|
|
2682
|
+
abortSignal: opts.abortSignal,
|
|
1750
2683
|
taskId: task.id,
|
|
1751
2684
|
stepDescription: task.title,
|
|
1752
2685
|
disableDecomposition: true,
|
|
@@ -1754,6 +2687,10 @@ class OrchestratorAgent {
|
|
|
1754
2687
|
projectId: project?.id || opts.projectId || state.projectId || null,
|
|
1755
2688
|
workspacePath: project?.folder?.trim() || undefined,
|
|
1756
2689
|
persistConversationMessages: false,
|
|
2690
|
+
storedAttachments: opts.storedAttachments,
|
|
2691
|
+
workerImageDeliveryMode: opts.storedAttachments?.length ? 'reference' : undefined,
|
|
2692
|
+
importedPlanRunId: opts.importedPlanRunId,
|
|
2693
|
+
cliSessionScopeKey: opts.cliSessionScopeKey,
|
|
1757
2694
|
onWorkerChunk: opts.onWorkerChunk,
|
|
1758
2695
|
onProgress: (progress) => {
|
|
1759
2696
|
if (progress.event === 'step-started') {
|
|
@@ -1767,11 +2704,19 @@ class OrchestratorAgent {
|
|
|
1767
2704
|
}
|
|
1768
2705
|
},
|
|
1769
2706
|
}));
|
|
2707
|
+
if (opts.abortSignal?.aborted || isAbortLikeError(result?.error)) {
|
|
2708
|
+
state.finalResponseDraft = '';
|
|
2709
|
+
return '';
|
|
2710
|
+
}
|
|
1770
2711
|
const stillActive = data.getTodoTask(task.id, 'active');
|
|
1771
2712
|
const completed = data.getTodoTask(task.id, 'completed');
|
|
2713
|
+
if (opts.abortSignal?.aborted) {
|
|
2714
|
+
state.finalResponseDraft = '';
|
|
2715
|
+
return '';
|
|
2716
|
+
}
|
|
1772
2717
|
if (stillActive?.blocker_summary && stillActive.blocker_question) {
|
|
1773
2718
|
this.recordOrchestrationAudit(state, 'run_workflow', 'blocked', `${owner.name} blocked on TODO: ${task.title}`, { taskId: task.id, blockerSummary: stillActive.blocker_summary });
|
|
1774
|
-
return await this.handleBlockedTodoTask(stillActive, opts, state,
|
|
2719
|
+
return await this.handleBlockedTodoTask(stillActive, opts, state, orchestratorActor, project);
|
|
1775
2720
|
}
|
|
1776
2721
|
if (!completed) {
|
|
1777
2722
|
const error = result.steps.find((step) => step.error)?.error || this.stripWorkerProtocol(result.mergedResult || '') || 'Worker did not complete the queued task.';
|
|
@@ -1780,19 +2725,46 @@ class OrchestratorAgent {
|
|
|
1780
2725
|
this.recordOrchestrationAudit(state, 'run_workflow', 'blocked', message, { taskId: task.id });
|
|
1781
2726
|
return message;
|
|
1782
2727
|
}
|
|
2728
|
+
completedTaskIds.push(completed.id);
|
|
2729
|
+
// Pick up any TODOs that were newly inserted during THIS iteration
|
|
2730
|
+
// (fix cycles, re-QA companions, worker-directed insert_task) —
|
|
2731
|
+
// i.e. active now but not active before the iteration began. They
|
|
2732
|
+
// go to the front of pending so the inline cycle completes before
|
|
2733
|
+
// the original chain continues. Stale/older active TODOs from
|
|
2734
|
+
// prior turns are NOT swept in (they were in beforeActiveIds).
|
|
2735
|
+
const activeAfter = data.listActiveTodoTasksForConversation(conversationId);
|
|
2736
|
+
const newlyInserted = [];
|
|
2737
|
+
for (const t of activeAfter) {
|
|
2738
|
+
if (!beforeActiveIds.has(t.id) && !seen.has(t.id)) {
|
|
2739
|
+
newlyInserted.push(t.id);
|
|
2740
|
+
seen.add(t.id);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
if (newlyInserted.length > 0) {
|
|
2744
|
+
dynamicallyInsertedCount += newlyInserted.length;
|
|
2745
|
+
if (dynamicallyInsertedCount > maxDynamicallyInsertedTasks) {
|
|
2746
|
+
const message = 'The workflow created too many follow-up TODOs and was stopped to prevent an infinite loop.';
|
|
2747
|
+
state.finalResponseDraft = message;
|
|
2748
|
+
this.recordOrchestrationAudit(state, 'run_workflow', 'blocked', message, { maxDynamicallyInsertedTasks, dynamicallyInsertedCount, initialTaskCount });
|
|
2749
|
+
return message;
|
|
2750
|
+
}
|
|
2751
|
+
pending.unshift(...newlyInserted);
|
|
2752
|
+
}
|
|
1783
2753
|
}
|
|
1784
|
-
const message = 'The workflow exceeded the automatic dispatch limit before completing.';
|
|
1785
|
-
state.finalResponseDraft = message;
|
|
1786
|
-
this.recordOrchestrationAudit(state, 'run_workflow', 'blocked', message, { maxSteps: 20 });
|
|
1787
|
-
return message;
|
|
1788
2754
|
}
|
|
1789
|
-
async handleBlockedTodoTask(task, opts, state,
|
|
2755
|
+
async handleBlockedTodoTask(task, opts, state, orchestratorActor, project) {
|
|
1790
2756
|
const policy = data.getEffectiveOrchestrationPolicy(project?.id || opts.projectId || state.projectId || undefined);
|
|
1791
2757
|
const artifacts = data.listTodoArtifactsForTask(task.id, 'active').map((item) => item.path_or_ref);
|
|
1792
2758
|
const completedTasks = data.listCompletedTodoTasksForConversation(task.conversation_id || state.conversationId || '');
|
|
1793
2759
|
const workerRole = task.task_type || task.next_worker_role || 'general';
|
|
2760
|
+
if (/provider returned a quota\/rate-limit error/i.test(task.blocker_checked || '')) {
|
|
2761
|
+
const exact = task.blocker_summary || 'Provider limit reached.';
|
|
2762
|
+
const response = `Provider returned:\n\n${exact}\n\nFix the provider account limit or wait for the retry window, then resume the plan.`;
|
|
2763
|
+
state.finalResponseDraft = response;
|
|
2764
|
+
return response;
|
|
2765
|
+
}
|
|
1794
2766
|
const blockedPrompt = (0, orchestrator_blocked_prompt_1.buildBlockedWorkerOrchestratorPrompt)({
|
|
1795
|
-
orchestratorName:
|
|
2767
|
+
orchestratorName: orchestratorActor.name,
|
|
1796
2768
|
workerName: task.owner_name || 'Worker',
|
|
1797
2769
|
workerRole,
|
|
1798
2770
|
taskTitle: task.title,
|
|
@@ -1811,10 +2783,11 @@ class OrchestratorAgent {
|
|
|
1811
2783
|
try {
|
|
1812
2784
|
rawResponse = await this.runNodeWithRetry('finalize_response', state, async () => this.runOrchestratorPrompt(blockedPrompt, `You are ${this.orchestratorRuntime.agentName}. Write the user-facing blocker message after work has already started. Be concise, outcome-first, and do not restate the full original request.`, {
|
|
1813
2785
|
conversationId: task.conversation_id || state.conversationId || null,
|
|
1814
|
-
orchestratorBot,
|
|
2786
|
+
orchestratorBot: orchestratorActor.bot || undefined,
|
|
1815
2787
|
projectId: project?.id || opts.projectId || state.projectId || null,
|
|
1816
2788
|
workspacePath: project?.folder?.trim() || undefined,
|
|
1817
2789
|
disableTools: true,
|
|
2790
|
+
abortSignal: opts.abortSignal,
|
|
1818
2791
|
}));
|
|
1819
2792
|
}
|
|
1820
2793
|
catch (error) {
|
|
@@ -1827,8 +2800,14 @@ class OrchestratorAgent {
|
|
|
1827
2800
|
state.finalResponseDraft = response;
|
|
1828
2801
|
return response;
|
|
1829
2802
|
}
|
|
1830
|
-
async finalizeQueuedTodoChain(conversationId, opts, state,
|
|
1831
|
-
const
|
|
2803
|
+
async finalizeQueuedTodoChain(conversationId, opts, state, orchestratorActor, project, workflowName, completedTaskIds = []) {
|
|
2804
|
+
const ids = completedTaskIds
|
|
2805
|
+
.map((id) => Number(id))
|
|
2806
|
+
.filter((id, index, arr) => Number.isFinite(id) && id > 0 && arr.indexOf(id) === index);
|
|
2807
|
+
const sourceTasks = ids
|
|
2808
|
+
.map((id) => data.getTodoTask(id, 'completed'))
|
|
2809
|
+
.filter((task) => Boolean(task));
|
|
2810
|
+
const completedTasks = sourceTasks.map((task) => ({
|
|
1832
2811
|
...task,
|
|
1833
2812
|
artifactRefs: data.listTodoArtifactsForTask(task.id, 'completed').map((item) => item.path_or_ref),
|
|
1834
2813
|
}));
|
|
@@ -1838,7 +2817,7 @@ class OrchestratorAgent {
|
|
|
1838
2817
|
}
|
|
1839
2818
|
const policy = data.getEffectiveOrchestrationPolicy(project?.id || opts.projectId || state.projectId || undefined);
|
|
1840
2819
|
const finalPrompt = (0, orchestrator_final_response_prompt_1.buildOrchestratorFinalResponsePrompt)({
|
|
1841
|
-
orchestratorName:
|
|
2820
|
+
orchestratorName: orchestratorActor.name,
|
|
1842
2821
|
projectName: project?.name || null,
|
|
1843
2822
|
workspacePath: project?.folder || null,
|
|
1844
2823
|
effectivePolicy: policy,
|
|
@@ -1848,10 +2827,11 @@ class OrchestratorAgent {
|
|
|
1848
2827
|
try {
|
|
1849
2828
|
response = await this.runNodeWithRetry('finalize_response', state, async () => this.runOrchestratorPrompt(finalPrompt, `You are ${this.orchestratorRuntime.agentName}. Write the final user-facing answer after the workflow is complete. Do not restate the full request. Say what was created, where it is, the concise result, and only any important caveat. Do not claim everything was verified, defect-free, or ready for use unless a completed verification step explicitly established that.`, {
|
|
1850
2829
|
conversationId,
|
|
1851
|
-
orchestratorBot,
|
|
2830
|
+
orchestratorBot: orchestratorActor.bot || undefined,
|
|
1852
2831
|
projectId: project?.id || opts.projectId || state.projectId || null,
|
|
1853
2832
|
workspacePath: project?.folder?.trim() || undefined,
|
|
1854
2833
|
disableTools: true,
|
|
2834
|
+
abortSignal: opts.abortSignal,
|
|
1855
2835
|
}));
|
|
1856
2836
|
}
|
|
1857
2837
|
catch (error) {
|
|
@@ -1867,27 +2847,31 @@ class OrchestratorAgent {
|
|
|
1867
2847
|
: 'Completed automatic TODO dispatch.', { completedTaskCount: completedTasks.length });
|
|
1868
2848
|
return response;
|
|
1869
2849
|
}
|
|
1870
|
-
buildSingleDelegateTodoStep(prompt, owner,
|
|
2850
|
+
buildSingleDelegateTodoStep(prompt, conversationId, owner, orchestratorActor, projectName, workspacePath, effectivePolicy) {
|
|
1871
2851
|
const taskType = this.normalizeTaskTypeForWorker(owner, 'coding');
|
|
2852
|
+
const recentContext = this.buildRecentConversationContextForWorker(conversationId);
|
|
1872
2853
|
return {
|
|
1873
|
-
title: `${owner.name}:
|
|
1874
|
-
details: `Delegated by ${
|
|
2854
|
+
title: `${owner.name}: handle the delegated request`,
|
|
2855
|
+
details: `Delegated by ${orchestratorActor.name}. Complete this single-worker request directly.`,
|
|
1875
2856
|
owner,
|
|
1876
2857
|
taskType,
|
|
1877
2858
|
taskPrompt: this.buildWorkerTaskPrompt({
|
|
1878
2859
|
originalPrompt: prompt,
|
|
1879
|
-
stepInstruction: 'Handle
|
|
2860
|
+
stepInstruction: 'Handle the user request from this conversation.',
|
|
1880
2861
|
owner,
|
|
1881
2862
|
previousWorker: null,
|
|
1882
2863
|
nextWorker: null,
|
|
2864
|
+
conversationId,
|
|
1883
2865
|
projectName,
|
|
1884
2866
|
workspacePath,
|
|
1885
2867
|
effectivePolicy,
|
|
2868
|
+
recentContext,
|
|
1886
2869
|
}),
|
|
1887
2870
|
successCriteria: `Complete the delegated ${taskType} task and return a concise result summary.`,
|
|
1888
2871
|
};
|
|
1889
2872
|
}
|
|
1890
|
-
buildTodoStepsFromTemplate(prompt, workflowTemplate,
|
|
2873
|
+
buildTodoStepsFromTemplate(prompt, conversationId, workflowTemplate, orchestratorActor, projectName, workspacePath, effectivePolicy) {
|
|
2874
|
+
const recentContext = this.buildRecentConversationContextForWorker(conversationId);
|
|
1891
2875
|
const steps = workflowTemplate.steps
|
|
1892
2876
|
.sort((a, b) => a.order_index - b.order_index)
|
|
1893
2877
|
.map((step) => {
|
|
@@ -1913,14 +2897,315 @@ class OrchestratorAgent {
|
|
|
1913
2897
|
owner: step.owner,
|
|
1914
2898
|
previousWorker: steps[index - 1]?.owner || null,
|
|
1915
2899
|
nextWorker: steps[index + 1]?.owner || null,
|
|
2900
|
+
conversationId,
|
|
1916
2901
|
projectName,
|
|
1917
2902
|
workspacePath,
|
|
1918
2903
|
effectivePolicy,
|
|
2904
|
+
recentContext,
|
|
1919
2905
|
}),
|
|
1920
2906
|
successCriteria: `Complete step ${index + 1} of "${workflowTemplate.name}" and hand off the necessary output.`,
|
|
1921
2907
|
}));
|
|
1922
2908
|
}
|
|
1923
|
-
|
|
2909
|
+
buildSelectedWorkflowTemplateHint(workflowTemplate) {
|
|
2910
|
+
return {
|
|
2911
|
+
id: workflowTemplate.id,
|
|
2912
|
+
name: workflowTemplate.name,
|
|
2913
|
+
description: workflowTemplate.description || null,
|
|
2914
|
+
steps: workflowTemplate.steps
|
|
2915
|
+
.slice()
|
|
2916
|
+
.sort((a, b) => a.order_index - b.order_index)
|
|
2917
|
+
.map((step, index) => {
|
|
2918
|
+
const bot = data.getAgentProfile(step.agent_id);
|
|
2919
|
+
return {
|
|
2920
|
+
index,
|
|
2921
|
+
bot_name: bot?.name || step.agent_id,
|
|
2922
|
+
role_priorities: bot ? data.getAgentRolePriorities(bot) : [],
|
|
2923
|
+
saved_instruction: step.instruction?.trim() || null,
|
|
2924
|
+
is_checkpoint: step.is_checkpoint === 1,
|
|
2925
|
+
};
|
|
2926
|
+
}),
|
|
2927
|
+
};
|
|
2928
|
+
}
|
|
2929
|
+
createPendingWorkflowProposal(input) {
|
|
2930
|
+
this.deleteStalePendingWorkflowProposals(input.conversationId, input.orchestratorActor);
|
|
2931
|
+
const workflowArgs = this.buildPendingWorkflowProposalArgs(input.prompt, input.candidateWorkflowPlan, input.selectedWorkflowTemplateHint);
|
|
2932
|
+
if (workflowArgs.steps.length === 0)
|
|
2933
|
+
return [];
|
|
2934
|
+
const existingActive = data.listActiveTodoTasksForConversation(input.conversationId);
|
|
2935
|
+
const result = input.executeCreateWorkflow(workflowArgs, {
|
|
2936
|
+
conversationId: input.conversationId,
|
|
2937
|
+
projectId: input.projectId,
|
|
2938
|
+
orchestratorActor: input.orchestratorActor,
|
|
2939
|
+
resolution: {
|
|
2940
|
+
activeBots: input.activeBots,
|
|
2941
|
+
botsWithActiveTodos: new Set(existingActive.map((task) => task.owner_name).filter(Boolean)),
|
|
2942
|
+
},
|
|
2943
|
+
userPrompt: input.prompt,
|
|
2944
|
+
recentContext: input.recentContext || null,
|
|
2945
|
+
});
|
|
2946
|
+
if (!result || result.kind === 'error' || !Array.isArray(result.tasks))
|
|
2947
|
+
return [];
|
|
2948
|
+
const confidence = this.summarizeCandidateConfidence(input.candidateWorkflowPlan.steps);
|
|
2949
|
+
const stepReasons = input.candidateWorkflowPlan.steps
|
|
2950
|
+
.map((step) => {
|
|
2951
|
+
const role = step.suggested_role ? ` as ${step.suggested_role}` : '';
|
|
2952
|
+
const confidenceLabel = step.confidence || 'low';
|
|
2953
|
+
const reason = step.reason || 'No specific role signal; using configured priority.';
|
|
2954
|
+
return `- ${step.bot_name}${role}: ${confidenceLabel} - ${reason}`;
|
|
2955
|
+
})
|
|
2956
|
+
.join('\n');
|
|
2957
|
+
const details = [
|
|
2958
|
+
'System-proposed workflow. Awaiting orchestrator approval; workers have not started.',
|
|
2959
|
+
`Source: ${input.candidateWorkflowPlan.source}.`,
|
|
2960
|
+
`Confidence: ${confidence}.`,
|
|
2961
|
+
`Reason: ${input.candidateWorkflowPlan.reason}`,
|
|
2962
|
+
stepReasons ? `Step confidence:\n${stepReasons}` : '',
|
|
2963
|
+
].join('\n');
|
|
2964
|
+
for (const task of result.tasks) {
|
|
2965
|
+
try {
|
|
2966
|
+
data.editTodoTask(task.id, {
|
|
2967
|
+
details,
|
|
2968
|
+
actor: { actorType: 'orchestrator', actorId: input.orchestratorActor.actorId },
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2971
|
+
catch {
|
|
2972
|
+
// Best effort. The task remains visible even if details were not updated.
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
return result.tasks.map((task) => task.id);
|
|
2976
|
+
}
|
|
2977
|
+
deleteStalePendingWorkflowProposals(conversationId, orchestratorActor) {
|
|
2978
|
+
const marker = 'System-proposed workflow. Awaiting orchestrator approval; workers have not started.';
|
|
2979
|
+
for (const task of data.listActiveTodoTasksForConversation(conversationId)) {
|
|
2980
|
+
if (!String(task.details || '').includes(marker))
|
|
2981
|
+
continue;
|
|
2982
|
+
try {
|
|
2983
|
+
data.deleteTodoTask(task.id, {
|
|
2984
|
+
actor: { actorType: 'orchestrator', actorId: orchestratorActor.actorId },
|
|
2985
|
+
});
|
|
2986
|
+
}
|
|
2987
|
+
catch {
|
|
2988
|
+
// Leave concurrently touched tasks alone.
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
summarizeCandidateConfidence(steps) {
|
|
2993
|
+
if (steps.length === 0)
|
|
2994
|
+
return 'low';
|
|
2995
|
+
const values = steps.map((step) => step.confidence || 'low');
|
|
2996
|
+
if (values.every((value) => value === 'high'))
|
|
2997
|
+
return 'high';
|
|
2998
|
+
if (values.every((value) => value === 'low'))
|
|
2999
|
+
return 'low';
|
|
3000
|
+
if (values.some((value) => value === 'low'))
|
|
3001
|
+
return 'medium';
|
|
3002
|
+
return 'medium';
|
|
3003
|
+
}
|
|
3004
|
+
buildPendingWorkflowProposalArgs(prompt, candidate, selectedWorkflowTemplateHint) {
|
|
3005
|
+
const target = this.inferWorkflowTargetForTitle(prompt);
|
|
3006
|
+
const templateStepsByIndex = new Map((selectedWorkflowTemplateHint?.steps || []).map((step) => [step.index, step]));
|
|
3007
|
+
return {
|
|
3008
|
+
steps: candidate.steps.map((step, index) => {
|
|
3009
|
+
const role = step.suggested_role || step.role_priorities[0] || '';
|
|
3010
|
+
const action = this.workflowActionForRole(role, prompt);
|
|
3011
|
+
const templateInstruction = templateStepsByIndex.get(step.index)?.saved_instruction || '';
|
|
3012
|
+
const nextStep = candidate.steps[index + 1] || null;
|
|
3013
|
+
const title = this.buildCandidateWorkflowTitle({
|
|
3014
|
+
action,
|
|
3015
|
+
role,
|
|
3016
|
+
target,
|
|
3017
|
+
nextBotName: nextStep?.bot_name || null,
|
|
3018
|
+
});
|
|
3019
|
+
return {
|
|
3020
|
+
bot_name: step.bot_name,
|
|
3021
|
+
role: role || undefined,
|
|
3022
|
+
title,
|
|
3023
|
+
task_instructions: templateInstruction || this.buildCandidateWorkflowInstructions({
|
|
3024
|
+
action,
|
|
3025
|
+
role,
|
|
3026
|
+
target,
|
|
3027
|
+
nextBotName: nextStep?.bot_name || null,
|
|
3028
|
+
nextRole: nextStep?.suggested_role || nextStep?.role_priorities?.[0] || null,
|
|
3029
|
+
}),
|
|
3030
|
+
};
|
|
3031
|
+
}),
|
|
3032
|
+
};
|
|
3033
|
+
}
|
|
3034
|
+
workflowActionForRole(role, prompt) {
|
|
3035
|
+
const normalized = String(role || '').trim().toLowerCase();
|
|
3036
|
+
if (this.roleHasCapability(role, 'prompt_building'))
|
|
3037
|
+
return 'Generate prompt';
|
|
3038
|
+
if (this.roleHasCapability(role, 'qa'))
|
|
3039
|
+
return 'QA';
|
|
3040
|
+
if (this.roleHasCapability(role, 'verify'))
|
|
3041
|
+
return 'Verify';
|
|
3042
|
+
if (this.roleHasCapability(role, 'deployment'))
|
|
3043
|
+
return 'Deploy';
|
|
3044
|
+
if (this.roleHasCapability(role, 'research') || this.roleHasCapability(role, 'design'))
|
|
3045
|
+
return 'Plan';
|
|
3046
|
+
if (this.roleHasCapability(role, 'coding')) {
|
|
3047
|
+
const promptText = String(prompt || '').toLowerCase();
|
|
3048
|
+
if (/\b(clean\s+up|improve|redesign|ui\/?ux|make .*better|polish)\b/.test(promptText))
|
|
3049
|
+
return 'Improve';
|
|
3050
|
+
if (/\b(fix|repair|correct)\b/.test(promptText))
|
|
3051
|
+
return 'Fix';
|
|
3052
|
+
if (/\b(update|modify|edit|change)\b/.test(promptText))
|
|
3053
|
+
return 'Update';
|
|
3054
|
+
return 'Build';
|
|
3055
|
+
}
|
|
3056
|
+
if (/\b(qa|quality assurance|review|test)\b/.test(normalized))
|
|
3057
|
+
return 'QA';
|
|
3058
|
+
if (/\b(verify|verification|validate)\b/.test(normalized))
|
|
3059
|
+
return 'Verify';
|
|
3060
|
+
if (/\b(research|brainstorm|plan|planning|design)\b/.test(normalized))
|
|
3061
|
+
return 'Plan';
|
|
3062
|
+
if (/\b(deploy|deployment|release|ship)\b/.test(normalized))
|
|
3063
|
+
return 'Deploy';
|
|
3064
|
+
if (/\b(code|coding|build|implement|create|write|develop)\b/.test(normalized)) {
|
|
3065
|
+
const promptText = String(prompt || '').toLowerCase();
|
|
3066
|
+
if (/\b(clean\s+up|improve|redesign|ui\/?ux|make .*better|polish)\b/.test(promptText))
|
|
3067
|
+
return 'Improve';
|
|
3068
|
+
if (/\b(fix|repair|correct)\b/.test(promptText))
|
|
3069
|
+
return 'Fix';
|
|
3070
|
+
if (/\b(update|modify|edit|change)\b/.test(promptText))
|
|
3071
|
+
return 'Update';
|
|
3072
|
+
return 'Build';
|
|
3073
|
+
}
|
|
3074
|
+
return 'Handle';
|
|
3075
|
+
}
|
|
3076
|
+
buildCandidateWorkflowTitle(input) {
|
|
3077
|
+
if (this.roleHasCapability(input.role, 'prompt_building')) {
|
|
3078
|
+
return input.nextBotName
|
|
3079
|
+
? `Generate ${input.nextBotName} handoff for ${input.target}`
|
|
3080
|
+
: `Generate prompt for ${input.target}`;
|
|
3081
|
+
}
|
|
3082
|
+
return `${input.action} ${input.target}`;
|
|
3083
|
+
}
|
|
3084
|
+
buildCandidateWorkflowInstructions(input) {
|
|
3085
|
+
if (this.roleHasCapability(input.role, 'prompt_building')) {
|
|
3086
|
+
const next = input.nextBotName
|
|
3087
|
+
? `${input.nextBotName}${input.nextRole ? ` (${input.nextRole})` : ''}`
|
|
3088
|
+
: 'the next worker';
|
|
3089
|
+
return [
|
|
3090
|
+
`Create a professional, detailed worker prompt for ${next} to handle ${input.target}.`,
|
|
3091
|
+
`Include the target artifact/path, the current issue or requested change, exact expected behavior, scope limits, constraints, and acceptance checks.`,
|
|
3092
|
+
'Put the finished prompt in complete_worker_task.handoff_prompt so the next worker can act without guessing.',
|
|
3093
|
+
].join(' ');
|
|
3094
|
+
}
|
|
3095
|
+
const nextLine = input.nextBotName
|
|
3096
|
+
? ` When done, hand off enough detail for ${input.nextBotName}${input.nextRole ? ` (${input.nextRole})` : ''} to continue without guessing.`
|
|
3097
|
+
: '';
|
|
3098
|
+
return `${input.action} ${input.target} for the current user request.${nextLine}`;
|
|
3099
|
+
}
|
|
3100
|
+
roleHasCapability(role, capabilityId) {
|
|
3101
|
+
return (0, capabilities_1.matchCapabilityIds)([String(role || '')]).includes(capabilityId);
|
|
3102
|
+
}
|
|
3103
|
+
inferWorkflowTargetForTitle(prompt) {
|
|
3104
|
+
const text = String(prompt || '');
|
|
3105
|
+
const fileMatch = text.match(/\b([A-Za-z0-9_-]+\.(?:html|tsx?|jsx?|css|md|txt|json))\b/);
|
|
3106
|
+
if (fileMatch?.[1])
|
|
3107
|
+
return fileMatch[1];
|
|
3108
|
+
if (/\boutfitter\b/i.test(text) || /\belk hunting\b/i.test(text))
|
|
3109
|
+
return 'outfitter page';
|
|
3110
|
+
if (/\b(web\s?page|website|html|landing page|page)\b/i.test(text))
|
|
3111
|
+
return 'requested page';
|
|
3112
|
+
if (/\binstall(?:er)?\b/i.test(text))
|
|
3113
|
+
return 'installer';
|
|
3114
|
+
if (/\brelease|deploy|download\b/i.test(text))
|
|
3115
|
+
return 'release';
|
|
3116
|
+
return 'request';
|
|
3117
|
+
}
|
|
3118
|
+
buildExplicitBotSequenceCandidatePlan(prompt, activeBots) {
|
|
3119
|
+
const text = String(prompt || '');
|
|
3120
|
+
if (!text.trim())
|
|
3121
|
+
return null;
|
|
3122
|
+
const matches = [];
|
|
3123
|
+
for (const bot of activeBots) {
|
|
3124
|
+
const escaped = bot.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3125
|
+
const match = new RegExp(`\\b${escaped}\\b`, 'i').exec(text);
|
|
3126
|
+
if (match && Number.isFinite(match.index)) {
|
|
3127
|
+
matches.push({ bot, index: match.index });
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
const ordered = matches
|
|
3131
|
+
.sort((a, b) => a.index - b.index)
|
|
3132
|
+
.filter((match, index, arr) => arr.findIndex((other) => other.bot.id === match.bot.id) === index);
|
|
3133
|
+
if (ordered.length < 2)
|
|
3134
|
+
return null;
|
|
3135
|
+
if (!this.hasExplicitBotWorkflowSignal(text))
|
|
3136
|
+
return null;
|
|
3137
|
+
return {
|
|
3138
|
+
source: 'explicit_bot_sequence',
|
|
3139
|
+
reason: `User explicitly mentioned multiple bots in order: ${ordered.map((item) => item.bot.name).join(' -> ')}.`,
|
|
3140
|
+
steps: ordered.map((item, index) => {
|
|
3141
|
+
const inference = this.inferSuggestedRoleNearBot(text, item.index, ordered[index + 1]?.index, item.bot);
|
|
3142
|
+
return {
|
|
3143
|
+
index,
|
|
3144
|
+
bot_name: item.bot.name,
|
|
3145
|
+
role_priorities: data.getAgentRolePriorities(item.bot),
|
|
3146
|
+
suggested_role: inference.role,
|
|
3147
|
+
confidence: inference.confidence,
|
|
3148
|
+
reason: inference.reason,
|
|
3149
|
+
};
|
|
3150
|
+
}),
|
|
3151
|
+
};
|
|
3152
|
+
}
|
|
3153
|
+
hasExplicitBotWorkflowSignal(prompt) {
|
|
3154
|
+
const text = prompt.toLowerCase();
|
|
3155
|
+
if (/\b(have|use|ask|tell|delegate|assign|workflow)\b/.test(text))
|
|
3156
|
+
return true;
|
|
3157
|
+
if (/\bthen\b|\bafter that\b|\bfollowed by\b/.test(text))
|
|
3158
|
+
return true;
|
|
3159
|
+
return /\b(code|coding|build|implement|create|write|develop|edit|update|modify|fix|prompt|qa|quality assurance|review|test|testing|verify|verification|validate|research|plan|brainstorm)\b/.test(text);
|
|
3160
|
+
}
|
|
3161
|
+
inferSuggestedRoleNearBot(prompt, startIndex, nextStartIndex, bot) {
|
|
3162
|
+
const roles = data.getAgentRolePriorities(bot);
|
|
3163
|
+
const segmentEnd = nextStartIndex === undefined ? prompt.length : nextStartIndex;
|
|
3164
|
+
const segment = prompt.slice(startIndex, segmentEnd).toLowerCase();
|
|
3165
|
+
const roleKeywordGroups = [
|
|
3166
|
+
{
|
|
3167
|
+
keywords: /\b(?:create|write|generate|draft|prepare|build|make)\s+(?:a\s+|the\s+)?(?:better\s+|professional\s+|detailed\s+)?(?:user\s+|worker\s+|handoff\s+)?prompt\b|\bhandoff\s+prompt\b|\bprompt\s+for\b/i,
|
|
3168
|
+
capabilityId: 'prompt_building',
|
|
3169
|
+
reason: 'User asked this worker to create or prepare a prompt/handoff.',
|
|
3170
|
+
},
|
|
3171
|
+
{ keywords: /\b(qa|quality assurance|review|test|testing|audit)\b/i, capabilityId: 'qa', reason: 'User asked this worker to review, test, or QA.' },
|
|
3172
|
+
{ keywords: /\b(verify|verification|validate|validation)\b/i, capabilityId: 'verify', reason: 'User asked this worker to verify or validate.' },
|
|
3173
|
+
{ keywords: /\b(deploy|deployment|release|ship|publish)\b/i, capabilityId: 'deployment', reason: 'User asked this worker to deploy, release, or publish.' },
|
|
3174
|
+
{ keywords: /\b(code|coding|build|implement|create|write|develop|edit|update|modify|fix|clean\s+up|improve|redesign)\b/i, capabilityId: 'coding', reason: 'User asked this worker to create, modify, or fix an artifact.' },
|
|
3175
|
+
{ keywords: /\b(design|ui\/?ux|ux|visual|layout)\b/i, capabilityId: 'design', reason: 'User asked this worker to handle design or UI/UX planning.' },
|
|
3176
|
+
{ keywords: /\b(research|investigate|analy[sz]e|analysis|brainstorm|plan|planning)\b/i, capabilityId: 'research', reason: 'User asked this worker to research or plan.' },
|
|
3177
|
+
];
|
|
3178
|
+
for (const group of roleKeywordGroups) {
|
|
3179
|
+
if (!group.keywords.test(segment))
|
|
3180
|
+
continue;
|
|
3181
|
+
const matchedRole = (0, capabilities_1.findRoleLabelForCapability)(roles, group.capabilityId);
|
|
3182
|
+
const botHasCapability = roles.some((role) => (0, capabilities_1.matchCapabilityIds)([role]).includes(group.capabilityId));
|
|
3183
|
+
return {
|
|
3184
|
+
role: matchedRole,
|
|
3185
|
+
confidence: botHasCapability ? 'high' : 'medium',
|
|
3186
|
+
reason: botHasCapability
|
|
3187
|
+
? group.reason
|
|
3188
|
+
: `${group.reason} The worker was named explicitly; role is informational because it is not in that worker's configured priorities.`,
|
|
3189
|
+
};
|
|
3190
|
+
}
|
|
3191
|
+
return {
|
|
3192
|
+
role: roles[0] || null,
|
|
3193
|
+
confidence: 'low',
|
|
3194
|
+
reason: 'No specific capability keyword was found near this bot name; using the worker priority list.',
|
|
3195
|
+
};
|
|
3196
|
+
}
|
|
3197
|
+
reportCandidateWorkflowProgress(opts, candidate) {
|
|
3198
|
+
const names = candidate.steps
|
|
3199
|
+
.map((step) => step.suggested_role ? `${step.bot_name} (${step.suggested_role})` : step.bot_name)
|
|
3200
|
+
.filter(Boolean);
|
|
3201
|
+
const prefix = candidate.source === 'explicit_bot_sequence'
|
|
3202
|
+
? 'Detected requested bot sequence'
|
|
3203
|
+
: 'Detected requested workflow';
|
|
3204
|
+
const summary = names.length > 0 ? `${prefix}: ${names.join(' -> ')}.` : prefix + '.';
|
|
3205
|
+
opts.onProgress(`Orchestrator:: ${summary} Awaiting orchestrator approval before starting workers.`);
|
|
3206
|
+
}
|
|
3207
|
+
buildTodoStepsFromAssignments(prompt, conversationId, roleAssignments, intent, orchestratorActor, projectId, projectName, workspacePath, effectivePolicy) {
|
|
3208
|
+
const recentContext = this.buildRecentConversationContextForWorker(conversationId);
|
|
1924
3209
|
const roleOrder = this.inferWorkflowRoleOrder(prompt, roleAssignments, intent);
|
|
1925
3210
|
const owners = roleOrder
|
|
1926
3211
|
.map((role) => {
|
|
@@ -1933,7 +3218,7 @@ class OrchestratorAgent {
|
|
|
1933
3218
|
.filter((item) => Boolean(item));
|
|
1934
3219
|
return owners.map((item, index) => ({
|
|
1935
3220
|
title: `${item.agent.name}: ${this.defaultTodoTitleForRole(item.role, prompt)}`,
|
|
1936
|
-
details: `Workflow step assigned by ${
|
|
3221
|
+
details: `Workflow step assigned by ${orchestratorActor.name}.`,
|
|
1937
3222
|
owner: item.agent,
|
|
1938
3223
|
taskType: this.normalizeTaskTypeForWorker(item.agent, item.role === 'research' ? 'research' : item.role === 'qa' ? 'qa' : 'coding'),
|
|
1939
3224
|
taskPrompt: this.buildWorkerTaskPrompt({
|
|
@@ -1942,80 +3227,75 @@ class OrchestratorAgent {
|
|
|
1942
3227
|
owner: item.agent,
|
|
1943
3228
|
previousWorker: owners[index - 1]?.agent || null,
|
|
1944
3229
|
nextWorker: owners[index + 1]?.agent || null,
|
|
3230
|
+
conversationId,
|
|
1945
3231
|
projectName: projectName || null,
|
|
1946
3232
|
workspacePath: workspacePath || null,
|
|
1947
3233
|
effectivePolicy: effectivePolicy || data.getEffectiveOrchestrationPolicy(projectId),
|
|
3234
|
+
recentContext,
|
|
1948
3235
|
}),
|
|
1949
3236
|
successCriteria: this.defaultSuccessCriteriaForRole(item.role),
|
|
1950
3237
|
}));
|
|
1951
3238
|
}
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
if (assignments.research && /\b(research|brainstorm|plan|planning)\b/i.test(prompt))
|
|
1971
|
-
fallback.push('research');
|
|
1972
|
-
if (assignments.coding)
|
|
1973
|
-
fallback.push('coding');
|
|
1974
|
-
if (assignments.qa)
|
|
1975
|
-
fallback.push('qa');
|
|
1976
|
-
if (fallback.length > 0)
|
|
1977
|
-
return Array.from(new Set(fallback));
|
|
3239
|
+
/**
|
|
3240
|
+
* @deprecated Phase B of orchestration-plan.txt.
|
|
3241
|
+
* Kept only to avoid breaking the non-local_desktop (connected) code paths
|
|
3242
|
+
* that still call it. In local_desktop mode the LLM-tool-dispatch path
|
|
3243
|
+
* makes this regex unreachable. Remove after connected-mode orchestration
|
|
3244
|
+
* is migrated to tool dispatch.
|
|
3245
|
+
*/
|
|
3246
|
+
/**
|
|
3247
|
+
* DEPRECATED — returns []. Previously inferred workflow step order from
|
|
3248
|
+
* prompt prose via regex. Codex audit HIGH #6: runtime must not scan
|
|
3249
|
+
* English to decide workflow shape. The tool-dispatch path's
|
|
3250
|
+
* create_workflow is the structured replacement — the orchestrator LLM
|
|
3251
|
+
* decides the ordered role list from the prompt and bot priorities and
|
|
3252
|
+
* emits structured steps. Kept as a stub so any remaining legacy caller
|
|
3253
|
+
* produces an empty role order (which downstream callers handle by
|
|
3254
|
+
* falling through to their no-role-inferred branch).
|
|
3255
|
+
*/
|
|
3256
|
+
inferWorkflowRoleOrder(_prompt, _assignments, _intent) {
|
|
1978
3257
|
return [];
|
|
1979
3258
|
}
|
|
1980
3259
|
normalizeTaskTypeForWorker(owner, fallback) {
|
|
1981
|
-
const
|
|
1982
|
-
if (
|
|
3260
|
+
const inferred = (0, capabilities_1.inferTaskTypeFromCapabilities)(null, data.getAgentRolePriorities(owner), fallback || 'coding');
|
|
3261
|
+
if (['qa', 'research', 'verify', 'coding', 'deploy'].includes(inferred)) {
|
|
3262
|
+
return inferred;
|
|
3263
|
+
}
|
|
3264
|
+
if (this.agentHasAnyRole(owner, ['qa', 'review', 'reviewer']))
|
|
1983
3265
|
return 'qa';
|
|
1984
|
-
if (
|
|
3266
|
+
if (this.agentHasAnyRole(owner, ['research', 'researcher', 'analyst', 'advisor', 'brainstorm', 'brainstorming', 'planning', 'plan', 'design']))
|
|
1985
3267
|
return 'research';
|
|
1986
|
-
if (
|
|
3268
|
+
if (this.agentHasAnyRole(owner, ['verify', 'verification']))
|
|
1987
3269
|
return 'verify';
|
|
1988
|
-
if (
|
|
1989
|
-
return 'deploy';
|
|
1990
|
-
if (roleClass === 'coding' || roleClass === 'code' || roleClass === 'builder' || roleClass === 'developer' || roleClass === 'engineer')
|
|
3270
|
+
if (this.agentHasAnyRole(owner, ['coding', 'code', 'builder', 'developer', 'engineer']))
|
|
1991
3271
|
return 'coding';
|
|
3272
|
+
if (this.agentHasAnyRole(owner, ['deploy', 'deployment']))
|
|
3273
|
+
return 'deploy';
|
|
1992
3274
|
return (fallback || 'coding');
|
|
1993
3275
|
}
|
|
1994
3276
|
buildWorkerTaskPrompt(input) {
|
|
1995
|
-
const workerRole =
|
|
3277
|
+
const workerRole = this.getOrchestrationRoleLabel(input.owner);
|
|
1996
3278
|
const taskType = this.normalizeTaskTypeForWorker(input.owner, null);
|
|
3279
|
+
const bootstrapHistoryFilePath = this.prepareBootstrapHistoryFile(input.conversationId);
|
|
3280
|
+
const isFirstInChain = !input.previousWorker;
|
|
3281
|
+
const includeOriginalPrompt = this.shouldIncludeOriginalPromptForWorker(input.owner, isFirstInChain);
|
|
1997
3282
|
const composedTask = [
|
|
1998
3283
|
`Step instruction: ${input.stepInstruction}`,
|
|
1999
3284
|
taskType === 'qa'
|
|
2000
|
-
? 'QA scope:
|
|
3285
|
+
? 'QA scope: Verify the delivered work against the assigned task, previous handoff, explicit success criteria, and original user request when provided. Use the tools and checks needed for that assignment.'
|
|
2001
3286
|
: taskType === 'verify'
|
|
2002
3287
|
? 'Verification scope: Compare the delivered work against the original user request and the explicit success criteria.'
|
|
2003
3288
|
: '',
|
|
2004
|
-
|
|
2005
|
-
? 'QA handoff rule:
|
|
3289
|
+
this.agentHasAnyRole(input.nextWorker, ['qa', 'review', 'reviewer'])
|
|
3290
|
+
? 'QA handoff rule: Include concrete implementation claims, artifact paths, known caveats, and recommended checks. Do not narrow the next QA worker beyond the user request, workflow instruction, or explicit success criteria.'
|
|
2006
3291
|
: '',
|
|
2007
|
-
|
|
2008
|
-
? 'QA handoff rule:
|
|
2009
|
-
: '',
|
|
2010
|
-
taskType === 'qa'
|
|
2011
|
-
? null
|
|
3292
|
+
this.agentHasAnyRole(input.nextWorker, ['qa', 'review', 'reviewer'])
|
|
3293
|
+
? 'QA handoff rule: Let QA decide which available tools or runtime checks are needed for the assigned verification.'
|
|
2012
3294
|
: '',
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
? null
|
|
2018
|
-
: input.originalPrompt,
|
|
3295
|
+
isFirstInChain && input.recentContext?.trim() ? 'Recent conversation context:' : null,
|
|
3296
|
+
isFirstInChain && input.recentContext?.trim() ? input.recentContext.trim() : null,
|
|
3297
|
+
includeOriginalPrompt ? 'Original user request:' : null,
|
|
3298
|
+
includeOriginalPrompt ? input.originalPrompt : null,
|
|
2019
3299
|
].join('\n');
|
|
2020
3300
|
return (0, worker_operating_prompt_1.buildWorkerOperatingPrompt)({
|
|
2021
3301
|
workerName: input.owner.name,
|
|
@@ -2030,26 +3310,60 @@ class OrchestratorAgent {
|
|
|
2030
3310
|
defaultLine: 'No confirmed special policy is set.',
|
|
2031
3311
|
}),
|
|
2032
3312
|
previousWorkerName: input.previousWorker?.name || null,
|
|
2033
|
-
previousWorkerRole: input.previousWorker ?
|
|
3313
|
+
previousWorkerRole: input.previousWorker ? this.getOrchestrationRoleLabel(input.previousWorker) : null,
|
|
2034
3314
|
nextWorkerName: input.nextWorker?.name || null,
|
|
2035
|
-
nextWorkerRole: input.nextWorker ?
|
|
3315
|
+
nextWorkerRole: input.nextWorker ? this.getOrchestrationRoleLabel(input.nextWorker) : null,
|
|
3316
|
+
bootstrapHistoryFilePath,
|
|
3317
|
+
capabilityInstructions: (0, capabilities_1.buildCapabilityInstructionBlock)(data.getAgentRolePriorities(input.owner)),
|
|
3318
|
+
});
|
|
3319
|
+
}
|
|
3320
|
+
buildRecentConversationContextForWorker(conversationId) {
|
|
3321
|
+
const contextWindow = (0, context_window_1.getPromptContextWindow)(conversationId, safeguards_1.SAFEGUARDS.CONTEXT_WINDOW_TURNS);
|
|
3322
|
+
const lines = [];
|
|
3323
|
+
const summary = contextWindow.summary?.summary_text?.trim();
|
|
3324
|
+
if (summary) {
|
|
3325
|
+
lines.push('[Rolling Summary]');
|
|
3326
|
+
lines.push(contextWindow.carriedForward ? '(carried forward from the previous conversation in this topic)' : '');
|
|
3327
|
+
lines.push(summary);
|
|
3328
|
+
}
|
|
3329
|
+
if (contextWindow.turns.length > 0) {
|
|
3330
|
+
lines.push('[Recent Turns]');
|
|
3331
|
+
lines.push((0, context_window_1.formatTurnsForPrompt)(contextWindow.turns));
|
|
3332
|
+
}
|
|
3333
|
+
if (lines.length === 0) {
|
|
3334
|
+
const currentTurns = (0, context_window_1.getRecentTurns)(conversationId, safeguards_1.SAFEGUARDS.CONTEXT_WINDOW_TURNS);
|
|
3335
|
+
if (currentTurns.length > 0) {
|
|
3336
|
+
lines.push('[Recent Turns]');
|
|
3337
|
+
lines.push((0, context_window_1.formatTurnsForPrompt)(currentTurns));
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
return lines.filter((line) => line !== '').join('\n').slice(0, 10000);
|
|
3341
|
+
}
|
|
3342
|
+
prepareBootstrapHistoryFile(conversationId) {
|
|
3343
|
+
const normalizedConversationId = String(conversationId || '').trim();
|
|
3344
|
+
if (!normalizedConversationId)
|
|
3345
|
+
return null;
|
|
3346
|
+
const topicId = data.getPrimaryTopicIdForConversation(normalizedConversationId) || undefined;
|
|
3347
|
+
return (0, cli_bootstrap_history_1.writeConversationBootstrapHistoryFile)({
|
|
3348
|
+
conversationId: normalizedConversationId,
|
|
3349
|
+
topicId,
|
|
2036
3350
|
});
|
|
2037
3351
|
}
|
|
2038
|
-
defaultTodoTitleForRole(role,
|
|
3352
|
+
defaultTodoTitleForRole(role, _prompt) {
|
|
2039
3353
|
if (role === 'research')
|
|
2040
|
-
return
|
|
3354
|
+
return 'Research and plan the user request';
|
|
2041
3355
|
if (role === 'qa')
|
|
2042
|
-
return
|
|
2043
|
-
return
|
|
3356
|
+
return 'QA and review the delivered work';
|
|
3357
|
+
return 'Build and implement the requested work';
|
|
2044
3358
|
}
|
|
2045
|
-
defaultStepInstructionForRole(role,
|
|
3359
|
+
defaultStepInstructionForRole(role, _prompt) {
|
|
2046
3360
|
if (role === 'research') {
|
|
2047
|
-
return 'Research, brainstorm, or plan the
|
|
3361
|
+
return 'Research, brainstorm, or plan the user request. Hand off whatever context the next worker needs to continue.';
|
|
2048
3362
|
}
|
|
2049
3363
|
if (role === 'qa') {
|
|
2050
|
-
return 'QA the completed work against the previous worker handoff, delivered artifact,
|
|
3364
|
+
return 'QA the completed work against the assigned task, previous worker handoff, delivered artifact, explicit success criteria, and original user request when provided. Use the available tools and checks needed for the assignment. If issues remain, prepare a precise fix handoff for the previous worker.';
|
|
2051
3365
|
}
|
|
2052
|
-
return
|
|
3366
|
+
return 'Implement the assigned work using the current task and previous worker handoff as context.';
|
|
2053
3367
|
}
|
|
2054
3368
|
defaultSuccessCriteriaForRole(role) {
|
|
2055
3369
|
if (role === 'research')
|
|
@@ -2083,10 +3397,65 @@ class OrchestratorAgent {
|
|
|
2083
3397
|
return scopedMatch;
|
|
2084
3398
|
return allAgents.find((agent) => agent.name.trim().toLowerCase() === normalized);
|
|
2085
3399
|
}
|
|
3400
|
+
resolveDefaultFallbackAgent(projectId, orchestratorBot, roleAssignments) {
|
|
3401
|
+
const candidates = [];
|
|
3402
|
+
const defaultAgent = data.getDefaultAgentProfile();
|
|
3403
|
+
if (defaultAgent)
|
|
3404
|
+
candidates.push(defaultAgent);
|
|
3405
|
+
if (roleAssignments.coding)
|
|
3406
|
+
candidates.push(this.findAgentByName(roleAssignments.coding, projectId));
|
|
3407
|
+
const project = projectId ? data.getProject(projectId) : undefined;
|
|
3408
|
+
const projectScoped = project?.bot_ids?.length
|
|
3409
|
+
? data.listAgentProfiles().filter((agent) => project.bot_ids.includes(agent.id))
|
|
3410
|
+
: data.listAgentProfiles();
|
|
3411
|
+
candidates.push(...projectScoped.filter((agent) => !(0, orchestrator_profile_1.isOrchestratorProfile)(agent)));
|
|
3412
|
+
return candidates.find((agent) => !!agent
|
|
3413
|
+
&& agent.id !== orchestratorBot.id
|
|
3414
|
+
&& !(0, orchestrator_profile_1.isOrchestratorProfile)(agent));
|
|
3415
|
+
}
|
|
2086
3416
|
async executeOrchestratorOwnedWork(prompt, conversationId, orchestratorBot, roleAssignments, project, opts, state, options) {
|
|
2087
|
-
|
|
2088
|
-
|
|
3417
|
+
const executionLabel = this.orchestratorRuntime.kind === 'clerk'
|
|
3418
|
+
? 'Clerk kept the work.'
|
|
3419
|
+
: `Selected orchestrator bot ${orchestratorBot.name} kept the work.`;
|
|
3420
|
+
this.recordOrchestrationAudit(state, 'execute_direct', 'executed', executionLabel, {
|
|
3421
|
+
orchestratorBotId: orchestratorBot.id,
|
|
3422
|
+
orchestratorBotName: orchestratorBot.name,
|
|
3423
|
+
runtimeKind: this.orchestratorRuntime.kind,
|
|
3424
|
+
});
|
|
3425
|
+
this.reportHiddenRoleProgress(opts, 'orchestrator', this.orchestratorRuntime.kind === 'clerk'
|
|
3426
|
+
? 'Orchestrator is working on the request'
|
|
3427
|
+
: `${orchestratorBot.name} is working on the request`);
|
|
3428
|
+
if (this.orchestratorRuntime.kind === 'clerk') {
|
|
3429
|
+
try {
|
|
3430
|
+
const response = await this.executeClerkOwnedWorkDirect(prompt, conversationId, orchestratorBot, project, opts);
|
|
3431
|
+
if (!response.trim()) {
|
|
3432
|
+
return {
|
|
3433
|
+
ok: false,
|
|
3434
|
+
response: 'Orchestrator returned an empty response.',
|
|
3435
|
+
error: 'Worker returned an empty response.',
|
|
3436
|
+
};
|
|
3437
|
+
}
|
|
3438
|
+
return this.finalizeOwnedExecutionResult({
|
|
3439
|
+
id: `orchestrator-direct-${Date.now()}`,
|
|
3440
|
+
conversationId,
|
|
3441
|
+
status: 'completed',
|
|
3442
|
+
steps: [],
|
|
3443
|
+
mergedResult: response,
|
|
3444
|
+
startedAt: Date.now(),
|
|
3445
|
+
completedAt: Date.now(),
|
|
3446
|
+
}, prompt, conversationId, orchestratorBot, roleAssignments, project, opts, state, options);
|
|
3447
|
+
}
|
|
3448
|
+
catch (err) {
|
|
3449
|
+
const failureReason = err?.message || 'Unknown error';
|
|
3450
|
+
return {
|
|
3451
|
+
ok: false,
|
|
3452
|
+
response: `Orchestrator encountered an issue: ${failureReason}`,
|
|
3453
|
+
error: failureReason,
|
|
3454
|
+
};
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
2089
3457
|
const result = await this.runNodeWithRetry('execute_direct', state, async () => this.workflowEngine.execute(prompt, conversationId, orchestratorBot.id, {
|
|
3458
|
+
abortSignal: opts.abortSignal,
|
|
2090
3459
|
disableDecomposition: true,
|
|
2091
3460
|
isOrchestrated: false,
|
|
2092
3461
|
projectId: project?.id || state.projectId || null,
|
|
@@ -2096,6 +3465,7 @@ class OrchestratorAgent {
|
|
|
2096
3465
|
// also persist would create duplicate assistant messages.
|
|
2097
3466
|
persistConversationMessages: false,
|
|
2098
3467
|
workerMode: false,
|
|
3468
|
+
storedAttachments: opts.storedAttachments,
|
|
2099
3469
|
onWorkerChunk: opts.onWorkerChunk,
|
|
2100
3470
|
onProgress: (progress) => {
|
|
2101
3471
|
if (progress.event === 'step-failed') {
|
|
@@ -2165,12 +3535,15 @@ class OrchestratorAgent {
|
|
|
2165
3535
|
(0, state_1.addHelperRoleUsage)(state, 'dispatch_controller');
|
|
2166
3536
|
(0, state_1.addDelegateTarget)(state, delegateBot.name);
|
|
2167
3537
|
const result = await this.runNodeWithRetry('delegate_specialist', state, async () => this.workflowEngine.execute(delegationPrompt, conversationId, delegateBot.id, {
|
|
3538
|
+
abortSignal: opts.abortSignal,
|
|
2168
3539
|
disableDecomposition: true,
|
|
2169
3540
|
isOrchestrated: false,
|
|
2170
3541
|
workerMode: false,
|
|
2171
3542
|
projectId: project?.id || state.projectId || null,
|
|
2172
3543
|
workspacePath: project?.folder?.trim() || undefined,
|
|
2173
3544
|
persistConversationMessages: true,
|
|
3545
|
+
storedAttachments: opts.storedAttachments,
|
|
3546
|
+
workerImageDeliveryMode: opts.storedAttachments?.length ? 'reference' : undefined,
|
|
2174
3547
|
onWorkerChunk: opts.onWorkerChunk,
|
|
2175
3548
|
onProgress: (progress) => {
|
|
2176
3549
|
if (progress.event === 'step-started') {
|
|
@@ -2195,9 +3568,25 @@ class OrchestratorAgent {
|
|
|
2195
3568
|
return 'research';
|
|
2196
3569
|
return 'discuss';
|
|
2197
3570
|
}
|
|
3571
|
+
/**
|
|
3572
|
+
* Builds the chunk-forwarding callback used by both the bot path and the
|
|
3573
|
+
* non-bot path of runOrchestratorPrompt. Wraps the exported helper.
|
|
3574
|
+
*/
|
|
3575
|
+
buildNarrationStreamGate(onNarrationChunk) {
|
|
3576
|
+
return buildNarrationStreamGate(onNarrationChunk);
|
|
3577
|
+
}
|
|
2198
3578
|
async runOrchestratorPrompt(userPrompt, systemPrompt, opts) {
|
|
2199
3579
|
if (this.orchestratorRuntime.kind === 'bot' && opts.orchestratorBot) {
|
|
3580
|
+
// Bot-backed orchestrator: stream chunks via the workflow engine.
|
|
3581
|
+
// The workflow engine receives onWorkerChunk events of type 'chunk'
|
|
3582
|
+
// for normal streaming text from the bot. We wrap onNarrationChunk
|
|
3583
|
+
// in the same gate that the non-bot path uses, so the sentinel/JSON
|
|
3584
|
+
// protection logic is consistent.
|
|
3585
|
+
const narrationGate = opts.onNarrationChunk
|
|
3586
|
+
? this.buildNarrationStreamGate(opts.onNarrationChunk)
|
|
3587
|
+
: null;
|
|
2200
3588
|
const result = await this.workflowEngine.execute(userPrompt, opts.conversationId, opts.orchestratorBot.id, {
|
|
3589
|
+
abortSignal: opts.abortSignal,
|
|
2201
3590
|
disableDecomposition: true,
|
|
2202
3591
|
disableTools: opts.disableTools === true,
|
|
2203
3592
|
isOrchestrated: false,
|
|
@@ -2205,7 +3594,18 @@ class OrchestratorAgent {
|
|
|
2205
3594
|
workspacePath: opts.workspacePath,
|
|
2206
3595
|
persistConversationMessages: false,
|
|
2207
3596
|
workerMode: false,
|
|
3597
|
+
storedAttachments: opts.storedAttachments,
|
|
2208
3598
|
systemPromptOverride: systemPrompt,
|
|
3599
|
+
...(narrationGate ? {
|
|
3600
|
+
onWorkerChunk: async (event) => {
|
|
3601
|
+
// Only forward 'chunk' events (the bot's streaming text) into
|
|
3602
|
+
// the narration gate. Other events (step_start, tool_call, etc.)
|
|
3603
|
+
// are not narration content.
|
|
3604
|
+
if (event.type === 'chunk' && event.text) {
|
|
3605
|
+
await narrationGate(event.text);
|
|
3606
|
+
}
|
|
3607
|
+
},
|
|
3608
|
+
} : {}),
|
|
2209
3609
|
});
|
|
2210
3610
|
this.setLastResponseMeta({
|
|
2211
3611
|
agentName: opts.orchestratorBot.name,
|
|
@@ -2214,60 +3614,57 @@ class OrchestratorAgent {
|
|
|
2214
3614
|
});
|
|
2215
3615
|
return this.stripWorkerProtocol(result.mergedResult || '');
|
|
2216
3616
|
}
|
|
3617
|
+
// Pattern C: stream the front-door LLM response through the narration gate.
|
|
3618
|
+
const onChunk = opts.onNarrationChunk
|
|
3619
|
+
? this.buildNarrationStreamGate(opts.onNarrationChunk)
|
|
3620
|
+
: undefined;
|
|
2217
3621
|
const response = await this.orchestratorRuntime.llm.chat({
|
|
2218
3622
|
messages: [{ role: 'user', content: userPrompt }],
|
|
2219
3623
|
system: systemPrompt,
|
|
2220
|
-
stream:
|
|
3624
|
+
stream: !!onChunk,
|
|
2221
3625
|
runtimeMode: 'local_desktop',
|
|
3626
|
+
abortSignal: opts.abortSignal,
|
|
3627
|
+
...(onChunk ? { onChunk } : {}),
|
|
2222
3628
|
});
|
|
2223
3629
|
this.setLastResponseMeta({
|
|
2224
|
-
agentName: this.orchestratorRuntime.agentName,
|
|
3630
|
+
agentName: this.isLocalDesktopRuntime() ? 'Orchestrator' : this.orchestratorRuntime.agentName,
|
|
2225
3631
|
botId: this.orchestratorRuntime.botId,
|
|
2226
3632
|
modelLabel: this.orchestratorRuntime.modelLabel,
|
|
2227
3633
|
});
|
|
2228
3634
|
return String(response.content || '').trim();
|
|
2229
3635
|
}
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
if (roles.length >= 2)
|
|
2251
|
-
return roles;
|
|
2252
|
-
if (mentionsWorkflow && roleAssignments.coding && roleAssignments.qa) {
|
|
2253
|
-
return roleAssignments.research && !onlyCodingAndQa
|
|
2254
|
-
? ['research', 'coding', 'qa']
|
|
2255
|
-
: ['coding', 'qa'];
|
|
2256
|
-
}
|
|
2257
|
-
return roles;
|
|
3636
|
+
/**
|
|
3637
|
+
* @deprecated Phase B of orchestration-plan.txt.
|
|
3638
|
+
* Unreachable from local_desktop — that path always uses the LLM-tool
|
|
3639
|
+
* dispatch in orchestration/dispatch-runner. Retained only for the
|
|
3640
|
+
* non-local_desktop (connected/server) code paths that have not yet
|
|
3641
|
+
* been migrated. Remove after connected-mode migration.
|
|
3642
|
+
*/
|
|
3643
|
+
/**
|
|
3644
|
+
* DEPRECATED — returns []. Previously scanned the user prompt via regex
|
|
3645
|
+
* for role keywords (research/code/fix/qa/review/verify/after qa/use my
|
|
3646
|
+
* team/bot names). Its output drove legacy workflow step construction.
|
|
3647
|
+
* Codex audit HIGH #6: remove as an execution source. The orchestrator
|
|
3648
|
+
* LLM decides the ordered role list via create_workflow now; code
|
|
3649
|
+
* shouldn't infer "Ben -> John -> Ver" from English. Kept as a stub
|
|
3650
|
+
* until all legacy callers are removed; downstream paths already
|
|
3651
|
+
* handle an empty return by falling through to structured dispatch
|
|
3652
|
+
* or user-prompted clarification.
|
|
3653
|
+
*/
|
|
3654
|
+
inferRequestedWorkflowRoles(_prompt, _roleAssignments) {
|
|
3655
|
+
return [];
|
|
2258
3656
|
}
|
|
2259
3657
|
orchestratorOwnsWorkflowRole(role, orchestratorBot, roleAssignments) {
|
|
2260
3658
|
const orchestratorName = orchestratorBot.name.toLowerCase();
|
|
2261
3659
|
const assignedName = roleAssignments[role]?.toLowerCase();
|
|
2262
3660
|
if (assignedName)
|
|
2263
3661
|
return assignedName === orchestratorName;
|
|
2264
|
-
const roleClass = String(orchestratorBot.role_class || '').trim().toLowerCase();
|
|
2265
3662
|
if (role === 'coding')
|
|
2266
|
-
return ['code', 'coding', 'coder', 'builder', 'developer', 'engineer', 'orchestrator', 'manager']
|
|
3663
|
+
return this.agentHasAnyRole(orchestratorBot, ['code', 'coding', 'coder', 'builder', 'developer', 'engineer', 'orchestrator', 'manager']);
|
|
2267
3664
|
if (role === 'qa')
|
|
2268
|
-
return ['qa', 'review', 'reviewer']
|
|
3665
|
+
return this.agentHasAnyRole(orchestratorBot, ['qa', 'review', 'reviewer']);
|
|
2269
3666
|
if (role === 'research')
|
|
2270
|
-
return ['research', 'researcher', 'analyst', 'advisor']
|
|
3667
|
+
return this.agentHasAnyRole(orchestratorBot, ['research', 'researcher', 'analyst', 'advisor', 'brainstorming', 'planning', 'design']);
|
|
2271
3668
|
return false;
|
|
2272
3669
|
}
|
|
2273
3670
|
resolveWorkflowRoleTarget(role, orchestratorBot, roleAssignments) {
|
|
@@ -2363,7 +3760,7 @@ class OrchestratorAgent {
|
|
|
2363
3760
|
'Do not build the final artifact in this step.',
|
|
2364
3761
|
'Your deliverable is a concrete implementation direction document that the coding step can use immediately.',
|
|
2365
3762
|
'',
|
|
2366
|
-
`Original user request
|
|
3763
|
+
`Original user request:\n${originalPrompt}`,
|
|
2367
3764
|
artifactContext,
|
|
2368
3765
|
'',
|
|
2369
3766
|
'Your job: research the request, pressure-test the approach, and recommend the best implementation direction.',
|
|
@@ -2377,7 +3774,7 @@ class OrchestratorAgent {
|
|
|
2377
3774
|
'Do not perform QA in this step.',
|
|
2378
3775
|
'Your deliverable is the actual file or code change requested by the user.',
|
|
2379
3776
|
'',
|
|
2380
|
-
`Original user request
|
|
3777
|
+
`Original user request:\n${originalPrompt}`,
|
|
2381
3778
|
artifactContext,
|
|
2382
3779
|
researchContext,
|
|
2383
3780
|
qaContext,
|
|
@@ -2395,7 +3792,7 @@ class OrchestratorAgent {
|
|
|
2395
3792
|
'You are the QA/review step in a multi-step workflow.',
|
|
2396
3793
|
'Do not redo the coding or research work in this step unless the file is missing and you must report that blocker.',
|
|
2397
3794
|
'',
|
|
2398
|
-
`Original user request
|
|
3795
|
+
`Original user request:\n${originalPrompt}`,
|
|
2399
3796
|
artifactContext,
|
|
2400
3797
|
researchContext,
|
|
2401
3798
|
implementationContext,
|
|
@@ -2604,6 +4001,23 @@ class OrchestratorAgent {
|
|
|
2604
4001
|
});
|
|
2605
4002
|
}
|
|
2606
4003
|
this.reportHiddenRoleProgress(opts, 'dispatch_controller', `Connecting request to ${agent.name}`);
|
|
4004
|
+
const forwardProxyWorkerChunk = (event) => {
|
|
4005
|
+
if (!opts.onWorkerChunk)
|
|
4006
|
+
return;
|
|
4007
|
+
if (event.type === 'chunk') {
|
|
4008
|
+
opts.onWorkerChunk({ ...event, type: 'worker_chunk' });
|
|
4009
|
+
return;
|
|
4010
|
+
}
|
|
4011
|
+
if (event.type === 'tool_call') {
|
|
4012
|
+
opts.onWorkerChunk({ ...event, type: 'worker_tool_call' });
|
|
4013
|
+
return;
|
|
4014
|
+
}
|
|
4015
|
+
if (event.type === 'tool_result') {
|
|
4016
|
+
opts.onWorkerChunk({ ...event, type: 'worker_tool_result' });
|
|
4017
|
+
return;
|
|
4018
|
+
}
|
|
4019
|
+
opts.onWorkerChunk(event);
|
|
4020
|
+
};
|
|
2607
4021
|
// Update conversation routing mode to 'proxy'
|
|
2608
4022
|
try {
|
|
2609
4023
|
const db = data.getDb();
|
|
@@ -2613,14 +4027,17 @@ class OrchestratorAgent {
|
|
|
2613
4027
|
// Execute single step — disable decomposition for proxy mode
|
|
2614
4028
|
const runProxyStep = (stepPrompt) => this.runNodeWithRetry('delegate_specialist', state, async () => this.workflowEngine.execute(stepPrompt, conversationId, agent.id, {
|
|
2615
4029
|
apiKey: this.resolveApiKey(agent.provider),
|
|
4030
|
+
abortSignal: opts.abortSignal,
|
|
2616
4031
|
disableDecomposition: true,
|
|
2617
|
-
isOrchestrated:
|
|
4032
|
+
isOrchestrated: false,
|
|
2618
4033
|
projectId: state?.projectId ?? null,
|
|
2619
4034
|
workspacePath: state?.projectId
|
|
2620
4035
|
? data.getProject(state.projectId)?.folder?.trim() || undefined
|
|
2621
4036
|
: undefined,
|
|
2622
4037
|
persistConversationMessages: false,
|
|
2623
|
-
|
|
4038
|
+
storedAttachments: opts.storedAttachments,
|
|
4039
|
+
workerImageDeliveryMode: opts.storedAttachments?.length ? 'reference' : undefined,
|
|
4040
|
+
onWorkerChunk: forwardProxyWorkerChunk,
|
|
2624
4041
|
onProgress: (progress) => {
|
|
2625
4042
|
if (progress.event === 'step-started') {
|
|
2626
4043
|
this.reportHiddenRoleProgress(opts, 'orchestrator', `${progress.step.agentName} is working on the delegated step`);
|
|
@@ -2888,17 +4305,22 @@ class OrchestratorAgent {
|
|
|
2888
4305
|
coderRetryLimit: effectivePolicy.coderRetryLimit,
|
|
2889
4306
|
workspacePath: effectiveWorkspacePath,
|
|
2890
4307
|
projectId: project?.id || opts.projectId || null,
|
|
4308
|
+
storedAttachments: opts.storedAttachments,
|
|
4309
|
+
workerImageDeliveryMode: opts.storedAttachments?.length ? 'reference' : undefined,
|
|
2891
4310
|
onProgress: progressHandler,
|
|
2892
4311
|
onWorkerChunk: opts.onWorkerChunk,
|
|
2893
4312
|
})
|
|
2894
4313
|
: this.workflowEngine.execute(prompt, conversationId, defaultAgent.id, {
|
|
2895
4314
|
apiKey: this.resolveApiKey(defaultAgent.provider),
|
|
4315
|
+
abortSignal: opts.abortSignal,
|
|
2896
4316
|
taskId: todoTaskId,
|
|
2897
4317
|
isOrchestrated: true,
|
|
2898
4318
|
roleAssignments,
|
|
2899
4319
|
coderRetryLimit: effectivePolicy.coderRetryLimit,
|
|
2900
4320
|
workspacePath: effectiveWorkspacePath,
|
|
2901
4321
|
projectId: project?.id || opts.projectId || null,
|
|
4322
|
+
storedAttachments: opts.storedAttachments,
|
|
4323
|
+
workerImageDeliveryMode: opts.storedAttachments?.length ? 'reference' : undefined,
|
|
2902
4324
|
onProgress: progressHandler,
|
|
2903
4325
|
onWorkerChunk: opts.onWorkerChunk,
|
|
2904
4326
|
}));
|
|
@@ -3047,6 +4469,83 @@ class OrchestratorAgent {
|
|
|
3047
4469
|
reportHiddenRoleProgress(opts, roleName, detail) {
|
|
3048
4470
|
opts.onProgress(`${roleName}::${detail}`);
|
|
3049
4471
|
}
|
|
4472
|
+
/**
|
|
4473
|
+
* Returns a callback that forwards orchestrator narration chunks to the
|
|
4474
|
+
* desktop as 'chunk' WorkerChunkEvents tagged with the orchestrator bot's identity.
|
|
4475
|
+
* This is how the front-door narration appears live in the orchestrator card.
|
|
4476
|
+
*/
|
|
4477
|
+
makeOrchestratorNarrationForwarder(opts, orchestratorBot) {
|
|
4478
|
+
if (!opts.onWorkerChunk)
|
|
4479
|
+
return undefined;
|
|
4480
|
+
const stepId = `orchestrator-narration-${orchestratorBot.id}`;
|
|
4481
|
+
return (text) => {
|
|
4482
|
+
if (!text)
|
|
4483
|
+
return;
|
|
4484
|
+
try {
|
|
4485
|
+
opts.onWorkerChunk?.({
|
|
4486
|
+
type: 'chunk',
|
|
4487
|
+
stepId,
|
|
4488
|
+
agentName: 'Orchestrator',
|
|
4489
|
+
description: 'Orchestrator narration',
|
|
4490
|
+
stepIndex: -1,
|
|
4491
|
+
totalSteps: 0,
|
|
4492
|
+
text,
|
|
4493
|
+
});
|
|
4494
|
+
}
|
|
4495
|
+
catch {
|
|
4496
|
+
// best-effort streaming
|
|
4497
|
+
}
|
|
4498
|
+
};
|
|
4499
|
+
}
|
|
4500
|
+
/**
|
|
4501
|
+
* Synthesizes a short factual narration sentence when the LLM front-door call
|
|
4502
|
+
* failed entirely and Funolio fell back to a system suggestion. Never returns
|
|
4503
|
+
* robotic boilerplate — uses real bot/role names whenever available.
|
|
4504
|
+
*/
|
|
4505
|
+
synthesizeFallbackNarration(decision, roleAssignments) {
|
|
4506
|
+
const mode = decision?.mode;
|
|
4507
|
+
if (!mode)
|
|
4508
|
+
return null;
|
|
4509
|
+
if (mode === 'delegate') {
|
|
4510
|
+
const target = decision.delegate_target?.trim();
|
|
4511
|
+
if (target && target.toUpperCase() !== 'NONE') {
|
|
4512
|
+
return `Routing this to ${target}.`;
|
|
4513
|
+
}
|
|
4514
|
+
return 'Routing this to the appropriate worker.';
|
|
4515
|
+
}
|
|
4516
|
+
if (mode === 'workflow') {
|
|
4517
|
+
// Use real worker names from assignments if available.
|
|
4518
|
+
const workerNames = [];
|
|
4519
|
+
if (roleAssignments) {
|
|
4520
|
+
if (roleAssignments.research)
|
|
4521
|
+
workerNames.push(roleAssignments.research);
|
|
4522
|
+
if (roleAssignments.coding)
|
|
4523
|
+
workerNames.push(roleAssignments.coding);
|
|
4524
|
+
if (roleAssignments.qa)
|
|
4525
|
+
workerNames.push(roleAssignments.qa);
|
|
4526
|
+
}
|
|
4527
|
+
const uniqueNames = Array.from(new Set(workerNames.map((n) => String(n || '').trim()).filter(Boolean)));
|
|
4528
|
+
if (uniqueNames.length === 0) {
|
|
4529
|
+
return 'Setting up a multi-step workflow for this.';
|
|
4530
|
+
}
|
|
4531
|
+
if (uniqueNames.length === 1) {
|
|
4532
|
+
return `Handing this to ${uniqueNames[0]} as a workflow.`;
|
|
4533
|
+
}
|
|
4534
|
+
if (uniqueNames.length === 2) {
|
|
4535
|
+
return `Setting up a workflow with ${uniqueNames[0]} and ${uniqueNames[1]}.`;
|
|
4536
|
+
}
|
|
4537
|
+
const last = uniqueNames[uniqueNames.length - 1];
|
|
4538
|
+
const front = uniqueNames.slice(0, -1).join(', ');
|
|
4539
|
+
return `Setting up a workflow with ${front}, and ${last}.`;
|
|
4540
|
+
}
|
|
4541
|
+
if (mode === 'clarify') {
|
|
4542
|
+
return 'I need a quick clarification before I can route this.';
|
|
4543
|
+
}
|
|
4544
|
+
if (mode === 'execute_self') {
|
|
4545
|
+
return 'Working on this now.';
|
|
4546
|
+
}
|
|
4547
|
+
return null;
|
|
4548
|
+
}
|
|
3050
4549
|
formatConfirmationRequest(prompt) {
|
|
3051
4550
|
const singleLine = String(prompt || '').replace(/\s+/g, ' ').trim();
|
|
3052
4551
|
if (!singleLine)
|
|
@@ -3168,58 +4667,126 @@ class OrchestratorAgent {
|
|
|
3168
4667
|
hasRoleAssignments(assignments) {
|
|
3169
4668
|
return Boolean(assignments.coding || assignments.qa || assignments.research);
|
|
3170
4669
|
}
|
|
3171
|
-
|
|
3172
|
-
const raw = data.getProjectSetting(projectId, ORCHESTRATOR_ROLE_SETTING_KEY);
|
|
3173
|
-
if (!raw)
|
|
3174
|
-
return {};
|
|
3175
|
-
try {
|
|
3176
|
-
const parsed = JSON.parse(raw);
|
|
3177
|
-
return {
|
|
3178
|
-
coding: parsed.coding || undefined,
|
|
3179
|
-
qa: parsed.qa || undefined,
|
|
3180
|
-
research: parsed.research || undefined,
|
|
3181
|
-
};
|
|
3182
|
-
}
|
|
3183
|
-
catch {
|
|
3184
|
-
return {};
|
|
3185
|
-
}
|
|
3186
|
-
}
|
|
3187
|
-
mergeRoleAssignments(base, saved, inferred, incoming) {
|
|
4670
|
+
mergeRoleAssignments(base, inferred, incoming) {
|
|
3188
4671
|
return {
|
|
3189
|
-
coding: incoming.coding ||
|
|
3190
|
-
qa: incoming.qa ||
|
|
3191
|
-
research: incoming.research ||
|
|
4672
|
+
coding: incoming.coding || base.coding || inferred.coding,
|
|
4673
|
+
qa: incoming.qa || base.qa || inferred.qa,
|
|
4674
|
+
research: incoming.research || base.research || inferred.research,
|
|
3192
4675
|
};
|
|
3193
4676
|
}
|
|
3194
4677
|
deriveRoleAssignmentsFromProject(projectId, orchestratorBot) {
|
|
3195
4678
|
const bots = this.listProjectScopedBots(projectId);
|
|
3196
4679
|
const result = {};
|
|
3197
|
-
const roleClass = String(orchestratorBot?.role_class || '').trim().toLowerCase();
|
|
3198
4680
|
if (orchestratorBot?.name) {
|
|
3199
|
-
if (['code', 'coding', 'coder', 'builder', 'developer', 'engineer']
|
|
4681
|
+
if (this.agentHasAnyRole(orchestratorBot, ['code', 'coding', 'coder', 'builder', 'developer', 'engineer'])) {
|
|
3200
4682
|
result.coding = orchestratorBot.name;
|
|
3201
4683
|
}
|
|
3202
|
-
else if (['qa', 'review', 'reviewer']
|
|
4684
|
+
else if (this.agentHasAnyRole(orchestratorBot, ['qa', 'review', 'reviewer'])) {
|
|
3203
4685
|
result.qa = orchestratorBot.name;
|
|
3204
4686
|
}
|
|
3205
|
-
else if (['research', 'researcher', 'analyst', 'advisor']
|
|
4687
|
+
else if (this.agentHasAnyRole(orchestratorBot, ['research', 'researcher', 'analyst', 'advisor', 'brainstorming', 'planning', 'design'])) {
|
|
3206
4688
|
result.research = orchestratorBot.name;
|
|
3207
4689
|
}
|
|
3208
4690
|
}
|
|
3209
|
-
const findByRole = (roles) => bots.find((bot) =>
|
|
4691
|
+
const findByRole = (roles) => bots.find((bot) => this.agentHasAnyRole(bot, roles));
|
|
3210
4692
|
result.coding = result.coding || findByRole(['code', 'coding', 'coder', 'builder', 'developer', 'engineer'])?.name;
|
|
3211
4693
|
result.qa = result.qa || findByRole(['qa', 'review', 'reviewer'])?.name;
|
|
3212
|
-
result.research = result.research || findByRole(['research', 'researcher', 'analyst', 'advisor'])?.name;
|
|
4694
|
+
result.research = result.research || findByRole(['research', 'researcher', 'analyst', 'advisor', 'brainstorming', 'planning', 'design'])?.name;
|
|
3213
4695
|
return result;
|
|
3214
4696
|
}
|
|
3215
|
-
listProjectScopedBots(projectId) {
|
|
4697
|
+
listProjectScopedBots(projectId, opts) {
|
|
4698
|
+
const maybeFilterOrchestrators = (bots) => opts?.includeOrchestratorProfiles ? bots : (0, orchestrator_profile_1.filterOutOrchestratorProfiles)(bots);
|
|
3216
4699
|
if (!projectId)
|
|
3217
|
-
return (
|
|
4700
|
+
return maybeFilterOrchestrators(data.listAgentProfiles());
|
|
3218
4701
|
const project = data.getProject(projectId);
|
|
3219
4702
|
const botIds = new Set(project?.bot_ids || []);
|
|
3220
4703
|
const allBots = data.listAgentProfiles();
|
|
3221
4704
|
const scoped = allBots.filter((bot) => botIds.has(bot.id));
|
|
3222
|
-
return (
|
|
4705
|
+
return maybeFilterOrchestrators(scoped.length > 0 ? scoped : allBots);
|
|
4706
|
+
}
|
|
4707
|
+
resolvePlanImportBot(plannerBotId, projectId, orchestratorBot) {
|
|
4708
|
+
const candidates = this.listProjectScopedBots(projectId, { includeOrchestratorProfiles: true })
|
|
4709
|
+
.filter((bot) => bot.is_active === 1);
|
|
4710
|
+
const orchestratorCandidate = orchestratorBot && orchestratorBot.is_active === 1
|
|
4711
|
+
? orchestratorBot
|
|
4712
|
+
: null;
|
|
4713
|
+
const pool = orchestratorCandidate && !candidates.some((bot) => bot.id === orchestratorCandidate.id)
|
|
4714
|
+
? [...candidates, orchestratorCandidate]
|
|
4715
|
+
: candidates;
|
|
4716
|
+
const selected = pool.find((bot) => bot.id === plannerBotId);
|
|
4717
|
+
if (!selected) {
|
|
4718
|
+
throw new Error('Choose an active planner bot for this project.');
|
|
4719
|
+
}
|
|
4720
|
+
return selected;
|
|
4721
|
+
}
|
|
4722
|
+
async runPlanImportPlannerTurn(input) {
|
|
4723
|
+
const syntheticConversationId = `${input.conversationId}::plan-import`;
|
|
4724
|
+
if (input.plannerBot.provider === 'claude-cli') {
|
|
4725
|
+
const manager = (0, local_cli_pty_manager_1.getLocalCliPtySessionManager)();
|
|
4726
|
+
try {
|
|
4727
|
+
const result = await manager.runTurn({
|
|
4728
|
+
conversationId: syntheticConversationId,
|
|
4729
|
+
botId: input.plannerBot.id,
|
|
4730
|
+
provider: 'claude-cli',
|
|
4731
|
+
botSettings: {
|
|
4732
|
+
claude: {
|
|
4733
|
+
model: input.plannerBot.model,
|
|
4734
|
+
effortLevel: input.plannerBot.claude_effort_level,
|
|
4735
|
+
outputStyle: input.plannerBot.claude_output_style,
|
|
4736
|
+
fastMode: input.plannerBot.claude_fast_mode === 1,
|
|
4737
|
+
permissionsJson: input.plannerBot.claude_permissions_json,
|
|
4738
|
+
},
|
|
4739
|
+
},
|
|
4740
|
+
cwd: input.workspacePath,
|
|
4741
|
+
systemPrompt: input.systemPrompt,
|
|
4742
|
+
messages: input.messages,
|
|
4743
|
+
forceFreshSession: true,
|
|
4744
|
+
newSessionId: data.generateNextSessionId(),
|
|
4745
|
+
abortSignal: input.abortSignal,
|
|
4746
|
+
});
|
|
4747
|
+
return String(result.content || '').trim();
|
|
4748
|
+
}
|
|
4749
|
+
finally {
|
|
4750
|
+
manager.closeSessionByConversation(syntheticConversationId, input.plannerBot.id);
|
|
4751
|
+
}
|
|
4752
|
+
}
|
|
4753
|
+
if (input.plannerBot.provider === 'codex-cli' && !(0, storage_mode_1.isServerStorageMode)()) {
|
|
4754
|
+
const manager = (0, codex_app_server_manager_1.getCodexAppServerManager)();
|
|
4755
|
+
try {
|
|
4756
|
+
const result = await manager.runTurn({
|
|
4757
|
+
runtimeMode: 'local_desktop',
|
|
4758
|
+
conversationId: syntheticConversationId,
|
|
4759
|
+
botId: input.plannerBot.id,
|
|
4760
|
+
botName: input.plannerBot.name,
|
|
4761
|
+
cwd: input.workspacePath,
|
|
4762
|
+
systemPrompt: input.systemPrompt,
|
|
4763
|
+
messages: input.messages,
|
|
4764
|
+
forceFreshSession: true,
|
|
4765
|
+
model: input.plannerBot.model || null,
|
|
4766
|
+
projectId: input.projectId,
|
|
4767
|
+
codexSettings: {
|
|
4768
|
+
reasoningEffort: input.plannerBot.codex_reasoning_effort,
|
|
4769
|
+
reasoningSummary: input.plannerBot.codex_reasoning_summary,
|
|
4770
|
+
personality: input.plannerBot.codex_personality,
|
|
4771
|
+
serviceTier: input.plannerBot.codex_service_tier,
|
|
4772
|
+
sandboxPolicy: input.plannerBot.codex_sandbox_policy,
|
|
4773
|
+
approvalPolicy: input.plannerBot.codex_approval_policy,
|
|
4774
|
+
},
|
|
4775
|
+
abortSignal: input.abortSignal,
|
|
4776
|
+
});
|
|
4777
|
+
return String(result.content || '').trim();
|
|
4778
|
+
}
|
|
4779
|
+
finally {
|
|
4780
|
+
manager.closeSessionByConversation(syntheticConversationId, input.plannerBot.id);
|
|
4781
|
+
}
|
|
4782
|
+
}
|
|
4783
|
+
const response = await input.plannerRuntime.llm.chat({
|
|
4784
|
+
system: input.systemPrompt,
|
|
4785
|
+
messages: input.messages,
|
|
4786
|
+
maxOutputTokens: 20_000,
|
|
4787
|
+
abortSignal: input.abortSignal,
|
|
4788
|
+
});
|
|
4789
|
+
return String(response?.content || '').trim();
|
|
3223
4790
|
}
|
|
3224
4791
|
policyRoleAssignments(policy) {
|
|
3225
4792
|
return {
|
|
@@ -3424,35 +4991,6 @@ class OrchestratorAgent {
|
|
|
3424
4991
|
}
|
|
3425
4992
|
return undefined;
|
|
3426
4993
|
}
|
|
3427
|
-
persistRoleAssignments(projectId, conversationId, agentId, assignments) {
|
|
3428
|
-
const merged = this.mergeRoleAssignments({}, this.loadRoleAssignments(projectId), this.deriveRoleAssignmentsFromProject(projectId), assignments);
|
|
3429
|
-
data.setProjectSetting(projectId, ORCHESTRATOR_ROLE_SETTING_KEY, JSON.stringify(merged));
|
|
3430
|
-
const parts = [
|
|
3431
|
-
merged.coding ? `${merged.coding} handles coding` : null,
|
|
3432
|
-
merged.qa ? `${merged.qa} handles QA` : null,
|
|
3433
|
-
merged.research ? `${merged.research} handles research` : null,
|
|
3434
|
-
].filter(Boolean);
|
|
3435
|
-
if (parts.length > 0 && (0, storage_mode_1.isServerStorageMode)()) {
|
|
3436
|
-
data.upsertMemoryFact({
|
|
3437
|
-
agentId,
|
|
3438
|
-
conversationId,
|
|
3439
|
-
factType: 'operating_instruction',
|
|
3440
|
-
content: `Default orchestrator assignments: ${parts.join('; ')}.`,
|
|
3441
|
-
extractionMethod: 'orchestrator',
|
|
3442
|
-
});
|
|
3443
|
-
}
|
|
3444
|
-
data.logAdminAudit({
|
|
3445
|
-
actorType: 'orchestrator',
|
|
3446
|
-
actorId: 'Project Manager',
|
|
3447
|
-
projectId,
|
|
3448
|
-
resourceType: 'project_setting',
|
|
3449
|
-
resourceId: `${projectId}:${ORCHESTRATOR_ROLE_SETTING_KEY}`,
|
|
3450
|
-
action: 'set_orchestrator_role_assignments',
|
|
3451
|
-
request: assignments,
|
|
3452
|
-
after: merged,
|
|
3453
|
-
result: 'ok',
|
|
3454
|
-
});
|
|
3455
|
-
}
|
|
3456
4994
|
roleAssignmentParticipants(assignments, agents) {
|
|
3457
4995
|
return Array.from(new Set([assignments.coding, assignments.qa, assignments.research]
|
|
3458
4996
|
.filter((name) => Boolean(name))
|