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/workflow-engine.js
CHANGED
|
@@ -49,6 +49,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
49
49
|
};
|
|
50
50
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
51
|
exports.WorkflowEngine = void 0;
|
|
52
|
+
exports.resolveWorkflowCliSessionTransportForTest = resolveWorkflowCliSessionTransportForTest;
|
|
53
|
+
exports.resolveWorkflowClaudeUseConptyForTest = resolveWorkflowClaudeUseConptyForTest;
|
|
54
|
+
exports.buildDeferredImageReferenceBlockForTest = buildDeferredImageReferenceBlockForTest;
|
|
55
|
+
exports.parseImageEscalationRequestForTest = parseImageEscalationRequestForTest;
|
|
52
56
|
exports.getWorkflowEngine = getWorkflowEngine;
|
|
53
57
|
const events_1 = require("events");
|
|
54
58
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -59,39 +63,147 @@ const approval_1 = require("./approval");
|
|
|
59
63
|
const clerk_model_1 = require("./clerk-model");
|
|
60
64
|
const status_parser_1 = require("./orchestration/status-parser");
|
|
61
65
|
const safeguards_1 = require("./orchestration/safeguards");
|
|
66
|
+
const config_1 = require("./config");
|
|
62
67
|
const data = __importStar(require("./local-data"));
|
|
63
68
|
const orchestrator_profile_1 = require("./orchestrator-profile");
|
|
64
69
|
const subscription_runtime_1 = require("./auth/subscription-runtime");
|
|
65
70
|
const runtime_context_1 = require("./runtime-context");
|
|
66
71
|
const local_cli_pty_manager_1 = require("./local-cli-pty-manager");
|
|
72
|
+
const codex_app_server_manager_1 = require("./codex-app-server-manager");
|
|
67
73
|
const cli_session_epoch_1 = require("./cli-session-epoch");
|
|
74
|
+
const cli_bootstrap_history_1 = require("./cli-bootstrap-history");
|
|
75
|
+
const context_window_1 = require("./context-window");
|
|
76
|
+
const capabilities_1 = require("./orchestration/capabilities");
|
|
68
77
|
// ─── Workflow Engine ─────────────────────────────────────────────
|
|
69
78
|
const MAX_STEP_ATTEMPTS = safeguards_1.SAFEGUARDS.MAX_AGENT_ATTEMPTS;
|
|
70
79
|
const MAX_STEPS = safeguards_1.SAFEGUARDS.MAX_WORKFLOW_STEPS;
|
|
71
80
|
function isInteractiveAuthFailure(text) {
|
|
72
|
-
return /\b(not logged in|please run \/login|unauthorized|invalid api key|authentication required)\b/i.test(text);
|
|
81
|
+
return /\b(not logged in|please run \/login|login required|unauthorized|invalid authorization|invalid api key|missing api key|authentication required|invalid authentication credentials|invalid bearer token|token expired|session expired|credentials expired|expired credentials|reauthenticate)\b/i.test(text);
|
|
73
82
|
}
|
|
74
83
|
const LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT = 2;
|
|
84
|
+
function shouldUseConptyForWorkflowClaudePty() {
|
|
85
|
+
// ConPTY can surface transient console-host flashes on Windows during
|
|
86
|
+
// long-running Claude PTY worker turns. Keep direct chat unchanged, but use
|
|
87
|
+
// the legacy hidden winpty backend for orchestration/workflow Claude runs.
|
|
88
|
+
return process.platform !== 'win32';
|
|
89
|
+
}
|
|
90
|
+
function isClaudeFreshWorkflowSessionStartupFailure(err) {
|
|
91
|
+
return err?.code === 'CLAUDE_FRESH_SESSION_STARTUP_TIMEOUT'
|
|
92
|
+
|| err?.name === 'ClaudeFreshSessionStartupTimeoutError'
|
|
93
|
+
|| /fresh session startup timed out/i.test(String(err?.message || err || ''));
|
|
94
|
+
}
|
|
95
|
+
function createWorkflowAbortError() {
|
|
96
|
+
const err = new Error('aborted');
|
|
97
|
+
err.name = 'AbortError';
|
|
98
|
+
err.code = 'ABORT_ERR';
|
|
99
|
+
return err;
|
|
100
|
+
}
|
|
101
|
+
function isWorkflowAbortError(err) {
|
|
102
|
+
return err?.name === 'AbortError'
|
|
103
|
+
|| err?.code === 'ABORT_ERR'
|
|
104
|
+
|| /\baborted\b/i.test(String(err?.message || err || ''));
|
|
105
|
+
}
|
|
106
|
+
function throwIfWorkflowAborted(abortSignal) {
|
|
107
|
+
if (!abortSignal?.aborted)
|
|
108
|
+
return;
|
|
109
|
+
throw createWorkflowAbortError();
|
|
110
|
+
}
|
|
75
111
|
function shouldRetrySelectedWorkflowRuntime(err) {
|
|
76
112
|
const text = String(err?.message || err || '').toLowerCase();
|
|
77
113
|
if (!text)
|
|
78
114
|
return false;
|
|
79
|
-
if (/\b(no api key|configure one in settings|not available on this machine|not installed|please run \/login|not logged in|invalid api key)\b/i.test(text)) {
|
|
115
|
+
if (/\b(no api key|missing api key|configure one in settings|not available on this machine|not installed|please run \/login|login required|not logged in|unauthorized|invalid authorization|invalid api key|authentication required|invalid authentication credentials|invalid bearer token|token expired|session expired|credentials expired|expired credentials|reauthenticate)\b/i.test(text)) {
|
|
80
116
|
return false;
|
|
81
117
|
}
|
|
82
|
-
return /\b(429|rate limit|timeout|timed out|temporar|temporarily|econnreset|etimedout|enotfound|econnrefused|socket hang up|network|try again|overloaded|busy)\b/i.test(text);
|
|
118
|
+
return /\b(429|rate limit|timeout|timed out|temporar|temporarily|econnreset|etimedout|enotfound|econnrefused|socket hang up|network|try again|overloaded|busy|no meaningful pty activity)\b/i.test(text);
|
|
83
119
|
}
|
|
84
|
-
async function pauseWorkflowRuntimeRetry(attempt) {
|
|
120
|
+
async function pauseWorkflowRuntimeRetry(attempt, abortSignal) {
|
|
85
121
|
const delayMs = attempt <= 1 ? 750 : 1500;
|
|
86
|
-
|
|
122
|
+
throwIfWorkflowAborted(abortSignal);
|
|
123
|
+
await Promise.race([
|
|
124
|
+
new Promise((resolve) => setTimeout(resolve, delayMs)),
|
|
125
|
+
new Promise((_, reject) => {
|
|
126
|
+
if (!abortSignal)
|
|
127
|
+
return;
|
|
128
|
+
const onAbort = () => {
|
|
129
|
+
abortSignal.removeEventListener('abort', onAbort);
|
|
130
|
+
reject(createWorkflowAbortError());
|
|
131
|
+
};
|
|
132
|
+
abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
133
|
+
}),
|
|
134
|
+
]);
|
|
135
|
+
throwIfWorkflowAborted(abortSignal);
|
|
87
136
|
}
|
|
88
|
-
function buildWorkflowPtyConversationKey(conversationId, step, workflowContext) {
|
|
137
|
+
function buildWorkflowPtyConversationKey(conversationId, step, workflowContext, cliSessionScopeKey) {
|
|
138
|
+
const explicitScope = String(cliSessionScopeKey || '').trim();
|
|
139
|
+
if (explicitScope)
|
|
140
|
+
return explicitScope;
|
|
89
141
|
if (conversationId)
|
|
90
142
|
return conversationId;
|
|
91
143
|
if (workflowContext?.workflowId)
|
|
92
144
|
return `workflow:${workflowContext.workflowId}`;
|
|
93
145
|
return `adhoc:${step.agentId}:${step.id}`;
|
|
94
146
|
}
|
|
147
|
+
function clearFailedWorkflowCliSessionEpoch(conversationId, botId, sessionId, cliSessionScopeKey) {
|
|
148
|
+
const normalizedSessionId = String(sessionId || '').trim();
|
|
149
|
+
if (!conversationId || !normalizedSessionId)
|
|
150
|
+
return;
|
|
151
|
+
try {
|
|
152
|
+
const cleared = data.clearCliSessionEpochIfMatches(conversationId, botId, normalizedSessionId, cliSessionScopeKey);
|
|
153
|
+
if (cleared) {
|
|
154
|
+
console.warn(`[workflow-engine] cleared poisoned cli_session_epoch conversationId=${conversationId} botId=${botId} sessionId=${normalizedSessionId}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
console.warn(`[workflow-engine] failed to clear cli_session_epoch conversationId=${conversationId} botId=${botId} sessionId=${normalizedSessionId}: ${err?.message || err}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function resolveWorkflowCliSessionTransport(providerName, isLocalCliSession) {
|
|
162
|
+
if (!isLocalCliSession)
|
|
163
|
+
return 'none';
|
|
164
|
+
if (providerName === 'codex-cli')
|
|
165
|
+
return 'codex-app-server';
|
|
166
|
+
return 'pty';
|
|
167
|
+
}
|
|
168
|
+
function resolveWorkflowCliSessionTransportForTest(providerName, isLocalCliSession) {
|
|
169
|
+
return resolveWorkflowCliSessionTransport(providerName, isLocalCliSession);
|
|
170
|
+
}
|
|
171
|
+
function resolveWorkflowClaudeUseConptyForTest() {
|
|
172
|
+
return shouldUseConptyForWorkflowClaudePty();
|
|
173
|
+
}
|
|
174
|
+
function listImageAttachments(attachments) {
|
|
175
|
+
return (attachments || []).filter((attachment) => attachment.mimeType?.startsWith('image/'));
|
|
176
|
+
}
|
|
177
|
+
function buildDeferredImageReferenceBlock(attachments) {
|
|
178
|
+
const imageAttachments = listImageAttachments(attachments);
|
|
179
|
+
if (imageAttachments.length === 0)
|
|
180
|
+
return '';
|
|
181
|
+
const lines = [
|
|
182
|
+
'[Original Image Context Available]',
|
|
183
|
+
...imageAttachments.map((attachment) => `- id: ${attachment.id} (${attachment.filename})`),
|
|
184
|
+
'Use the assigned task, prior worker artifacts, and any available memory/history search tools first.',
|
|
185
|
+
'If you still need the original image, reply exactly with one line:',
|
|
186
|
+
'ESCALATE_IMAGE <id>',
|
|
187
|
+
'Use one of the listed ids. Do not include any other text.',
|
|
188
|
+
];
|
|
189
|
+
return lines.join('\n');
|
|
190
|
+
}
|
|
191
|
+
function buildDeferredImageReferenceBlockForTest(attachments) {
|
|
192
|
+
return buildDeferredImageReferenceBlock(attachments);
|
|
193
|
+
}
|
|
194
|
+
function parseImageEscalationRequest(text, attachments) {
|
|
195
|
+
const imageAttachments = listImageAttachments(attachments);
|
|
196
|
+
if (imageAttachments.length === 0)
|
|
197
|
+
return null;
|
|
198
|
+
const match = String(text || '').match(/^\s*ESCALATE_IMAGE\s+([A-Za-z0-9._:-]+)\s*$/m);
|
|
199
|
+
if (!match)
|
|
200
|
+
return null;
|
|
201
|
+
const requestedId = match[1].trim();
|
|
202
|
+
return imageAttachments.find((attachment) => attachment.id === requestedId) || null;
|
|
203
|
+
}
|
|
204
|
+
function parseImageEscalationRequestForTest(text, attachments) {
|
|
205
|
+
return parseImageEscalationRequest(text, attachments)?.id || null;
|
|
206
|
+
}
|
|
95
207
|
class WorkflowEngine extends events_1.EventEmitter {
|
|
96
208
|
projectDir;
|
|
97
209
|
runtimeMode;
|
|
@@ -114,9 +226,16 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
114
226
|
const effectiveRuntimeMode = (0, runtime_context_1.normalizeRuntimeMode)(opts?.runtimeMode || this.runtimeMode);
|
|
115
227
|
const clerk = (0, clerk_model_1.getClerk)({ runtimeMode: effectiveRuntimeMode });
|
|
116
228
|
const allProfiles = data.listAgentProfiles();
|
|
117
|
-
// Filter out orchestrator
|
|
118
|
-
|
|
119
|
-
const agents =
|
|
229
|
+
// Filter out pure orchestrators but keep orchestrator bots that also have a worker role.
|
|
230
|
+
const PURE_ORCHESTRATOR_ROLES = new Set(['orchestrator', 'project manager']);
|
|
231
|
+
const agents = allProfiles.filter((agent) => {
|
|
232
|
+
const rc = String(agent.role_class || '').trim().toLowerCase();
|
|
233
|
+
if (agent.is_orchestrator === 1 && PURE_ORCHESTRATOR_ROLES.has(rc))
|
|
234
|
+
return false;
|
|
235
|
+
if (!agent.is_orchestrator && (0, orchestrator_profile_1.isOrchestratorProfile)(agent))
|
|
236
|
+
return false;
|
|
237
|
+
return true;
|
|
238
|
+
});
|
|
120
239
|
const profile = data.getAgentProfile(agentId) || agents[0];
|
|
121
240
|
if (!profile) {
|
|
122
241
|
throw new Error('No agent profile configured');
|
|
@@ -126,7 +245,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
126
245
|
let steps;
|
|
127
246
|
if (clerk && planningAgents.length > 1 && !opts?.disableDecomposition) {
|
|
128
247
|
// Ask clerk if this needs decomposition
|
|
129
|
-
steps = await this.decomposeTask(clerk, prompt, planningAgents, profile, opts?.roleAssignments);
|
|
248
|
+
steps = await this.decomposeTask(clerk, prompt, planningAgents, profile, opts?.roleAssignments, effectiveRuntimeMode);
|
|
130
249
|
}
|
|
131
250
|
else {
|
|
132
251
|
// Single agent, single step
|
|
@@ -323,8 +442,12 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
323
442
|
throw new Error(`Workflow step ${index + 1} references missing bot ${templateStep.agent_id}`);
|
|
324
443
|
}
|
|
325
444
|
const instruction = templateStep.instruction?.trim() || `Complete stage ${index + 1} for this TODO item.`;
|
|
326
|
-
|
|
327
|
-
|
|
445
|
+
// Role comes from the agent's structured role_class; checkpoint
|
|
446
|
+
// status comes from the template's explicit is_checkpoint flag
|
|
447
|
+
// or a QA-role agent. No prose scanning.
|
|
448
|
+
const agentRole = agent.role_class || undefined;
|
|
449
|
+
const agentIsQa = this.inferRoleForStep({ role: agentRole }) === 'qa';
|
|
450
|
+
const isCheckpointLike = templateStep.is_checkpoint === 1 || agentIsQa;
|
|
328
451
|
return {
|
|
329
452
|
id: templateStep.id,
|
|
330
453
|
index,
|
|
@@ -334,6 +457,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
334
457
|
agentName: agent.name,
|
|
335
458
|
provider: agent.provider,
|
|
336
459
|
model: agent.model,
|
|
460
|
+
role: agentRole,
|
|
337
461
|
dependsOn: index === 0 ? [] : [index - 1],
|
|
338
462
|
status: 'pending',
|
|
339
463
|
attempts: 0,
|
|
@@ -458,8 +582,27 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
458
582
|
/**
|
|
459
583
|
* Use clerk to decompose a complex task into sub-steps with agent routing.
|
|
460
584
|
*/
|
|
461
|
-
async decomposeTask(clerk, prompt, agents, defaultProfile, roleAssignments) {
|
|
585
|
+
async decomposeTask(clerk, prompt, agents, defaultProfile, roleAssignments, runtimeMode) {
|
|
462
586
|
const deterministicPlan = this.buildDeterministicPlan(prompt, agents, defaultProfile, roleAssignments);
|
|
587
|
+
if ((0, runtime_context_1.normalizeRuntimeMode)(runtimeMode || this.runtimeMode) === 'local_desktop') {
|
|
588
|
+
if (deterministicPlan)
|
|
589
|
+
return deterministicPlan;
|
|
590
|
+
return [{
|
|
591
|
+
id: 'step-0',
|
|
592
|
+
index: 0,
|
|
593
|
+
description: prompt.slice(0, 100),
|
|
594
|
+
prompt,
|
|
595
|
+
agentId: defaultProfile.id,
|
|
596
|
+
agentName: defaultProfile.name,
|
|
597
|
+
provider: defaultProfile.provider,
|
|
598
|
+
model: defaultProfile.model,
|
|
599
|
+
role: defaultProfile.role_class || undefined,
|
|
600
|
+
dependsOn: [],
|
|
601
|
+
status: 'pending',
|
|
602
|
+
attempts: 0,
|
|
603
|
+
maxAttempts: MAX_STEP_ATTEMPTS,
|
|
604
|
+
}];
|
|
605
|
+
}
|
|
463
606
|
try {
|
|
464
607
|
const plannedSteps = await clerk.planWorkflow({
|
|
465
608
|
prompt,
|
|
@@ -470,6 +613,11 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
470
613
|
const mapped = plannedSteps.slice(0, MAX_STEPS).map((step, index) => {
|
|
471
614
|
// Route to the named agent or default
|
|
472
615
|
const targetAgent = this.resolveStepAgent(step, agents, defaultProfile, roleAssignments);
|
|
616
|
+
// Prefer an explicit role on the planned step; otherwise carry the
|
|
617
|
+
// resolved agent's role so downstream code doesn't fall back to
|
|
618
|
+
// regex classification on prose.
|
|
619
|
+
const explicitRole = typeof step.role === 'string' && step.role.trim() ? step.role.trim().toLowerCase() : undefined;
|
|
620
|
+
const agentRole = targetAgent.role_class || undefined;
|
|
473
621
|
return {
|
|
474
622
|
id: `step-${index}`,
|
|
475
623
|
index,
|
|
@@ -479,6 +627,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
479
627
|
agentName: targetAgent.name,
|
|
480
628
|
provider: targetAgent.provider,
|
|
481
629
|
model: targetAgent.model,
|
|
630
|
+
role: explicitRole || agentRole,
|
|
482
631
|
dependsOn: step.dependsOn || [],
|
|
483
632
|
status: 'pending',
|
|
484
633
|
expectedOutput: step.expectedOutput || undefined,
|
|
@@ -504,7 +653,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
504
653
|
step.loopCount = 0;
|
|
505
654
|
}
|
|
506
655
|
}
|
|
507
|
-
return
|
|
656
|
+
return mapped;
|
|
508
657
|
}
|
|
509
658
|
}
|
|
510
659
|
catch {
|
|
@@ -554,6 +703,14 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
554
703
|
}
|
|
555
704
|
return steps.length >= 2 ? steps : null;
|
|
556
705
|
}
|
|
706
|
+
/**
|
|
707
|
+
* @deprecated Phase B of orchestration-plan.txt.
|
|
708
|
+
* The LLM-tool-dispatch path in local_desktop creates TODOs directly with
|
|
709
|
+
* the LLM's explicit step list, so this regex-based role inference no
|
|
710
|
+
* longer drives dispatch. Still called from decomposeTask's fallback for
|
|
711
|
+
* non-local_desktop and legacy planner paths. Remove after those are
|
|
712
|
+
* migrated to tool dispatch.
|
|
713
|
+
*/
|
|
557
714
|
determineDesiredRoleSequence(prompt, roleAssignments) {
|
|
558
715
|
const normalized = prompt.toLowerCase();
|
|
559
716
|
const explicitResearch = /\b(brain|gpt|research(?:es|ed|ing)?|investigat|analy[sz]e|analysis|sound idea|review the idea|pressure[- ]?test|brainstorm(?:ing)?)\b/.test(normalized)
|
|
@@ -607,6 +764,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
607
764
|
agentName: agent.name,
|
|
608
765
|
provider: agent.provider,
|
|
609
766
|
model: agent.model,
|
|
767
|
+
role: 'research',
|
|
610
768
|
dependsOn,
|
|
611
769
|
status: 'pending',
|
|
612
770
|
expectedOutput: 'Clear implementation guidance, risks, and recommendations for the downstream coder.',
|
|
@@ -623,6 +781,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
623
781
|
agentName: agent.name,
|
|
624
782
|
provider: agent.provider,
|
|
625
783
|
model: agent.model,
|
|
784
|
+
role: 'coding',
|
|
626
785
|
dependsOn,
|
|
627
786
|
status: 'pending',
|
|
628
787
|
expectedOutput: 'Completed deliverable that satisfies the user request.',
|
|
@@ -639,6 +798,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
639
798
|
agentName: agent.name,
|
|
640
799
|
provider: agent.provider,
|
|
641
800
|
model: agent.model,
|
|
801
|
+
role: 'qa',
|
|
642
802
|
dependsOn,
|
|
643
803
|
status: 'pending',
|
|
644
804
|
expectedOutput: 'PASS if the deliverable fully satisfies the request, otherwise FAIL with actionable defects.',
|
|
@@ -685,17 +845,45 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
685
845
|
: role === 'qa'
|
|
686
846
|
? 'Your role is QA. Verify the delivered work, then complete the TODO through the worker tool or create the next fix handoff.'
|
|
687
847
|
: 'Your role is implementer. Do the assigned work, then complete the TODO through the worker tool.';
|
|
848
|
+
const botSettingsPrompt = this.buildBotSettingsPrompt(profile);
|
|
849
|
+
const capabilityInstructions = profile
|
|
850
|
+
? (0, capabilities_1.buildCapabilityInstructionBlock)(data.getAgentRolePriorities(profile))
|
|
851
|
+
: '';
|
|
688
852
|
return [
|
|
689
|
-
`You are ${profile?.name || 'a workflow worker'}.`,
|
|
853
|
+
botSettingsPrompt || `You are ${profile?.name || 'a workflow worker'}.`,
|
|
854
|
+
'',
|
|
855
|
+
'[Orchestrated Worker Guardrails]',
|
|
690
856
|
'You are executing one orchestrated TODO task.',
|
|
691
857
|
roleLine,
|
|
692
858
|
'Follow the assignment in the user message exactly.',
|
|
693
859
|
'Use available tools when needed.',
|
|
694
860
|
'Do not return a plain-text final answer until you have called complete_worker_task or block_worker_task.',
|
|
861
|
+
'Do not create side TODOs with add_task while working an assigned TODO. Use complete_worker_task.handoff_prompt or insert_task when follow-up work is needed.',
|
|
695
862
|
'Do not add workflow planning, orchestration narration, or TODO-management commentary outside those tools.',
|
|
696
863
|
'If blocked after checking available context, call block_worker_task with blocker_summary, checked_context, and user_question.',
|
|
697
864
|
'If the task is complete, call complete_worker_task with output_summary and any needed handoff prompt or inserted next task.',
|
|
698
|
-
|
|
865
|
+
capabilityInstructions ? '' : null,
|
|
866
|
+
capabilityInstructions || null,
|
|
867
|
+
].filter(Boolean).join('\n');
|
|
868
|
+
}
|
|
869
|
+
buildBotSettingsPrompt(profile) {
|
|
870
|
+
if (!profile)
|
|
871
|
+
return '';
|
|
872
|
+
const directPrompt = String(profile.final_prompt || profile.soul_md || '').trim();
|
|
873
|
+
if (directPrompt)
|
|
874
|
+
return directPrompt;
|
|
875
|
+
const sections = [`You are ${profile.name}.`];
|
|
876
|
+
const addSection = (label, value) => {
|
|
877
|
+
const text = String(value || '').trim();
|
|
878
|
+
if (text)
|
|
879
|
+
sections.push('', label, text);
|
|
880
|
+
};
|
|
881
|
+
addSection('PURPOSE', profile.purpose_md);
|
|
882
|
+
addSection('IDENTITY', profile.identity_summary);
|
|
883
|
+
addSection('SKILLS', profile.skills_md);
|
|
884
|
+
addSection('TOOLS', profile.tools_md);
|
|
885
|
+
addSection('MEMORY', profile.memory_md);
|
|
886
|
+
return sections.join('\n');
|
|
699
887
|
}
|
|
700
888
|
buildOrchestratedTodoHandledResult(taskId) {
|
|
701
889
|
if (!taskId)
|
|
@@ -718,13 +906,18 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
718
906
|
return Boolean(data.getTodoTask(taskId, 'completed')
|
|
719
907
|
|| data.getTodoTask(taskId, 'active')?.blocker_summary);
|
|
720
908
|
}
|
|
721
|
-
buildWorkflowStepPrompt(step, pinnedContext, handoffArtifacts) {
|
|
722
|
-
const
|
|
909
|
+
buildWorkflowStepPrompt(step, pinnedContext, handoffArtifacts, handoffPrompts = [], localDesktopHandoffMode = false) {
|
|
910
|
+
const primaryPrompt = step.prompt.trim();
|
|
911
|
+
const sections = [primaryPrompt];
|
|
912
|
+
if (localDesktopHandoffMode && handoffPrompts.length > 0) {
|
|
913
|
+
const label = handoffPrompts.length === 1 ? 'Previous worker handoff' : 'Previous worker handoffs';
|
|
914
|
+
sections.push(`${label}:\n${handoffPrompts.join('\n\n')}`);
|
|
915
|
+
}
|
|
723
916
|
if (pinnedContext.length > 0) {
|
|
724
917
|
sections.push(`Pinned context:\n${pinnedContext.join('\n\n')}`);
|
|
725
918
|
}
|
|
726
919
|
if (handoffArtifacts.length > 0) {
|
|
727
|
-
sections.push(
|
|
920
|
+
sections.push(`${localDesktopHandoffMode ? 'Supporting artifacts from previous steps' : 'Input artifacts from previous steps'}:\n${handoffArtifacts.join('\n\n')}`);
|
|
728
921
|
}
|
|
729
922
|
return sections.join('\n\n');
|
|
730
923
|
}
|
|
@@ -766,68 +959,39 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
766
959
|
}
|
|
767
960
|
return defaultProfile;
|
|
768
961
|
}
|
|
962
|
+
/**
|
|
963
|
+
* Resolve a step's role. Prefers the structured `role` field set at plan
|
|
964
|
+
* creation (from an explicit planner decision or the bot's role_class).
|
|
965
|
+
* Falls through to a small normalizer that maps common role synonyms
|
|
966
|
+
* (e.g. "verify" → "qa") onto the three canonical workflow roles.
|
|
967
|
+
* Does NOT scan prose. Phase B of orchestration-plan.txt: pass/fail,
|
|
968
|
+
* routing, and task creation must come from structured data, not from
|
|
969
|
+
* English wording.
|
|
970
|
+
*/
|
|
769
971
|
inferRoleForStep(step) {
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
const researchPattern = /\b(research(?:es|ed|ing)?|investigat(?:e|es|ed|ing)|analysis|analy[sz](?:e|es|ed|ing)|compar(?:e|es|ed|ing)|inspect(?:s|ed|ing)?|look up|discover(?:s|ed|ing)?|defin(?:e|es|ed|ing)|direction|spec(?:ification)?s?|requirements?)\b/;
|
|
775
|
-
const codingPattern = /\b(code(?:s|d|ing)?|implement(?:s|ed|ing)?|build(?:s|ed|ing)?|creat(?:e|es|ed|ing)|write(?:s|n)?|writ(?:ing)?|fix(?:es|ed|ing)?|edit(?:s|ed|ing)?|updat(?:e|es|ed|ing)|refactor(?:s|ed|ing)?)\b/;
|
|
776
|
-
if (leadingVerbMatch) {
|
|
777
|
-
const verb = leadingVerbMatch[1];
|
|
778
|
-
if (/(qa|review|verif|validation|test|check)/.test(verb))
|
|
779
|
-
return 'qa';
|
|
780
|
-
if (/(research|investigat|analysis|analy|compar|inspect|discover|defin)/.test(verb))
|
|
781
|
-
return 'research';
|
|
782
|
-
if (/(build|creat|write|writ|fix|edit|updat|refactor|implement|code)/.test(verb))
|
|
783
|
-
return 'coding';
|
|
784
|
-
}
|
|
785
|
-
if (qaPattern.test(description) && !codingPattern.test(description))
|
|
972
|
+
const raw = typeof step.role === 'string' ? step.role.trim().toLowerCase() : '';
|
|
973
|
+
if (!raw)
|
|
974
|
+
return null;
|
|
975
|
+
if (raw === 'qa' || raw === 'review' || raw === 'reviewer' || raw === 'quality' || raw === 'quality assurance')
|
|
786
976
|
return 'qa';
|
|
787
|
-
if (
|
|
788
|
-
return '
|
|
789
|
-
if (
|
|
790
|
-
return 'research';
|
|
791
|
-
if (codingPattern.test(prompt) && !qaPattern.test(prompt))
|
|
792
|
-
return 'coding';
|
|
793
|
-
if (qaPattern.test(prompt))
|
|
977
|
+
if (raw === 'verify' || raw === 'verification')
|
|
978
|
+
return 'qa';
|
|
979
|
+
if (/\b(verify|verification)\b/.test(raw))
|
|
794
980
|
return 'qa';
|
|
795
|
-
if (
|
|
981
|
+
if (raw === 'research' || raw === 'researcher' || raw === 'analysis' || raw === 'analyst' || raw === 'planning' || raw === 'planner' || raw === 'brainstorming' || raw === 'design' || raw === 'designer')
|
|
796
982
|
return 'research';
|
|
797
|
-
if (
|
|
983
|
+
if (raw === 'coding' || raw === 'code' || raw === 'coder' || raw === 'build' || raw === 'building' || raw === 'builder' || raw === 'implement' || raw === 'implementation' || raw === 'dev' || raw === 'developer' || raw === 'engineering' || raw === 'commit' || raw === 'deploy')
|
|
798
984
|
return 'coding';
|
|
985
|
+
// Comma-separated multi-roles (legacy) — take the first canonical match.
|
|
986
|
+
if (!/[,/]/.test(raw))
|
|
987
|
+
return null;
|
|
988
|
+
for (const part of raw.split(/[,/]/).map((s) => s.trim()).filter(Boolean)) {
|
|
989
|
+
const mapped = this.inferRoleForStep({ role: part });
|
|
990
|
+
if (mapped)
|
|
991
|
+
return mapped;
|
|
992
|
+
}
|
|
799
993
|
return null;
|
|
800
994
|
}
|
|
801
|
-
pruneTrailingPostQaSteps(steps) {
|
|
802
|
-
const firstQaIndex = steps.findIndex((step) => this.inferRoleForStep(step) === 'qa');
|
|
803
|
-
if (firstQaIndex < 0)
|
|
804
|
-
return steps;
|
|
805
|
-
const shouldDrop = (step) => {
|
|
806
|
-
if (step.index <= firstQaIndex)
|
|
807
|
-
return false;
|
|
808
|
-
const normalized = `${step.description || ''} ${step.prompt || ''}`.toLowerCase();
|
|
809
|
-
return /\b(if applicable|based on qa feedback|based on feedback|revise|revision|finalize|confirm completion|compile instructions|ensure the file is accessible|ensure file is accessible)\b/.test(normalized);
|
|
810
|
-
};
|
|
811
|
-
const filtered = steps.filter((step) => !shouldDrop(step));
|
|
812
|
-
if (filtered.length === steps.length)
|
|
813
|
-
return steps;
|
|
814
|
-
return filtered.map((step, newIndex) => {
|
|
815
|
-
const oldIndex = step.index;
|
|
816
|
-
const retainedPriorSteps = filtered
|
|
817
|
-
.filter((candidate) => candidate.index < oldIndex)
|
|
818
|
-
.map((candidate) => candidate.index);
|
|
819
|
-
const reindexedDeps = (step.dependsOn || [])
|
|
820
|
-
.filter((dep) => retainedPriorSteps.includes(dep))
|
|
821
|
-
.map((dep) => filtered.findIndex((candidate) => candidate.index === dep))
|
|
822
|
-
.filter((dep) => dep >= 0);
|
|
823
|
-
return {
|
|
824
|
-
...step,
|
|
825
|
-
index: newIndex,
|
|
826
|
-
id: step.id || `step-${newIndex}`,
|
|
827
|
-
dependsOn: reindexedDeps,
|
|
828
|
-
};
|
|
829
|
-
});
|
|
830
|
-
}
|
|
831
995
|
findPriorCodingStepIndex(steps, currentIndex) {
|
|
832
996
|
const priorCodingStep = [...steps]
|
|
833
997
|
.filter((candidate) => candidate.index < currentIndex)
|
|
@@ -868,6 +1032,15 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
868
1032
|
const failed = new Set();
|
|
869
1033
|
const allowMissingFooterForDirectStep = !opts?.isOrchestrated && !!opts?.disableDecomposition && steps.length === 1;
|
|
870
1034
|
while (completed.size + failed.size < steps.length) {
|
|
1035
|
+
if (opts?.abortSignal?.aborted) {
|
|
1036
|
+
for (const step of steps) {
|
|
1037
|
+
if (step.status === 'pending' || step.status === 'running') {
|
|
1038
|
+
step.status = 'skipped';
|
|
1039
|
+
step.error = 'Cancelled by user';
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
break;
|
|
1043
|
+
}
|
|
871
1044
|
// Find steps that are ready to run (all deps met)
|
|
872
1045
|
const ready = steps.filter(s => s.status === 'pending' &&
|
|
873
1046
|
s.dependsOn.every(dep => completed.has(dep)));
|
|
@@ -883,10 +1056,22 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
883
1056
|
}
|
|
884
1057
|
// Execute ready steps in parallel
|
|
885
1058
|
await Promise.all(ready.map(async (step) => {
|
|
1059
|
+
if (opts?.abortSignal?.aborted) {
|
|
1060
|
+
step.status = 'skipped';
|
|
1061
|
+
step.error = 'Cancelled by user';
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
886
1064
|
// Build context from completed dependency results — pass only RESULT artifact
|
|
887
1065
|
const handoffArtifacts = [];
|
|
1066
|
+
const handoffPrompts = [];
|
|
1067
|
+
const localDesktopHandoffMode = (0, runtime_context_1.normalizeRuntimeMode)(opts?.runtimeMode || this.runtimeMode) === 'local_desktop'
|
|
1068
|
+
&& opts?.isOrchestrated === true;
|
|
888
1069
|
for (const depIndex of step.dependsOn) {
|
|
889
1070
|
const depStep = steps[depIndex];
|
|
1071
|
+
const dependencyHandoff = String(depStep?._handoffPrompt || '').trim();
|
|
1072
|
+
if (localDesktopHandoffMode && dependencyHandoff) {
|
|
1073
|
+
handoffPrompts.push(dependencyHandoff);
|
|
1074
|
+
}
|
|
890
1075
|
if (depStep?.result) {
|
|
891
1076
|
const footer = (0, status_parser_1.parseStructuredFooter)(depStep.result);
|
|
892
1077
|
const artifact = footer.result || depStep.result;
|
|
@@ -904,7 +1089,45 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
904
1089
|
}
|
|
905
1090
|
}
|
|
906
1091
|
}
|
|
907
|
-
const
|
|
1092
|
+
const initialImageDeliveryMode = opts?.workerImageDeliveryMode || 'inline';
|
|
1093
|
+
const fullPrompt = this.buildWorkflowStepPrompt(step, pinnedContext, handoffArtifacts, handoffPrompts, localDesktopHandoffMode);
|
|
1094
|
+
// approvalRequired gate: when enabled, emit an approval request
|
|
1095
|
+
// event and wait for the listener to resolve before proceeding.
|
|
1096
|
+
const orchCfg = (0, config_1.loadConfig)();
|
|
1097
|
+
if (orchCfg.approvalRequired === true) {
|
|
1098
|
+
const approved = await new Promise((resolve) => {
|
|
1099
|
+
const timeout = setTimeout(() => resolve(false), 300_000); // 5-minute timeout
|
|
1100
|
+
this.emit('approval-request', {
|
|
1101
|
+
workflowId,
|
|
1102
|
+
step: { id: step.id, index: step.index, description: step.description, agentName: step.agentName },
|
|
1103
|
+
totalSteps: steps.length,
|
|
1104
|
+
resolve: (result) => {
|
|
1105
|
+
clearTimeout(timeout);
|
|
1106
|
+
resolve(result);
|
|
1107
|
+
},
|
|
1108
|
+
});
|
|
1109
|
+
// If there are no listeners for approval-request, auto-approve
|
|
1110
|
+
// to avoid hanging when no approval manager is wired up.
|
|
1111
|
+
if (this.listenerCount('approval-request') === 0) {
|
|
1112
|
+
clearTimeout(timeout);
|
|
1113
|
+
resolve(true);
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
if (!approved) {
|
|
1117
|
+
step.status = 'failed';
|
|
1118
|
+
step.error = 'Workflow step rejected during approval';
|
|
1119
|
+
step.completedAt = Date.now();
|
|
1120
|
+
failed.add(step.index);
|
|
1121
|
+
for (const s of steps) {
|
|
1122
|
+
if (s.dependsOn.includes(step.index) && s.status === 'pending') {
|
|
1123
|
+
s.status = 'skipped';
|
|
1124
|
+
s.error = 'Stopped because a prior step was rejected';
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
this.emitProgress(workflowId, step, steps, 'step-failed', opts?.onProgress);
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
908
1131
|
step.status = 'running';
|
|
909
1132
|
step.startedAt = Date.now();
|
|
910
1133
|
this.emitProgress(workflowId, step, steps, 'step-started', opts?.onProgress, undefined, opts?.onWorkerChunk);
|
|
@@ -916,6 +1139,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
916
1139
|
}
|
|
917
1140
|
catch { /* best effort */ }
|
|
918
1141
|
try {
|
|
1142
|
+
throwIfWorkflowAborted(opts?.abortSignal);
|
|
919
1143
|
// Build previous steps summary for workflow context
|
|
920
1144
|
const prevStepsText = steps
|
|
921
1145
|
.filter(s => s.status === 'completed' && s.index < step.index)
|
|
@@ -934,10 +1158,43 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
934
1158
|
retryCount: step.attempts,
|
|
935
1159
|
loopCount: step.loopCount,
|
|
936
1160
|
};
|
|
937
|
-
|
|
1161
|
+
let activeImageDeliveryMode = initialImageDeliveryMode;
|
|
1162
|
+
let result = await this.executeStep(step, fullPrompt, conversationId, { ...opts, workerImageDeliveryMode: activeImageDeliveryMode }, opts?.apiKey, wfContext);
|
|
1163
|
+
const requestedImage = activeImageDeliveryMode === 'reference'
|
|
1164
|
+
? parseImageEscalationRequest(result, opts?.storedAttachments)
|
|
1165
|
+
: null;
|
|
1166
|
+
if (requestedImage) {
|
|
1167
|
+
throwIfWorkflowAborted(opts?.abortSignal);
|
|
1168
|
+
this.emitProgress(workflowId, step, steps, 'step-progress', opts?.onProgress, `${step.agentName} requested the original image for ${step.description}`);
|
|
1169
|
+
activeImageDeliveryMode = 'inline';
|
|
1170
|
+
result = await this.executeStep(step, fullPrompt, conversationId, { ...opts, workerImageDeliveryMode: activeImageDeliveryMode }, opts?.apiKey, wfContext);
|
|
1171
|
+
const repeatedEscalation = parseImageEscalationRequest(result, opts?.storedAttachments);
|
|
1172
|
+
if (repeatedEscalation) {
|
|
1173
|
+
step.status = 'failed';
|
|
1174
|
+
step.result = result;
|
|
1175
|
+
step.error = `Worker requested image escalation again after the original image was already provided (${repeatedEscalation.id}).`;
|
|
1176
|
+
step.completedAt = Date.now();
|
|
1177
|
+
failed.add(step.index);
|
|
1178
|
+
this.emitProgress(workflowId, step, steps, 'step-failed', opts?.onProgress, undefined, opts?.onWorkerChunk);
|
|
1179
|
+
try {
|
|
1180
|
+
const execId = step._execId;
|
|
1181
|
+
if (execId)
|
|
1182
|
+
data.updateStepExecution(execId, {
|
|
1183
|
+
status: 'failed',
|
|
1184
|
+
attempts: step.attempts,
|
|
1185
|
+
error: step.error,
|
|
1186
|
+
completedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
catch { /* best effort */ }
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
938
1193
|
if (opts?.isOrchestrated && opts.taskId) {
|
|
1194
|
+
throwIfWorkflowAborted(opts?.abortSignal);
|
|
939
1195
|
const completedTodo = data.getTodoTask(opts.taskId, 'completed');
|
|
940
1196
|
if (completedTodo) {
|
|
1197
|
+
step._handoffPrompt = completedTodo.handoff_prompt || '';
|
|
941
1198
|
step.status = 'completed';
|
|
942
1199
|
step.result = completedTodo.output_summary || completedTodo.handoff_prompt || result;
|
|
943
1200
|
step.completedAt = Date.now();
|
|
@@ -1214,6 +1471,31 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1214
1471
|
catch { /* best effort */ }
|
|
1215
1472
|
}
|
|
1216
1473
|
catch (err) {
|
|
1474
|
+
if (isWorkflowAbortError(err)) {
|
|
1475
|
+
step.status = 'skipped';
|
|
1476
|
+
step.error = 'Cancelled by user';
|
|
1477
|
+
step.completedAt = Date.now();
|
|
1478
|
+
for (const s of steps) {
|
|
1479
|
+
if (s.status === 'pending') {
|
|
1480
|
+
s.status = 'skipped';
|
|
1481
|
+
s.error = 'Cancelled by user';
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
this.emitProgress(workflowId, step, steps, 'step-failed', opts?.onProgress, undefined, opts?.onWorkerChunk);
|
|
1485
|
+
try {
|
|
1486
|
+
const execId = step._execId;
|
|
1487
|
+
if (execId) {
|
|
1488
|
+
data.updateStepExecution(execId, {
|
|
1489
|
+
status: 'failed',
|
|
1490
|
+
attempts: step.attempts,
|
|
1491
|
+
error: step.error,
|
|
1492
|
+
completedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
catch { /* best effort */ }
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1217
1499
|
const authLikeFailure = typeof err?.message === 'string'
|
|
1218
1500
|
&& (err.message.startsWith('AUTH_REQUIRED:')
|
|
1219
1501
|
|| /invalid_grant/i.test(err.message)
|
|
@@ -1274,14 +1556,40 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1274
1556
|
}
|
|
1275
1557
|
step.attempts++;
|
|
1276
1558
|
step.error = err.message;
|
|
1277
|
-
|
|
1559
|
+
const providerLimitFailure = typeof err?.message === 'string'
|
|
1560
|
+
? this.classifyProviderLimitFailure(err.message)
|
|
1561
|
+
: null;
|
|
1562
|
+
const terminalProviderFailure = typeof err?.message === 'string' && this.isTerminalProviderFailure(err.message);
|
|
1563
|
+
if (providerLimitFailure || terminalProviderFailure) {
|
|
1278
1564
|
step.status = 'failed';
|
|
1565
|
+
step.result = providerLimitFailure?.exactMessage || err.message;
|
|
1566
|
+
step.error = providerLimitFailure?.exactMessage || err.message;
|
|
1279
1567
|
step.completedAt = Date.now();
|
|
1280
1568
|
failed.add(step.index);
|
|
1281
1569
|
for (const s of steps) {
|
|
1282
1570
|
if (s.dependsOn.includes(step.index) && s.status === 'pending') {
|
|
1283
1571
|
s.status = 'skipped';
|
|
1284
|
-
s.error =
|
|
1572
|
+
s.error = providerLimitFailure
|
|
1573
|
+
? 'Stopped because a prior step hit a provider usage/rate limit'
|
|
1574
|
+
: 'Stopped because a prior step failed with a provider/runtime error';
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
if (opts?.taskId) {
|
|
1578
|
+
try {
|
|
1579
|
+
data.markTodoTaskRuntimeBlocked(opts.taskId, {
|
|
1580
|
+
actor: { actorType: 'orchestrator', actorId: 'Workflow Engine' },
|
|
1581
|
+
blockerSummary: providerLimitFailure?.exactMessage || String(err?.message || err),
|
|
1582
|
+
checkedContext: providerLimitFailure
|
|
1583
|
+
? 'Provider returned a quota/rate-limit error during worker execution. The exact provider message is preserved verbatim.'
|
|
1584
|
+
: 'Provider/runtime error during worker execution. The exact provider or PTY/app-server failure reason is preserved verbatim.',
|
|
1585
|
+
userQuestion: providerLimitFailure
|
|
1586
|
+
? 'Fix the provider account limit or wait until the provider retry window resets, then resume this plan.'
|
|
1587
|
+
: 'Fix the provider/runtime issue, then resume this plan.',
|
|
1588
|
+
reasonCode: providerLimitFailure?.reasonCode || 'provider_runtime_error',
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
catch (blockErr) {
|
|
1592
|
+
console.warn(`[workflow-engine] failed to mark TODO ${opts.taskId} blocked after provider/runtime failure: ${blockErr?.message || blockErr}`);
|
|
1285
1593
|
}
|
|
1286
1594
|
}
|
|
1287
1595
|
this.emitProgress(workflowId, step, steps, 'step-failed', opts?.onProgress, undefined, opts?.onWorkerChunk);
|
|
@@ -1292,6 +1600,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1292
1600
|
status: 'failed',
|
|
1293
1601
|
attempts: step.attempts,
|
|
1294
1602
|
error: step.error,
|
|
1603
|
+
resultSummary: step.result?.slice(0, 200),
|
|
1295
1604
|
completedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
|
|
1296
1605
|
});
|
|
1297
1606
|
}
|
|
@@ -1424,20 +1733,216 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1424
1733
|
const effectivePrompt = opts?.isOrchestrated && opts?.taskId
|
|
1425
1734
|
? `CURRENT TODO ID: ${opts.taskId}\nUse this TODO id when calling complete_worker_task or block_worker_task.\n\n${prompt}`
|
|
1426
1735
|
: prompt;
|
|
1427
|
-
const
|
|
1736
|
+
const workerImageDeliveryMode = opts?.workerImageDeliveryMode || 'inline';
|
|
1737
|
+
const deferredImageReferenceBlock = workerImageDeliveryMode === 'reference'
|
|
1738
|
+
? buildDeferredImageReferenceBlock(opts?.storedAttachments)
|
|
1739
|
+
: '';
|
|
1740
|
+
if (deferredImageReferenceBlock) {
|
|
1741
|
+
systemPrompt = [
|
|
1742
|
+
systemPrompt,
|
|
1743
|
+
'',
|
|
1744
|
+
'If the user message includes an "Original Image Context Available" section and you truly need the original image, reply exactly with ESCALATE_IMAGE <id> and no other text.',
|
|
1745
|
+
].join('\n');
|
|
1746
|
+
}
|
|
1747
|
+
const promptWithDeferredImageNote = deferredImageReferenceBlock
|
|
1748
|
+
? `${effectivePrompt}\n\n${deferredImageReferenceBlock}`
|
|
1749
|
+
: effectivePrompt;
|
|
1750
|
+
// Build user message — include native images if the worker's provider supports them
|
|
1751
|
+
const workerAttachments = opts?.storedAttachments;
|
|
1752
|
+
const workerSupportsImages = activeRuntime.providerName === 'anthropic'
|
|
1753
|
+
|| activeRuntime.providerName === 'openai'
|
|
1754
|
+
|| activeRuntime.providerName === 'google'
|
|
1755
|
+
|| activeRuntime.providerName === 'codex-cli'
|
|
1756
|
+
|| activeRuntime.providerName === 'claude-cli';
|
|
1757
|
+
let userContent = promptWithDeferredImageNote;
|
|
1758
|
+
if (workerImageDeliveryMode === 'inline' && workerAttachments && workerAttachments.length > 0 && workerSupportsImages) {
|
|
1759
|
+
const parts = [
|
|
1760
|
+
{ type: 'text', text: promptWithDeferredImageNote },
|
|
1761
|
+
];
|
|
1762
|
+
for (const att of workerAttachments) {
|
|
1763
|
+
if (att.storagePath && att.mimeType?.startsWith('image/')) {
|
|
1764
|
+
try {
|
|
1765
|
+
const imageData = fs_1.default.readFileSync(att.storagePath).toString('base64');
|
|
1766
|
+
parts.push({ type: 'image', mimeType: att.mimeType, data: imageData });
|
|
1767
|
+
}
|
|
1768
|
+
catch { /* file may not exist */ }
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
if (parts.length > 1)
|
|
1772
|
+
userContent = parts;
|
|
1773
|
+
}
|
|
1774
|
+
const messages = [{ role: 'user', content: userContent }];
|
|
1428
1775
|
const promptChars = effectivePrompt.length;
|
|
1429
1776
|
const systemChars = systemPrompt.length;
|
|
1430
1777
|
const isLocalCliSession = effectiveRuntimeMode === 'local_desktop'
|
|
1431
1778
|
&& (activeRuntime.providerName === 'claude-cli' || activeRuntime.providerName === 'codex-cli');
|
|
1779
|
+
const workflowCliSessionTransport = resolveWorkflowCliSessionTransport(activeRuntime.providerName, isLocalCliSession);
|
|
1780
|
+
const cliSessionScopeKey = String(opts?.cliSessionScopeKey || '').trim() || null;
|
|
1432
1781
|
const hasPersistentConversation = isLocalCliSession && !!conversationId;
|
|
1433
1782
|
const cliSessionEpochPlan = hasPersistentConversation
|
|
1434
|
-
? (0, cli_session_epoch_1.
|
|
1435
|
-
: {
|
|
1783
|
+
? (0, cli_session_epoch_1.evaluateWarmSessionReuse)(conversationId, step.agentId, activeRuntime.providerName, undefined, cliSessionScopeKey)
|
|
1784
|
+
: {
|
|
1785
|
+
outcome: 'cold_start',
|
|
1786
|
+
existing: undefined,
|
|
1787
|
+
resumeSessionId: null,
|
|
1788
|
+
resetReason: null,
|
|
1789
|
+
topicId: null,
|
|
1790
|
+
};
|
|
1436
1791
|
let activeCliSessionId = cliSessionEpochPlan.resumeSessionId;
|
|
1437
1792
|
const cliEpochStartedAt = cliSessionEpochPlan.resumeSessionId
|
|
1438
1793
|
? (cliSessionEpochPlan.existing?.epoch_started_at || (0, cli_session_epoch_1.localTimestamp)())
|
|
1439
1794
|
: (0, cli_session_epoch_1.localTimestamp)();
|
|
1440
|
-
const ptyConversationKey = buildWorkflowPtyConversationKey(conversationId, step, workflowContext);
|
|
1795
|
+
const ptyConversationKey = buildWorkflowPtyConversationKey(conversationId, step, workflowContext, cliSessionScopeKey);
|
|
1796
|
+
let cliEpochResetReason = cliSessionEpochPlan.resetReason;
|
|
1797
|
+
// Resolve prompt context window early for session launch classification,
|
|
1798
|
+
// cross-bot context injection, and bootstrap decisions.
|
|
1799
|
+
const promptContextWindow = hasPersistentConversation
|
|
1800
|
+
? (0, context_window_1.getPromptContextWindow)(conversationId, 5, {
|
|
1801
|
+
targetBotId: step.agentId,
|
|
1802
|
+
targetBotName: step.agentName,
|
|
1803
|
+
})
|
|
1804
|
+
: { summary: null, turns: [], sourceConversationId: conversationId || '', carriedForward: false, mode: 'new_topic', allowBootstrap: false, lastCrossBotTurn: null };
|
|
1805
|
+
// Session lifecycle logging — mirrors local-server.ts invariant plus
|
|
1806
|
+
// structured telemetry events for diagnostics.
|
|
1807
|
+
const sessionLaunchReason = cliSessionEpochPlan.resumeSessionId
|
|
1808
|
+
? 'resumed'
|
|
1809
|
+
: promptContextWindow.mode === 'new_topic'
|
|
1810
|
+
? 'new_topic/no_context'
|
|
1811
|
+
: cliSessionEpochPlan.resetReason
|
|
1812
|
+
? cliSessionEpochPlan.resetReason
|
|
1813
|
+
: 'fresh_with_bootstrap';
|
|
1814
|
+
console.info(`[workflow-engine] session_launch_reason=${sessionLaunchReason} agent=${step.agentName} provider=${activeRuntime.providerName} conversationId=${conversationId || '(none)'} resumeSessionId=${cliSessionEpochPlan.resumeSessionId || '(none)'} mode=${promptContextWindow.mode || 'unknown'} bootstrap=${!!promptContextWindow.allowBootstrap}`);
|
|
1815
|
+
console.info(`[workflow-engine] cli_session_selected conversationId=${conversationId || '(none)'} botId=${step.agentId} provider=${activeRuntime.providerName} selectedSessionId=${cliSessionEpochPlan.resumeSessionId || '(none)'} resetReason=${cliSessionEpochPlan.resetReason || '(none)'}`);
|
|
1816
|
+
if (workflowCliSessionTransport === 'codex-app-server'
|
|
1817
|
+
&& cliSessionEpochPlan.outcome === 'epoch_reset'
|
|
1818
|
+
&& cliSessionEpochPlan.resetReason === 'token_limit') {
|
|
1819
|
+
console.info(`[workflow-engine] codex_thread_reset_token_limit conversationId=${conversationId || '(none)'} botId=${step.agentId} provider=${activeRuntime.providerName} previousSessionId=${cliSessionEpochPlan.existing?.session_id || '(none)'} topicId=${cliSessionEpochPlan.topicId || '(none)'}`);
|
|
1820
|
+
}
|
|
1821
|
+
if (cliSessionEpochPlan.resumeSessionId) {
|
|
1822
|
+
console.info(`[workflow-engine] cli_session_resuming conversationId=${conversationId || '(none)'} botId=${step.agentId} provider=${activeRuntime.providerName} sessionId=${cliSessionEpochPlan.resumeSessionId}`);
|
|
1823
|
+
}
|
|
1824
|
+
else if (promptContextWindow.mode === 'new_topic') {
|
|
1825
|
+
console.info(`[workflow-engine] cli_session_fresh_without_bootstrap_new_topic conversationId=${conversationId || '(none)'} botId=${step.agentId} provider=${activeRuntime.providerName}`);
|
|
1826
|
+
}
|
|
1827
|
+
else if (promptContextWindow.allowBootstrap) {
|
|
1828
|
+
console.info(`[workflow-engine] cli_session_fresh_bootstrap_applied conversationId=${conversationId || '(none)'} botId=${step.agentId} provider=${activeRuntime.providerName}`);
|
|
1829
|
+
}
|
|
1830
|
+
else {
|
|
1831
|
+
console.warn(`[workflow-engine] cli_session_fresh_without_bootstrap_unexpected conversationId=${conversationId || '(none)'} botId=${step.agentId} provider=${activeRuntime.providerName} mode=${promptContextWindow.mode || 'unknown'}`);
|
|
1832
|
+
}
|
|
1833
|
+
// For initial fresh sessions (not resumed) with existing context, write
|
|
1834
|
+
// bootstrap history and inject cross-bot context into the user message.
|
|
1835
|
+
// This ensures the invariant: fresh session for existing context = bootstrap required.
|
|
1836
|
+
if (hasPersistentConversation
|
|
1837
|
+
&& !cliSessionEpochPlan.resumeSessionId
|
|
1838
|
+
&& promptContextWindow.allowBootstrap
|
|
1839
|
+
&& conversationId) {
|
|
1840
|
+
const topicId = data.getPrimaryTopicIdForConversation(conversationId) || undefined;
|
|
1841
|
+
const bootstrapHistoryFilePath = (0, cli_bootstrap_history_1.writeConversationBootstrapHistoryFile)({
|
|
1842
|
+
conversationId,
|
|
1843
|
+
topicId,
|
|
1844
|
+
botId: step.agentId,
|
|
1845
|
+
botName: step.agentName,
|
|
1846
|
+
});
|
|
1847
|
+
if (bootstrapHistoryFilePath) {
|
|
1848
|
+
const bootstrapInstruction = [
|
|
1849
|
+
`Please read the history file at: ${bootstrapHistoryFilePath}`,
|
|
1850
|
+
'Use it for context only.',
|
|
1851
|
+
'There is no need to mention the file, its path, or that you loaded context unless the user explicitly asks about it.',
|
|
1852
|
+
'Then respond to the user request below.',
|
|
1853
|
+
'',
|
|
1854
|
+
].join('\n');
|
|
1855
|
+
const currentUserContent = messages[0]?.content;
|
|
1856
|
+
const currentText = typeof currentUserContent === 'string'
|
|
1857
|
+
? currentUserContent
|
|
1858
|
+
: Array.isArray(currentUserContent)
|
|
1859
|
+
? currentUserContent.filter((p) => p?.type === 'text').map((p) => p.text).join('\n')
|
|
1860
|
+
: String(currentUserContent || '');
|
|
1861
|
+
messages[0] = { role: 'user', content: `${bootstrapInstruction}${currentText}` };
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
// Inject cross-bot context for fresh sessions (not resumed) when the
|
|
1865
|
+
// previous turn was from a different bot. Do not rely on CLI session
|
|
1866
|
+
// memory for cross-bot context.
|
|
1867
|
+
if (hasPersistentConversation
|
|
1868
|
+
&& !cliSessionEpochPlan.resumeSessionId
|
|
1869
|
+
&& promptContextWindow.lastCrossBotTurn) {
|
|
1870
|
+
const crossBotBlock = (0, context_window_1.formatCrossBotPreviousTurn)(promptContextWindow.lastCrossBotTurn);
|
|
1871
|
+
const currentUserContent = messages[0]?.content;
|
|
1872
|
+
if (typeof currentUserContent === 'string') {
|
|
1873
|
+
messages[0] = { role: 'user', content: `${crossBotBlock}\n\n${currentUserContent}` };
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
// For resumed sessions, inject cross-bot context into the user message
|
|
1877
|
+
// since the CLI session memory only has same-bot context.
|
|
1878
|
+
if (hasPersistentConversation
|
|
1879
|
+
&& !!cliSessionEpochPlan.resumeSessionId
|
|
1880
|
+
&& promptContextWindow.lastCrossBotTurn) {
|
|
1881
|
+
const crossBotBlock = (0, context_window_1.formatCrossBotPreviousTurn)(promptContextWindow.lastCrossBotTurn);
|
|
1882
|
+
const currentUserContent = messages[0]?.content;
|
|
1883
|
+
if (typeof currentUserContent === 'string') {
|
|
1884
|
+
messages[0] = { role: 'user', content: `${crossBotBlock}\n\n${currentUserContent}` };
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
// Bootstrap fallback: when a resumed CLI session fails, rebuild the prompt
|
|
1888
|
+
// with bootstrap context (running summary + last 5 turns) before retrying
|
|
1889
|
+
// with a fresh session. Ensures prompt-building path and session runtime
|
|
1890
|
+
// reality always agree.
|
|
1891
|
+
let freshCliBootstrapFallbackApplied = false;
|
|
1892
|
+
let freshCliBootstrapFileWritten = false;
|
|
1893
|
+
const applyFreshCliBootstrapFallback = (reason) => {
|
|
1894
|
+
if (freshCliBootstrapFallbackApplied)
|
|
1895
|
+
return;
|
|
1896
|
+
freshCliBootstrapFallbackApplied = true;
|
|
1897
|
+
cliEpochResetReason = reason;
|
|
1898
|
+
if (!conversationId)
|
|
1899
|
+
return;
|
|
1900
|
+
const topicId = data.getPrimaryTopicIdForConversation(conversationId) || undefined;
|
|
1901
|
+
const freshPromptContextWindow = (0, context_window_1.getPromptContextWindow)(conversationId, 5, {
|
|
1902
|
+
targetBotId: step.agentId,
|
|
1903
|
+
targetBotName: step.agentName,
|
|
1904
|
+
});
|
|
1905
|
+
const bootstrapHistoryFilePath = freshPromptContextWindow.allowBootstrap
|
|
1906
|
+
? (0, cli_bootstrap_history_1.writeConversationBootstrapHistoryFile)({
|
|
1907
|
+
conversationId,
|
|
1908
|
+
topicId,
|
|
1909
|
+
botId: step.agentId,
|
|
1910
|
+
botName: step.agentName,
|
|
1911
|
+
})
|
|
1912
|
+
: null;
|
|
1913
|
+
freshCliBootstrapFileWritten = !!bootstrapHistoryFilePath;
|
|
1914
|
+
if (bootstrapHistoryFilePath) {
|
|
1915
|
+
const bootstrapInstruction = [
|
|
1916
|
+
`Please read the history file at: ${bootstrapHistoryFilePath}`,
|
|
1917
|
+
'Use it for context only.',
|
|
1918
|
+
'There is no need to mention the file, its path, or that you loaded context unless the user explicitly asks about it.',
|
|
1919
|
+
'Then respond to the user request below.',
|
|
1920
|
+
'',
|
|
1921
|
+
].join('\n');
|
|
1922
|
+
const currentUserContent = messages[0]?.content;
|
|
1923
|
+
const currentText = typeof currentUserContent === 'string'
|
|
1924
|
+
? currentUserContent
|
|
1925
|
+
: Array.isArray(currentUserContent)
|
|
1926
|
+
? currentUserContent.filter((p) => p?.type === 'text').map((p) => p.text).join('\n')
|
|
1927
|
+
: String(currentUserContent || '');
|
|
1928
|
+
messages[0] = { role: 'user', content: `${bootstrapInstruction}${currentText}` };
|
|
1929
|
+
}
|
|
1930
|
+
if (freshPromptContextWindow.lastCrossBotTurn) {
|
|
1931
|
+
const crossBotBlock = (0, context_window_1.formatCrossBotPreviousTurn)(freshPromptContextWindow.lastCrossBotTurn);
|
|
1932
|
+
const currentUserContent = messages[0]?.content;
|
|
1933
|
+
if (typeof currentUserContent === 'string') {
|
|
1934
|
+
messages[0] = { role: 'user', content: `${crossBotBlock}\n\n${currentUserContent}` };
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
console.info(`[workflow-engine] session_launch_reason=resume_failed agent=${step.agentName} provider=${activeRuntime.providerName} conversationId=${conversationId} bootstrap=${!!bootstrapHistoryFilePath}`);
|
|
1938
|
+
console.warn(`[workflow-engine] cli_session_resume_failed conversationId=${conversationId || '(none)'} botId=${step.agentId} provider=${activeRuntime.providerName} bootstrap=${!!bootstrapHistoryFilePath}`);
|
|
1939
|
+
if (bootstrapHistoryFilePath) {
|
|
1940
|
+
console.info(`[workflow-engine] cli_session_fresh_bootstrap_applied conversationId=${conversationId || '(none)'} botId=${step.agentId} provider=${activeRuntime.providerName} bootstrapReason=resume_failed`);
|
|
1941
|
+
}
|
|
1942
|
+
else {
|
|
1943
|
+
console.warn(`[workflow-engine] cli_session_fresh_without_bootstrap_unexpected conversationId=${conversationId || '(none)'} botId=${step.agentId} provider=${activeRuntime.providerName} bootstrapReason=resume_failed`);
|
|
1944
|
+
}
|
|
1945
|
+
};
|
|
1441
1946
|
console.info(`[workflow-engine] step runtime: agent=${step.agentName} provider=${activeRuntime.providerName} model=${activeRuntime.model} runtime=${activeRuntime.runtimeLabel || 'unknown'} promptChars=${promptChars} systemChars=${systemChars} tools=${opts?.disableTools ? 0 : toolDefs.length}`);
|
|
1442
1947
|
// Agentic loop
|
|
1443
1948
|
let iteration = 0;
|
|
@@ -1445,6 +1950,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1445
1950
|
let incompleteExecutionRecoveries = 0;
|
|
1446
1951
|
let selectedRuntimeRetryCount = 0;
|
|
1447
1952
|
while (iteration < MAX_ITERATIONS) {
|
|
1953
|
+
throwIfWorkflowAborted(opts?.abortSignal);
|
|
1448
1954
|
if (opts?.isOrchestrated && this.orchestratedTodoHandled(opts.taskId)) {
|
|
1449
1955
|
return this.buildOrchestratedTodoHandledResult(opts.taskId);
|
|
1450
1956
|
}
|
|
@@ -1464,38 +1970,285 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1464
1970
|
: 'auto';
|
|
1465
1971
|
let response;
|
|
1466
1972
|
try {
|
|
1467
|
-
if (
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1973
|
+
if (workflowCliSessionTransport === 'codex-app-server') {
|
|
1974
|
+
const codexAppServerManager = (0, codex_app_server_manager_1.getCodexAppServerManager)();
|
|
1975
|
+
let appServerAttempt = 0;
|
|
1976
|
+
while (true) {
|
|
1977
|
+
throwIfWorkflowAborted(opts?.abortSignal);
|
|
1978
|
+
appServerAttempt++;
|
|
1979
|
+
try {
|
|
1980
|
+
console.info(`[workflow-engine] codex_app_server_turn_start workflow=${workflowContext?.workflowId || '(none)'} step=${workflowContext?.stepIndex ?? 0} agent=${step.agentName} conversationId=${ptyConversationKey} taskId=${opts?.taskId ?? '(none)'} attempt=${appServerAttempt}`);
|
|
1981
|
+
const appServerResult = await codexAppServerManager.runTurn({
|
|
1982
|
+
runtimeMode: 'local_desktop',
|
|
1983
|
+
conversationId: ptyConversationKey,
|
|
1984
|
+
botId: step.agentId,
|
|
1985
|
+
botName: step.agentName,
|
|
1986
|
+
cwd: effectiveProjectDir,
|
|
1987
|
+
systemPrompt,
|
|
1988
|
+
messages,
|
|
1989
|
+
abortSignal: opts?.abortSignal,
|
|
1990
|
+
forceFreshSession: !hasPersistentConversation || (iteration === 1 && !codexAppServerManager.hasActiveSession(ptyConversationKey, step.agentId) && !cliSessionEpochPlan.resumeSessionId),
|
|
1991
|
+
resumeSessionId: cliSessionEpochPlan.resumeSessionId || undefined,
|
|
1992
|
+
model: activeRuntime.model,
|
|
1993
|
+
projectId: opts?.projectId ?? null,
|
|
1994
|
+
currentTodoTaskId: opts?.taskId,
|
|
1995
|
+
codexSettings: {
|
|
1996
|
+
reasoningEffort: profile?.codex_reasoning_effort,
|
|
1997
|
+
reasoningSummary: profile?.codex_reasoning_summary,
|
|
1998
|
+
personality: profile?.codex_personality,
|
|
1999
|
+
serviceTier: profile?.codex_service_tier,
|
|
2000
|
+
sandboxPolicy: profile?.codex_sandbox_policy,
|
|
2001
|
+
approvalPolicy: profile?.codex_approval_policy,
|
|
2002
|
+
},
|
|
2003
|
+
onChunk: async (chunk) => {
|
|
2004
|
+
streamedContent += chunk;
|
|
2005
|
+
if (opts?.onWorkerChunk) {
|
|
2006
|
+
opts.onWorkerChunk({
|
|
2007
|
+
type: opts?.isOrchestrated ? 'worker_chunk' : 'chunk',
|
|
2008
|
+
stepId: step.id,
|
|
2009
|
+
agentName: step.agentName,
|
|
2010
|
+
description: step.description,
|
|
2011
|
+
stepIndex: workflowContext?.stepIndex ?? 0,
|
|
2012
|
+
totalSteps: workflowContext?.totalSteps ?? 1,
|
|
2013
|
+
text: chunk,
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
const now = Date.now();
|
|
2017
|
+
if (now - lastChunkProgressAt < 8000)
|
|
2018
|
+
return;
|
|
2019
|
+
lastChunkProgressAt = now;
|
|
2020
|
+
const activeSteps = workflowContext?.workflowId
|
|
2021
|
+
? this.getActiveWorkflows().find((w) => w.id === workflowContext.workflowId)?.steps || [step]
|
|
2022
|
+
: [step];
|
|
2023
|
+
this.emitProgress(workflowContext?.workflowId || 'workflow', step, activeSteps, 'step-progress', opts?.onProgress, `${step.agentName} is still working on ${step.description}`);
|
|
2024
|
+
},
|
|
2025
|
+
// Forward Codex app-server tool/file events as worker_terminal_chunk-style detail lines.
|
|
2026
|
+
// This populates the worker child card details panel for Codex workers.
|
|
2027
|
+
onDetail: async (detail) => {
|
|
2028
|
+
if (!detail || !opts?.onWorkerChunk)
|
|
2029
|
+
return;
|
|
2030
|
+
opts.onWorkerChunk({
|
|
2031
|
+
type: 'worker_terminal_chunk',
|
|
2032
|
+
stepId: step.id,
|
|
2033
|
+
agentName: step.agentName,
|
|
2034
|
+
description: step.description,
|
|
2035
|
+
stepIndex: workflowContext?.stepIndex ?? 0,
|
|
2036
|
+
totalSteps: workflowContext?.totalSteps ?? 1,
|
|
2037
|
+
rawText: detail,
|
|
2038
|
+
});
|
|
2039
|
+
},
|
|
2040
|
+
// Forward structured tool call/result events from Codex app-server
|
|
2041
|
+
// (command_execution, file_change, mcpToolCall items) as
|
|
2042
|
+
// worker_tool_call / worker_tool_result events. This gives Codex
|
|
2043
|
+
// workers the same structured tool visibility as API-key workers.
|
|
2044
|
+
onToolEvent: async (event) => {
|
|
2045
|
+
if (!opts?.onWorkerChunk)
|
|
2046
|
+
return;
|
|
2047
|
+
if (event.kind === 'call') {
|
|
2048
|
+
opts.onWorkerChunk({
|
|
2049
|
+
type: opts?.isOrchestrated ? 'worker_tool_call' : 'tool_call',
|
|
2050
|
+
stepId: step.id,
|
|
2051
|
+
agentName: step.agentName,
|
|
2052
|
+
description: step.description,
|
|
2053
|
+
stepIndex: workflowContext?.stepIndex ?? 0,
|
|
2054
|
+
totalSteps: workflowContext?.totalSteps ?? 1,
|
|
2055
|
+
toolCallId: event.toolCallId,
|
|
2056
|
+
toolName: event.toolName,
|
|
2057
|
+
toolArguments: event.arguments,
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
else {
|
|
2061
|
+
opts.onWorkerChunk({
|
|
2062
|
+
type: opts?.isOrchestrated ? 'worker_tool_result' : 'tool_result',
|
|
2063
|
+
stepId: step.id,
|
|
2064
|
+
agentName: step.agentName,
|
|
2065
|
+
description: step.description,
|
|
2066
|
+
stepIndex: workflowContext?.stepIndex ?? 0,
|
|
2067
|
+
totalSteps: workflowContext?.totalSteps ?? 1,
|
|
2068
|
+
toolCallId: event.toolCallId,
|
|
2069
|
+
toolName: event.toolName,
|
|
2070
|
+
toolOutput: event.output,
|
|
2071
|
+
toolIsError: event.isError,
|
|
2072
|
+
});
|
|
2073
|
+
}
|
|
2074
|
+
},
|
|
2075
|
+
});
|
|
2076
|
+
if (appServerResult.sessionId) {
|
|
2077
|
+
activeCliSessionId = appServerResult.sessionId;
|
|
2078
|
+
}
|
|
2079
|
+
console.info(`[workflow-engine] codex_app_server_turn_done workflow=${workflowContext?.workflowId || '(none)'} step=${workflowContext?.stepIndex ?? 0} agent=${step.agentName} conversationId=${ptyConversationKey} taskId=${opts?.taskId ?? '(none)'} contentChars=${String(appServerResult.content || '').length}`);
|
|
2080
|
+
response = {
|
|
2081
|
+
content: appServerResult.content || '',
|
|
2082
|
+
usage: appServerResult.usage,
|
|
2083
|
+
};
|
|
2084
|
+
break;
|
|
2085
|
+
}
|
|
2086
|
+
catch (appServerErr) {
|
|
2087
|
+
console.error(`[workflow-engine] codex_app_server_turn_error workflow=${workflowContext?.workflowId || '(none)'} step=${workflowContext?.stepIndex ?? 0} agent=${step.agentName} conversationId=${ptyConversationKey} taskId=${opts?.taskId ?? '(none)'} attempt=${appServerAttempt}: ${appServerErr?.stack || appServerErr?.message || String(appServerErr)}`);
|
|
2088
|
+
if (!isWorkflowAbortError(appServerErr)) {
|
|
2089
|
+
clearFailedWorkflowCliSessionEpoch(conversationId, step.agentId, activeCliSessionId || cliSessionEpochPlan.resumeSessionId, cliSessionScopeKey);
|
|
2090
|
+
codexAppServerManager.closeSessionByConversation(ptyConversationKey, step.agentId);
|
|
2091
|
+
}
|
|
2092
|
+
if (appServerAttempt >= LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT
|
|
2093
|
+
|| !shouldRetrySelectedWorkflowRuntime(appServerErr)) {
|
|
2094
|
+
throw appServerErr;
|
|
2095
|
+
}
|
|
2096
|
+
if (cliSessionEpochPlan.resumeSessionId) {
|
|
2097
|
+
applyFreshCliBootstrapFallback('resume_failed');
|
|
2098
|
+
}
|
|
2099
|
+
console.warn(`[workflow-engine] ${step.agentName} selected runtime failed, retrying with a fresh session (${appServerAttempt + 1}/${LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT})`);
|
|
2100
|
+
await pauseWorkflowRuntimeRetry(appServerAttempt, opts?.abortSignal);
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
else if (workflowCliSessionTransport === 'pty') {
|
|
2105
|
+
if (activeRuntime.providerName !== 'claude-cli') {
|
|
2106
|
+
throw new Error(`Legacy PTY workflow sessions are Claude-only in local desktop mode; expected ${activeRuntime.providerName} to use Codex app-server.`);
|
|
1483
2107
|
}
|
|
1484
2108
|
const ptyManager = (0, local_cli_pty_manager_1.getLocalCliPtySessionManager)();
|
|
1485
2109
|
let ptyAttempt = 0;
|
|
2110
|
+
let forceFreshInteractiveCliSession = false;
|
|
2111
|
+
let currentAttemptSessionId = null;
|
|
2112
|
+
let currentAttemptWasFreshSession = false;
|
|
1486
2113
|
while (true) {
|
|
2114
|
+
throwIfWorkflowAborted(opts?.abortSignal);
|
|
1487
2115
|
ptyAttempt++;
|
|
1488
2116
|
try {
|
|
2117
|
+
const hasLivePtySession = ptyManager.hasActiveSession(ptyConversationKey, step.agentId);
|
|
2118
|
+
const isFreshSession = forceFreshInteractiveCliSession
|
|
2119
|
+
|| !hasPersistentConversation
|
|
2120
|
+
|| (iteration === 1 && !hasLivePtySession && !cliSessionEpochPlan.resumeSessionId);
|
|
2121
|
+
const newSessionId = activeRuntime.providerName === 'claude-cli' && isFreshSession
|
|
2122
|
+
? data.generateNextSessionId()
|
|
2123
|
+
: undefined;
|
|
2124
|
+
currentAttemptWasFreshSession = isFreshSession;
|
|
2125
|
+
currentAttemptSessionId = newSessionId || cliSessionEpochPlan.resumeSessionId || activeCliSessionId || null;
|
|
1489
2126
|
const ptyResult = await ptyManager.runTurn({
|
|
1490
2127
|
conversationId: ptyConversationKey,
|
|
1491
2128
|
botId: step.agentId,
|
|
1492
|
-
provider:
|
|
2129
|
+
provider: 'claude-cli',
|
|
2130
|
+
botSettings: {
|
|
2131
|
+
claude: {
|
|
2132
|
+
model: activeRuntime.model,
|
|
2133
|
+
effortLevel: profile?.claude_effort_level,
|
|
2134
|
+
outputStyle: profile?.claude_output_style,
|
|
2135
|
+
fastMode: profile?.claude_fast_mode === 1,
|
|
2136
|
+
permissionsJson: profile?.claude_permissions_json,
|
|
2137
|
+
},
|
|
2138
|
+
},
|
|
1493
2139
|
cwd: effectiveProjectDir,
|
|
2140
|
+
toolActorId: step.agentName,
|
|
2141
|
+
toolProjectId: opts?.projectId ?? null,
|
|
2142
|
+
currentTodoTaskId: opts?.taskId,
|
|
2143
|
+
useConpty: shouldUseConptyForWorkflowClaudePty(),
|
|
1494
2144
|
systemPrompt,
|
|
1495
2145
|
messages,
|
|
1496
|
-
|
|
2146
|
+
abortSignal: opts?.abortSignal,
|
|
2147
|
+
forceFreshSession: isFreshSession,
|
|
2148
|
+
resumeSessionId: isFreshSession ? undefined : (cliSessionEpochPlan.resumeSessionId || undefined),
|
|
2149
|
+
newSessionId,
|
|
2150
|
+
onRawChunk: async (chunk) => {
|
|
2151
|
+
if (opts?.onWorkerChunk) {
|
|
2152
|
+
opts.onWorkerChunk({
|
|
2153
|
+
type: 'worker_terminal_chunk',
|
|
2154
|
+
stepId: step.id,
|
|
2155
|
+
agentName: step.agentName,
|
|
2156
|
+
description: step.description,
|
|
2157
|
+
stepIndex: workflowContext?.stepIndex ?? 0,
|
|
2158
|
+
totalSteps: workflowContext?.totalSteps ?? 1,
|
|
2159
|
+
rawText: chunk,
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
},
|
|
2163
|
+
// Forward Claude PTY tool_use detail markers as worker_terminal_chunk
|
|
2164
|
+
// detail lines so they appear in the worker child card details panel.
|
|
2165
|
+
// Also detect __ACTIVITY__ markers and emit structured worker_tool_call
|
|
2166
|
+
// events so PTY workers get the same tool visibility as API-key workers.
|
|
1497
2167
|
onDetail: async (detail) => {
|
|
1498
|
-
|
|
2168
|
+
if (!detail || !opts?.onWorkerChunk)
|
|
2169
|
+
return;
|
|
2170
|
+
// Always forward as terminal chunk for textual details panel.
|
|
2171
|
+
opts.onWorkerChunk({
|
|
2172
|
+
type: 'worker_terminal_chunk',
|
|
2173
|
+
stepId: step.id,
|
|
2174
|
+
agentName: step.agentName,
|
|
2175
|
+
description: step.description,
|
|
2176
|
+
stepIndex: workflowContext?.stepIndex ?? 0,
|
|
2177
|
+
totalSteps: workflowContext?.totalSteps ?? 1,
|
|
2178
|
+
rawText: detail,
|
|
2179
|
+
});
|
|
2180
|
+
// Detect __ACTIVITY__<JSON> markers from PTY parser and translate
|
|
2181
|
+
// tool activities into structured worker_tool_call / worker_tool_result events.
|
|
2182
|
+
if (detail.startsWith('__ACTIVITY__')) {
|
|
2183
|
+
try {
|
|
2184
|
+
const json = detail.slice('__ACTIVITY__'.length);
|
|
2185
|
+
const activity = JSON.parse(json);
|
|
2186
|
+
if (!activity || typeof activity !== 'object')
|
|
2187
|
+
return;
|
|
2188
|
+
if (activity.type === 'tool_call' && typeof activity.label === 'string') {
|
|
2189
|
+
// Every tool_use block from the Claude PTY parser
|
|
2190
|
+
opts.onWorkerChunk({
|
|
2191
|
+
type: opts?.isOrchestrated ? 'worker_tool_call' : 'tool_call',
|
|
2192
|
+
stepId: step.id,
|
|
2193
|
+
agentName: step.agentName,
|
|
2194
|
+
description: step.description,
|
|
2195
|
+
stepIndex: workflowContext?.stepIndex ?? 0,
|
|
2196
|
+
totalSteps: workflowContext?.totalSteps ?? 1,
|
|
2197
|
+
toolCallId: typeof activity.key === 'string' ? activity.key : `claude-tool-${Date.now()}`,
|
|
2198
|
+
toolName: activity.label,
|
|
2199
|
+
toolArguments: undefined,
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
else if (activity.type === 'tool_result' && typeof activity.label === 'string') {
|
|
2203
|
+
// Result block from the Claude PTY parser
|
|
2204
|
+
opts.onWorkerChunk({
|
|
2205
|
+
type: opts?.isOrchestrated ? 'worker_tool_result' : 'tool_result',
|
|
2206
|
+
stepId: step.id,
|
|
2207
|
+
agentName: step.agentName,
|
|
2208
|
+
description: step.description,
|
|
2209
|
+
stepIndex: workflowContext?.stepIndex ?? 0,
|
|
2210
|
+
totalSteps: workflowContext?.totalSteps ?? 1,
|
|
2211
|
+
toolCallId: typeof activity.key === 'string' ? activity.key : `claude-tool-result-${Date.now()}`,
|
|
2212
|
+
toolName: 'tool',
|
|
2213
|
+
toolOutput: activity.label,
|
|
2214
|
+
toolIsError: !!activity.isError,
|
|
2215
|
+
});
|
|
2216
|
+
}
|
|
2217
|
+
else if (activity.type === 'working' && typeof activity.label === 'string') {
|
|
2218
|
+
// Legacy 'working' activities (sub-agent updates from child files)
|
|
2219
|
+
opts.onWorkerChunk({
|
|
2220
|
+
type: opts?.isOrchestrated ? 'worker_tool_call' : 'tool_call',
|
|
2221
|
+
stepId: step.id,
|
|
2222
|
+
agentName: step.agentName,
|
|
2223
|
+
description: step.description,
|
|
2224
|
+
stepIndex: workflowContext?.stepIndex ?? 0,
|
|
2225
|
+
totalSteps: workflowContext?.totalSteps ?? 1,
|
|
2226
|
+
toolCallId: typeof activity.key === 'string' ? activity.key : `claude-tool-${Date.now()}`,
|
|
2227
|
+
toolName: activity.label,
|
|
2228
|
+
toolArguments: undefined,
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
2231
|
+
else if (activity.type === 'subagent_started' && typeof activity.label === 'string') {
|
|
2232
|
+
opts.onWorkerChunk({
|
|
2233
|
+
type: opts?.isOrchestrated ? 'worker_tool_call' : 'tool_call',
|
|
2234
|
+
stepId: step.id,
|
|
2235
|
+
agentName: step.agentName,
|
|
2236
|
+
description: step.description,
|
|
2237
|
+
stepIndex: workflowContext?.stepIndex ?? 0,
|
|
2238
|
+
totalSteps: workflowContext?.totalSteps ?? 1,
|
|
2239
|
+
toolCallId: typeof activity.key === 'string' ? activity.key : `claude-agent-${Date.now()}`,
|
|
2240
|
+
toolName: 'Agent',
|
|
2241
|
+
toolArguments: { description: activity.label },
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
catch {
|
|
2246
|
+
// malformed __ACTIVITY__ marker — ignore
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
},
|
|
2250
|
+
onChunk: async (chunk) => {
|
|
2251
|
+
streamedContent += chunk;
|
|
1499
2252
|
if (opts?.onWorkerChunk) {
|
|
1500
2253
|
opts.onWorkerChunk({
|
|
1501
2254
|
type: opts?.isOrchestrated ? 'worker_chunk' : 'chunk',
|
|
@@ -1504,7 +2257,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1504
2257
|
description: step.description,
|
|
1505
2258
|
stepIndex: workflowContext?.stepIndex ?? 0,
|
|
1506
2259
|
totalSteps: workflowContext?.totalSteps ?? 1,
|
|
1507
|
-
text:
|
|
2260
|
+
text: chunk,
|
|
1508
2261
|
});
|
|
1509
2262
|
}
|
|
1510
2263
|
const now = Date.now();
|
|
@@ -1527,12 +2280,41 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1527
2280
|
break;
|
|
1528
2281
|
}
|
|
1529
2282
|
catch (ptyErr) {
|
|
2283
|
+
if (isWorkflowAbortError(ptyErr)) {
|
|
2284
|
+
throw ptyErr;
|
|
2285
|
+
}
|
|
2286
|
+
const startupRetry = isClaudeFreshWorkflowSessionStartupFailure(ptyErr);
|
|
2287
|
+
if (startupRetry || currentAttemptWasFreshSession || cliSessionEpochPlan.resumeSessionId) {
|
|
2288
|
+
ptyManager.logSessionFailureByConversation(ptyConversationKey, step.agentId, startupRetry
|
|
2289
|
+
? 'workflow_fresh_session_startup_failure_before_kill'
|
|
2290
|
+
: currentAttemptWasFreshSession
|
|
2291
|
+
? 'workflow_fresh_session_failure_before_kill'
|
|
2292
|
+
: 'workflow_resume_session_failure_before_kill', ptyErr);
|
|
2293
|
+
}
|
|
2294
|
+
clearFailedWorkflowCliSessionEpoch(conversationId, step.agentId, currentAttemptSessionId || activeCliSessionId || cliSessionEpochPlan.resumeSessionId, cliSessionScopeKey);
|
|
2295
|
+
if (startupRetry || currentAttemptWasFreshSession) {
|
|
2296
|
+
forceFreshInteractiveCliSession = true;
|
|
2297
|
+
ptyManager.closeSessionByConversation(ptyConversationKey, step.agentId);
|
|
2298
|
+
}
|
|
2299
|
+
const resumeFailureFallback = !!cliSessionEpochPlan.resumeSessionId && !currentAttemptWasFreshSession;
|
|
2300
|
+
if (resumeFailureFallback) {
|
|
2301
|
+
forceFreshInteractiveCliSession = true;
|
|
2302
|
+
applyFreshCliBootstrapFallback('resume_failed');
|
|
2303
|
+
ptyManager.closeSessionByConversation(ptyConversationKey, step.agentId);
|
|
2304
|
+
}
|
|
1530
2305
|
if (ptyAttempt >= LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT
|
|
1531
2306
|
|| !shouldRetrySelectedWorkflowRuntime(ptyErr)) {
|
|
1532
2307
|
throw ptyErr;
|
|
1533
2308
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
2309
|
+
const retryDetail = startupRetry
|
|
2310
|
+
? `[workflow-engine] Fresh ${activeRuntime.providerName} session ${currentAttemptSessionId || '(unknown)'} did not finish startup before the transcript became available; killing it and retrying with a new session (${ptyAttempt + 1}/${LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT})`
|
|
2311
|
+
: resumeFailureFallback
|
|
2312
|
+
? `[workflow-engine] Stored ${activeRuntime.providerName} session ${currentAttemptSessionId || '(unknown)'} failed to resume (${ptyErr?.message || ptyErr}); retrying with a fresh bootstrapped session (${ptyAttempt + 1}/${LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT})`
|
|
2313
|
+
: currentAttemptWasFreshSession
|
|
2314
|
+
? `[workflow-engine] Fresh ${activeRuntime.providerName} session ${currentAttemptSessionId || '(unknown)'} failed (${ptyErr?.message || ptyErr}); killing it and retrying with a new session (${ptyAttempt + 1}/${LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT})`
|
|
2315
|
+
: `[workflow-engine] ${step.agentName} selected runtime failed (${ptyErr?.message || ptyErr}), retrying the same connection (${ptyAttempt + 1}/${LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT})`;
|
|
2316
|
+
console.warn(retryDetail);
|
|
2317
|
+
await pauseWorkflowRuntimeRetry(ptyAttempt, opts?.abortSignal);
|
|
1536
2318
|
}
|
|
1537
2319
|
}
|
|
1538
2320
|
}
|
|
@@ -1579,6 +2361,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1579
2361
|
},
|
|
1580
2362
|
tools: opts?.disableTools ? undefined : toolDefs,
|
|
1581
2363
|
cwd: effectiveProjectDir,
|
|
2364
|
+
abortSignal: opts?.abortSignal,
|
|
1582
2365
|
});
|
|
1583
2366
|
// Flush remaining chunk buffer
|
|
1584
2367
|
if (opts?.onWorkerChunk && chunkBuffer) {
|
|
@@ -1603,7 +2386,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1603
2386
|
&& shouldRetrySelectedWorkflowRuntime(err)) {
|
|
1604
2387
|
selectedRuntimeRetryCount++;
|
|
1605
2388
|
console.warn(`[workflow-engine] ${step.agentName} selected runtime failed, retrying the same connection (${selectedRuntimeRetryCount + 1}/${LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT})`);
|
|
1606
|
-
await pauseWorkflowRuntimeRetry(selectedRuntimeRetryCount);
|
|
2389
|
+
await pauseWorkflowRuntimeRetry(selectedRuntimeRetryCount, opts?.abortSignal);
|
|
1607
2390
|
iteration--;
|
|
1608
2391
|
continue;
|
|
1609
2392
|
}
|
|
@@ -1631,29 +2414,43 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1631
2414
|
throw err;
|
|
1632
2415
|
}
|
|
1633
2416
|
if (hasPersistentConversation && activeCliSessionId && !response.toolCalls?.length) {
|
|
2417
|
+
if (cliSessionEpochPlan.resumeSessionId && !freshCliBootstrapFallbackApplied) {
|
|
2418
|
+
console.info(`[workflow-engine] cli_session_resume_succeeded conversationId=${conversationId} botId=${step.agentId} provider=${activeRuntime.providerName} sessionId=${activeCliSessionId}`);
|
|
2419
|
+
}
|
|
2420
|
+
const usedBootstrap = freshCliBootstrapFileWritten
|
|
2421
|
+
|| (!cliSessionEpochPlan.resumeSessionId && !!promptContextWindow.allowBootstrap);
|
|
2422
|
+
const bootstrapReason = freshCliBootstrapFileWritten
|
|
2423
|
+
? 'resume_failed'
|
|
2424
|
+
: (!cliSessionEpochPlan.resumeSessionId && !!promptContextWindow.allowBootstrap)
|
|
2425
|
+
? 'fresh_with_context'
|
|
2426
|
+
: null;
|
|
2427
|
+
console.info(`[workflow-engine] cli_turn_telemetry conversationId=${conversationId || '(none)'} botId=${step.agentId} provider=${activeRuntime.providerName} selectedSessionId=${cliSessionEpochPlan.resumeSessionId || '(none)'} actualSessionId=${activeCliSessionId} promptContextMode=${promptContextWindow.mode || 'unknown'} usedBootstrap=${usedBootstrap} bootstrapReason=${bootstrapReason || '(none)'} resetReason=${cliEpochResetReason || '(none)'}`);
|
|
1634
2428
|
const nextEpochTurnCount = cliSessionEpochPlan.resumeSessionId
|
|
1635
2429
|
? ((cliSessionEpochPlan.existing?.epoch_turn_count || 0) + 1)
|
|
1636
2430
|
: 1;
|
|
1637
2431
|
data.upsertCliSessionEpoch({
|
|
1638
2432
|
conversationId: conversationId,
|
|
2433
|
+
sessionScopeKey: cliSessionScopeKey,
|
|
1639
2434
|
botId: step.agentId,
|
|
1640
2435
|
provider: activeRuntime.providerName,
|
|
1641
2436
|
sessionId: activeCliSessionId,
|
|
1642
2437
|
epochTurnCount: nextEpochTurnCount,
|
|
1643
2438
|
lastInputTokens: response.usage?.inputTokens ?? null,
|
|
1644
2439
|
lastOutputTokens: response.usage?.outputTokens ?? null,
|
|
1645
|
-
resetReason:
|
|
2440
|
+
resetReason: cliEpochResetReason,
|
|
1646
2441
|
epochStartedAt: cliEpochStartedAt,
|
|
1647
2442
|
lastUsedAt: (0, cli_session_epoch_1.localTimestamp)(),
|
|
1648
2443
|
});
|
|
1649
2444
|
}
|
|
1650
2445
|
if (response.toolCalls && response.toolCalls.length > 0) {
|
|
2446
|
+
throwIfWorkflowAborted(opts?.abortSignal);
|
|
1651
2447
|
messages.push({
|
|
1652
2448
|
role: 'assistant',
|
|
1653
2449
|
content: response.content || '',
|
|
1654
2450
|
toolCalls: response.toolCalls,
|
|
1655
2451
|
});
|
|
1656
2452
|
for (const tc of response.toolCalls) {
|
|
2453
|
+
throwIfWorkflowAborted(opts?.abortSignal);
|
|
1657
2454
|
const permMode = unrestrictedCliProvider
|
|
1658
2455
|
? 'autopilot'
|
|
1659
2456
|
: (profile?.permission_mode || 'autopilot');
|
|
@@ -1677,6 +2474,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1677
2474
|
}
|
|
1678
2475
|
const result = await (0, index_2.executeAndVerify)({ id: tc.id, name: tc.name, arguments: tc.arguments }, toolCtx);
|
|
1679
2476
|
const output = result.success ? result.output : `ERROR: ${result.error || 'Unknown'}`;
|
|
2477
|
+
throwIfWorkflowAborted(opts?.abortSignal);
|
|
1680
2478
|
messages.push({ role: 'tool', content: output, toolCallId: tc.id, toolName: tc.name });
|
|
1681
2479
|
// Emit tool_result event
|
|
1682
2480
|
if (opts?.onWorkerChunk) {
|
|
@@ -2130,15 +2928,38 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
2130
2928
|
return count >= ERROR_LOOP_THRESHOLD;
|
|
2131
2929
|
}
|
|
2132
2930
|
isTerminalProviderFailure(errorMessage) {
|
|
2931
|
+
const normalized = String(errorMessage || '').toLowerCase();
|
|
2932
|
+
return Boolean(this.classifyProviderLimitFailure(errorMessage))
|
|
2933
|
+
|| this.isProviderAuthFailure(errorMessage)
|
|
2934
|
+
|| /\b(pty session|app-server|fresh session startup timed out|transcript file|no meaningful pty activity|session exited while waiting|failed to resume|session closed during active turn)\b/i.test(normalized);
|
|
2935
|
+
}
|
|
2936
|
+
isProviderAuthFailure(errorMessage) {
|
|
2133
2937
|
const normalized = errorMessage.toLowerCase();
|
|
2134
|
-
return normalized.includes('
|
|
2938
|
+
return normalized.includes('refresh_token_reused')
|
|
2939
|
+
|| normalized.includes('invalid_grant')
|
|
2940
|
+
|| normalized.includes('oauth token refresh failed');
|
|
2941
|
+
}
|
|
2942
|
+
classifyProviderLimitFailure(errorMessage) {
|
|
2943
|
+
const exactMessage = String(errorMessage || '').trim();
|
|
2944
|
+
const normalized = exactMessage.toLowerCase();
|
|
2945
|
+
if (!exactMessage)
|
|
2946
|
+
return null;
|
|
2947
|
+
if (normalized.includes('rate limit')
|
|
2135
2948
|
|| normalized.includes('rate limited')
|
|
2949
|
+
|| normalized.includes('api error 429')
|
|
2950
|
+
|| normalized.includes('current session limit')) {
|
|
2951
|
+
return { reasonCode: 'provider_rate_limit', exactMessage };
|
|
2952
|
+
}
|
|
2953
|
+
if (normalized.includes("you've hit your org's monthly usage limit")
|
|
2954
|
+
|| normalized.includes('monthly usage limit')
|
|
2955
|
+
|| normalized.includes('usage limit reached')
|
|
2956
|
+
|| normalized.includes('quota exceeded')
|
|
2136
2957
|
|| normalized.includes('exceeded your current quota')
|
|
2137
2958
|
|| normalized.includes('insufficient_quota')
|
|
2138
|
-
|| normalized.includes('billing details')
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2959
|
+
|| normalized.includes('billing details')) {
|
|
2960
|
+
return { reasonCode: 'provider_quota_limit', exactMessage };
|
|
2961
|
+
}
|
|
2962
|
+
return null;
|
|
2142
2963
|
}
|
|
2143
2964
|
/**
|
|
2144
2965
|
* Replan remaining work when retries are exhausted.
|