funolio-agent 1.0.53 → 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/approval.d.ts +1 -6
- package/dist/approval.d.ts.map +1 -1
- package/dist/approval.js +2 -7
- package/dist/approval.js.map +1 -1
- 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 +5 -1
- package/dist/bot-manager.d.ts.map +1 -1
- package/dist/bot-manager.js +46 -27
- 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/cli-session-registry.d.ts +35 -0
- package/dist/cli-session-registry.d.ts.map +1 -0
- package/dist/cli-session-registry.js +177 -0
- package/dist/cli-session-registry.js.map +1 -0
- package/dist/cli.js +62 -0
- package/dist/cli.js.map +1 -1
- package/dist/codex-app-server-manager.d.ts +189 -0
- package/dist/codex-app-server-manager.d.ts.map +1 -0
- package/dist/codex-app-server-manager.js +1468 -0
- package/dist/codex-app-server-manager.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +8 -30
- package/dist/commands/init.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/setup.d.ts +4 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +9 -25
- package/dist/commands/setup.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 +559 -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/completion-marker.d.ts +7 -0
- package/dist/completion-marker.d.ts.map +1 -0
- package/dist/completion-marker.js +28 -0
- package/dist/completion-marker.js.map +1 -0
- package/dist/config.d.ts +7 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +184 -60
- 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 +210 -17
- package/dist/context-window.js.map +1 -1
- package/dist/live-activity.d.ts +31 -0
- package/dist/live-activity.d.ts.map +1 -0
- package/dist/live-activity.js +36 -0
- package/dist/live-activity.js.map +1 -0
- 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 +186 -0
- package/dist/local-cli-pty-manager.d.ts.map +1 -1
- package/dist/local-cli-pty-manager.js +2581 -164
- 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 +276 -5
- package/dist/local-data.d.ts.map +1 -1
- package/dist/local-data.js +1201 -86
- 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 +428 -2
- 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 +55 -0
- package/dist/local-server.d.ts.map +1 -1
- package/dist/local-server.js +3281 -441
- 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 +6 -0
- package/dist/message-loop.d.ts.map +1 -1
- package/dist/message-loop.js +281 -89
- package/dist/message-loop.js.map +1 -1
- package/dist/mqtt-client.d.ts +44 -1
- package/dist/mqtt-client.d.ts.map +1 -1
- package/dist/mqtt-client.js +284 -46
- 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/oauth.d.ts.map +1 -1
- package/dist/oauth.js +69 -29
- package/dist/oauth.js.map +1 -1
- 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 +15 -0
- package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
- package/dist/orchestration/orchestrator-operating-prompt.js +206 -20
- 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/validation.d.ts +40 -0
- package/dist/orchestration/validation.d.ts.map +1 -0
- package/dist/orchestration/validation.js +203 -0
- package/dist/orchestration/validation.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 +214 -33
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +2200 -1100
- 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-prompt.d.ts.map +1 -1
- package/dist/providers/claude-cli-prompt.js +49 -5
- package/dist/providers/claude-cli-prompt.js.map +1 -1
- package/dist/providers/claude-cli.d.ts.map +1 -1
- package/dist/providers/claude-cli.js +81 -5
- 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 +204 -26
- 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/response-guard.js +1 -1
- package/dist/response-guard.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 +20 -5
- package/dist/tools/admin-tools.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -1
- package/dist/tools/index.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 +16 -0
- package/dist/tools/search-conversation-history.d.ts.map +1 -0
- package/dist/tools/search-conversation-history.js +334 -0
- package/dist/tools/search-conversation-history.js.map +1 -0
- 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 +20 -0
- package/dist/wizard-state.d.ts.map +1 -1
- package/dist/wizard-state.js +90 -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 +44 -2
- package/dist/workflow-engine.d.ts.map +1 -1
- package/dist/workflow-engine.js +932 -111
- package/dist/workflow-engine.js.map +1 -1
- package/package.json +2 -2
package/dist/orchestrator.js
CHANGED
|
@@ -47,25 +47,37 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
47
47
|
})();
|
|
48
48
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
49
|
exports.OrchestratorAgent = void 0;
|
|
50
|
+
exports.buildLocalDesktopOrchestratorRuntime = buildLocalDesktopOrchestratorRuntime;
|
|
51
|
+
exports.buildNarrationStreamGate = buildNarrationStreamGate;
|
|
52
|
+
const index_1 = require("./providers/index");
|
|
53
|
+
const approval_1 = require("./approval");
|
|
50
54
|
const status_parser_1 = require("./orchestration/status-parser");
|
|
55
|
+
const validation_1 = require("./orchestration/validation");
|
|
51
56
|
const front_door_policy_1 = require("./orchestration/front-door-policy");
|
|
52
57
|
const deterministic_path_1 = require("./orchestration/deterministic-path");
|
|
53
58
|
const orchestrator_operating_prompt_1 = require("./orchestration/orchestrator-operating-prompt");
|
|
54
59
|
const policy_prompt_1 = require("./orchestration/policy-prompt");
|
|
60
|
+
const safeguards_1 = require("./orchestration/safeguards");
|
|
55
61
|
const orchestrator_blocked_prompt_1 = require("./orchestration/orchestrator-blocked-prompt");
|
|
56
62
|
const orchestrator_final_response_prompt_1 = require("./orchestration/orchestrator-final-response-prompt");
|
|
57
63
|
const worker_operating_prompt_1 = require("./orchestration/worker-operating-prompt");
|
|
64
|
+
const capabilities_1 = require("./orchestration/capabilities");
|
|
58
65
|
const policy_detection_1 = require("./policy-detection");
|
|
59
66
|
const execution_contract_1 = require("./execution-contract");
|
|
60
67
|
const state_1 = require("./orchestration/state");
|
|
61
68
|
const orchestrator_profile_1 = require("./orchestrator-profile");
|
|
62
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");
|
|
63
73
|
const data = __importStar(require("./local-data"));
|
|
74
|
+
const cli_session_epoch_1 = require("./cli-session-epoch");
|
|
64
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");
|
|
65
78
|
const fs = __importStar(require("fs"));
|
|
66
79
|
const os = __importStar(require("os"));
|
|
67
80
|
const path = __importStar(require("path"));
|
|
68
|
-
const ORCHESTRATOR_ROLE_SETTING_KEY = 'orchestrator_role_assignments';
|
|
69
81
|
const ORCHESTRATOR_WORKFLOW_SETTING_KEY = 'orchestrator_preferred_workflow';
|
|
70
82
|
const ORCHESTRATOR_TEMPLATE_SETTING_KEY = 'orchestrator_default_workflow_template_id';
|
|
71
83
|
const HEARTBEAT_MS = 30_000;
|
|
@@ -80,13 +92,283 @@ const ORCHESTRATION_NODE_RETRY_LIMITS = {
|
|
|
80
92
|
finalize_response: 0,
|
|
81
93
|
require_confirmation: 0,
|
|
82
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
|
+
}
|
|
83
100
|
// ─── Orchestrator Agent ──────────────────────────────────────────
|
|
101
|
+
function resolveLocalProviderCredentials(providerName, profile, explicitConnection) {
|
|
102
|
+
if (index_1.CLI_PROVIDERS.has(providerName)) {
|
|
103
|
+
return { apiKey: 'cli-auth' };
|
|
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
|
+
}
|
|
111
|
+
const directConnection = profile?.provider_connection_id
|
|
112
|
+
? data.getProviderConnection(profile.provider_connection_id)
|
|
113
|
+
: undefined;
|
|
114
|
+
if (directConnection?.oauth_token) {
|
|
115
|
+
return { apiKey: directConnection.oauth_token, authMode: 'oauth-bearer' };
|
|
116
|
+
}
|
|
117
|
+
if (directConnection?.api_key_enc) {
|
|
118
|
+
return { apiKey: directConnection.api_key_enc };
|
|
119
|
+
}
|
|
120
|
+
const providerConnection = data.findProviderConnection(providerName);
|
|
121
|
+
if (providerConnection?.oauth_token) {
|
|
122
|
+
return { apiKey: providerConnection.oauth_token, authMode: 'oauth-bearer' };
|
|
123
|
+
}
|
|
124
|
+
if (providerConnection?.api_key_enc) {
|
|
125
|
+
return { apiKey: providerConnection.api_key_enc };
|
|
126
|
+
}
|
|
127
|
+
if (profile?.api_key_enc) {
|
|
128
|
+
return { apiKey: profile.api_key_enc };
|
|
129
|
+
}
|
|
130
|
+
switch (providerName) {
|
|
131
|
+
case 'anthropic':
|
|
132
|
+
if (process.env.ANTHROPIC_API_KEY)
|
|
133
|
+
return { apiKey: process.env.ANTHROPIC_API_KEY };
|
|
134
|
+
break;
|
|
135
|
+
case 'openai':
|
|
136
|
+
if (process.env.OPENAI_API_KEY)
|
|
137
|
+
return { apiKey: process.env.OPENAI_API_KEY };
|
|
138
|
+
break;
|
|
139
|
+
case 'google':
|
|
140
|
+
if (process.env.GOOGLE_API_KEY)
|
|
141
|
+
return { apiKey: process.env.GOOGLE_API_KEY };
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
throw new Error(`No credentials configured for provider ${providerName}.`);
|
|
145
|
+
}
|
|
146
|
+
function buildLocalDesktopOrchestratorRuntime(selectedBot) {
|
|
147
|
+
if (data.isClerkOrchestratorEnabled()) {
|
|
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';
|
|
154
|
+
if (!providerName) {
|
|
155
|
+
throw new Error('Clerk is selected as orchestrator, but no clerk provider is configured.');
|
|
156
|
+
}
|
|
157
|
+
const credentials = resolveLocalProviderCredentials(providerName, null, clerkConnection);
|
|
158
|
+
const llm = (0, index_1.createProvider)(providerName, {
|
|
159
|
+
apiKey: credentials.apiKey,
|
|
160
|
+
model,
|
|
161
|
+
runtimeMode: 'local_desktop',
|
|
162
|
+
...(credentials.authMode ? { authMode: credentials.authMode } : {}),
|
|
163
|
+
});
|
|
164
|
+
return {
|
|
165
|
+
kind: 'clerk',
|
|
166
|
+
llm,
|
|
167
|
+
providerName,
|
|
168
|
+
model,
|
|
169
|
+
agentName: 'Orchestrator',
|
|
170
|
+
botId: null,
|
|
171
|
+
modelLabel: model || null,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
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();
|
|
180
|
+
if (!profile) {
|
|
181
|
+
throw new Error('No orchestrator bot is configured. Mark a bot with is_orchestrator=1 or enable Clerk orchestration.');
|
|
182
|
+
}
|
|
183
|
+
return buildLocalDesktopRuntimeFromProfile(profile);
|
|
184
|
+
}
|
|
185
|
+
function buildLocalDesktopRuntimeFromProfile(profile) {
|
|
186
|
+
const credentials = resolveLocalProviderCredentials(profile.provider, profile);
|
|
187
|
+
const llm = (0, index_1.createProvider)(profile.provider, {
|
|
188
|
+
apiKey: credentials.apiKey,
|
|
189
|
+
model: (profile.model || '').trim() || 'default',
|
|
190
|
+
runtimeMode: 'local_desktop',
|
|
191
|
+
...(credentials.authMode ? { authMode: credentials.authMode } : {}),
|
|
192
|
+
});
|
|
193
|
+
return {
|
|
194
|
+
kind: 'bot',
|
|
195
|
+
llm,
|
|
196
|
+
providerName: profile.provider,
|
|
197
|
+
model: profile.model || null,
|
|
198
|
+
agentName: profile.name,
|
|
199
|
+
botId: profile.id,
|
|
200
|
+
modelLabel: profile.model || profile.name,
|
|
201
|
+
};
|
|
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
|
+
}
|
|
84
332
|
class OrchestratorAgent {
|
|
85
|
-
|
|
333
|
+
orchestratorRuntime;
|
|
86
334
|
workflowEngine;
|
|
87
335
|
lastResponseMeta = null;
|
|
88
|
-
constructor(
|
|
89
|
-
|
|
336
|
+
constructor(orchestratorRuntime, workflowEngine) {
|
|
337
|
+
const runtimeCandidate = orchestratorRuntime;
|
|
338
|
+
if (runtimeCandidate && typeof runtimeCandidate.kind === 'string') {
|
|
339
|
+
this.orchestratorRuntime = orchestratorRuntime;
|
|
340
|
+
}
|
|
341
|
+
else if (runtimeCandidate && typeof runtimeCandidate.respond === 'function') {
|
|
342
|
+
const runtimeInfo = typeof runtimeCandidate.getRuntimeInfo === 'function'
|
|
343
|
+
? runtimeCandidate.getRuntimeInfo()
|
|
344
|
+
: {};
|
|
345
|
+
this.orchestratorRuntime = {
|
|
346
|
+
kind: 'clerk',
|
|
347
|
+
llm: {
|
|
348
|
+
chat: async (options) => ({
|
|
349
|
+
content: await runtimeCandidate.respond(String(options.messages?.[0]?.content || ''), String(options.system || '')),
|
|
350
|
+
}),
|
|
351
|
+
},
|
|
352
|
+
providerName: runtimeInfo?.provider || 'openai',
|
|
353
|
+
model: runtimeInfo?.model || null,
|
|
354
|
+
agentName: 'Orchestrator',
|
|
355
|
+
botId: null,
|
|
356
|
+
modelLabel: runtimeInfo?.model || null,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
this.orchestratorRuntime = {
|
|
361
|
+
kind: 'clerk',
|
|
362
|
+
llm: {
|
|
363
|
+
chat: async () => ({ content: '' }),
|
|
364
|
+
},
|
|
365
|
+
providerName: 'openai',
|
|
366
|
+
model: null,
|
|
367
|
+
agentName: 'Orchestrator',
|
|
368
|
+
botId: null,
|
|
369
|
+
modelLabel: null,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
90
372
|
this.workflowEngine = workflowEngine;
|
|
91
373
|
}
|
|
92
374
|
isLocalDesktopRuntime() {
|
|
@@ -106,6 +388,8 @@ class OrchestratorAgent {
|
|
|
106
388
|
return await work(attempt);
|
|
107
389
|
}
|
|
108
390
|
catch (error) {
|
|
391
|
+
if (isAbortLikeError(error))
|
|
392
|
+
throw error;
|
|
109
393
|
if (attempt >= maxRetries)
|
|
110
394
|
throw error;
|
|
111
395
|
attempt += 1;
|
|
@@ -132,11 +416,52 @@ class OrchestratorAgent {
|
|
|
132
416
|
add('GPT');
|
|
133
417
|
return Array.from(refs);
|
|
134
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
|
+
}
|
|
135
458
|
describeAgentResponsibilities(agent) {
|
|
136
459
|
const candidates = [
|
|
137
460
|
agent.purpose_md,
|
|
138
461
|
agent.identity_summary,
|
|
139
462
|
agent.skills_md,
|
|
463
|
+
agent.orchestration_role_label,
|
|
464
|
+
agent.orchestration_role_class,
|
|
140
465
|
agent.role_label,
|
|
141
466
|
agent.role_class,
|
|
142
467
|
].map((value) => String(value || '').replace(/\s+/g, ' ').trim()).filter(Boolean);
|
|
@@ -151,7 +476,7 @@ class OrchestratorAgent {
|
|
|
151
476
|
return cleaned.length > 180 ? `${cleaned.slice(0, 177).trim()}...` : cleaned;
|
|
152
477
|
}
|
|
153
478
|
}
|
|
154
|
-
const normalizedRole =
|
|
479
|
+
const normalizedRole = this.getOrchestrationRoleClass(agent);
|
|
155
480
|
if (normalizedRole === 'code' || normalizedRole === 'coding') {
|
|
156
481
|
return 'implementation, code changes, fixes, and app changes';
|
|
157
482
|
}
|
|
@@ -177,7 +502,7 @@ class OrchestratorAgent {
|
|
|
177
502
|
const orchestrationState = (0, state_1.createOrchestrationState)({
|
|
178
503
|
conversationId: conversationId || null,
|
|
179
504
|
projectId: conversationProjectId || null,
|
|
180
|
-
orchestratorBotId:
|
|
505
|
+
orchestratorBotId: opts.orchestratorBotIdHint || null,
|
|
181
506
|
userPromptRaw: prompt,
|
|
182
507
|
});
|
|
183
508
|
orchestrationState.requestedArtifactTargets = this.extractRequestedArtifactTargets(prompt);
|
|
@@ -196,24 +521,36 @@ class OrchestratorAgent {
|
|
|
196
521
|
return this.applyPendingPolicy(conversationId, pendingPolicy);
|
|
197
522
|
}
|
|
198
523
|
const promptAssignments = this.parseRoleAssignments(prompt);
|
|
199
|
-
const selectedOrchestrator = this.
|
|
200
|
-
|
|
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) {
|
|
201
538
|
this.setLastResponseMeta({
|
|
202
539
|
agentName: 'Orchestrator',
|
|
203
540
|
botId: selectedOrchestrator.id,
|
|
204
541
|
modelLabel: null,
|
|
205
542
|
});
|
|
206
543
|
}
|
|
207
|
-
if (
|
|
208
|
-
orchestrationState.orchestratorBotId =
|
|
544
|
+
if (dispatchOrchestratorBot) {
|
|
545
|
+
orchestrationState.orchestratorBotId = dispatchOrchestratorBot.id;
|
|
209
546
|
}
|
|
210
547
|
const initialProject = conversationProjectId ? data.getProject(conversationProjectId) : undefined;
|
|
211
548
|
const initialPolicy = data.getEffectiveOrchestrationPolicy(conversationProjectId || undefined);
|
|
212
|
-
const initialAssignments = this.mergeRoleAssignments(this.policyRoleAssignments(initialPolicy),
|
|
549
|
+
const initialAssignments = this.mergeRoleAssignments(this.policyRoleAssignments(initialPolicy), this.deriveRoleAssignmentsFromProject(conversationProjectId, dispatchOrchestratorBot), promptAssignments);
|
|
213
550
|
const initialOverview = conversationProjectId ? data.getProjectOverview(conversationProjectId) : undefined;
|
|
214
551
|
// If a specific workflow template was selected by the user, skip front-door classification
|
|
215
552
|
// and go straight to workflow execution.
|
|
216
|
-
if (opts.workflowTemplateId &&
|
|
553
|
+
if (opts.workflowTemplateId && hasConfiguredOrchestratorRuntime) {
|
|
217
554
|
const template = data.getWorkflowTemplate(opts.workflowTemplateId);
|
|
218
555
|
if (template) {
|
|
219
556
|
orchestrationState.intent = 'workflow';
|
|
@@ -232,14 +569,21 @@ class OrchestratorAgent {
|
|
|
232
569
|
const validation = this.validateIntentExecution(prompt, workflowIntent, initialPolicy);
|
|
233
570
|
if (validation.ok) {
|
|
234
571
|
this.applyExecutionSpecToState(orchestrationState, validation.executionSpec);
|
|
235
|
-
|
|
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);
|
|
236
580
|
}
|
|
237
581
|
}
|
|
238
582
|
}
|
|
239
|
-
const namedWorkflowTemplate =
|
|
583
|
+
const namedWorkflowTemplate = hasConfiguredOrchestratorRuntime
|
|
240
584
|
? this.resolveWorkflowTemplateByPrompt(conversationProjectId, prompt)
|
|
241
585
|
: undefined;
|
|
242
|
-
if (namedWorkflowTemplate &&
|
|
586
|
+
if (namedWorkflowTemplate && hasConfiguredOrchestratorRuntime) {
|
|
243
587
|
orchestrationState.intent = 'workflow';
|
|
244
588
|
this.recordOrchestrationAudit(orchestrationState, 'choose_path', 'path_selected', `User explicitly named workflow template: ${namedWorkflowTemplate.name}`, { workflowTemplateId: namedWorkflowTemplate.id });
|
|
245
589
|
const workflowIntent = {
|
|
@@ -256,49 +600,48 @@ class OrchestratorAgent {
|
|
|
256
600
|
const validation = this.validateIntentExecution(prompt, workflowIntent, initialPolicy);
|
|
257
601
|
if (validation.ok) {
|
|
258
602
|
this.applyExecutionSpecToState(orchestrationState, validation.executionSpec);
|
|
259
|
-
|
|
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);
|
|
260
611
|
}
|
|
261
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;
|
|
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.';
|
|
631
|
+
}
|
|
262
632
|
if (selectedOrchestrator) {
|
|
263
633
|
const frontDoorResult = await this.handleOrchestratorFrontDoor(prompt, conversationId, opts, selectedOrchestrator, conversationProjectId, initialProject, initialPolicy, promptAssignments, initialAssignments, initialOverview, orchestrationState);
|
|
264
634
|
if (frontDoorResult !== null)
|
|
265
635
|
return frontDoorResult;
|
|
266
636
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
orchestrationState
|
|
273
|
-
if (this.applyClassificationGuards(prompt, intent, routingMode)) {
|
|
274
|
-
(0, state_1.markMisrouteCorrected)(orchestrationState);
|
|
275
|
-
}
|
|
276
|
-
this.reportHiddenRoleProgress(opts, 'intent_classifier', `Classified request as ${this.describeIntentForActivity(intent)}`);
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
279
|
-
this.reportHiddenRoleProgress(opts, 'intent_classifier', 'Classifying request');
|
|
280
|
-
(0, state_1.addHelperRoleUsage)(orchestrationState, 'intent_classifier');
|
|
281
|
-
routedPlan = await this.decomposeAndRouteUserMessage(prompt, conversationId, opts.projectId, routingMode);
|
|
282
|
-
const routedIntent = routedPlan?.intents.length === 1
|
|
283
|
-
? this.buildIntentAnalysisFromRoutedIntent(routedPlan.intents[0])
|
|
284
|
-
: undefined;
|
|
285
|
-
if (routedIntent) {
|
|
286
|
-
intent = routedIntent;
|
|
287
|
-
if (this.applyClassificationGuards(prompt, intent, routingMode)) {
|
|
288
|
-
(0, state_1.markMisrouteCorrected)(orchestrationState);
|
|
289
|
-
}
|
|
290
|
-
this.reportHiddenRoleProgress(opts, 'intent_classifier', `Routed single intent as ${this.describeIntentForActivity(intent)}`);
|
|
291
|
-
}
|
|
292
|
-
else {
|
|
293
|
-
this.reportHiddenRoleProgress(opts, 'intent_classifier', 'Classifying request');
|
|
294
|
-
intent = await this.classifyUserMessage(prompt, opts.projectId, routingMode);
|
|
295
|
-
orchestrationState.fallbackUsed = true;
|
|
296
|
-
if (this.applyClassificationGuards(prompt, intent, routingMode)) {
|
|
297
|
-
(0, state_1.markMisrouteCorrected)(orchestrationState);
|
|
298
|
-
}
|
|
299
|
-
this.reportHiddenRoleProgress(opts, 'intent_classifier', `Classified request as ${this.describeIntentForActivity(intent)}`);
|
|
300
|
-
}
|
|
637
|
+
this.reportHiddenRoleProgress(opts, 'intent_classifier', selectedOrchestrator ? 'Recovering routing from code fallback' : 'Classifying request');
|
|
638
|
+
(0, state_1.addHelperRoleUsage)(orchestrationState, 'intent_classifier');
|
|
639
|
+
const intent = this.fallbackClassifyUserMessage(prompt, routingMode);
|
|
640
|
+
orchestrationState.fallbackUsed = true;
|
|
641
|
+
if (this.applyClassificationGuards(prompt, intent, routingMode)) {
|
|
642
|
+
(0, state_1.markMisrouteCorrected)(orchestrationState);
|
|
301
643
|
}
|
|
644
|
+
this.reportHiddenRoleProgress(opts, 'intent_classifier', `Classified request as ${this.describeIntentForActivity(intent)}`);
|
|
302
645
|
orchestrationState.intent = intent.intent;
|
|
303
646
|
orchestrationState.taskType = intent.primaryMode;
|
|
304
647
|
this.recordOrchestrationAudit(orchestrationState, 'understand_request', 'classified', intent.reasoning || `Classified as ${intent.primaryMode}.`, {
|
|
@@ -319,23 +662,19 @@ class OrchestratorAgent {
|
|
|
319
662
|
const effectiveProjectId = refreshedConversation?.project_id || resolvedProjectId;
|
|
320
663
|
const effectiveProject = effectiveProjectId ? data.getProject(effectiveProjectId) : undefined;
|
|
321
664
|
const effectivePolicy = data.getEffectiveOrchestrationPolicy(effectiveProjectId || conversationProjectId || undefined);
|
|
322
|
-
const effectiveOrchestrator =
|
|
323
|
-
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);
|
|
324
667
|
if (effectiveProjectId &&
|
|
325
668
|
this.hasRoleAssignments(promptAssignments) &&
|
|
326
669
|
!this.hasMode(intent, 'POLICY_UPDATE')) {
|
|
327
|
-
this.
|
|
328
|
-
|
|
670
|
+
this.persistPreferredWorkflow(effectiveProjectId, conversationId, this.orchestratorRuntime.kind === 'clerk'
|
|
671
|
+
? 'clerk-orchestrator'
|
|
672
|
+
: (effectiveOrchestrator?.id || opts.orchestratorBotIdHint || 'orchestrator'), effectiveAssignments);
|
|
329
673
|
}
|
|
330
674
|
const projectOverview = effectiveProjectId ? data.getProjectOverview(effectiveProjectId) : undefined;
|
|
331
675
|
if (this.isOrchestratorControlMessage(prompt)) {
|
|
332
676
|
return this.formatOrchestratorControlResponse(prompt, effectiveAssignments, projectOverview);
|
|
333
677
|
}
|
|
334
|
-
if (routedPlan && routedPlan.intents.length > 1) {
|
|
335
|
-
(0, state_1.setOrchestrationPath)(orchestrationState, 'workflow', 'workflow');
|
|
336
|
-
this.recordOrchestrationAudit(orchestrationState, 'choose_path', 'path_selected', 'Selected ordered multi-intent workflow path.', { intentCount: routedPlan.intents.length });
|
|
337
|
-
return this.handleOrderedIntentSequence(routedPlan, prompt, conversationId, opts, promptAssignments, effectiveProjectId, effectiveProject, effectivePolicy, effectiveAssignments, projectOverview);
|
|
338
|
-
}
|
|
339
678
|
if (this.hasMode(intent, 'POLICY_UPDATE')) {
|
|
340
679
|
(0, state_1.addHelperRoleUsage)(orchestrationState, 'policy_interpreter');
|
|
341
680
|
this.reportHiddenRoleProgress(opts, 'policy_interpreter', 'Reviewing requested orchestration policy changes');
|
|
@@ -474,22 +813,15 @@ class OrchestratorAgent {
|
|
|
474
813
|
buildFrontDoorEffectivePolicy(policy, orchestratorBot, roleAssignments, taskType) {
|
|
475
814
|
const normalize = (value) => String(value || '').trim().toLowerCase();
|
|
476
815
|
const orchestratorName = normalize(orchestratorBot.name);
|
|
477
|
-
const roleClass = normalize(orchestratorBot.role_class);
|
|
478
816
|
const codingOwner = normalize(roleAssignments.coding);
|
|
479
817
|
const qaOwner = normalize(roleAssignments.qa);
|
|
480
818
|
const next = { ...policy };
|
|
481
819
|
const orchestratorOwnsCoding = orchestratorName.length > 0
|
|
482
820
|
&& (orchestratorName === codingOwner
|
|
483
|
-
||
|
|
484
|
-
|| roleClass === 'coding'
|
|
485
|
-
|| roleClass === 'coder'
|
|
486
|
-
|| roleClass === 'builder'
|
|
487
|
-
|| roleClass === 'developer');
|
|
821
|
+
|| this.agentHasAnyRole(orchestratorBot, ['code', 'coding', 'coder', 'builder', 'developer']));
|
|
488
822
|
const orchestratorOwnsQa = orchestratorName.length > 0
|
|
489
823
|
&& (orchestratorName === qaOwner
|
|
490
|
-
||
|
|
491
|
-
|| roleClass === 'review'
|
|
492
|
-
|| roleClass === 'reviewer');
|
|
824
|
+
|| this.agentHasAnyRole(orchestratorBot, ['qa', 'review', 'reviewer']));
|
|
493
825
|
if (taskType === 'coding' && orchestratorOwnsCoding) {
|
|
494
826
|
next.allowOrchestratorCode = true;
|
|
495
827
|
}
|
|
@@ -498,437 +830,6 @@ class OrchestratorAgent {
|
|
|
498
830
|
}
|
|
499
831
|
return next;
|
|
500
832
|
}
|
|
501
|
-
async decomposeAndRouteUserMessage(prompt, conversationId, projectId, routingMode) {
|
|
502
|
-
try {
|
|
503
|
-
const agents = (0, orchestrator_profile_1.filterOutOrchestratorProfiles)(data.listAgentProfiles());
|
|
504
|
-
const firstPass = await this.clerk.decomposeUserIntents({
|
|
505
|
-
prompt,
|
|
506
|
-
agents,
|
|
507
|
-
});
|
|
508
|
-
if (!firstPass?.intents?.length)
|
|
509
|
-
return null;
|
|
510
|
-
let plan = firstPass;
|
|
511
|
-
if (firstPass.intents.some((intent) => intent.needsContext)) {
|
|
512
|
-
const recentTurnsText = this.buildIntentVerifierRecentTurns(conversationId);
|
|
513
|
-
const verified = await this.clerk.verifyDecomposedUserIntents({
|
|
514
|
-
prompt,
|
|
515
|
-
priorPlan: firstPass,
|
|
516
|
-
recentTurnsText,
|
|
517
|
-
});
|
|
518
|
-
if (verified?.intents?.length) {
|
|
519
|
-
plan = verified;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
const clarificationIntents = plan.intents.filter((intent) => intent.needsClarification);
|
|
523
|
-
if (clarificationIntents.length > 0) {
|
|
524
|
-
return {
|
|
525
|
-
intents: clarificationIntents.map((intent) => ({
|
|
526
|
-
id: intent.id,
|
|
527
|
-
request: intent.request,
|
|
528
|
-
category: 'CLARIFICATION_NEEDED',
|
|
529
|
-
targetAgent: undefined,
|
|
530
|
-
dependsOn: [...intent.dependsOn],
|
|
531
|
-
requirements: [...intent.requirements],
|
|
532
|
-
confidence: intent.confidence || 'MEDIUM',
|
|
533
|
-
needsClarification: true,
|
|
534
|
-
clarificationQuestions: [...(intent.clarificationQuestions || [])],
|
|
535
|
-
reasoning: plan.reasoning || 'Intent verification requires clarification before routing.',
|
|
536
|
-
})),
|
|
537
|
-
reasoning: plan.reasoning,
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
const routed = [];
|
|
541
|
-
for (const atom of plan.intents) {
|
|
542
|
-
const routedAtom = await this.clerk.routeIntentAtom({
|
|
543
|
-
atom,
|
|
544
|
-
agents,
|
|
545
|
-
routingMode,
|
|
546
|
-
});
|
|
547
|
-
if (!routedAtom)
|
|
548
|
-
return null;
|
|
549
|
-
routed.push(routedAtom);
|
|
550
|
-
}
|
|
551
|
-
const collapsed = this.collapseSingleWorkerPlan(prompt, routed);
|
|
552
|
-
return {
|
|
553
|
-
intents: collapsed,
|
|
554
|
-
reasoning: plan.reasoning,
|
|
555
|
-
};
|
|
556
|
-
}
|
|
557
|
-
catch {
|
|
558
|
-
return null;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
buildIntentVerifierRecentTurns(conversationId) {
|
|
562
|
-
try {
|
|
563
|
-
const context = (0, context_window_1.getPromptContextWindow)(conversationId, 3);
|
|
564
|
-
return (0, context_window_1.formatTurnsForPrompt)(context.turns);
|
|
565
|
-
}
|
|
566
|
-
catch {
|
|
567
|
-
return '(no recent messages)';
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
collapseSingleWorkerPlan(prompt, intents) {
|
|
571
|
-
if (intents.length <= 1)
|
|
572
|
-
return intents;
|
|
573
|
-
if (this.hasExplicitOrderingLanguage(prompt))
|
|
574
|
-
return intents;
|
|
575
|
-
if (!this.hasConcreteArtifactDetails(prompt))
|
|
576
|
-
return intents;
|
|
577
|
-
if (this.countMentionedAgents(prompt) > 1)
|
|
578
|
-
return intents;
|
|
579
|
-
if (!intents.every((intent) => intent.category === 'ASK_WORKER' || intent.category === 'FULL_WORKFLOW'))
|
|
580
|
-
return intents;
|
|
581
|
-
const resolvedTargets = Array.from(new Set(intents
|
|
582
|
-
.map((intent) => (intent.targetAgent || '').trim().toLowerCase())
|
|
583
|
-
.filter(Boolean)));
|
|
584
|
-
if (resolvedTargets.length > 1 && this.countMentionedAgents(prompt) > 0)
|
|
585
|
-
return intents;
|
|
586
|
-
const mergedRequirements = Array.from(new Set(intents
|
|
587
|
-
.flatMap((intent) => [intent.request, ...intent.requirements])
|
|
588
|
-
.map((item) => item.trim())
|
|
589
|
-
.filter(Boolean)));
|
|
590
|
-
const collapsedConfidence = intents.some((intent) => (intent.confidence || 'MEDIUM') === 'LOW')
|
|
591
|
-
? 'LOW'
|
|
592
|
-
: intents.some((intent) => (intent.confidence || 'MEDIUM') === 'MEDIUM')
|
|
593
|
-
? 'MEDIUM'
|
|
594
|
-
: 'HIGH';
|
|
595
|
-
return [{
|
|
596
|
-
...intents[0],
|
|
597
|
-
category: 'ASK_WORKER',
|
|
598
|
-
request: prompt.trim(),
|
|
599
|
-
targetAgent: this.countMentionedAgents(prompt) > 0
|
|
600
|
-
? intents.find((intent) => intent.targetAgent)?.targetAgent
|
|
601
|
-
: undefined,
|
|
602
|
-
dependsOn: [],
|
|
603
|
-
requirements: mergedRequirements,
|
|
604
|
-
confidence: collapsedConfidence,
|
|
605
|
-
needsClarification: intents.some((intent) => intent.needsClarification),
|
|
606
|
-
clarificationQuestions: Array.from(new Set(intents.flatMap((intent) => intent.clarificationQuestions || []))),
|
|
607
|
-
reasoning: 'Collapsed multiple single-worker fragments into one deliverable for one worker.',
|
|
608
|
-
}];
|
|
609
|
-
}
|
|
610
|
-
hasExplicitOrderingLanguage(prompt) {
|
|
611
|
-
return /\b(before|after|then|first|last|next|once that is done|once done|if that succeeds|after that|follow the workflow|workflow|todo|handoff)\b/i.test(prompt);
|
|
612
|
-
}
|
|
613
|
-
buildIntentAnalysisFromRoutedIntent(routed) {
|
|
614
|
-
let primaryMode = 'DIRECT_CONVERSATION';
|
|
615
|
-
let userFacingMode = 'DIRECT_RESPONSE';
|
|
616
|
-
let targetScope = 'SELF';
|
|
617
|
-
let isMultiStep = false;
|
|
618
|
-
switch (routed.category) {
|
|
619
|
-
case 'ASK_WORKER':
|
|
620
|
-
primaryMode = 'PROXY_MODE';
|
|
621
|
-
userFacingMode = 'ASK_WORKER';
|
|
622
|
-
targetScope = 'ONE_WORKER';
|
|
623
|
-
break;
|
|
624
|
-
case 'STATUS_CHECK':
|
|
625
|
-
primaryMode = 'STATUS_INQUIRY';
|
|
626
|
-
userFacingMode = 'STATUS_CHECK';
|
|
627
|
-
break;
|
|
628
|
-
case 'POLICY_CONFIGURATION':
|
|
629
|
-
primaryMode = 'POLICY_UPDATE';
|
|
630
|
-
userFacingMode = 'POLICY_CONFIGURATION';
|
|
631
|
-
break;
|
|
632
|
-
case 'FULL_WORKFLOW':
|
|
633
|
-
primaryMode = 'WORKFLOW_MODE';
|
|
634
|
-
userFacingMode = 'FULL_WORKFLOW';
|
|
635
|
-
targetScope = 'MULTI_WORKER';
|
|
636
|
-
isMultiStep = true;
|
|
637
|
-
break;
|
|
638
|
-
case 'MEMORY_CAPTURE':
|
|
639
|
-
primaryMode = 'MEMORY_CAPTURE';
|
|
640
|
-
userFacingMode = 'DIRECT_RESPONSE';
|
|
641
|
-
break;
|
|
642
|
-
default:
|
|
643
|
-
primaryMode = 'DIRECT_CONVERSATION';
|
|
644
|
-
userFacingMode = 'DIRECT_RESPONSE';
|
|
645
|
-
break;
|
|
646
|
-
}
|
|
647
|
-
const intent = this.inferIntentFromRequest(routed.request, routed.category);
|
|
648
|
-
const targetAgent = routed.targetAgent;
|
|
649
|
-
return {
|
|
650
|
-
primaryMode,
|
|
651
|
-
secondaryModes: [],
|
|
652
|
-
executionOrder: [primaryMode],
|
|
653
|
-
userFacingMode,
|
|
654
|
-
targetScope,
|
|
655
|
-
confidence: routed.confidence,
|
|
656
|
-
intent,
|
|
657
|
-
projectMatch: undefined,
|
|
658
|
-
topicMatch: undefined,
|
|
659
|
-
targetAgent,
|
|
660
|
-
isMultiStep,
|
|
661
|
-
reasoning: routed.reasoning,
|
|
662
|
-
needsClarification: routed.category === 'CLARIFICATION_NEEDED' || routed.needsClarification,
|
|
663
|
-
clarificationQuestions: routed.clarificationQuestions,
|
|
664
|
-
};
|
|
665
|
-
}
|
|
666
|
-
inferIntentFromRequest(request, category) {
|
|
667
|
-
const normalized = request.toLowerCase();
|
|
668
|
-
if (category === 'POLICY_CONFIGURATION')
|
|
669
|
-
return 'plan';
|
|
670
|
-
if (category === 'STATUS_CHECK')
|
|
671
|
-
return 'question';
|
|
672
|
-
if (/\b(brainstorm|research|pressure-test|feasibility|is this a good idea|is this sound|evaluate this idea)\b/i.test(normalized)) {
|
|
673
|
-
return 'brainstorm';
|
|
674
|
-
}
|
|
675
|
-
if (/\b(review|qa|test|audit|verify)\b/i.test(normalized))
|
|
676
|
-
return 'review';
|
|
677
|
-
if (/\b(plan|outline|roadmap|approach)\b/i.test(normalized))
|
|
678
|
-
return 'plan';
|
|
679
|
-
if (/\b(build|code|implement|create|fix|write|update)\b/i.test(normalized))
|
|
680
|
-
return 'build';
|
|
681
|
-
if (/\b(what|how|why|when|where|show|list|find|check)\b/i.test(normalized))
|
|
682
|
-
return 'question';
|
|
683
|
-
return category === 'ASK_WORKER' ? 'discuss' : 'simple';
|
|
684
|
-
}
|
|
685
|
-
async handleOrderedIntentSequence(routedPlan, originalPrompt, conversationId, opts, promptAssignments, effectiveProjectId, effectiveProject, effectivePolicy, effectiveAssignments, projectOverview) {
|
|
686
|
-
const normalizedIntents = this.normalizeOrderedIntentSequence(originalPrompt, routedPlan.intents, effectiveAssignments);
|
|
687
|
-
this.reportHiddenRoleProgress(opts, 'intent_classifier', `Decomposed request into ${normalizedIntents.length} ordered intents`);
|
|
688
|
-
if (normalizedIntents.length > 1) {
|
|
689
|
-
opts.onProgress(this.formatOrderedIntentPlanPreview(normalizedIntents));
|
|
690
|
-
}
|
|
691
|
-
const persistedPlan = data.createIntentExecutionPlan({
|
|
692
|
-
conversationId,
|
|
693
|
-
sourcePrompt: originalPrompt,
|
|
694
|
-
reasoning: routedPlan.reasoning,
|
|
695
|
-
intents: normalizedIntents.map((intent, index) => ({
|
|
696
|
-
atomId: intent.id,
|
|
697
|
-
orderIndex: index + 1,
|
|
698
|
-
request: intent.request,
|
|
699
|
-
category: intent.category,
|
|
700
|
-
targetAgent: intent.targetAgent,
|
|
701
|
-
requirements: intent.requirements,
|
|
702
|
-
dependsOn: intent.dependsOn,
|
|
703
|
-
confidence: intent.confidence,
|
|
704
|
-
needsClarification: intent.needsClarification,
|
|
705
|
-
clarificationQuestions: intent.clarificationQuestions,
|
|
706
|
-
reasoning: intent.reasoning,
|
|
707
|
-
})),
|
|
708
|
-
});
|
|
709
|
-
data.updateIntentExecutionPlan(persistedPlan.id, { status: 'running' });
|
|
710
|
-
const lines = [];
|
|
711
|
-
let completedIntents = 0;
|
|
712
|
-
for (let index = 0; index < normalizedIntents.length; index += 1) {
|
|
713
|
-
const routed = normalizedIntents[index];
|
|
714
|
-
const persistedAtom = persistedPlan.atoms[index];
|
|
715
|
-
if (routed.category === 'CLARIFICATION_NEEDED' || routed.needsClarification) {
|
|
716
|
-
if (persistedAtom) {
|
|
717
|
-
data.updateIntentExecutionAtom(persistedAtom.id, {
|
|
718
|
-
status: 'blocked',
|
|
719
|
-
error: 'Clarification required before execution.',
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
data.updateIntentExecutionPlan(persistedPlan.id, {
|
|
723
|
-
status: 'blocked',
|
|
724
|
-
completedIntents,
|
|
725
|
-
});
|
|
726
|
-
const clarificationIntent = this.buildIntentAnalysisFromRoutedIntent(routed);
|
|
727
|
-
return this.formatClarificationResponse(clarificationIntent, projectOverview);
|
|
728
|
-
}
|
|
729
|
-
const intent = this.buildIntentAnalysisFromRoutedIntent(routed);
|
|
730
|
-
if (persistedAtom) {
|
|
731
|
-
data.updateIntentExecutionAtom(persistedAtom.id, {
|
|
732
|
-
status: 'running',
|
|
733
|
-
startedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
|
|
734
|
-
error: null,
|
|
735
|
-
});
|
|
736
|
-
}
|
|
737
|
-
const executionValidation = this.validateIntentExecution(routed.request, intent, effectivePolicy);
|
|
738
|
-
if (!executionValidation.ok) {
|
|
739
|
-
if (persistedAtom) {
|
|
740
|
-
data.updateIntentExecutionAtom(persistedAtom.id, {
|
|
741
|
-
status: 'failed',
|
|
742
|
-
error: executionValidation.reason,
|
|
743
|
-
});
|
|
744
|
-
}
|
|
745
|
-
data.updateIntentExecutionPlan(persistedPlan.id, {
|
|
746
|
-
status: 'failed',
|
|
747
|
-
completedIntents,
|
|
748
|
-
});
|
|
749
|
-
if (executionValidation.clarificationQuestions?.length) {
|
|
750
|
-
return this.formatClarificationResponse({ ...intent, clarificationQuestions: executionValidation.clarificationQuestions }, projectOverview);
|
|
751
|
-
}
|
|
752
|
-
return `I couldn't execute step ${index + 1} safely: ${executionValidation.reason}`;
|
|
753
|
-
}
|
|
754
|
-
const executionSpec = executionValidation.executionSpec;
|
|
755
|
-
const stepHeader = `**Step ${index + 1}:** ${routed.request}`;
|
|
756
|
-
if (this.hasMode(intent, 'POLICY_UPDATE')) {
|
|
757
|
-
const policyResponse = await this.handlePolicyUpdate(routed.request, conversationId, intent, this.parseRoleAssignments(routed.request), effectiveProjectId, projectOverview, effectivePolicy);
|
|
758
|
-
if (persistedAtom) {
|
|
759
|
-
data.updateIntentExecutionAtom(persistedAtom.id, {
|
|
760
|
-
status: 'completed',
|
|
761
|
-
resultSummary: this.summarizeResultForIntentPlan(policyResponse),
|
|
762
|
-
completedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
completedIntents += 1;
|
|
766
|
-
if (index < routedPlan.intents.length - 1) {
|
|
767
|
-
data.updateIntentExecutionPlan(persistedPlan.id, {
|
|
768
|
-
status: 'blocked',
|
|
769
|
-
completedIntents,
|
|
770
|
-
});
|
|
771
|
-
return `${policyResponse}\n\nI paused the remaining ordered steps until the policy change is confirmed.`;
|
|
772
|
-
}
|
|
773
|
-
data.updateIntentExecutionPlan(persistedPlan.id, {
|
|
774
|
-
status: 'completed',
|
|
775
|
-
completedIntents,
|
|
776
|
-
finishedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
|
|
777
|
-
});
|
|
778
|
-
return policyResponse;
|
|
779
|
-
}
|
|
780
|
-
if (this.hasMode(intent, 'STATUS_INQUIRY')) {
|
|
781
|
-
const statusResponse = this.formatStatusInquiryResponse(conversationId, effectiveProjectId);
|
|
782
|
-
if (persistedAtom) {
|
|
783
|
-
data.updateIntentExecutionAtom(persistedAtom.id, {
|
|
784
|
-
status: 'completed',
|
|
785
|
-
resultSummary: this.summarizeResultForIntentPlan(statusResponse),
|
|
786
|
-
completedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
|
|
787
|
-
});
|
|
788
|
-
}
|
|
789
|
-
completedIntents += 1;
|
|
790
|
-
lines.push(stepHeader);
|
|
791
|
-
lines.push(statusResponse);
|
|
792
|
-
continue;
|
|
793
|
-
}
|
|
794
|
-
if (this.hasMode(intent, 'MEMORY_CAPTURE')) {
|
|
795
|
-
const memoryResponse = this.formatMemoryCaptureResponse(routed.request);
|
|
796
|
-
if (persistedAtom) {
|
|
797
|
-
data.updateIntentExecutionAtom(persistedAtom.id, {
|
|
798
|
-
status: 'completed',
|
|
799
|
-
resultSummary: this.summarizeResultForIntentPlan(memoryResponse),
|
|
800
|
-
completedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
|
-
completedIntents += 1;
|
|
804
|
-
lines.push(stepHeader);
|
|
805
|
-
lines.push(memoryResponse);
|
|
806
|
-
continue;
|
|
807
|
-
}
|
|
808
|
-
if (executionSpec.executionMode === 'direct') {
|
|
809
|
-
const directResponse = this.handleDirectConversation(routed.request, projectOverview, effectiveAssignments);
|
|
810
|
-
if (persistedAtom) {
|
|
811
|
-
data.updateIntentExecutionAtom(persistedAtom.id, {
|
|
812
|
-
status: 'completed',
|
|
813
|
-
resultSummary: this.summarizeResultForIntentPlan(directResponse),
|
|
814
|
-
completedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
|
|
815
|
-
});
|
|
816
|
-
}
|
|
817
|
-
completedIntents += 1;
|
|
818
|
-
lines.push(stepHeader);
|
|
819
|
-
lines.push(directResponse);
|
|
820
|
-
continue;
|
|
821
|
-
}
|
|
822
|
-
if (executionSpec.executionMode === 'proxy') {
|
|
823
|
-
const proxyPrompt = this.buildSimpleWorkerPrompt(routed.request, executionSpec.targetAgents[0] || intent.targetAgent || 'the worker');
|
|
824
|
-
const proxyResult = await this.executeProxyRequest(proxyPrompt, conversationId, intent, opts, executionSpec, effectiveAssignments);
|
|
825
|
-
const proxyResponse = proxyResult.response;
|
|
826
|
-
if (!proxyResult.ok) {
|
|
827
|
-
if (persistedAtom) {
|
|
828
|
-
data.updateIntentExecutionAtom(persistedAtom.id, {
|
|
829
|
-
status: 'failed',
|
|
830
|
-
error: proxyResult.error || proxyResponse,
|
|
831
|
-
completedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
|
|
832
|
-
});
|
|
833
|
-
}
|
|
834
|
-
data.updateIntentExecutionPlan(persistedPlan.id, {
|
|
835
|
-
status: 'failed',
|
|
836
|
-
completedIntents,
|
|
837
|
-
finishedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
|
|
838
|
-
});
|
|
839
|
-
lines.push(stepHeader);
|
|
840
|
-
lines.push(proxyResponse);
|
|
841
|
-
return lines.join('\n\n');
|
|
842
|
-
}
|
|
843
|
-
if (persistedAtom) {
|
|
844
|
-
data.updateIntentExecutionAtom(persistedAtom.id, {
|
|
845
|
-
status: 'completed',
|
|
846
|
-
resultSummary: this.summarizeResultForIntentPlan(proxyResponse),
|
|
847
|
-
completedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
|
|
848
|
-
});
|
|
849
|
-
}
|
|
850
|
-
completedIntents += 1;
|
|
851
|
-
lines.push(stepHeader);
|
|
852
|
-
lines.push(proxyResponse);
|
|
853
|
-
continue;
|
|
854
|
-
}
|
|
855
|
-
if (executionSpec.executionMode === 'workflow') {
|
|
856
|
-
const workflowResponse = await this.handleOrchestratedRequest(routed.request, conversationId, intent, executionSpec, opts, effectiveAssignments, effectiveProject);
|
|
857
|
-
if (persistedAtom) {
|
|
858
|
-
data.updateIntentExecutionAtom(persistedAtom.id, {
|
|
859
|
-
status: 'completed',
|
|
860
|
-
resultSummary: this.summarizeResultForIntentPlan(workflowResponse),
|
|
861
|
-
completedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
|
|
862
|
-
});
|
|
863
|
-
}
|
|
864
|
-
completedIntents += 1;
|
|
865
|
-
lines.push(stepHeader);
|
|
866
|
-
lines.push(workflowResponse);
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
data.updateIntentExecutionPlan(persistedPlan.id, {
|
|
870
|
-
status: 'completed',
|
|
871
|
-
completedIntents,
|
|
872
|
-
finishedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
|
|
873
|
-
});
|
|
874
|
-
return lines.join('\n\n');
|
|
875
|
-
}
|
|
876
|
-
normalizeOrderedIntentSequence(prompt, intents, assignments) {
|
|
877
|
-
const collapsed = this.collapseSingleWorkerPlan(prompt, intents);
|
|
878
|
-
const normalized = collapsed.map((intent) => this.applyImplicitWorkerTarget(intent, assignments));
|
|
879
|
-
return this.dropRedundantSequentialWorkerHandoffs(normalized);
|
|
880
|
-
}
|
|
881
|
-
applyImplicitWorkerTarget(intent, assignments) {
|
|
882
|
-
if (intent.category !== 'ASK_WORKER' || intent.targetAgent || intent.needsClarification) {
|
|
883
|
-
return intent;
|
|
884
|
-
}
|
|
885
|
-
const targetAgent = this.resolveImplicitWorkerTarget(intent.request, intent.requirements, assignments);
|
|
886
|
-
return targetAgent ? { ...intent, targetAgent } : intent;
|
|
887
|
-
}
|
|
888
|
-
resolveImplicitWorkerTarget(request, requirements, assignments) {
|
|
889
|
-
const text = `${request}\n${requirements.join('\n')}`.toLowerCase();
|
|
890
|
-
if (/\b(research|brainstorm|feasibility|evaluate|pressure-test|is this sound|idea)\b/i.test(text)) {
|
|
891
|
-
return assignments.research || assignments.coding || assignments.qa;
|
|
892
|
-
}
|
|
893
|
-
if (/\b(qa|review|verify|test|audit|check)\b/i.test(text)) {
|
|
894
|
-
return assignments.qa || assignments.coding || assignments.research;
|
|
895
|
-
}
|
|
896
|
-
if (/\b(build|code|implement|create|fix|write|update|design)\b/i.test(text)) {
|
|
897
|
-
return assignments.coding || assignments.qa || assignments.research;
|
|
898
|
-
}
|
|
899
|
-
return assignments.coding || assignments.research || assignments.qa;
|
|
900
|
-
}
|
|
901
|
-
dropRedundantSequentialWorkerHandoffs(intents) {
|
|
902
|
-
if (intents.length <= 1)
|
|
903
|
-
return intents;
|
|
904
|
-
const filtered = [];
|
|
905
|
-
for (let index = 0; index < intents.length; index += 1) {
|
|
906
|
-
const current = intents[index];
|
|
907
|
-
const next = intents[index + 1];
|
|
908
|
-
const currentText = current.request.toLowerCase();
|
|
909
|
-
const nextText = next?.request.toLowerCase() || '';
|
|
910
|
-
const redundantFeedbackStep = Boolean(next &&
|
|
911
|
-
current.category === 'ASK_WORKER' &&
|
|
912
|
-
next.category === 'ASK_WORKER' &&
|
|
913
|
-
current.targetAgent &&
|
|
914
|
-
next.targetAgent &&
|
|
915
|
-
current.targetAgent.toLowerCase() === next.targetAgent.toLowerCase() &&
|
|
916
|
-
/\b(give|send|provide|pass along)\b.*\b(feedback|qa)\b/.test(currentText) &&
|
|
917
|
-
/\b(fix|implement|apply|address)\b/.test(nextText));
|
|
918
|
-
if (!redundantFeedbackStep) {
|
|
919
|
-
filtered.push(current);
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
return filtered;
|
|
923
|
-
}
|
|
924
|
-
formatOrderedIntentPlanPreview(intents) {
|
|
925
|
-
const lines = [`**Here's my plan (${intents.length} steps):**`];
|
|
926
|
-
intents.forEach((intent, index) => {
|
|
927
|
-
const agentLabel = intent.targetAgent ? ` (${intent.targetAgent})` : '';
|
|
928
|
-
lines.push(`${index + 1}. ${intent.request}${agentLabel}`);
|
|
929
|
-
});
|
|
930
|
-
return lines.join('\n');
|
|
931
|
-
}
|
|
932
833
|
shouldForceProxy(prompt, intent) {
|
|
933
834
|
if (intent.primaryMode === 'PROXY_MODE')
|
|
934
835
|
return true;
|
|
@@ -942,67 +843,16 @@ class OrchestratorAgent {
|
|
|
942
843
|
const mentionedAgents = this.countMentionedAgents(prompt);
|
|
943
844
|
return mentionedAgents === 1 && /\b(ask|let|have|talk to|discuss with|check with)\b/i.test(prompt);
|
|
944
845
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
const explicitWorkflowRoles = this.inferRequestedWorkflowRoles(prompt, {});
|
|
956
|
-
const explicitWorkflowRequest = explicitWorkflowRoles.length >= 2 && this.hasConcreteArtifactDetails(prompt);
|
|
957
|
-
if (targetAgent && !intent.targetAgent) {
|
|
958
|
-
intent.targetAgent = targetAgent;
|
|
959
|
-
}
|
|
960
|
-
if (explicitWorkflowRequest) {
|
|
961
|
-
intent.primaryMode = 'WORKFLOW_MODE';
|
|
962
|
-
intent.userFacingMode = 'FULL_WORKFLOW';
|
|
963
|
-
intent.targetScope = 'MULTI_WORKER';
|
|
964
|
-
intent.isMultiStep = true;
|
|
965
|
-
intent.intent = 'build';
|
|
966
|
-
intent.targetAgent = undefined;
|
|
967
|
-
intent.needsClarification = false;
|
|
968
|
-
intent.clarificationQuestions = undefined;
|
|
969
|
-
intent.secondaryModes = intent.secondaryModes.filter((mode) => mode !== 'PROXY_MODE');
|
|
970
|
-
intent.executionOrder = ['WORKFLOW_MODE'];
|
|
971
|
-
}
|
|
972
|
-
if (intent.primaryMode === 'PROXY_MODE' && !intent.targetAgent) {
|
|
973
|
-
intent.needsClarification = true;
|
|
974
|
-
intent.clarificationQuestions = Array.from(new Set([
|
|
975
|
-
...(intent.clarificationQuestions || []),
|
|
976
|
-
'Which worker should handle this request?',
|
|
977
|
-
]));
|
|
978
|
-
}
|
|
979
|
-
if (needsConcreteArtifact &&
|
|
980
|
-
(intent.primaryMode === 'WORKFLOW_MODE' || intent.primaryMode === 'PROXY_MODE')) {
|
|
981
|
-
intent.needsClarification = true;
|
|
982
|
-
intent.clarificationQuestions = Array.from(new Set([
|
|
983
|
-
...(intent.clarificationQuestions || []),
|
|
984
|
-
'What specific idea, artifact, or content should the team work on?',
|
|
985
|
-
]));
|
|
986
|
-
}
|
|
987
|
-
if (intent.primaryMode === 'WORKFLOW_MODE' && !intent.isMultiStep && intent.targetScope !== 'MULTI_WORKER') {
|
|
988
|
-
intent.needsClarification = true;
|
|
989
|
-
intent.clarificationQuestions = Array.from(new Set([
|
|
990
|
-
...(intent.clarificationQuestions || []),
|
|
991
|
-
'What multi-step workflow do you want me to coordinate?',
|
|
992
|
-
]));
|
|
993
|
-
}
|
|
994
|
-
if (intent.needsClarification && this.hasConcreteArtifactDetails(prompt)) {
|
|
995
|
-
const remaining = (intent.clarificationQuestions || []).filter((question) => !/specific idea, artifact, or content/i.test(question));
|
|
996
|
-
intent.clarificationQuestions = remaining.length > 0 ? remaining : undefined;
|
|
997
|
-
intent.needsClarification = Boolean(intent.clarificationQuestions?.length);
|
|
998
|
-
}
|
|
999
|
-
const after = JSON.stringify({
|
|
1000
|
-
primaryMode: intent.primaryMode,
|
|
1001
|
-
targetAgent: intent.targetAgent || null,
|
|
1002
|
-
needsClarification: !!intent.needsClarification,
|
|
1003
|
-
clarificationQuestions: intent.clarificationQuestions || [],
|
|
1004
|
-
});
|
|
1005
|
-
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;
|
|
1006
856
|
}
|
|
1007
857
|
formatOrchestratorControlResponse(prompt, assignments, overview) {
|
|
1008
858
|
const lines = [];
|
|
@@ -1056,49 +906,17 @@ class OrchestratorAgent {
|
|
|
1056
906
|
return [agent.name.trim()].filter(Boolean);
|
|
1057
907
|
}
|
|
1058
908
|
/**
|
|
1059
|
-
*
|
|
1060
|
-
*
|
|
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.
|
|
1061
918
|
*/
|
|
1062
|
-
|
|
1063
|
-
try {
|
|
1064
|
-
const agents = (0, orchestrator_profile_1.filterOutOrchestratorProfiles)(data.listAgentProfiles());
|
|
1065
|
-
const projects = data.listProjects();
|
|
1066
|
-
const topics = data.listTopics({ limit: 20 });
|
|
1067
|
-
const parsed = await this.clerk.classifyOrchestratorIntent({
|
|
1068
|
-
prompt,
|
|
1069
|
-
agents,
|
|
1070
|
-
projects,
|
|
1071
|
-
topics,
|
|
1072
|
-
projectId,
|
|
1073
|
-
routingMode,
|
|
1074
|
-
});
|
|
1075
|
-
if (parsed) {
|
|
1076
|
-
const primaryMode = this.normalizeMode(parsed.primaryMode);
|
|
1077
|
-
const secondaryModes = this.normalizeModeList(parsed.secondaryModes);
|
|
1078
|
-
const executionOrder = this.normalizeModeList(parsed.executionOrder);
|
|
1079
|
-
return {
|
|
1080
|
-
primaryMode,
|
|
1081
|
-
secondaryModes,
|
|
1082
|
-
executionOrder: executionOrder.length > 0
|
|
1083
|
-
? executionOrder
|
|
1084
|
-
: [primaryMode, ...secondaryModes.filter((mode) => mode !== primaryMode)],
|
|
1085
|
-
userFacingMode: parsed.userFacingMode,
|
|
1086
|
-
targetScope: parsed.targetScope,
|
|
1087
|
-
confidence: parsed.confidence,
|
|
1088
|
-
intent: parsed.intent || 'simple',
|
|
1089
|
-
projectMatch: undefined,
|
|
1090
|
-
topicMatch: undefined,
|
|
1091
|
-
targetAgent: parsed.targetAgent || undefined,
|
|
1092
|
-
isMultiStep: Boolean(parsed.isMultiStep),
|
|
1093
|
-
reasoning: parsed.reasoning || '',
|
|
1094
|
-
needsClarification: Boolean(parsed.needsClarification),
|
|
1095
|
-
clarificationQuestions: Array.isArray(parsed.clarificationQuestions) ? parsed.clarificationQuestions.map(String) : undefined,
|
|
1096
|
-
};
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
catch { /* fall through */ }
|
|
1100
|
-
return this.fallbackClassifyUserMessage(prompt, routingMode);
|
|
1101
|
-
// Default: treat as simple single-step
|
|
919
|
+
fallbackClassifyUserMessage(_prompt, _routingMode) {
|
|
1102
920
|
return {
|
|
1103
921
|
primaryMode: 'DIRECT_CONVERSATION',
|
|
1104
922
|
secondaryModes: [],
|
|
@@ -1107,106 +925,13 @@ class OrchestratorAgent {
|
|
|
1107
925
|
targetScope: 'SELF',
|
|
1108
926
|
confidence: 'LOW',
|
|
1109
927
|
intent: 'simple',
|
|
1110
|
-
isMultiStep: false,
|
|
1111
|
-
reasoning: 'Intent analysis failed - defaulting to direct conversation',
|
|
1112
|
-
};
|
|
1113
|
-
}
|
|
1114
|
-
normalizeMode(raw) {
|
|
1115
|
-
const value = String(raw || '').trim().toUpperCase();
|
|
1116
|
-
switch (value) {
|
|
1117
|
-
case 'POLICY_UPDATE':
|
|
1118
|
-
case 'MEMORY_CAPTURE':
|
|
1119
|
-
case 'STATUS_INQUIRY':
|
|
1120
|
-
case 'PROXY_MODE':
|
|
1121
|
-
case 'WORKFLOW_MODE':
|
|
1122
|
-
return value;
|
|
1123
|
-
default:
|
|
1124
|
-
return 'DIRECT_CONVERSATION';
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
normalizeModeList(raw) {
|
|
1128
|
-
if (!Array.isArray(raw))
|
|
1129
|
-
return [];
|
|
1130
|
-
return raw
|
|
1131
|
-
.map((item) => this.normalizeMode(item))
|
|
1132
|
-
.filter((mode, index, list) => list.indexOf(mode) === index);
|
|
1133
|
-
}
|
|
1134
|
-
fallbackClassifyUserMessage(prompt, routingMode) {
|
|
1135
|
-
const normalized = prompt.toLowerCase();
|
|
1136
|
-
const targetAgent = this.extractTargetAgentName(prompt);
|
|
1137
|
-
const hasPolicyLanguage = this.isExplicitPolicyMessage(prompt);
|
|
1138
|
-
const hasMemoryLanguage = /\b(i am going to give you|things to remember|remember this|save this as a rule|project rules)\b/i.test(prompt);
|
|
1139
|
-
const isStatus = /\b(what is happening|status|what is going on|what's going on|where are we|progress update|keep me updated)\b/i.test(prompt);
|
|
1140
|
-
const isGreeting = /\b(hello|hi|hey|are you there|good morning|good afternoon|good evening)\b/i.test(prompt);
|
|
1141
|
-
const needsWorkflow = /\b(todo|workflow|handoff|qa\b|review after|send .* then|first .* then|team workflow|multiple llms)\b/i.test(normalized)
|
|
1142
|
-
|| (/\bbrain\b/i.test(prompt) && /\bben\b/i.test(prompt))
|
|
1143
|
-
|| (/\bben\b/i.test(prompt) && /\bjohn\b/i.test(prompt));
|
|
1144
|
-
const isProxy = Boolean(targetAgent) || (routingMode === 'proxy' && !hasPolicyLanguage && !isStatus);
|
|
1145
|
-
let primaryMode = 'DIRECT_CONVERSATION';
|
|
1146
|
-
const secondaryModes = [];
|
|
1147
|
-
let intent = 'simple';
|
|
1148
|
-
let isMultiStep = false;
|
|
1149
|
-
if (hasPolicyLanguage) {
|
|
1150
|
-
primaryMode = 'POLICY_UPDATE';
|
|
1151
|
-
intent = 'plan';
|
|
1152
|
-
secondaryModes.push('DIRECT_CONVERSATION');
|
|
1153
|
-
}
|
|
1154
|
-
else if (hasMemoryLanguage) {
|
|
1155
|
-
primaryMode = 'MEMORY_CAPTURE';
|
|
1156
|
-
}
|
|
1157
|
-
else if (isStatus) {
|
|
1158
|
-
primaryMode = 'STATUS_INQUIRY';
|
|
1159
|
-
intent = 'question';
|
|
1160
|
-
}
|
|
1161
|
-
else if (needsWorkflow) {
|
|
1162
|
-
primaryMode = 'WORKFLOW_MODE';
|
|
1163
|
-
intent = 'build';
|
|
1164
|
-
isMultiStep = true;
|
|
1165
|
-
}
|
|
1166
|
-
else if (isProxy) {
|
|
1167
|
-
primaryMode = 'PROXY_MODE';
|
|
1168
|
-
intent = 'discuss';
|
|
1169
|
-
}
|
|
1170
|
-
else if (isGreeting || this.isProjectKnowledgeRequest(prompt) || this.isOrchestratorControlMessage(prompt)) {
|
|
1171
|
-
primaryMode = 'DIRECT_CONVERSATION';
|
|
1172
|
-
intent = 'question';
|
|
1173
|
-
}
|
|
1174
|
-
const executionOrder = [primaryMode];
|
|
1175
|
-
if (hasPolicyLanguage && needsWorkflow && !secondaryModes.includes('WORKFLOW_MODE')) {
|
|
1176
|
-
secondaryModes.push('WORKFLOW_MODE');
|
|
1177
|
-
executionOrder.push('WORKFLOW_MODE');
|
|
1178
|
-
}
|
|
1179
|
-
return {
|
|
1180
|
-
primaryMode,
|
|
1181
|
-
secondaryModes,
|
|
1182
|
-
executionOrder,
|
|
1183
|
-
userFacingMode: primaryMode === 'POLICY_UPDATE'
|
|
1184
|
-
? 'POLICY_CONFIGURATION'
|
|
1185
|
-
: primaryMode === 'STATUS_INQUIRY'
|
|
1186
|
-
? 'STATUS_CHECK'
|
|
1187
|
-
: primaryMode === 'PROXY_MODE'
|
|
1188
|
-
? 'ASK_WORKER'
|
|
1189
|
-
: primaryMode === 'WORKFLOW_MODE'
|
|
1190
|
-
? 'FULL_WORKFLOW'
|
|
1191
|
-
: intent === 'plan'
|
|
1192
|
-
? 'PLANNING'
|
|
1193
|
-
: 'DIRECT_RESPONSE',
|
|
1194
|
-
targetScope: primaryMode === 'PROXY_MODE'
|
|
1195
|
-
? 'ONE_WORKER'
|
|
1196
|
-
: primaryMode === 'WORKFLOW_MODE'
|
|
1197
|
-
? 'MULTI_WORKER'
|
|
1198
|
-
: 'SELF',
|
|
1199
|
-
confidence: hasPolicyLanguage || isStatus || needsWorkflow || isProxy ? 'MEDIUM' : 'LOW',
|
|
1200
|
-
intent,
|
|
1201
928
|
projectMatch: undefined,
|
|
1202
929
|
topicMatch: undefined,
|
|
1203
|
-
targetAgent,
|
|
1204
|
-
isMultiStep,
|
|
1205
|
-
reasoning: 'Fallback
|
|
1206
|
-
needsClarification:
|
|
1207
|
-
clarificationQuestions:
|
|
1208
|
-
? [`What specific idea, artifact, or content should ${targetAgent || 'the specialist'} review?`]
|
|
1209
|
-
: 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,
|
|
1210
935
|
};
|
|
1211
936
|
}
|
|
1212
937
|
isExplicitPolicyMessage(prompt) {
|
|
@@ -1312,6 +1037,7 @@ class OrchestratorAgent {
|
|
|
1312
1037
|
this.reportHiddenRoleProgress(opts, 'dispatch_controller', `Routing readiness ping to ${agent.name}`);
|
|
1313
1038
|
const result = await this.workflowEngine.execute(readinessPrompt, conversationId, agent.id, {
|
|
1314
1039
|
apiKey: this.resolveApiKey(agent.provider),
|
|
1040
|
+
abortSignal: opts.abortSignal,
|
|
1315
1041
|
disableDecomposition: true,
|
|
1316
1042
|
});
|
|
1317
1043
|
if (result.status === 'completed' || result.status === 'partial') {
|
|
@@ -1525,14 +1251,686 @@ class OrchestratorAgent {
|
|
|
1525
1251
|
.replace(/\n?STATUS:\s*(PASS|FAIL|COMPLETE|BLOCKED|UNKNOWN)\s*$/i, '')
|
|
1526
1252
|
.trim() || rawText.trim();
|
|
1527
1253
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
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);
|
|
1532
1272
|
if (bot)
|
|
1533
1273
|
return bot;
|
|
1534
1274
|
}
|
|
1535
|
-
|
|
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.');
|
|
1536
1934
|
}
|
|
1537
1935
|
async handleOrchestratorFrontDoor(prompt, conversationId, opts, orchestratorBot, projectId, project, policy, promptAssignments, roleAssignments, overview, state) {
|
|
1538
1936
|
(0, state_1.addHelperRoleUsage)(state, 'orchestrator_front_door');
|
|
@@ -1543,19 +1941,30 @@ class OrchestratorAgent {
|
|
|
1543
1941
|
const recentSummaryText = contextWindow.summary?.summary_text?.trim()
|
|
1544
1942
|
? `${contextWindow.carriedForward ? '(carried forward from the previous conversation in this topic)\n' : ''}${contextWindow.summary.summary_text.trim()}`
|
|
1545
1943
|
: '(none)';
|
|
1546
|
-
// Build specialists list
|
|
1944
|
+
// Build specialists list — include all bots except pure orchestrators.
|
|
1945
|
+
// A bot that is_orchestrator but also has a worker role_class (code, qa, research, etc.)
|
|
1946
|
+
// should still appear as an available worker.
|
|
1547
1947
|
const allProfiles = data.listAgentProfiles();
|
|
1548
|
-
const
|
|
1948
|
+
const PURE_ORCHESTRATOR_ROLES = new Set(['orchestrator', 'project manager']);
|
|
1949
|
+
const specialists = allProfiles
|
|
1950
|
+
.filter((agent) => {
|
|
1951
|
+
const rc = this.getOrchestrationRoleClass(agent);
|
|
1952
|
+
if (agent.is_orchestrator === 1 && PURE_ORCHESTRATOR_ROLES.has(rc))
|
|
1953
|
+
return false;
|
|
1954
|
+
if (!agent.is_orchestrator && (0, orchestrator_profile_1.isOrchestratorProfile)(agent))
|
|
1955
|
+
return false;
|
|
1956
|
+
return true;
|
|
1957
|
+
})
|
|
1549
1958
|
.map((agent) => ({
|
|
1550
1959
|
name: agent.name,
|
|
1551
|
-
roleLabel:
|
|
1960
|
+
roleLabel: this.getOrchestrationRoleLabel(agent),
|
|
1552
1961
|
references: this.getAgentRoutingReferences(agent),
|
|
1553
1962
|
responsibilities: this.describeAgentResponsibilities(agent),
|
|
1554
1963
|
}));
|
|
1555
1964
|
const signalSpecialists = allProfiles
|
|
1556
1965
|
.map((agent) => ({
|
|
1557
1966
|
name: agent.name,
|
|
1558
|
-
roleLabel:
|
|
1967
|
+
roleLabel: this.getOrchestrationRoleLabel(agent),
|
|
1559
1968
|
references: this.getAgentRoutingReferences(agent),
|
|
1560
1969
|
responsibilities: this.describeAgentResponsibilities(agent),
|
|
1561
1970
|
}));
|
|
@@ -1586,12 +1995,56 @@ class OrchestratorAgent {
|
|
|
1586
1995
|
fastPath: false,
|
|
1587
1996
|
});
|
|
1588
1997
|
}
|
|
1589
|
-
const useSinglePassOrchestrator = this.isLocalDesktopRuntime();
|
|
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;
|
|
2010
|
+
const localPromptContract = useSinglePassOrchestrator
|
|
2011
|
+
? (index_1.CLI_PROVIDERS.has(orchestratorBot.provider)
|
|
2012
|
+
&& conversationId
|
|
2013
|
+
&& !!orchestratorCliSessionPlan?.resumeSessionId
|
|
2014
|
+
? 'cli_recurring'
|
|
2015
|
+
: 'api_or_fresh_cli')
|
|
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;
|
|
1590
2042
|
const operatingPrompt = (0, orchestrator_operating_prompt_1.buildOrchestratorOperatingPrompt)({
|
|
1591
|
-
orchestratorName:
|
|
1592
|
-
primaryRole:
|
|
2043
|
+
orchestratorName: orchestratorDisplayName,
|
|
2044
|
+
primaryRole: isClerkRuntime ? 'project manager' : data.getAgentOrchestrationRoleLabel(orchestratorBot),
|
|
1593
2045
|
specialists,
|
|
1594
2046
|
workflowNames,
|
|
2047
|
+
localPromptContract,
|
|
1595
2048
|
entryMode: frontDoorSignals.explicitOrchestrationRequested ? 'orchestration_bias' : 'normal',
|
|
1596
2049
|
signalSummary: {
|
|
1597
2050
|
matchedBots: frontDoorSignals.matchedBots,
|
|
@@ -1610,7 +2063,9 @@ class OrchestratorAgent {
|
|
|
1610
2063
|
}),
|
|
1611
2064
|
recentSummary: recentSummaryText,
|
|
1612
2065
|
lastFiveTurns: recentTurnsText,
|
|
2066
|
+
bootstrapHistoryFilePath: orchestratorBootstrapHistoryFilePath,
|
|
1613
2067
|
decisionOnly: !useSinglePassOrchestrator,
|
|
2068
|
+
defaultBotName: data.getDefaultAgentProfile()?.name || roleAssignments.coding || null,
|
|
1614
2069
|
});
|
|
1615
2070
|
const decisionPrompt = [
|
|
1616
2071
|
'This is a routing decision only. Do not perform the task.',
|
|
@@ -1621,20 +2076,21 @@ class OrchestratorAgent {
|
|
|
1621
2076
|
'Return strict JSON only.',
|
|
1622
2077
|
].join('\n');
|
|
1623
2078
|
let decision = null;
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
}, 8_000);
|
|
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);
|
|
1629
2083
|
try {
|
|
1630
2084
|
if (useSinglePassOrchestrator) {
|
|
1631
2085
|
const result = await this.runNodeWithRetry('understand_request', state, async () => this.workflowEngine.execute(prompt, conversationId, orchestratorBot.id, {
|
|
2086
|
+
abortSignal: opts.abortSignal,
|
|
1632
2087
|
disableDecomposition: true,
|
|
1633
2088
|
isOrchestrated: false,
|
|
1634
2089
|
projectId: projectId || null,
|
|
1635
2090
|
workspacePath: project?.folder?.trim() || undefined,
|
|
1636
2091
|
persistConversationMessages: false,
|
|
1637
2092
|
workerMode: false,
|
|
2093
|
+
storedAttachments: opts.storedAttachments,
|
|
1638
2094
|
systemPromptOverride: operatingPrompt,
|
|
1639
2095
|
onWorkerChunk: opts.onWorkerChunk,
|
|
1640
2096
|
onProgress: (progress) => {
|
|
@@ -1656,14 +2112,16 @@ class OrchestratorAgent {
|
|
|
1656
2112
|
}
|
|
1657
2113
|
}
|
|
1658
2114
|
else {
|
|
1659
|
-
const
|
|
1660
|
-
|
|
1661
|
-
|
|
2115
|
+
const rawDecision = await this.runNodeWithRetry('understand_request', state, async () => this.runOrchestratorPrompt(decisionPrompt, operatingPrompt, {
|
|
2116
|
+
conversationId: null,
|
|
2117
|
+
orchestratorBot,
|
|
1662
2118
|
projectId: projectId || null,
|
|
1663
2119
|
workspacePath: project?.folder?.trim() || undefined,
|
|
1664
|
-
|
|
2120
|
+
disableTools: true,
|
|
2121
|
+
abortSignal: opts.abortSignal,
|
|
2122
|
+
onNarrationChunk: this.makeOrchestratorNarrationForwarder(opts, orchestratorBot),
|
|
1665
2123
|
}));
|
|
1666
|
-
decision = (0, orchestrator_operating_prompt_1.parseOrchestratorFrontDoorDecision)(
|
|
2124
|
+
decision = (0, orchestrator_operating_prompt_1.parseOrchestratorFrontDoorDecision)(rawDecision);
|
|
1667
2125
|
}
|
|
1668
2126
|
}
|
|
1669
2127
|
catch {
|
|
@@ -1721,7 +2179,20 @@ class OrchestratorAgent {
|
|
|
1721
2179
|
};
|
|
1722
2180
|
}
|
|
1723
2181
|
}
|
|
1724
|
-
|
|
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);
|
|
1725
2196
|
}
|
|
1726
2197
|
buildFrontDoorSystemSuggestion(prompt, frontDoorSignals, promptAssignments, roleAssignments) {
|
|
1727
2198
|
const normalizeName = (value) => String(value || '').trim().toLowerCase();
|
|
@@ -1766,7 +2237,7 @@ class OrchestratorAgent {
|
|
|
1766
2237
|
delegateRole,
|
|
1767
2238
|
};
|
|
1768
2239
|
}
|
|
1769
|
-
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) {
|
|
1770
2241
|
if (!decision) {
|
|
1771
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' });
|
|
1772
2243
|
return null;
|
|
@@ -1774,21 +2245,16 @@ class OrchestratorAgent {
|
|
|
1774
2245
|
const highRiskPrompt = this.isHighRiskPrompt(prompt);
|
|
1775
2246
|
state.riskLevel = highRiskPrompt ? 'high' : state.riskLevel;
|
|
1776
2247
|
this.recordOrchestrationAudit(state, 'risk_gate', 'risk_assessed', highRiskPrompt ? 'Prompt matched high-risk action patterns.' : 'No high-risk action patterns matched before execution.', { highRiskPrompt });
|
|
1777
|
-
const normalizedDecision =
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
});
|
|
1788
|
-
if (normalizedDecision.corrected) {
|
|
1789
|
-
(0, state_1.markMisrouteCorrected)(state);
|
|
1790
|
-
}
|
|
1791
|
-
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
|
+
};
|
|
1792
2258
|
state.intent = decision.mode;
|
|
1793
2259
|
state.taskType = normalizedDecision.taskType;
|
|
1794
2260
|
this.recordOrchestrationAudit(state, 'choose_path', 'path_selected', normalizedDecision.correctionReason || decision.reason || `Orchestrator selected ${decision.mode}.`, {
|
|
@@ -1831,11 +2297,39 @@ class OrchestratorAgent {
|
|
|
1831
2297
|
return response;
|
|
1832
2298
|
}
|
|
1833
2299
|
if (decision.mode === 'execute_self') {
|
|
1834
|
-
// No guardrails, no confirmation checkpoints, no validation blocking.
|
|
1835
|
-
// Ben just does the work — like Claude CLI.
|
|
1836
2300
|
(0, state_1.setOrchestrationPath)(state, 'direct', 'direct');
|
|
1837
2301
|
const directResult = await this.executeOrchestratorOwnedWork(prompt, conversationId, orchestratorBot, roleAssignments, project, opts, state);
|
|
1838
|
-
|
|
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);
|
|
1839
2333
|
}
|
|
1840
2334
|
if (decision.mode === 'delegate') {
|
|
1841
2335
|
const delegateTarget = this.resolveDelegatedAgentName(decision, roleAssignments);
|
|
@@ -1877,7 +2371,7 @@ class OrchestratorAgent {
|
|
|
1877
2371
|
if (executionSpec.requiresConfirmation) {
|
|
1878
2372
|
return this.createConfirmationCheckpoint(delegatePrompt, conversationId, delegateIntent, executionSpec, projectId, roleAssignments, state);
|
|
1879
2373
|
}
|
|
1880
|
-
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);
|
|
1881
2375
|
}
|
|
1882
2376
|
if (decision.mode === 'workflow') {
|
|
1883
2377
|
const workflowObjective = decision.workflow_request?.trim() || prompt;
|
|
@@ -1907,19 +2401,32 @@ class OrchestratorAgent {
|
|
|
1907
2401
|
return this.createConfirmationCheckpoint(prompt, conversationId, workflowIntent, validation.executionSpec, projectId, roleAssignments, state);
|
|
1908
2402
|
}
|
|
1909
2403
|
this.recordOrchestrationAudit(state, 'run_workflow', 'delegated', 'Workflow execution is using the original user prompt as the execution source of truth.', { workflowObjective });
|
|
1910
|
-
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));
|
|
1911
2405
|
}
|
|
1912
2406
|
return null;
|
|
1913
2407
|
}
|
|
1914
2408
|
resolveDelegatedAgentName(decision, roleAssignments) {
|
|
1915
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
|
+
}
|
|
1916
2423
|
if (target && target.toLowerCase() !== 'none')
|
|
1917
2424
|
return target;
|
|
1918
|
-
if (
|
|
2425
|
+
if (role === 'coding')
|
|
1919
2426
|
return roleAssignments.coding;
|
|
1920
|
-
if (
|
|
2427
|
+
if (role === 'qa')
|
|
1921
2428
|
return roleAssignments.qa;
|
|
1922
|
-
if (
|
|
2429
|
+
if (role === 'research')
|
|
1923
2430
|
return roleAssignments.research;
|
|
1924
2431
|
return undefined;
|
|
1925
2432
|
}
|
|
@@ -1983,7 +2490,7 @@ class OrchestratorAgent {
|
|
|
1983
2490
|
locked: true,
|
|
1984
2491
|
};
|
|
1985
2492
|
}
|
|
1986
|
-
async queueDelegateTodo(prompt, conversationId, intent, opts, executionSpec, roleAssignments, state,
|
|
2493
|
+
async queueDelegateTodo(prompt, conversationId, intent, opts, executionSpec, roleAssignments, state, orchestratorActor, delegateTarget) {
|
|
1987
2494
|
const projectId = opts.projectId || state.projectId || null;
|
|
1988
2495
|
const project = projectId ? data.getProject(projectId) : undefined;
|
|
1989
2496
|
const targetAgent = this.findAgentByName(delegateTarget, project?.id || undefined);
|
|
@@ -1994,7 +2501,7 @@ class OrchestratorAgent {
|
|
|
1994
2501
|
clarificationQuestions: [`I could not resolve the target bot "${delegateTarget}". Which worker should own this task?`],
|
|
1995
2502
|
}, projectId ? data.getProjectOverview(projectId) : undefined);
|
|
1996
2503
|
}
|
|
1997
|
-
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));
|
|
1998
2505
|
const task = data.addTodoTask({
|
|
1999
2506
|
projectId,
|
|
2000
2507
|
conversationId,
|
|
@@ -2007,7 +2514,7 @@ class OrchestratorAgent {
|
|
|
2007
2514
|
taskType: step.taskType,
|
|
2008
2515
|
profileName: 'Programming',
|
|
2009
2516
|
taskPrompt: step.taskPrompt,
|
|
2010
|
-
actor: { actorType: 'orchestrator', actorId:
|
|
2517
|
+
actor: { actorType: 'orchestrator', actorId: orchestratorActor.actorId },
|
|
2011
2518
|
});
|
|
2012
2519
|
this.markConversationOrchestrated(conversationId);
|
|
2013
2520
|
this.recordOrchestrationAudit(state, 'delegate_specialist', 'delegated', `Queued delegated TODO for ${targetAgent.name}.`, {
|
|
@@ -2027,14 +2534,14 @@ class OrchestratorAgent {
|
|
|
2027
2534
|
].join('\n');
|
|
2028
2535
|
return state.finalResponseDraft;
|
|
2029
2536
|
}
|
|
2030
|
-
return await this.dispatchQueuedTodoChain(conversationId, opts, state,
|
|
2537
|
+
return await this.dispatchQueuedTodoChain(conversationId, opts, state, orchestratorActor, project, { taskIds: [task.id], workflowName: null });
|
|
2031
2538
|
}
|
|
2032
|
-
async queueWorkflowTodoPlan(prompt, conversationId, intent, executionSpec, opts, roleAssignments, project, state,
|
|
2539
|
+
async queueWorkflowTodoPlan(prompt, conversationId, intent, executionSpec, opts, roleAssignments, project, state, orchestratorActor, workflowTemplate) {
|
|
2033
2540
|
const projectId = project?.id || opts.projectId || state.projectId || null;
|
|
2034
2541
|
const effectivePolicy = data.getEffectiveOrchestrationPolicy(projectId || undefined);
|
|
2035
2542
|
const plannedSteps = workflowTemplate
|
|
2036
|
-
? this.buildTodoStepsFromTemplate(prompt, workflowTemplate,
|
|
2037
|
-
: 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);
|
|
2038
2545
|
if (plannedSteps.length === 0) {
|
|
2039
2546
|
const overview = projectId ? data.getProjectOverview(projectId) : undefined;
|
|
2040
2547
|
return this.formatClarificationResponse({
|
|
@@ -2060,9 +2567,9 @@ class OrchestratorAgent {
|
|
|
2060
2567
|
profileName: 'Programming',
|
|
2061
2568
|
nextWorkerBotId: next?.owner.id || null,
|
|
2062
2569
|
nextWorkerName: next?.owner.name || null,
|
|
2063
|
-
nextWorkerRole: next ? (next.owner
|
|
2570
|
+
nextWorkerRole: next ? data.getAgentOrchestrationRoleLabel(next.owner) : null,
|
|
2064
2571
|
taskPrompt: step.taskPrompt,
|
|
2065
|
-
actor: { actorType: 'orchestrator', actorId:
|
|
2572
|
+
actor: { actorType: 'orchestrator', actorId: orchestratorActor.actorId },
|
|
2066
2573
|
});
|
|
2067
2574
|
createdTasks.push(task);
|
|
2068
2575
|
(0, state_1.addDelegateTarget)(state, step.owner.name);
|
|
@@ -2091,18 +2598,72 @@ class OrchestratorAgent {
|
|
|
2091
2598
|
state.finalResponseDraft = lines.join('\n');
|
|
2092
2599
|
return state.finalResponseDraft;
|
|
2093
2600
|
}
|
|
2094
|
-
return await this.dispatchQueuedTodoChain(conversationId, opts, state,
|
|
2601
|
+
return await this.dispatchQueuedTodoChain(conversationId, opts, state, orchestratorActor, project, {
|
|
2095
2602
|
taskIds: createdTasks.map((task) => task.id),
|
|
2096
2603
|
workflowName: workflowTemplate?.name || null,
|
|
2097
2604
|
});
|
|
2098
2605
|
}
|
|
2099
|
-
async dispatchQueuedTodoChain(conversationId, opts, state,
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
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
|
+
}
|
|
2103
2657
|
if (!task) {
|
|
2104
|
-
return await this.finalizeQueuedTodoChain(conversationId, opts, state,
|
|
2658
|
+
return await this.finalizeQueuedTodoChain(conversationId, opts, state, orchestratorActor, project, context.workflowName, completedTaskIds);
|
|
2105
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));
|
|
2106
2667
|
const owner = task.owner_bot_id
|
|
2107
2668
|
? data.getAgentProfile(task.owner_bot_id)
|
|
2108
2669
|
: this.findAgentByName(task.owner_name || undefined, project?.id || undefined);
|
|
@@ -2113,8 +2674,12 @@ class OrchestratorAgent {
|
|
|
2113
2674
|
return message;
|
|
2114
2675
|
}
|
|
2115
2676
|
const taskPrompt = task.task_prompt?.trim() || task.details || task.title;
|
|
2677
|
+
if (opts.importedPlanRunId) {
|
|
2678
|
+
data.touchImportedPlanRun(opts.importedPlanRunId);
|
|
2679
|
+
}
|
|
2116
2680
|
const result = await this.runNodeWithRetry('run_workflow', state, async () => this.workflowEngine.execute(taskPrompt, conversationId, owner.id, {
|
|
2117
2681
|
apiKey: this.resolveApiKey(owner.provider),
|
|
2682
|
+
abortSignal: opts.abortSignal,
|
|
2118
2683
|
taskId: task.id,
|
|
2119
2684
|
stepDescription: task.title,
|
|
2120
2685
|
disableDecomposition: true,
|
|
@@ -2122,6 +2687,10 @@ class OrchestratorAgent {
|
|
|
2122
2687
|
projectId: project?.id || opts.projectId || state.projectId || null,
|
|
2123
2688
|
workspacePath: project?.folder?.trim() || undefined,
|
|
2124
2689
|
persistConversationMessages: false,
|
|
2690
|
+
storedAttachments: opts.storedAttachments,
|
|
2691
|
+
workerImageDeliveryMode: opts.storedAttachments?.length ? 'reference' : undefined,
|
|
2692
|
+
importedPlanRunId: opts.importedPlanRunId,
|
|
2693
|
+
cliSessionScopeKey: opts.cliSessionScopeKey,
|
|
2125
2694
|
onWorkerChunk: opts.onWorkerChunk,
|
|
2126
2695
|
onProgress: (progress) => {
|
|
2127
2696
|
if (progress.event === 'step-started') {
|
|
@@ -2135,11 +2704,19 @@ class OrchestratorAgent {
|
|
|
2135
2704
|
}
|
|
2136
2705
|
},
|
|
2137
2706
|
}));
|
|
2707
|
+
if (opts.abortSignal?.aborted || isAbortLikeError(result?.error)) {
|
|
2708
|
+
state.finalResponseDraft = '';
|
|
2709
|
+
return '';
|
|
2710
|
+
}
|
|
2138
2711
|
const stillActive = data.getTodoTask(task.id, 'active');
|
|
2139
2712
|
const completed = data.getTodoTask(task.id, 'completed');
|
|
2713
|
+
if (opts.abortSignal?.aborted) {
|
|
2714
|
+
state.finalResponseDraft = '';
|
|
2715
|
+
return '';
|
|
2716
|
+
}
|
|
2140
2717
|
if (stillActive?.blocker_summary && stillActive.blocker_question) {
|
|
2141
2718
|
this.recordOrchestrationAudit(state, 'run_workflow', 'blocked', `${owner.name} blocked on TODO: ${task.title}`, { taskId: task.id, blockerSummary: stillActive.blocker_summary });
|
|
2142
|
-
return await this.handleBlockedTodoTask(stillActive, opts, state,
|
|
2719
|
+
return await this.handleBlockedTodoTask(stillActive, opts, state, orchestratorActor, project);
|
|
2143
2720
|
}
|
|
2144
2721
|
if (!completed) {
|
|
2145
2722
|
const error = result.steps.find((step) => step.error)?.error || this.stripWorkerProtocol(result.mergedResult || '') || 'Worker did not complete the queued task.';
|
|
@@ -2148,19 +2725,46 @@ class OrchestratorAgent {
|
|
|
2148
2725
|
this.recordOrchestrationAudit(state, 'run_workflow', 'blocked', message, { taskId: task.id });
|
|
2149
2726
|
return message;
|
|
2150
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
|
+
}
|
|
2151
2753
|
}
|
|
2152
|
-
const message = 'The workflow exceeded the automatic dispatch limit before completing.';
|
|
2153
|
-
state.finalResponseDraft = message;
|
|
2154
|
-
this.recordOrchestrationAudit(state, 'run_workflow', 'blocked', message, { maxSteps: 20 });
|
|
2155
|
-
return message;
|
|
2156
2754
|
}
|
|
2157
|
-
async handleBlockedTodoTask(task, opts, state,
|
|
2755
|
+
async handleBlockedTodoTask(task, opts, state, orchestratorActor, project) {
|
|
2158
2756
|
const policy = data.getEffectiveOrchestrationPolicy(project?.id || opts.projectId || state.projectId || undefined);
|
|
2159
2757
|
const artifacts = data.listTodoArtifactsForTask(task.id, 'active').map((item) => item.path_or_ref);
|
|
2160
2758
|
const completedTasks = data.listCompletedTodoTasksForConversation(task.conversation_id || state.conversationId || '');
|
|
2161
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
|
+
}
|
|
2162
2766
|
const blockedPrompt = (0, orchestrator_blocked_prompt_1.buildBlockedWorkerOrchestratorPrompt)({
|
|
2163
|
-
orchestratorName:
|
|
2767
|
+
orchestratorName: orchestratorActor.name,
|
|
2164
2768
|
workerName: task.owner_name || 'Worker',
|
|
2165
2769
|
workerRole,
|
|
2166
2770
|
taskTitle: task.title,
|
|
@@ -2177,7 +2781,14 @@ class OrchestratorAgent {
|
|
|
2177
2781
|
});
|
|
2178
2782
|
let rawResponse = '';
|
|
2179
2783
|
try {
|
|
2180
|
-
rawResponse = await this.runNodeWithRetry('finalize_response', state, async () => this.
|
|
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.`, {
|
|
2785
|
+
conversationId: task.conversation_id || state.conversationId || null,
|
|
2786
|
+
orchestratorBot: orchestratorActor.bot || undefined,
|
|
2787
|
+
projectId: project?.id || opts.projectId || state.projectId || null,
|
|
2788
|
+
workspacePath: project?.folder?.trim() || undefined,
|
|
2789
|
+
disableTools: true,
|
|
2790
|
+
abortSignal: opts.abortSignal,
|
|
2791
|
+
}));
|
|
2181
2792
|
}
|
|
2182
2793
|
catch (error) {
|
|
2183
2794
|
this.recordOrchestrationAudit(state, 'finalize_response', 'blocked', `Blocked final response generation failed: ${error?.message || error}`);
|
|
@@ -2189,8 +2800,14 @@ class OrchestratorAgent {
|
|
|
2189
2800
|
state.finalResponseDraft = response;
|
|
2190
2801
|
return response;
|
|
2191
2802
|
}
|
|
2192
|
-
async finalizeQueuedTodoChain(conversationId, opts, state,
|
|
2193
|
-
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) => ({
|
|
2194
2811
|
...task,
|
|
2195
2812
|
artifactRefs: data.listTodoArtifactsForTask(task.id, 'completed').map((item) => item.path_or_ref),
|
|
2196
2813
|
}));
|
|
@@ -2200,7 +2817,7 @@ class OrchestratorAgent {
|
|
|
2200
2817
|
}
|
|
2201
2818
|
const policy = data.getEffectiveOrchestrationPolicy(project?.id || opts.projectId || state.projectId || undefined);
|
|
2202
2819
|
const finalPrompt = (0, orchestrator_final_response_prompt_1.buildOrchestratorFinalResponsePrompt)({
|
|
2203
|
-
orchestratorName:
|
|
2820
|
+
orchestratorName: orchestratorActor.name,
|
|
2204
2821
|
projectName: project?.name || null,
|
|
2205
2822
|
workspacePath: project?.folder || null,
|
|
2206
2823
|
effectivePolicy: policy,
|
|
@@ -2208,7 +2825,14 @@ class OrchestratorAgent {
|
|
|
2208
2825
|
});
|
|
2209
2826
|
let response = '';
|
|
2210
2827
|
try {
|
|
2211
|
-
response = await this.runNodeWithRetry('finalize_response', state, async () => this.
|
|
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.`, {
|
|
2829
|
+
conversationId,
|
|
2830
|
+
orchestratorBot: orchestratorActor.bot || undefined,
|
|
2831
|
+
projectId: project?.id || opts.projectId || state.projectId || null,
|
|
2832
|
+
workspacePath: project?.folder?.trim() || undefined,
|
|
2833
|
+
disableTools: true,
|
|
2834
|
+
abortSignal: opts.abortSignal,
|
|
2835
|
+
}));
|
|
2212
2836
|
}
|
|
2213
2837
|
catch (error) {
|
|
2214
2838
|
this.recordOrchestrationAudit(state, 'finalize_response', 'blocked', `Final response generation failed: ${error?.message || error}`);
|
|
@@ -2223,27 +2847,31 @@ class OrchestratorAgent {
|
|
|
2223
2847
|
: 'Completed automatic TODO dispatch.', { completedTaskCount: completedTasks.length });
|
|
2224
2848
|
return response;
|
|
2225
2849
|
}
|
|
2226
|
-
buildSingleDelegateTodoStep(prompt, owner,
|
|
2850
|
+
buildSingleDelegateTodoStep(prompt, conversationId, owner, orchestratorActor, projectName, workspacePath, effectivePolicy) {
|
|
2227
2851
|
const taskType = this.normalizeTaskTypeForWorker(owner, 'coding');
|
|
2852
|
+
const recentContext = this.buildRecentConversationContextForWorker(conversationId);
|
|
2228
2853
|
return {
|
|
2229
|
-
title: `${owner.name}:
|
|
2230
|
-
details: `Delegated by ${
|
|
2854
|
+
title: `${owner.name}: handle the delegated request`,
|
|
2855
|
+
details: `Delegated by ${orchestratorActor.name}. Complete this single-worker request directly.`,
|
|
2231
2856
|
owner,
|
|
2232
2857
|
taskType,
|
|
2233
2858
|
taskPrompt: this.buildWorkerTaskPrompt({
|
|
2234
2859
|
originalPrompt: prompt,
|
|
2235
|
-
stepInstruction: 'Handle
|
|
2860
|
+
stepInstruction: 'Handle the user request from this conversation.',
|
|
2236
2861
|
owner,
|
|
2237
2862
|
previousWorker: null,
|
|
2238
2863
|
nextWorker: null,
|
|
2864
|
+
conversationId,
|
|
2239
2865
|
projectName,
|
|
2240
2866
|
workspacePath,
|
|
2241
2867
|
effectivePolicy,
|
|
2868
|
+
recentContext,
|
|
2242
2869
|
}),
|
|
2243
2870
|
successCriteria: `Complete the delegated ${taskType} task and return a concise result summary.`,
|
|
2244
2871
|
};
|
|
2245
2872
|
}
|
|
2246
|
-
buildTodoStepsFromTemplate(prompt, workflowTemplate,
|
|
2873
|
+
buildTodoStepsFromTemplate(prompt, conversationId, workflowTemplate, orchestratorActor, projectName, workspacePath, effectivePolicy) {
|
|
2874
|
+
const recentContext = this.buildRecentConversationContextForWorker(conversationId);
|
|
2247
2875
|
const steps = workflowTemplate.steps
|
|
2248
2876
|
.sort((a, b) => a.order_index - b.order_index)
|
|
2249
2877
|
.map((step) => {
|
|
@@ -2269,14 +2897,315 @@ class OrchestratorAgent {
|
|
|
2269
2897
|
owner: step.owner,
|
|
2270
2898
|
previousWorker: steps[index - 1]?.owner || null,
|
|
2271
2899
|
nextWorker: steps[index + 1]?.owner || null,
|
|
2900
|
+
conversationId,
|
|
2272
2901
|
projectName,
|
|
2273
2902
|
workspacePath,
|
|
2274
2903
|
effectivePolicy,
|
|
2904
|
+
recentContext,
|
|
2275
2905
|
}),
|
|
2276
2906
|
successCriteria: `Complete step ${index + 1} of "${workflowTemplate.name}" and hand off the necessary output.`,
|
|
2277
2907
|
}));
|
|
2278
2908
|
}
|
|
2279
|
-
|
|
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);
|
|
2280
3209
|
const roleOrder = this.inferWorkflowRoleOrder(prompt, roleAssignments, intent);
|
|
2281
3210
|
const owners = roleOrder
|
|
2282
3211
|
.map((role) => {
|
|
@@ -2289,7 +3218,7 @@ class OrchestratorAgent {
|
|
|
2289
3218
|
.filter((item) => Boolean(item));
|
|
2290
3219
|
return owners.map((item, index) => ({
|
|
2291
3220
|
title: `${item.agent.name}: ${this.defaultTodoTitleForRole(item.role, prompt)}`,
|
|
2292
|
-
details: `Workflow step assigned by ${
|
|
3221
|
+
details: `Workflow step assigned by ${orchestratorActor.name}.`,
|
|
2293
3222
|
owner: item.agent,
|
|
2294
3223
|
taskType: this.normalizeTaskTypeForWorker(item.agent, item.role === 'research' ? 'research' : item.role === 'qa' ? 'qa' : 'coding'),
|
|
2295
3224
|
taskPrompt: this.buildWorkerTaskPrompt({
|
|
@@ -2298,80 +3227,75 @@ class OrchestratorAgent {
|
|
|
2298
3227
|
owner: item.agent,
|
|
2299
3228
|
previousWorker: owners[index - 1]?.agent || null,
|
|
2300
3229
|
nextWorker: owners[index + 1]?.agent || null,
|
|
3230
|
+
conversationId,
|
|
2301
3231
|
projectName: projectName || null,
|
|
2302
3232
|
workspacePath: workspacePath || null,
|
|
2303
3233
|
effectivePolicy: effectivePolicy || data.getEffectiveOrchestrationPolicy(projectId),
|
|
3234
|
+
recentContext,
|
|
2304
3235
|
}),
|
|
2305
3236
|
successCriteria: this.defaultSuccessCriteriaForRole(item.role),
|
|
2306
3237
|
}));
|
|
2307
3238
|
}
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
if (assignments.research && /\b(research|brainstorm|plan|planning)\b/i.test(prompt))
|
|
2327
|
-
fallback.push('research');
|
|
2328
|
-
if (assignments.coding)
|
|
2329
|
-
fallback.push('coding');
|
|
2330
|
-
if (assignments.qa)
|
|
2331
|
-
fallback.push('qa');
|
|
2332
|
-
if (fallback.length > 0)
|
|
2333
|
-
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) {
|
|
2334
3257
|
return [];
|
|
2335
3258
|
}
|
|
2336
3259
|
normalizeTaskTypeForWorker(owner, fallback) {
|
|
2337
|
-
const
|
|
2338
|
-
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']))
|
|
2339
3265
|
return 'qa';
|
|
2340
|
-
if (
|
|
3266
|
+
if (this.agentHasAnyRole(owner, ['research', 'researcher', 'analyst', 'advisor', 'brainstorm', 'brainstorming', 'planning', 'plan', 'design']))
|
|
2341
3267
|
return 'research';
|
|
2342
|
-
if (
|
|
3268
|
+
if (this.agentHasAnyRole(owner, ['verify', 'verification']))
|
|
2343
3269
|
return 'verify';
|
|
2344
|
-
if (
|
|
2345
|
-
return 'deploy';
|
|
2346
|
-
if (roleClass === 'coding' || roleClass === 'code' || roleClass === 'builder' || roleClass === 'developer' || roleClass === 'engineer')
|
|
3270
|
+
if (this.agentHasAnyRole(owner, ['coding', 'code', 'builder', 'developer', 'engineer']))
|
|
2347
3271
|
return 'coding';
|
|
3272
|
+
if (this.agentHasAnyRole(owner, ['deploy', 'deployment']))
|
|
3273
|
+
return 'deploy';
|
|
2348
3274
|
return (fallback || 'coding');
|
|
2349
3275
|
}
|
|
2350
3276
|
buildWorkerTaskPrompt(input) {
|
|
2351
|
-
const workerRole =
|
|
3277
|
+
const workerRole = this.getOrchestrationRoleLabel(input.owner);
|
|
2352
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);
|
|
2353
3282
|
const composedTask = [
|
|
2354
3283
|
`Step instruction: ${input.stepInstruction}`,
|
|
2355
3284
|
taskType === 'qa'
|
|
2356
|
-
? '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.'
|
|
2357
3286
|
: taskType === 'verify'
|
|
2358
3287
|
? 'Verification scope: Compare the delivered work against the original user request and the explicit success criteria.'
|
|
2359
3288
|
: '',
|
|
2360
|
-
|
|
2361
|
-
? 'QA handoff rule:
|
|
2362
|
-
: '',
|
|
2363
|
-
String(input.nextWorker?.role_label || input.nextWorker?.role_class || '').trim().toLowerCase() === 'qa'
|
|
2364
|
-
? 'QA handoff rule: Do not ask the next QA worker to use memory search, image analysis, or broad codebase discovery unless a specific missing expectation cannot be checked from the artifact and nearby implementation.'
|
|
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.'
|
|
2365
3291
|
: '',
|
|
2366
|
-
|
|
2367
|
-
?
|
|
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.'
|
|
2368
3294
|
: '',
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
? null
|
|
2374
|
-
: 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,
|
|
2375
3299
|
].join('\n');
|
|
2376
3300
|
return (0, worker_operating_prompt_1.buildWorkerOperatingPrompt)({
|
|
2377
3301
|
workerName: input.owner.name,
|
|
@@ -2386,26 +3310,60 @@ class OrchestratorAgent {
|
|
|
2386
3310
|
defaultLine: 'No confirmed special policy is set.',
|
|
2387
3311
|
}),
|
|
2388
3312
|
previousWorkerName: input.previousWorker?.name || null,
|
|
2389
|
-
previousWorkerRole: input.previousWorker ?
|
|
3313
|
+
previousWorkerRole: input.previousWorker ? this.getOrchestrationRoleLabel(input.previousWorker) : null,
|
|
2390
3314
|
nextWorkerName: input.nextWorker?.name || null,
|
|
2391
|
-
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,
|
|
2392
3350
|
});
|
|
2393
3351
|
}
|
|
2394
|
-
defaultTodoTitleForRole(role,
|
|
3352
|
+
defaultTodoTitleForRole(role, _prompt) {
|
|
2395
3353
|
if (role === 'research')
|
|
2396
|
-
return
|
|
3354
|
+
return 'Research and plan the user request';
|
|
2397
3355
|
if (role === 'qa')
|
|
2398
|
-
return
|
|
2399
|
-
return
|
|
3356
|
+
return 'QA and review the delivered work';
|
|
3357
|
+
return 'Build and implement the requested work';
|
|
2400
3358
|
}
|
|
2401
|
-
defaultStepInstructionForRole(role,
|
|
3359
|
+
defaultStepInstructionForRole(role, _prompt) {
|
|
2402
3360
|
if (role === 'research') {
|
|
2403
|
-
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.';
|
|
2404
3362
|
}
|
|
2405
3363
|
if (role === 'qa') {
|
|
2406
|
-
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.';
|
|
2407
3365
|
}
|
|
2408
|
-
return
|
|
3366
|
+
return 'Implement the assigned work using the current task and previous worker handoff as context.';
|
|
2409
3367
|
}
|
|
2410
3368
|
defaultSuccessCriteriaForRole(role) {
|
|
2411
3369
|
if (role === 'research')
|
|
@@ -2439,10 +3397,65 @@ class OrchestratorAgent {
|
|
|
2439
3397
|
return scopedMatch;
|
|
2440
3398
|
return allAgents.find((agent) => agent.name.trim().toLowerCase() === normalized);
|
|
2441
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
|
+
}
|
|
2442
3416
|
async executeOrchestratorOwnedWork(prompt, conversationId, orchestratorBot, roleAssignments, project, opts, state, options) {
|
|
2443
|
-
|
|
2444
|
-
|
|
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
|
+
}
|
|
2445
3457
|
const result = await this.runNodeWithRetry('execute_direct', state, async () => this.workflowEngine.execute(prompt, conversationId, orchestratorBot.id, {
|
|
3458
|
+
abortSignal: opts.abortSignal,
|
|
2446
3459
|
disableDecomposition: true,
|
|
2447
3460
|
isOrchestrated: false,
|
|
2448
3461
|
projectId: project?.id || state.projectId || null,
|
|
@@ -2452,6 +3465,7 @@ class OrchestratorAgent {
|
|
|
2452
3465
|
// also persist would create duplicate assistant messages.
|
|
2453
3466
|
persistConversationMessages: false,
|
|
2454
3467
|
workerMode: false,
|
|
3468
|
+
storedAttachments: opts.storedAttachments,
|
|
2455
3469
|
onWorkerChunk: opts.onWorkerChunk,
|
|
2456
3470
|
onProgress: (progress) => {
|
|
2457
3471
|
if (progress.event === 'step-failed') {
|
|
@@ -2521,12 +3535,15 @@ class OrchestratorAgent {
|
|
|
2521
3535
|
(0, state_1.addHelperRoleUsage)(state, 'dispatch_controller');
|
|
2522
3536
|
(0, state_1.addDelegateTarget)(state, delegateBot.name);
|
|
2523
3537
|
const result = await this.runNodeWithRetry('delegate_specialist', state, async () => this.workflowEngine.execute(delegationPrompt, conversationId, delegateBot.id, {
|
|
3538
|
+
abortSignal: opts.abortSignal,
|
|
2524
3539
|
disableDecomposition: true,
|
|
2525
3540
|
isOrchestrated: false,
|
|
2526
3541
|
workerMode: false,
|
|
2527
3542
|
projectId: project?.id || state.projectId || null,
|
|
2528
3543
|
workspacePath: project?.folder?.trim() || undefined,
|
|
2529
3544
|
persistConversationMessages: true,
|
|
3545
|
+
storedAttachments: opts.storedAttachments,
|
|
3546
|
+
workerImageDeliveryMode: opts.storedAttachments?.length ? 'reference' : undefined,
|
|
2530
3547
|
onWorkerChunk: opts.onWorkerChunk,
|
|
2531
3548
|
onProgress: (progress) => {
|
|
2532
3549
|
if (progress.event === 'step-started') {
|
|
@@ -2551,47 +3568,103 @@ class OrchestratorAgent {
|
|
|
2551
3568
|
return 'research';
|
|
2552
3569
|
return 'discuss';
|
|
2553
3570
|
}
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
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
|
+
}
|
|
3578
|
+
async runOrchestratorPrompt(userPrompt, systemPrompt, opts) {
|
|
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;
|
|
3588
|
+
const result = await this.workflowEngine.execute(userPrompt, opts.conversationId, opts.orchestratorBot.id, {
|
|
3589
|
+
abortSignal: opts.abortSignal,
|
|
3590
|
+
disableDecomposition: true,
|
|
3591
|
+
disableTools: opts.disableTools === true,
|
|
3592
|
+
isOrchestrated: false,
|
|
3593
|
+
projectId: opts.projectId ?? null,
|
|
3594
|
+
workspacePath: opts.workspacePath,
|
|
3595
|
+
persistConversationMessages: false,
|
|
3596
|
+
workerMode: false,
|
|
3597
|
+
storedAttachments: opts.storedAttachments,
|
|
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
|
+
} : {}),
|
|
3609
|
+
});
|
|
3610
|
+
this.setLastResponseMeta({
|
|
3611
|
+
agentName: opts.orchestratorBot.name,
|
|
3612
|
+
botId: opts.orchestratorBot.id,
|
|
3613
|
+
modelLabel: this.orchestratorRuntime.modelLabel,
|
|
3614
|
+
});
|
|
3615
|
+
return this.stripWorkerProtocol(result.mergedResult || '');
|
|
2566
3616
|
}
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
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;
|
|
3621
|
+
const response = await this.orchestratorRuntime.llm.chat({
|
|
3622
|
+
messages: [{ role: 'user', content: userPrompt }],
|
|
3623
|
+
system: systemPrompt,
|
|
3624
|
+
stream: !!onChunk,
|
|
3625
|
+
runtimeMode: 'local_desktop',
|
|
3626
|
+
abortSignal: opts.abortSignal,
|
|
3627
|
+
...(onChunk ? { onChunk } : {}),
|
|
3628
|
+
});
|
|
3629
|
+
this.setLastResponseMeta({
|
|
3630
|
+
agentName: this.isLocalDesktopRuntime() ? 'Orchestrator' : this.orchestratorRuntime.agentName,
|
|
3631
|
+
botId: this.orchestratorRuntime.botId,
|
|
3632
|
+
modelLabel: this.orchestratorRuntime.modelLabel,
|
|
3633
|
+
});
|
|
3634
|
+
return String(response.content || '').trim();
|
|
3635
|
+
}
|
|
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 [];
|
|
2582
3656
|
}
|
|
2583
3657
|
orchestratorOwnsWorkflowRole(role, orchestratorBot, roleAssignments) {
|
|
2584
3658
|
const orchestratorName = orchestratorBot.name.toLowerCase();
|
|
2585
3659
|
const assignedName = roleAssignments[role]?.toLowerCase();
|
|
2586
3660
|
if (assignedName)
|
|
2587
3661
|
return assignedName === orchestratorName;
|
|
2588
|
-
const roleClass = String(orchestratorBot.role_class || '').trim().toLowerCase();
|
|
2589
3662
|
if (role === 'coding')
|
|
2590
|
-
return ['code', 'coding', 'coder', 'builder', 'developer', 'engineer', 'orchestrator', 'manager']
|
|
3663
|
+
return this.agentHasAnyRole(orchestratorBot, ['code', 'coding', 'coder', 'builder', 'developer', 'engineer', 'orchestrator', 'manager']);
|
|
2591
3664
|
if (role === 'qa')
|
|
2592
|
-
return ['qa', 'review', 'reviewer']
|
|
3665
|
+
return this.agentHasAnyRole(orchestratorBot, ['qa', 'review', 'reviewer']);
|
|
2593
3666
|
if (role === 'research')
|
|
2594
|
-
return ['research', 'researcher', 'analyst', 'advisor']
|
|
3667
|
+
return this.agentHasAnyRole(orchestratorBot, ['research', 'researcher', 'analyst', 'advisor', 'brainstorming', 'planning', 'design']);
|
|
2595
3668
|
return false;
|
|
2596
3669
|
}
|
|
2597
3670
|
resolveWorkflowRoleTarget(role, orchestratorBot, roleAssignments) {
|
|
@@ -2687,7 +3760,7 @@ class OrchestratorAgent {
|
|
|
2687
3760
|
'Do not build the final artifact in this step.',
|
|
2688
3761
|
'Your deliverable is a concrete implementation direction document that the coding step can use immediately.',
|
|
2689
3762
|
'',
|
|
2690
|
-
`Original user request
|
|
3763
|
+
`Original user request:\n${originalPrompt}`,
|
|
2691
3764
|
artifactContext,
|
|
2692
3765
|
'',
|
|
2693
3766
|
'Your job: research the request, pressure-test the approach, and recommend the best implementation direction.',
|
|
@@ -2701,7 +3774,7 @@ class OrchestratorAgent {
|
|
|
2701
3774
|
'Do not perform QA in this step.',
|
|
2702
3775
|
'Your deliverable is the actual file or code change requested by the user.',
|
|
2703
3776
|
'',
|
|
2704
|
-
`Original user request
|
|
3777
|
+
`Original user request:\n${originalPrompt}`,
|
|
2705
3778
|
artifactContext,
|
|
2706
3779
|
researchContext,
|
|
2707
3780
|
qaContext,
|
|
@@ -2719,7 +3792,7 @@ class OrchestratorAgent {
|
|
|
2719
3792
|
'You are the QA/review step in a multi-step workflow.',
|
|
2720
3793
|
'Do not redo the coding or research work in this step unless the file is missing and you must report that blocker.',
|
|
2721
3794
|
'',
|
|
2722
|
-
`Original user request
|
|
3795
|
+
`Original user request:\n${originalPrompt}`,
|
|
2723
3796
|
artifactContext,
|
|
2724
3797
|
researchContext,
|
|
2725
3798
|
implementationContext,
|
|
@@ -2879,31 +3952,6 @@ class OrchestratorAgent {
|
|
|
2879
3952
|
}
|
|
2880
3953
|
return currentProjectId || intentProjectId;
|
|
2881
3954
|
}
|
|
2882
|
-
/**
|
|
2883
|
-
* Handle simple requests — route to best agent, return response.
|
|
2884
|
-
*/
|
|
2885
|
-
async handleSimpleRequest(prompt, conversationId, intent, opts) {
|
|
2886
|
-
const agents = (0, orchestrator_profile_1.filterOutOrchestratorProfiles)(data.listAgentProfiles());
|
|
2887
|
-
if (agents.length === 0)
|
|
2888
|
-
return 'No worker agents configured. Please add an agent in Settings.';
|
|
2889
|
-
const route = await this.clerk.routeTask(prompt, agents);
|
|
2890
|
-
this.reportHiddenRoleProgress(opts, 'dispatch_controller', `Routing request to ${route.agentName}`);
|
|
2891
|
-
this.publishOrchestratorStatus(opts, {
|
|
2892
|
-
phase: 'delegating',
|
|
2893
|
-
steps: [{ index: 0, description: prompt.slice(0, 80), agentName: route.agentName, status: 'running' }],
|
|
2894
|
-
currentStep: 0,
|
|
2895
|
-
totalSteps: 1,
|
|
2896
|
-
});
|
|
2897
|
-
// Execute via workflow engine — disable decomposition for simple mode
|
|
2898
|
-
const result = await this.workflowEngine.execute(prompt, conversationId, route.agentId, {
|
|
2899
|
-
apiKey: this.resolveApiKey(route.provider),
|
|
2900
|
-
disableDecomposition: true,
|
|
2901
|
-
onWorkerChunk: opts.onWorkerChunk,
|
|
2902
|
-
});
|
|
2903
|
-
this.publishOrchestratorStatus(opts, { phase: 'complete', totalSteps: 1, currentStep: 1 });
|
|
2904
|
-
// Strip worker protocol before returning to user
|
|
2905
|
-
return this.stripWorkerProtocol(result.mergedResult);
|
|
2906
|
-
}
|
|
2907
3955
|
/**
|
|
2908
3956
|
* Handle proxy/discussion requests — forward to specific agent.
|
|
2909
3957
|
*/
|
|
@@ -2953,6 +4001,23 @@ class OrchestratorAgent {
|
|
|
2953
4001
|
});
|
|
2954
4002
|
}
|
|
2955
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
|
+
};
|
|
2956
4021
|
// Update conversation routing mode to 'proxy'
|
|
2957
4022
|
try {
|
|
2958
4023
|
const db = data.getDb();
|
|
@@ -2962,14 +4027,17 @@ class OrchestratorAgent {
|
|
|
2962
4027
|
// Execute single step — disable decomposition for proxy mode
|
|
2963
4028
|
const runProxyStep = (stepPrompt) => this.runNodeWithRetry('delegate_specialist', state, async () => this.workflowEngine.execute(stepPrompt, conversationId, agent.id, {
|
|
2964
4029
|
apiKey: this.resolveApiKey(agent.provider),
|
|
4030
|
+
abortSignal: opts.abortSignal,
|
|
2965
4031
|
disableDecomposition: true,
|
|
2966
|
-
isOrchestrated:
|
|
4032
|
+
isOrchestrated: false,
|
|
2967
4033
|
projectId: state?.projectId ?? null,
|
|
2968
4034
|
workspacePath: state?.projectId
|
|
2969
4035
|
? data.getProject(state.projectId)?.folder?.trim() || undefined
|
|
2970
4036
|
: undefined,
|
|
2971
4037
|
persistConversationMessages: false,
|
|
2972
|
-
|
|
4038
|
+
storedAttachments: opts.storedAttachments,
|
|
4039
|
+
workerImageDeliveryMode: opts.storedAttachments?.length ? 'reference' : undefined,
|
|
4040
|
+
onWorkerChunk: forwardProxyWorkerChunk,
|
|
2973
4041
|
onProgress: (progress) => {
|
|
2974
4042
|
if (progress.event === 'step-started') {
|
|
2975
4043
|
this.reportHiddenRoleProgress(opts, 'orchestrator', `${progress.step.agentName} is working on the delegated step`);
|
|
@@ -3129,15 +4197,25 @@ class OrchestratorAgent {
|
|
|
3129
4197
|
if (state && workflowTemplate) {
|
|
3130
4198
|
state.preferredWorkflowUsed = true;
|
|
3131
4199
|
}
|
|
3132
|
-
|
|
3133
|
-
// workflow planning should be done by the orchestrator LLM, not the clerk.
|
|
3134
|
-
// The clerk should only do summaries, extraction, and compression.
|
|
4200
|
+
const inferredWorkflowRoles = this.inferRequestedWorkflowRoles(prompt, roleAssignments);
|
|
3135
4201
|
const plannedSteps = !workflowTemplate
|
|
3136
|
-
?
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
4202
|
+
? inferredWorkflowRoles.map((role, index) => {
|
|
4203
|
+
const ownerName = roleAssignments[role] || defaultAgent.name;
|
|
4204
|
+
return {
|
|
4205
|
+
description: role === 'research'
|
|
4206
|
+
? 'Research and pressure-test the request'
|
|
4207
|
+
: role === 'qa'
|
|
4208
|
+
? 'Review and QA the completed work'
|
|
4209
|
+
: 'Implement the requested work',
|
|
4210
|
+
agentName: ownerName,
|
|
4211
|
+
expectedOutput: role === 'qa'
|
|
4212
|
+
? 'PASS if the work meets the request, otherwise FAIL with actionable defects'
|
|
4213
|
+
: role === 'research'
|
|
4214
|
+
? 'Clear research findings and implementation guidance'
|
|
4215
|
+
: 'Completed implementation that satisfies the request',
|
|
4216
|
+
dependsOn: index > 0 ? [index - 1] : [],
|
|
4217
|
+
};
|
|
4218
|
+
})
|
|
3141
4219
|
: null;
|
|
3142
4220
|
let todoTaskId;
|
|
3143
4221
|
if (executionSpec.allowTodoCreation) {
|
|
@@ -3227,17 +4305,22 @@ class OrchestratorAgent {
|
|
|
3227
4305
|
coderRetryLimit: effectivePolicy.coderRetryLimit,
|
|
3228
4306
|
workspacePath: effectiveWorkspacePath,
|
|
3229
4307
|
projectId: project?.id || opts.projectId || null,
|
|
4308
|
+
storedAttachments: opts.storedAttachments,
|
|
4309
|
+
workerImageDeliveryMode: opts.storedAttachments?.length ? 'reference' : undefined,
|
|
3230
4310
|
onProgress: progressHandler,
|
|
3231
4311
|
onWorkerChunk: opts.onWorkerChunk,
|
|
3232
4312
|
})
|
|
3233
4313
|
: this.workflowEngine.execute(prompt, conversationId, defaultAgent.id, {
|
|
3234
4314
|
apiKey: this.resolveApiKey(defaultAgent.provider),
|
|
4315
|
+
abortSignal: opts.abortSignal,
|
|
3235
4316
|
taskId: todoTaskId,
|
|
3236
4317
|
isOrchestrated: true,
|
|
3237
4318
|
roleAssignments,
|
|
3238
4319
|
coderRetryLimit: effectivePolicy.coderRetryLimit,
|
|
3239
4320
|
workspacePath: effectiveWorkspacePath,
|
|
3240
4321
|
projectId: project?.id || opts.projectId || null,
|
|
4322
|
+
storedAttachments: opts.storedAttachments,
|
|
4323
|
+
workerImageDeliveryMode: opts.storedAttachments?.length ? 'reference' : undefined,
|
|
3241
4324
|
onProgress: progressHandler,
|
|
3242
4325
|
onWorkerChunk: opts.onWorkerChunk,
|
|
3243
4326
|
}));
|
|
@@ -3307,6 +4390,36 @@ class OrchestratorAgent {
|
|
|
3307
4390
|
}
|
|
3308
4391
|
return lines.join('\n');
|
|
3309
4392
|
}
|
|
4393
|
+
/**
|
|
4394
|
+
* Run completion validation on a synthesized response and append warnings
|
|
4395
|
+
* if the validation finds error-severity issues. Non-blocking — always
|
|
4396
|
+
* returns a response, possibly with a validation warning appended.
|
|
4397
|
+
*/
|
|
4398
|
+
applyCompletionValidation(synthesized, result, conversationId, executionMode) {
|
|
4399
|
+
try {
|
|
4400
|
+
const auditMessages = conversationId
|
|
4401
|
+
? data.getMessages(conversationId, { limit: 200 }).map((m) => ({
|
|
4402
|
+
role: m.role,
|
|
4403
|
+
content: m.content,
|
|
4404
|
+
tool_calls_json: m.tool_calls_json,
|
|
4405
|
+
}))
|
|
4406
|
+
: [];
|
|
4407
|
+
const validation = (0, validation_1.validateCompletion)(result, synthesized, auditMessages, executionMode);
|
|
4408
|
+
if (!validation.valid) {
|
|
4409
|
+
console.warn(`[orchestrator] Completion validation failed (confidence=${validation.confidence}):`, JSON.stringify(validation.issues));
|
|
4410
|
+
return synthesized + (0, validation_1.formatValidationWarning)(validation);
|
|
4411
|
+
}
|
|
4412
|
+
if (validation.issues.length > 0) {
|
|
4413
|
+
console.info(`[orchestrator] Completion validation passed with warnings (confidence=${validation.confidence}):`, JSON.stringify(validation.issues));
|
|
4414
|
+
}
|
|
4415
|
+
return synthesized;
|
|
4416
|
+
}
|
|
4417
|
+
catch (err) {
|
|
4418
|
+
// Validation must never block the response
|
|
4419
|
+
console.warn('[orchestrator] Completion validation error (non-blocking):', err);
|
|
4420
|
+
return synthesized;
|
|
4421
|
+
}
|
|
4422
|
+
}
|
|
3310
4423
|
/**
|
|
3311
4424
|
* Publish orchestrator status via MQTT for UI rendering.
|
|
3312
4425
|
*/
|
|
@@ -3356,6 +4469,83 @@ class OrchestratorAgent {
|
|
|
3356
4469
|
reportHiddenRoleProgress(opts, roleName, detail) {
|
|
3357
4470
|
opts.onProgress(`${roleName}::${detail}`);
|
|
3358
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
|
+
}
|
|
3359
4549
|
formatConfirmationRequest(prompt) {
|
|
3360
4550
|
const singleLine = String(prompt || '').replace(/\s+/g, ' ').trim();
|
|
3361
4551
|
if (!singleLine)
|
|
@@ -3474,76 +4664,129 @@ class OrchestratorAgent {
|
|
|
3474
4664
|
}
|
|
3475
4665
|
return assignments;
|
|
3476
4666
|
}
|
|
3477
|
-
clerkModelLabel() {
|
|
3478
|
-
const runtime = this.clerk.getRuntimeInfo();
|
|
3479
|
-
if (!runtime.model)
|
|
3480
|
-
return 'Clerk';
|
|
3481
|
-
return `${runtime.model} | Clerk`;
|
|
3482
|
-
}
|
|
3483
|
-
async respondAsClerk(userPrompt, systemPrompt) {
|
|
3484
|
-
const response = await this.clerk.respond(userPrompt, systemPrompt);
|
|
3485
|
-
this.setLastResponseMeta({
|
|
3486
|
-
agentName: 'Clerk',
|
|
3487
|
-
botId: null,
|
|
3488
|
-
modelLabel: this.clerkModelLabel(),
|
|
3489
|
-
});
|
|
3490
|
-
return response;
|
|
3491
|
-
}
|
|
3492
4667
|
hasRoleAssignments(assignments) {
|
|
3493
4668
|
return Boolean(assignments.coding || assignments.qa || assignments.research);
|
|
3494
4669
|
}
|
|
3495
|
-
|
|
3496
|
-
const raw = data.getProjectSetting(projectId, ORCHESTRATOR_ROLE_SETTING_KEY);
|
|
3497
|
-
if (!raw)
|
|
3498
|
-
return {};
|
|
3499
|
-
try {
|
|
3500
|
-
const parsed = JSON.parse(raw);
|
|
3501
|
-
return {
|
|
3502
|
-
coding: parsed.coding || undefined,
|
|
3503
|
-
qa: parsed.qa || undefined,
|
|
3504
|
-
research: parsed.research || undefined,
|
|
3505
|
-
};
|
|
3506
|
-
}
|
|
3507
|
-
catch {
|
|
3508
|
-
return {};
|
|
3509
|
-
}
|
|
3510
|
-
}
|
|
3511
|
-
mergeRoleAssignments(base, saved, inferred, incoming) {
|
|
4670
|
+
mergeRoleAssignments(base, inferred, incoming) {
|
|
3512
4671
|
return {
|
|
3513
|
-
coding: incoming.coding ||
|
|
3514
|
-
qa: incoming.qa ||
|
|
3515
|
-
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,
|
|
3516
4675
|
};
|
|
3517
4676
|
}
|
|
3518
4677
|
deriveRoleAssignmentsFromProject(projectId, orchestratorBot) {
|
|
3519
4678
|
const bots = this.listProjectScopedBots(projectId);
|
|
3520
4679
|
const result = {};
|
|
3521
|
-
const roleClass = String(orchestratorBot?.role_class || '').trim().toLowerCase();
|
|
3522
4680
|
if (orchestratorBot?.name) {
|
|
3523
|
-
if (['code', 'coding', 'coder', 'builder', 'developer', 'engineer']
|
|
4681
|
+
if (this.agentHasAnyRole(orchestratorBot, ['code', 'coding', 'coder', 'builder', 'developer', 'engineer'])) {
|
|
3524
4682
|
result.coding = orchestratorBot.name;
|
|
3525
4683
|
}
|
|
3526
|
-
else if (['qa', 'review', 'reviewer']
|
|
4684
|
+
else if (this.agentHasAnyRole(orchestratorBot, ['qa', 'review', 'reviewer'])) {
|
|
3527
4685
|
result.qa = orchestratorBot.name;
|
|
3528
4686
|
}
|
|
3529
|
-
else if (['research', 'researcher', 'analyst', 'advisor']
|
|
4687
|
+
else if (this.agentHasAnyRole(orchestratorBot, ['research', 'researcher', 'analyst', 'advisor', 'brainstorming', 'planning', 'design'])) {
|
|
3530
4688
|
result.research = orchestratorBot.name;
|
|
3531
4689
|
}
|
|
3532
4690
|
}
|
|
3533
|
-
const findByRole = (roles) => bots.find((bot) =>
|
|
4691
|
+
const findByRole = (roles) => bots.find((bot) => this.agentHasAnyRole(bot, roles));
|
|
3534
4692
|
result.coding = result.coding || findByRole(['code', 'coding', 'coder', 'builder', 'developer', 'engineer'])?.name;
|
|
3535
4693
|
result.qa = result.qa || findByRole(['qa', 'review', 'reviewer'])?.name;
|
|
3536
|
-
result.research = result.research || findByRole(['research', 'researcher', 'analyst', 'advisor'])?.name;
|
|
4694
|
+
result.research = result.research || findByRole(['research', 'researcher', 'analyst', 'advisor', 'brainstorming', 'planning', 'design'])?.name;
|
|
3537
4695
|
return result;
|
|
3538
4696
|
}
|
|
3539
|
-
listProjectScopedBots(projectId) {
|
|
4697
|
+
listProjectScopedBots(projectId, opts) {
|
|
4698
|
+
const maybeFilterOrchestrators = (bots) => opts?.includeOrchestratorProfiles ? bots : (0, orchestrator_profile_1.filterOutOrchestratorProfiles)(bots);
|
|
3540
4699
|
if (!projectId)
|
|
3541
|
-
return (
|
|
4700
|
+
return maybeFilterOrchestrators(data.listAgentProfiles());
|
|
3542
4701
|
const project = data.getProject(projectId);
|
|
3543
4702
|
const botIds = new Set(project?.bot_ids || []);
|
|
3544
4703
|
const allBots = data.listAgentProfiles();
|
|
3545
4704
|
const scoped = allBots.filter((bot) => botIds.has(bot.id));
|
|
3546
|
-
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();
|
|
3547
4790
|
}
|
|
3548
4791
|
policyRoleAssignments(policy) {
|
|
3549
4792
|
return {
|
|
@@ -3670,128 +4913,14 @@ class OrchestratorAgent {
|
|
|
3670
4913
|
isPolicyConfirmation(prompt) {
|
|
3671
4914
|
return /\b(yes|yes save it|save it|save this policy|apply it|apply this|confirm|confirmed|do that|sounds right)\b/i.test(prompt.trim());
|
|
3672
4915
|
}
|
|
3673
|
-
determinePolicyScope(prompt, projectId) {
|
|
3674
|
-
if (!projectId)
|
|
3675
|
-
return 'user';
|
|
3676
|
-
if (/\b(for|in|on|within)\s+(this|the)\s+project\b/i.test(prompt))
|
|
3677
|
-
return 'project';
|
|
3678
|
-
const project = data.getProject(projectId);
|
|
3679
|
-
if (project && new RegExp(`\\b${project.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i').test(prompt)) {
|
|
3680
|
-
return 'project';
|
|
3681
|
-
}
|
|
3682
|
-
return 'user';
|
|
3683
|
-
}
|
|
3684
|
-
extractPolicyPatch(prompt, assignments, currentPolicy) {
|
|
3685
|
-
const patch = {};
|
|
3686
|
-
if (/\b(?:you|o|orchestrator|project manager)\s+never\s+code(?:s)?\b|\bnot code\b/i.test(prompt))
|
|
3687
|
-
patch.allowOrchestratorCode = false;
|
|
3688
|
-
if (/\b(?:you|o|orchestrator|project manager)\s+never\s+qa\b|\bnot qa\b/i.test(prompt))
|
|
3689
|
-
patch.allowOrchestratorQa = false;
|
|
3690
|
-
if (/\b(?:you|o|orchestrator|project manager)\s+never\s+deploy(?:s)?\b|\bnot deploy\b/i.test(prompt))
|
|
3691
|
-
patch.allowOrchestratorDeploy = false;
|
|
3692
|
-
if (/\b(?:you|o|orchestrator|project manager)\s+never\s+simulate(?:s)?\b|\bdo not simulate\b|\bnot simulate\b/i.test(prompt))
|
|
3693
|
-
patch.allowOrchestratorSimulation = false;
|
|
3694
|
-
if (/\bcasual\b/i.test(prompt) && /\bdirect\b/i.test(prompt))
|
|
3695
|
-
patch.tone = 'casual_direct';
|
|
3696
|
-
else if (/\bcasual\b/i.test(prompt))
|
|
3697
|
-
patch.tone = 'casual';
|
|
3698
|
-
else if (/\bdirect\b/i.test(prompt))
|
|
3699
|
-
patch.tone = 'direct';
|
|
3700
|
-
if (/\bstrict\b/i.test(prompt))
|
|
3701
|
-
patch.policyEnforcementLevel = 'strict';
|
|
3702
|
-
if (/\bno more than (\d+)\s+times\b/i.test(prompt)) {
|
|
3703
|
-
patch.coderRetryLimit = Number(prompt.match(/\bno more than (\d+)\s+times\b/i)?.[1] || currentPolicy.coderRetryLimit || 2);
|
|
3704
|
-
}
|
|
3705
|
-
if (/\b30\s*seconds\b/i.test(prompt))
|
|
3706
|
-
patch.statusReportingMode = 'event_plus_heartbeat';
|
|
3707
|
-
if (assignments.research)
|
|
3708
|
-
patch.defaultIdeaReviewer = assignments.research;
|
|
3709
|
-
if (assignments.coding)
|
|
3710
|
-
patch.defaultCoder = assignments.coding;
|
|
3711
|
-
if (assignments.qa)
|
|
3712
|
-
patch.defaultQa = assignments.qa;
|
|
3713
|
-
return patch;
|
|
3714
|
-
}
|
|
3715
|
-
sanitizeInterpretedPolicyPatch(prompt, patch) {
|
|
3716
|
-
const sanitized = {};
|
|
3717
|
-
const normalized = prompt.toLowerCase();
|
|
3718
|
-
const mentionsCodePolicy = /\b(?:you|o|orchestrator|project manager)\b[\s\S]{0,30}\b(code|codes|coding)\b/.test(normalized);
|
|
3719
|
-
const mentionsQaPolicy = /\b(?:you|o|orchestrator|project manager)\b[\s\S]{0,30}\b(qa|review|test|testing)\b/.test(normalized);
|
|
3720
|
-
const mentionsDeployPolicy = /\b(?:you|o|orchestrator|project manager)\b[\s\S]{0,30}\bdeploy(?:s|ing)?\b/.test(normalized);
|
|
3721
|
-
const mentionsSimulationPolicy = /\b(?:you|o|orchestrator|project manager)\b[\s\S]{0,30}\bsimulat(?:e|es|ing|ion)\b/.test(normalized);
|
|
3722
|
-
if (patch.allowOrchestratorCode !== undefined && mentionsCodePolicy)
|
|
3723
|
-
sanitized.allowOrchestratorCode = patch.allowOrchestratorCode;
|
|
3724
|
-
if (patch.allowOrchestratorQa !== undefined && mentionsQaPolicy)
|
|
3725
|
-
sanitized.allowOrchestratorQa = patch.allowOrchestratorQa;
|
|
3726
|
-
if (patch.allowOrchestratorDeploy !== undefined && mentionsDeployPolicy)
|
|
3727
|
-
sanitized.allowOrchestratorDeploy = patch.allowOrchestratorDeploy;
|
|
3728
|
-
if (patch.allowOrchestratorSimulation !== undefined && mentionsSimulationPolicy)
|
|
3729
|
-
sanitized.allowOrchestratorSimulation = patch.allowOrchestratorSimulation;
|
|
3730
|
-
if (patch.defaultIdeaReviewer)
|
|
3731
|
-
sanitized.defaultIdeaReviewer = patch.defaultIdeaReviewer;
|
|
3732
|
-
if (patch.defaultCoder)
|
|
3733
|
-
sanitized.defaultCoder = patch.defaultCoder;
|
|
3734
|
-
if (patch.defaultBackendCoder)
|
|
3735
|
-
sanitized.defaultBackendCoder = patch.defaultBackendCoder;
|
|
3736
|
-
if (patch.defaultQa)
|
|
3737
|
-
sanitized.defaultQa = patch.defaultQa;
|
|
3738
|
-
if (patch.coderRetryLimit !== undefined)
|
|
3739
|
-
sanitized.coderRetryLimit = patch.coderRetryLimit;
|
|
3740
|
-
if (patch.statusReportingMode)
|
|
3741
|
-
sanitized.statusReportingMode = patch.statusReportingMode;
|
|
3742
|
-
if (patch.tone)
|
|
3743
|
-
sanitized.tone = patch.tone;
|
|
3744
|
-
if (patch.policyEnforcementLevel)
|
|
3745
|
-
sanitized.policyEnforcementLevel = patch.policyEnforcementLevel;
|
|
3746
|
-
return sanitized;
|
|
3747
|
-
}
|
|
3748
|
-
summarizePolicyPatch(patch) {
|
|
3749
|
-
const lines = [];
|
|
3750
|
-
if (patch.allowOrchestratorCode === false)
|
|
3751
|
-
lines.push('O should not code');
|
|
3752
|
-
if (patch.allowOrchestratorQa === false)
|
|
3753
|
-
lines.push('O should not QA');
|
|
3754
|
-
if (patch.allowOrchestratorDeploy === false)
|
|
3755
|
-
lines.push('O should not deploy');
|
|
3756
|
-
if (patch.allowOrchestratorSimulation === false)
|
|
3757
|
-
lines.push('O should not simulate work');
|
|
3758
|
-
if (patch.defaultIdeaReviewer)
|
|
3759
|
-
lines.push(`${patch.defaultIdeaReviewer} should review ideas by default`);
|
|
3760
|
-
if (patch.defaultCoder)
|
|
3761
|
-
lines.push(`${patch.defaultCoder} should code by default`);
|
|
3762
|
-
if (patch.defaultBackendCoder)
|
|
3763
|
-
lines.push(`${patch.defaultBackendCoder} should handle backend coding by default`);
|
|
3764
|
-
if (patch.defaultQa)
|
|
3765
|
-
lines.push(`${patch.defaultQa} should QA by default`);
|
|
3766
|
-
if (patch.coderRetryLimit !== undefined)
|
|
3767
|
-
lines.push(`Coder retry limit should be ${patch.coderRetryLimit}`);
|
|
3768
|
-
if (patch.statusReportingMode)
|
|
3769
|
-
lines.push(`Status reporting mode should be ${patch.statusReportingMode}`);
|
|
3770
|
-
if (patch.tone)
|
|
3771
|
-
lines.push(`Tone should be ${patch.tone.replace(/_/g, ' ')}`);
|
|
3772
|
-
if (patch.policyEnforcementLevel)
|
|
3773
|
-
lines.push(`Enforcement should be ${patch.policyEnforcementLevel}`);
|
|
3774
|
-
return lines;
|
|
3775
|
-
}
|
|
3776
4916
|
async handlePolicyUpdate(prompt, conversationId, intent, assignments, projectId, overview, currentPolicy) {
|
|
3777
|
-
const interpreted = await this.clerk.interpretPolicyUpdate({
|
|
3778
|
-
prompt,
|
|
3779
|
-
currentPolicy,
|
|
3780
|
-
projectName: overview?.project.name,
|
|
3781
|
-
}).catch(() => null);
|
|
3782
4917
|
const agentNames = data.listAgentProfiles().map((agent) => agent.name);
|
|
3783
|
-
const
|
|
3784
|
-
const
|
|
3785
|
-
...(0, policy_detection_1.extractPolicyPatchFromPrompt)(prompt, currentPolicy, { agentNames }),
|
|
3786
|
-
...sanitizedInterpretedPatch,
|
|
3787
|
-
};
|
|
3788
|
-
const changes = interpreted?.summaryLines?.length
|
|
3789
|
-
? interpreted.summaryLines
|
|
3790
|
-
: (0, policy_detection_1.summarizePolicyPatch)(patch);
|
|
4918
|
+
const patch = (0, policy_detection_1.extractPolicyPatchFromPrompt)(prompt, currentPolicy, { agentNames });
|
|
4919
|
+
const changes = (0, policy_detection_1.summarizePolicyPatch)(patch);
|
|
3791
4920
|
if (changes.length === 0) {
|
|
3792
4921
|
return this.formatPolicyUpdateResponse(prompt, intent, assignments, overview);
|
|
3793
4922
|
}
|
|
3794
|
-
const scope =
|
|
4923
|
+
const scope = (0, policy_detection_1.determinePolicyScope)(prompt, overview?.project.name || undefined);
|
|
3795
4924
|
data.setPendingOrchestrationPolicy({
|
|
3796
4925
|
conversationId,
|
|
3797
4926
|
projectId: scope === 'project' ? projectId || null : null,
|
|
@@ -3862,35 +4991,6 @@ class OrchestratorAgent {
|
|
|
3862
4991
|
}
|
|
3863
4992
|
return undefined;
|
|
3864
4993
|
}
|
|
3865
|
-
persistRoleAssignments(projectId, conversationId, agentId, assignments) {
|
|
3866
|
-
const merged = this.mergeRoleAssignments({}, this.loadRoleAssignments(projectId), this.deriveRoleAssignmentsFromProject(projectId), assignments);
|
|
3867
|
-
data.setProjectSetting(projectId, ORCHESTRATOR_ROLE_SETTING_KEY, JSON.stringify(merged));
|
|
3868
|
-
const parts = [
|
|
3869
|
-
merged.coding ? `${merged.coding} handles coding` : null,
|
|
3870
|
-
merged.qa ? `${merged.qa} handles QA` : null,
|
|
3871
|
-
merged.research ? `${merged.research} handles research` : null,
|
|
3872
|
-
].filter(Boolean);
|
|
3873
|
-
if (parts.length > 0 && (0, storage_mode_1.isServerStorageMode)()) {
|
|
3874
|
-
data.upsertMemoryFact({
|
|
3875
|
-
agentId,
|
|
3876
|
-
conversationId,
|
|
3877
|
-
factType: 'operating_instruction',
|
|
3878
|
-
content: `Default orchestrator assignments: ${parts.join('; ')}.`,
|
|
3879
|
-
extractionMethod: 'orchestrator',
|
|
3880
|
-
});
|
|
3881
|
-
}
|
|
3882
|
-
data.logAdminAudit({
|
|
3883
|
-
actorType: 'orchestrator',
|
|
3884
|
-
actorId: 'Project Manager',
|
|
3885
|
-
projectId,
|
|
3886
|
-
resourceType: 'project_setting',
|
|
3887
|
-
resourceId: `${projectId}:${ORCHESTRATOR_ROLE_SETTING_KEY}`,
|
|
3888
|
-
action: 'set_orchestrator_role_assignments',
|
|
3889
|
-
request: assignments,
|
|
3890
|
-
after: merged,
|
|
3891
|
-
result: 'ok',
|
|
3892
|
-
});
|
|
3893
|
-
}
|
|
3894
4994
|
roleAssignmentParticipants(assignments, agents) {
|
|
3895
4995
|
return Array.from(new Set([assignments.coding, assignments.qa, assignments.research]
|
|
3896
4996
|
.filter((name) => Boolean(name))
|