funolio-agent 1.0.75 → 1.1.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/credential-reader.d.ts.map +1 -1
- package/dist/auth/credential-reader.js +4 -3
- package/dist/auth/credential-reader.js.map +1 -1
- package/dist/auth/token-refresh.d.ts +8 -0
- package/dist/auth/token-refresh.d.ts.map +1 -1
- package/dist/auth/token-refresh.js +82 -52
- package/dist/auth/token-refresh.js.map +1 -1
- package/dist/auto-organizer.d.ts.map +1 -1
- package/dist/auto-organizer.js +6 -7
- package/dist/auto-organizer.js.map +1 -1
- package/dist/bench-prefix.d.ts +16 -0
- package/dist/bench-prefix.d.ts.map +1 -0
- package/dist/bench-prefix.js +25 -0
- package/dist/bench-prefix.js.map +1 -0
- package/dist/bot-manager.d.ts.map +1 -1
- package/dist/bot-manager.js +23 -14
- package/dist/bot-manager.js.map +1 -1
- package/dist/chat-sync.d.ts +42 -0
- package/dist/chat-sync.d.ts.map +1 -0
- package/dist/chat-sync.js +95 -0
- package/dist/chat-sync.js.map +1 -0
- package/dist/clerk-model.d.ts +7 -0
- package/dist/clerk-model.d.ts.map +1 -1
- package/dist/clerk-model.js +42 -8
- package/dist/clerk-model.js.map +1 -1
- package/dist/cli-bootstrap-history.d.ts +10 -0
- package/dist/cli-bootstrap-history.d.ts.map +1 -0
- package/dist/cli-bootstrap-history.js +112 -0
- package/dist/cli-bootstrap-history.js.map +1 -0
- package/dist/cli-models.d.ts +8 -0
- package/dist/cli-models.d.ts.map +1 -0
- package/dist/cli-models.js +91 -0
- package/dist/cli-models.js.map +1 -0
- package/dist/cli-session-epoch.d.ts +13 -3
- package/dist/cli-session-epoch.d.ts.map +1 -1
- package/dist/cli-session-epoch.js +53 -4
- package/dist/cli-session-epoch.js.map +1 -1
- package/dist/codex-app-server-manager.d.ts +64 -4
- package/dist/codex-app-server-manager.d.ts.map +1 -1
- package/dist/codex-app-server-manager.js +755 -55
- package/dist/codex-app-server-manager.js.map +1 -1
- package/dist/commands/pool.d.ts +32 -0
- package/dist/commands/pool.d.ts.map +1 -1
- package/dist/commands/pool.js +145 -66
- package/dist/commands/pool.js.map +1 -1
- package/dist/commands/start.d.ts +21 -0
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +484 -63
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +5 -2
- package/dist/commands/status.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +170 -58
- package/dist/config.js.map +1 -1
- package/dist/context-window.d.ts +37 -1
- package/dist/context-window.d.ts.map +1 -1
- package/dist/context-window.js +202 -16
- package/dist/context-window.js.map +1 -1
- package/dist/live-activity.d.ts +3 -1
- package/dist/live-activity.d.ts.map +1 -1
- package/dist/live-activity.js.map +1 -1
- package/dist/local-chat-execution.d.ts +114 -0
- package/dist/local-chat-execution.d.ts.map +1 -0
- package/dist/local-chat-execution.js +349 -0
- package/dist/local-chat-execution.js.map +1 -0
- package/dist/local-cli-pty-manager.d.ts +138 -3
- package/dist/local-cli-pty-manager.d.ts.map +1 -1
- package/dist/local-cli-pty-manager.js +1415 -111
- package/dist/local-cli-pty-manager.js.map +1 -1
- package/dist/local-conversation-gateway.d.ts +110 -0
- package/dist/local-conversation-gateway.d.ts.map +1 -0
- package/dist/local-conversation-gateway.js +175 -0
- package/dist/local-conversation-gateway.js.map +1 -0
- package/dist/local-data.d.ts +235 -5
- package/dist/local-data.d.ts.map +1 -1
- package/dist/local-data.js +1066 -87
- package/dist/local-data.js.map +1 -1
- package/dist/local-db.d.ts +6 -0
- package/dist/local-db.d.ts.map +1 -1
- package/dist/local-db.js +376 -4
- package/dist/local-db.js.map +1 -1
- package/dist/local-funnel.d.ts.map +1 -1
- package/dist/local-funnel.js +6 -5
- package/dist/local-funnel.js.map +1 -1
- package/dist/local-server.d.ts +30 -0
- package/dist/local-server.d.ts.map +1 -1
- package/dist/local-server.js +2898 -319
- package/dist/local-server.js.map +1 -1
- package/dist/managed-process-registry.d.ts +59 -0
- package/dist/managed-process-registry.d.ts.map +1 -0
- package/dist/managed-process-registry.js +390 -0
- package/dist/managed-process-registry.js.map +1 -0
- package/dist/mcp/claude-config-writer.d.ts +5 -5
- package/dist/mcp/claude-config-writer.d.ts.map +1 -1
- package/dist/mcp/claude-config-writer.js +19 -11
- package/dist/mcp/claude-config-writer.js.map +1 -1
- package/dist/mcp/index.d.ts +4 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/sync-cli-config.d.ts +42 -4
- package/dist/mcp/sync-cli-config.d.ts.map +1 -1
- package/dist/mcp/sync-cli-config.js +497 -17
- package/dist/mcp/sync-cli-config.js.map +1 -1
- package/dist/message-loop.d.ts.map +1 -1
- package/dist/message-loop.js +43 -1
- package/dist/message-loop.js.map +1 -1
- package/dist/mqtt-client.d.ts +34 -0
- package/dist/mqtt-client.d.ts.map +1 -1
- package/dist/mqtt-client.js +270 -45
- package/dist/mqtt-client.js.map +1 -1
- package/dist/mqtt-data-relay.d.ts +44 -0
- package/dist/mqtt-data-relay.d.ts.map +1 -0
- package/dist/mqtt-data-relay.js +106 -0
- package/dist/mqtt-data-relay.js.map +1 -0
- package/dist/orchestration/capabilities.d.ts +13 -0
- package/dist/orchestration/capabilities.d.ts.map +1 -0
- package/dist/orchestration/capabilities.js +152 -0
- package/dist/orchestration/capabilities.js.map +1 -0
- package/dist/orchestration/dispatch-executor.d.ts +83 -0
- package/dist/orchestration/dispatch-executor.d.ts.map +1 -0
- package/dist/orchestration/dispatch-executor.js +266 -0
- package/dist/orchestration/dispatch-executor.js.map +1 -0
- package/dist/orchestration/dispatch-hint.d.ts +134 -0
- package/dist/orchestration/dispatch-hint.d.ts.map +1 -0
- package/dist/orchestration/dispatch-hint.js +247 -0
- package/dist/orchestration/dispatch-hint.js.map +1 -0
- package/dist/orchestration/dispatch-runner.d.ts +106 -0
- package/dist/orchestration/dispatch-runner.d.ts.map +1 -0
- package/dist/orchestration/dispatch-runner.js +604 -0
- package/dist/orchestration/dispatch-runner.js.map +1 -0
- package/dist/orchestration/dispatch-tools.d.ts +167 -0
- package/dist/orchestration/dispatch-tools.d.ts.map +1 -0
- package/dist/orchestration/dispatch-tools.js +328 -0
- package/dist/orchestration/dispatch-tools.js.map +1 -0
- package/dist/orchestration/front-door-policy.d.ts +35 -10
- package/dist/orchestration/front-door-policy.d.ts.map +1 -1
- package/dist/orchestration/front-door-policy.js +30 -267
- package/dist/orchestration/front-door-policy.js.map +1 -1
- package/dist/orchestration/orchestrator-dispatch-prompt.d.ts +43 -0
- package/dist/orchestration/orchestrator-dispatch-prompt.d.ts.map +1 -0
- package/dist/orchestration/orchestrator-dispatch-prompt.js +267 -0
- package/dist/orchestration/orchestrator-dispatch-prompt.js.map +1 -0
- package/dist/orchestration/orchestrator-operating-prompt.d.ts +14 -0
- package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
- package/dist/orchestration/orchestrator-operating-prompt.js +157 -31
- package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
- package/dist/orchestration/plan-import.d.ts +39 -0
- package/dist/orchestration/plan-import.d.ts.map +1 -0
- package/dist/orchestration/plan-import.js +547 -0
- package/dist/orchestration/plan-import.js.map +1 -0
- package/dist/orchestration/worker-operating-prompt.d.ts +2 -0
- package/dist/orchestration/worker-operating-prompt.d.ts.map +1 -1
- package/dist/orchestration/worker-operating-prompt.js +36 -46
- package/dist/orchestration/worker-operating-prompt.js.map +1 -1
- package/dist/orchestrator.d.ts +195 -3
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +1970 -432
- package/dist/orchestrator.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +8 -4
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/claude-cli.d.ts.map +1 -1
- package/dist/providers/claude-cli.js +28 -3
- package/dist/providers/claude-cli.js.map +1 -1
- package/dist/providers/codex-cli.d.ts +10 -6
- package/dist/providers/codex-cli.d.ts.map +1 -1
- package/dist/providers/codex-cli.js +190 -17
- package/dist/providers/codex-cli.js.map +1 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +15 -5
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/index.d.ts +15 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +13 -5
- package/dist/providers/openai.js.map +1 -1
- package/dist/server-adapter.d.ts +8 -0
- package/dist/server-adapter.d.ts.map +1 -1
- package/dist/server-adapter.js +7 -0
- package/dist/server-adapter.js.map +1 -1
- package/dist/service-mode.d.ts +1 -1
- package/dist/service-mode.d.ts.map +1 -1
- package/dist/service-mode.js +64 -1
- package/dist/service-mode.js.map +1 -1
- package/dist/service-setup-only.d.ts +8 -0
- package/dist/service-setup-only.d.ts.map +1 -0
- package/dist/service-setup-only.js +37 -0
- package/dist/service-setup-only.js.map +1 -0
- package/dist/slash-commands.d.ts +21 -0
- package/dist/slash-commands.d.ts.map +1 -0
- package/dist/slash-commands.js +99 -0
- package/dist/slash-commands.js.map +1 -0
- package/dist/subagent/index.d.ts +4 -2
- package/dist/subagent/index.d.ts.map +1 -1
- package/dist/subagent/index.js.map +1 -1
- package/dist/summarization-pipeline.d.ts.map +1 -1
- package/dist/summarization-pipeline.js +1 -9
- package/dist/summarization-pipeline.js.map +1 -1
- package/dist/token-counter.d.ts.map +1 -1
- package/dist/token-counter.js +11 -4
- package/dist/token-counter.js.map +1 -1
- package/dist/tool-filter.d.ts.map +1 -1
- package/dist/tool-filter.js +10 -6
- package/dist/tool-filter.js.map +1 -1
- package/dist/tools/admin-tools.d.ts.map +1 -1
- package/dist/tools/admin-tools.js +13 -4
- package/dist/tools/admin-tools.js.map +1 -1
- package/dist/tools/run-command.d.ts.map +1 -1
- package/dist/tools/run-command.js +5 -1
- package/dist/tools/run-command.js.map +1 -1
- package/dist/tools/search-conversation-history.d.ts.map +1 -1
- package/dist/tools/search-conversation-history.js +12 -2
- package/dist/tools/search-conversation-history.js.map +1 -1
- package/dist/tools/todo-tasks.d.ts.map +1 -1
- package/dist/tools/todo-tasks.js +77 -5
- package/dist/tools/todo-tasks.js.map +1 -1
- package/dist/usage-log.d.ts +62 -0
- package/dist/usage-log.d.ts.map +1 -0
- package/dist/usage-log.js +98 -0
- package/dist/usage-log.js.map +1 -0
- package/dist/wizard-state.d.ts +13 -0
- package/dist/wizard-state.d.ts.map +1 -1
- package/dist/wizard-state.js +61 -3
- package/dist/wizard-state.js.map +1 -1
- package/dist/wizard-support.d.ts.map +1 -1
- package/dist/wizard-support.js +27 -1
- package/dist/wizard-support.js.map +1 -1
- package/dist/workflow-engine.d.ts +40 -1
- package/dist/workflow-engine.d.ts.map +1 -1
- package/dist/workflow-engine.js +753 -93
- package/dist/workflow-engine.js.map +1 -1
- package/package.json +2 -2
package/dist/workflow-engine.js
CHANGED
|
@@ -50,6 +50,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
50
50
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
51
|
exports.WorkflowEngine = void 0;
|
|
52
52
|
exports.resolveWorkflowCliSessionTransportForTest = resolveWorkflowCliSessionTransportForTest;
|
|
53
|
+
exports.resolveWorkflowClaudeUseConptyForTest = resolveWorkflowClaudeUseConptyForTest;
|
|
54
|
+
exports.buildDeferredImageReferenceBlockForTest = buildDeferredImageReferenceBlockForTest;
|
|
55
|
+
exports.parseImageEscalationRequestForTest = parseImageEscalationRequestForTest;
|
|
53
56
|
exports.getWorkflowEngine = getWorkflowEngine;
|
|
54
57
|
const events_1 = require("events");
|
|
55
58
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -68,33 +71,93 @@ const runtime_context_1 = require("./runtime-context");
|
|
|
68
71
|
const local_cli_pty_manager_1 = require("./local-cli-pty-manager");
|
|
69
72
|
const codex_app_server_manager_1 = require("./codex-app-server-manager");
|
|
70
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");
|
|
71
77
|
// ─── Workflow Engine ─────────────────────────────────────────────
|
|
72
78
|
const MAX_STEP_ATTEMPTS = safeguards_1.SAFEGUARDS.MAX_AGENT_ATTEMPTS;
|
|
73
79
|
const MAX_STEPS = safeguards_1.SAFEGUARDS.MAX_WORKFLOW_STEPS;
|
|
74
80
|
function isInteractiveAuthFailure(text) {
|
|
75
|
-
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);
|
|
76
82
|
}
|
|
77
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
|
+
}
|
|
78
111
|
function shouldRetrySelectedWorkflowRuntime(err) {
|
|
79
112
|
const text = String(err?.message || err || '').toLowerCase();
|
|
80
113
|
if (!text)
|
|
81
114
|
return false;
|
|
82
|
-
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)) {
|
|
83
116
|
return false;
|
|
84
117
|
}
|
|
85
|
-
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);
|
|
86
119
|
}
|
|
87
|
-
async function pauseWorkflowRuntimeRetry(attempt) {
|
|
120
|
+
async function pauseWorkflowRuntimeRetry(attempt, abortSignal) {
|
|
88
121
|
const delayMs = attempt <= 1 ? 750 : 1500;
|
|
89
|
-
|
|
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);
|
|
90
136
|
}
|
|
91
|
-
function buildWorkflowPtyConversationKey(conversationId, step, workflowContext) {
|
|
137
|
+
function buildWorkflowPtyConversationKey(conversationId, step, workflowContext, cliSessionScopeKey) {
|
|
138
|
+
const explicitScope = String(cliSessionScopeKey || '').trim();
|
|
139
|
+
if (explicitScope)
|
|
140
|
+
return explicitScope;
|
|
92
141
|
if (conversationId)
|
|
93
142
|
return conversationId;
|
|
94
143
|
if (workflowContext?.workflowId)
|
|
95
144
|
return `workflow:${workflowContext.workflowId}`;
|
|
96
145
|
return `adhoc:${step.agentId}:${step.id}`;
|
|
97
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
|
+
}
|
|
98
161
|
function resolveWorkflowCliSessionTransport(providerName, isLocalCliSession) {
|
|
99
162
|
if (!isLocalCliSession)
|
|
100
163
|
return 'none';
|
|
@@ -105,6 +168,42 @@ function resolveWorkflowCliSessionTransport(providerName, isLocalCliSession) {
|
|
|
105
168
|
function resolveWorkflowCliSessionTransportForTest(providerName, isLocalCliSession) {
|
|
106
169
|
return resolveWorkflowCliSessionTransport(providerName, isLocalCliSession);
|
|
107
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
|
+
}
|
|
108
207
|
class WorkflowEngine extends events_1.EventEmitter {
|
|
109
208
|
projectDir;
|
|
110
209
|
runtimeMode;
|
|
@@ -343,8 +442,12 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
343
442
|
throw new Error(`Workflow step ${index + 1} references missing bot ${templateStep.agent_id}`);
|
|
344
443
|
}
|
|
345
444
|
const instruction = templateStep.instruction?.trim() || `Complete stage ${index + 1} for this TODO item.`;
|
|
346
|
-
|
|
347
|
-
|
|
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;
|
|
348
451
|
return {
|
|
349
452
|
id: templateStep.id,
|
|
350
453
|
index,
|
|
@@ -354,6 +457,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
354
457
|
agentName: agent.name,
|
|
355
458
|
provider: agent.provider,
|
|
356
459
|
model: agent.model,
|
|
460
|
+
role: agentRole,
|
|
357
461
|
dependsOn: index === 0 ? [] : [index - 1],
|
|
358
462
|
status: 'pending',
|
|
359
463
|
attempts: 0,
|
|
@@ -509,6 +613,11 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
509
613
|
const mapped = plannedSteps.slice(0, MAX_STEPS).map((step, index) => {
|
|
510
614
|
// Route to the named agent or default
|
|
511
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;
|
|
512
621
|
return {
|
|
513
622
|
id: `step-${index}`,
|
|
514
623
|
index,
|
|
@@ -518,6 +627,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
518
627
|
agentName: targetAgent.name,
|
|
519
628
|
provider: targetAgent.provider,
|
|
520
629
|
model: targetAgent.model,
|
|
630
|
+
role: explicitRole || agentRole,
|
|
521
631
|
dependsOn: step.dependsOn || [],
|
|
522
632
|
status: 'pending',
|
|
523
633
|
expectedOutput: step.expectedOutput || undefined,
|
|
@@ -543,7 +653,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
543
653
|
step.loopCount = 0;
|
|
544
654
|
}
|
|
545
655
|
}
|
|
546
|
-
return
|
|
656
|
+
return mapped;
|
|
547
657
|
}
|
|
548
658
|
}
|
|
549
659
|
catch {
|
|
@@ -593,6 +703,14 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
593
703
|
}
|
|
594
704
|
return steps.length >= 2 ? steps : null;
|
|
595
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
|
+
*/
|
|
596
714
|
determineDesiredRoleSequence(prompt, roleAssignments) {
|
|
597
715
|
const normalized = prompt.toLowerCase();
|
|
598
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)
|
|
@@ -646,6 +764,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
646
764
|
agentName: agent.name,
|
|
647
765
|
provider: agent.provider,
|
|
648
766
|
model: agent.model,
|
|
767
|
+
role: 'research',
|
|
649
768
|
dependsOn,
|
|
650
769
|
status: 'pending',
|
|
651
770
|
expectedOutput: 'Clear implementation guidance, risks, and recommendations for the downstream coder.',
|
|
@@ -662,6 +781,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
662
781
|
agentName: agent.name,
|
|
663
782
|
provider: agent.provider,
|
|
664
783
|
model: agent.model,
|
|
784
|
+
role: 'coding',
|
|
665
785
|
dependsOn,
|
|
666
786
|
status: 'pending',
|
|
667
787
|
expectedOutput: 'Completed deliverable that satisfies the user request.',
|
|
@@ -678,6 +798,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
678
798
|
agentName: agent.name,
|
|
679
799
|
provider: agent.provider,
|
|
680
800
|
model: agent.model,
|
|
801
|
+
role: 'qa',
|
|
681
802
|
dependsOn,
|
|
682
803
|
status: 'pending',
|
|
683
804
|
expectedOutput: 'PASS if the deliverable fully satisfies the request, otherwise FAIL with actionable defects.',
|
|
@@ -724,17 +845,45 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
724
845
|
: role === 'qa'
|
|
725
846
|
? 'Your role is QA. Verify the delivered work, then complete the TODO through the worker tool or create the next fix handoff.'
|
|
726
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
|
+
: '';
|
|
727
852
|
return [
|
|
728
|
-
`You are ${profile?.name || 'a workflow worker'}.`,
|
|
853
|
+
botSettingsPrompt || `You are ${profile?.name || 'a workflow worker'}.`,
|
|
854
|
+
'',
|
|
855
|
+
'[Orchestrated Worker Guardrails]',
|
|
729
856
|
'You are executing one orchestrated TODO task.',
|
|
730
857
|
roleLine,
|
|
731
858
|
'Follow the assignment in the user message exactly.',
|
|
732
859
|
'Use available tools when needed.',
|
|
733
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.',
|
|
734
862
|
'Do not add workflow planning, orchestration narration, or TODO-management commentary outside those tools.',
|
|
735
863
|
'If blocked after checking available context, call block_worker_task with blocker_summary, checked_context, and user_question.',
|
|
736
864
|
'If the task is complete, call complete_worker_task with output_summary and any needed handoff prompt or inserted next task.',
|
|
737
|
-
|
|
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');
|
|
738
887
|
}
|
|
739
888
|
buildOrchestratedTodoHandledResult(taskId) {
|
|
740
889
|
if (!taskId)
|
|
@@ -758,12 +907,11 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
758
907
|
|| data.getTodoTask(taskId, 'active')?.blocker_summary);
|
|
759
908
|
}
|
|
760
909
|
buildWorkflowStepPrompt(step, pinnedContext, handoffArtifacts, handoffPrompts = [], localDesktopHandoffMode = false) {
|
|
761
|
-
const primaryPrompt =
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
sections.push(`Earlier handoff context:\n${handoffPrompts.slice(0, -1).join('\n\n')}`);
|
|
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')}`);
|
|
767
915
|
}
|
|
768
916
|
if (pinnedContext.length > 0) {
|
|
769
917
|
sections.push(`Pinned context:\n${pinnedContext.join('\n\n')}`);
|
|
@@ -811,68 +959,39 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
811
959
|
}
|
|
812
960
|
return defaultProfile;
|
|
813
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
|
+
*/
|
|
814
971
|
inferRoleForStep(step) {
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
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/;
|
|
820
|
-
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/;
|
|
821
|
-
if (leadingVerbMatch) {
|
|
822
|
-
const verb = leadingVerbMatch[1];
|
|
823
|
-
if (/(qa|review|verif|validation|test|check)/.test(verb))
|
|
824
|
-
return 'qa';
|
|
825
|
-
if (/(research|investigat|analysis|analy|compar|inspect|discover|defin)/.test(verb))
|
|
826
|
-
return 'research';
|
|
827
|
-
if (/(build|creat|write|writ|fix|edit|updat|refactor|implement|code)/.test(verb))
|
|
828
|
-
return 'coding';
|
|
829
|
-
}
|
|
830
|
-
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')
|
|
831
976
|
return 'qa';
|
|
832
|
-
if (
|
|
833
|
-
return '
|
|
834
|
-
if (
|
|
835
|
-
return 'research';
|
|
836
|
-
if (codingPattern.test(prompt) && !qaPattern.test(prompt))
|
|
837
|
-
return 'coding';
|
|
838
|
-
if (qaPattern.test(prompt))
|
|
977
|
+
if (raw === 'verify' || raw === 'verification')
|
|
978
|
+
return 'qa';
|
|
979
|
+
if (/\b(verify|verification)\b/.test(raw))
|
|
839
980
|
return 'qa';
|
|
840
|
-
if (
|
|
981
|
+
if (raw === 'research' || raw === 'researcher' || raw === 'analysis' || raw === 'analyst' || raw === 'planning' || raw === 'planner' || raw === 'brainstorming' || raw === 'design' || raw === 'designer')
|
|
841
982
|
return 'research';
|
|
842
|
-
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')
|
|
843
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
|
+
}
|
|
844
993
|
return null;
|
|
845
994
|
}
|
|
846
|
-
pruneTrailingPostQaSteps(steps) {
|
|
847
|
-
const firstQaIndex = steps.findIndex((step) => this.inferRoleForStep(step) === 'qa');
|
|
848
|
-
if (firstQaIndex < 0)
|
|
849
|
-
return steps;
|
|
850
|
-
const shouldDrop = (step) => {
|
|
851
|
-
if (step.index <= firstQaIndex)
|
|
852
|
-
return false;
|
|
853
|
-
const normalized = `${step.description || ''} ${step.prompt || ''}`.toLowerCase();
|
|
854
|
-
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);
|
|
855
|
-
};
|
|
856
|
-
const filtered = steps.filter((step) => !shouldDrop(step));
|
|
857
|
-
if (filtered.length === steps.length)
|
|
858
|
-
return steps;
|
|
859
|
-
return filtered.map((step, newIndex) => {
|
|
860
|
-
const oldIndex = step.index;
|
|
861
|
-
const retainedPriorSteps = filtered
|
|
862
|
-
.filter((candidate) => candidate.index < oldIndex)
|
|
863
|
-
.map((candidate) => candidate.index);
|
|
864
|
-
const reindexedDeps = (step.dependsOn || [])
|
|
865
|
-
.filter((dep) => retainedPriorSteps.includes(dep))
|
|
866
|
-
.map((dep) => filtered.findIndex((candidate) => candidate.index === dep))
|
|
867
|
-
.filter((dep) => dep >= 0);
|
|
868
|
-
return {
|
|
869
|
-
...step,
|
|
870
|
-
index: newIndex,
|
|
871
|
-
id: step.id || `step-${newIndex}`,
|
|
872
|
-
dependsOn: reindexedDeps,
|
|
873
|
-
};
|
|
874
|
-
});
|
|
875
|
-
}
|
|
876
995
|
findPriorCodingStepIndex(steps, currentIndex) {
|
|
877
996
|
const priorCodingStep = [...steps]
|
|
878
997
|
.filter((candidate) => candidate.index < currentIndex)
|
|
@@ -913,6 +1032,15 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
913
1032
|
const failed = new Set();
|
|
914
1033
|
const allowMissingFooterForDirectStep = !opts?.isOrchestrated && !!opts?.disableDecomposition && steps.length === 1;
|
|
915
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
|
+
}
|
|
916
1044
|
// Find steps that are ready to run (all deps met)
|
|
917
1045
|
const ready = steps.filter(s => s.status === 'pending' &&
|
|
918
1046
|
s.dependsOn.every(dep => completed.has(dep)));
|
|
@@ -928,6 +1056,11 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
928
1056
|
}
|
|
929
1057
|
// Execute ready steps in parallel
|
|
930
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
|
+
}
|
|
931
1064
|
// Build context from completed dependency results — pass only RESULT artifact
|
|
932
1065
|
const handoffArtifacts = [];
|
|
933
1066
|
const handoffPrompts = [];
|
|
@@ -956,6 +1089,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
956
1089
|
}
|
|
957
1090
|
}
|
|
958
1091
|
}
|
|
1092
|
+
const initialImageDeliveryMode = opts?.workerImageDeliveryMode || 'inline';
|
|
959
1093
|
const fullPrompt = this.buildWorkflowStepPrompt(step, pinnedContext, handoffArtifacts, handoffPrompts, localDesktopHandoffMode);
|
|
960
1094
|
// approvalRequired gate: when enabled, emit an approval request
|
|
961
1095
|
// event and wait for the listener to resolve before proceeding.
|
|
@@ -1005,6 +1139,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1005
1139
|
}
|
|
1006
1140
|
catch { /* best effort */ }
|
|
1007
1141
|
try {
|
|
1142
|
+
throwIfWorkflowAborted(opts?.abortSignal);
|
|
1008
1143
|
// Build previous steps summary for workflow context
|
|
1009
1144
|
const prevStepsText = steps
|
|
1010
1145
|
.filter(s => s.status === 'completed' && s.index < step.index)
|
|
@@ -1023,8 +1158,40 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1023
1158
|
retryCount: step.attempts,
|
|
1024
1159
|
loopCount: step.loopCount,
|
|
1025
1160
|
};
|
|
1026
|
-
|
|
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
|
+
}
|
|
1027
1193
|
if (opts?.isOrchestrated && opts.taskId) {
|
|
1194
|
+
throwIfWorkflowAborted(opts?.abortSignal);
|
|
1028
1195
|
const completedTodo = data.getTodoTask(opts.taskId, 'completed');
|
|
1029
1196
|
if (completedTodo) {
|
|
1030
1197
|
step._handoffPrompt = completedTodo.handoff_prompt || '';
|
|
@@ -1304,6 +1471,31 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1304
1471
|
catch { /* best effort */ }
|
|
1305
1472
|
}
|
|
1306
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
|
+
}
|
|
1307
1499
|
const authLikeFailure = typeof err?.message === 'string'
|
|
1308
1500
|
&& (err.message.startsWith('AUTH_REQUIRED:')
|
|
1309
1501
|
|| /invalid_grant/i.test(err.message)
|
|
@@ -1364,14 +1556,40 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1364
1556
|
}
|
|
1365
1557
|
step.attempts++;
|
|
1366
1558
|
step.error = err.message;
|
|
1367
|
-
|
|
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) {
|
|
1368
1564
|
step.status = 'failed';
|
|
1565
|
+
step.result = providerLimitFailure?.exactMessage || err.message;
|
|
1566
|
+
step.error = providerLimitFailure?.exactMessage || err.message;
|
|
1369
1567
|
step.completedAt = Date.now();
|
|
1370
1568
|
failed.add(step.index);
|
|
1371
1569
|
for (const s of steps) {
|
|
1372
1570
|
if (s.dependsOn.includes(step.index) && s.status === 'pending') {
|
|
1373
1571
|
s.status = 'skipped';
|
|
1374
|
-
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}`);
|
|
1375
1593
|
}
|
|
1376
1594
|
}
|
|
1377
1595
|
this.emitProgress(workflowId, step, steps, 'step-failed', opts?.onProgress, undefined, opts?.onWorkerChunk);
|
|
@@ -1382,6 +1600,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1382
1600
|
status: 'failed',
|
|
1383
1601
|
attempts: step.attempts,
|
|
1384
1602
|
error: step.error,
|
|
1603
|
+
resultSummary: step.result?.slice(0, 200),
|
|
1385
1604
|
completedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
|
|
1386
1605
|
});
|
|
1387
1606
|
}
|
|
@@ -1514,21 +1733,216 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1514
1733
|
const effectivePrompt = opts?.isOrchestrated && opts?.taskId
|
|
1515
1734
|
? `CURRENT TODO ID: ${opts.taskId}\nUse this TODO id when calling complete_worker_task or block_worker_task.\n\n${prompt}`
|
|
1516
1735
|
: prompt;
|
|
1517
|
-
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 }];
|
|
1518
1775
|
const promptChars = effectivePrompt.length;
|
|
1519
1776
|
const systemChars = systemPrompt.length;
|
|
1520
1777
|
const isLocalCliSession = effectiveRuntimeMode === 'local_desktop'
|
|
1521
1778
|
&& (activeRuntime.providerName === 'claude-cli' || activeRuntime.providerName === 'codex-cli');
|
|
1522
1779
|
const workflowCliSessionTransport = resolveWorkflowCliSessionTransport(activeRuntime.providerName, isLocalCliSession);
|
|
1780
|
+
const cliSessionScopeKey = String(opts?.cliSessionScopeKey || '').trim() || null;
|
|
1523
1781
|
const hasPersistentConversation = isLocalCliSession && !!conversationId;
|
|
1524
1782
|
const cliSessionEpochPlan = hasPersistentConversation
|
|
1525
|
-
? (0, cli_session_epoch_1.
|
|
1526
|
-
: {
|
|
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
|
+
};
|
|
1527
1791
|
let activeCliSessionId = cliSessionEpochPlan.resumeSessionId;
|
|
1528
1792
|
const cliEpochStartedAt = cliSessionEpochPlan.resumeSessionId
|
|
1529
1793
|
? (cliSessionEpochPlan.existing?.epoch_started_at || (0, cli_session_epoch_1.localTimestamp)())
|
|
1530
1794
|
: (0, cli_session_epoch_1.localTimestamp)();
|
|
1531
|
-
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
|
+
};
|
|
1532
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}`);
|
|
1533
1947
|
// Agentic loop
|
|
1534
1948
|
let iteration = 0;
|
|
@@ -1536,6 +1950,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1536
1950
|
let incompleteExecutionRecoveries = 0;
|
|
1537
1951
|
let selectedRuntimeRetryCount = 0;
|
|
1538
1952
|
while (iteration < MAX_ITERATIONS) {
|
|
1953
|
+
throwIfWorkflowAborted(opts?.abortSignal);
|
|
1539
1954
|
if (opts?.isOrchestrated && this.orchestratedTodoHandled(opts.taskId)) {
|
|
1540
1955
|
return this.buildOrchestratedTodoHandledResult(opts.taskId);
|
|
1541
1956
|
}
|
|
@@ -1559,8 +1974,10 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1559
1974
|
const codexAppServerManager = (0, codex_app_server_manager_1.getCodexAppServerManager)();
|
|
1560
1975
|
let appServerAttempt = 0;
|
|
1561
1976
|
while (true) {
|
|
1977
|
+
throwIfWorkflowAborted(opts?.abortSignal);
|
|
1562
1978
|
appServerAttempt++;
|
|
1563
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}`);
|
|
1564
1981
|
const appServerResult = await codexAppServerManager.runTurn({
|
|
1565
1982
|
runtimeMode: 'local_desktop',
|
|
1566
1983
|
conversationId: ptyConversationKey,
|
|
@@ -1569,7 +1986,8 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1569
1986
|
cwd: effectiveProjectDir,
|
|
1570
1987
|
systemPrompt,
|
|
1571
1988
|
messages,
|
|
1572
|
-
|
|
1989
|
+
abortSignal: opts?.abortSignal,
|
|
1990
|
+
forceFreshSession: !hasPersistentConversation || (iteration === 1 && !codexAppServerManager.hasActiveSession(ptyConversationKey, step.agentId) && !cliSessionEpochPlan.resumeSessionId),
|
|
1573
1991
|
resumeSessionId: cliSessionEpochPlan.resumeSessionId || undefined,
|
|
1574
1992
|
model: activeRuntime.model,
|
|
1575
1993
|
projectId: opts?.projectId ?? null,
|
|
@@ -1604,10 +2022,61 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1604
2022
|
: [step];
|
|
1605
2023
|
this.emitProgress(workflowContext?.workflowId || 'workflow', step, activeSteps, 'step-progress', opts?.onProgress, `${step.agentName} is still working on ${step.description}`);
|
|
1606
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
|
+
},
|
|
1607
2075
|
});
|
|
1608
2076
|
if (appServerResult.sessionId) {
|
|
1609
2077
|
activeCliSessionId = appServerResult.sessionId;
|
|
1610
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}`);
|
|
1611
2080
|
response = {
|
|
1612
2081
|
content: appServerResult.content || '',
|
|
1613
2082
|
usage: appServerResult.usage,
|
|
@@ -1615,13 +2084,20 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1615
2084
|
break;
|
|
1616
2085
|
}
|
|
1617
2086
|
catch (appServerErr) {
|
|
1618
|
-
|
|
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
|
+
}
|
|
1619
2092
|
if (appServerAttempt >= LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT
|
|
1620
2093
|
|| !shouldRetrySelectedWorkflowRuntime(appServerErr)) {
|
|
1621
2094
|
throw appServerErr;
|
|
1622
2095
|
}
|
|
1623
|
-
|
|
1624
|
-
|
|
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);
|
|
1625
2101
|
}
|
|
1626
2102
|
}
|
|
1627
2103
|
}
|
|
@@ -1631,17 +2107,46 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1631
2107
|
}
|
|
1632
2108
|
const ptyManager = (0, local_cli_pty_manager_1.getLocalCliPtySessionManager)();
|
|
1633
2109
|
let ptyAttempt = 0;
|
|
2110
|
+
let forceFreshInteractiveCliSession = false;
|
|
2111
|
+
let currentAttemptSessionId = null;
|
|
2112
|
+
let currentAttemptWasFreshSession = false;
|
|
1634
2113
|
while (true) {
|
|
2114
|
+
throwIfWorkflowAborted(opts?.abortSignal);
|
|
1635
2115
|
ptyAttempt++;
|
|
1636
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;
|
|
1637
2126
|
const ptyResult = await ptyManager.runTurn({
|
|
1638
2127
|
conversationId: ptyConversationKey,
|
|
1639
2128
|
botId: step.agentId,
|
|
1640
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
|
+
},
|
|
1641
2139
|
cwd: effectiveProjectDir,
|
|
2140
|
+
toolActorId: step.agentName,
|
|
2141
|
+
toolProjectId: opts?.projectId ?? null,
|
|
2142
|
+
currentTodoTaskId: opts?.taskId,
|
|
2143
|
+
useConpty: shouldUseConptyForWorkflowClaudePty(),
|
|
1642
2144
|
systemPrompt,
|
|
1643
2145
|
messages,
|
|
1644
|
-
|
|
2146
|
+
abortSignal: opts?.abortSignal,
|
|
2147
|
+
forceFreshSession: isFreshSession,
|
|
2148
|
+
resumeSessionId: isFreshSession ? undefined : (cliSessionEpochPlan.resumeSessionId || undefined),
|
|
2149
|
+
newSessionId,
|
|
1645
2150
|
onRawChunk: async (chunk) => {
|
|
1646
2151
|
if (opts?.onWorkerChunk) {
|
|
1647
2152
|
opts.onWorkerChunk({
|
|
@@ -1655,6 +2160,93 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1655
2160
|
});
|
|
1656
2161
|
}
|
|
1657
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.
|
|
2167
|
+
onDetail: async (detail) => {
|
|
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
|
+
},
|
|
1658
2250
|
onChunk: async (chunk) => {
|
|
1659
2251
|
streamedContent += chunk;
|
|
1660
2252
|
if (opts?.onWorkerChunk) {
|
|
@@ -1688,12 +2280,41 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1688
2280
|
break;
|
|
1689
2281
|
}
|
|
1690
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
|
+
}
|
|
1691
2305
|
if (ptyAttempt >= LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT
|
|
1692
2306
|
|| !shouldRetrySelectedWorkflowRuntime(ptyErr)) {
|
|
1693
2307
|
throw ptyErr;
|
|
1694
2308
|
}
|
|
1695
|
-
|
|
1696
|
-
|
|
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);
|
|
1697
2318
|
}
|
|
1698
2319
|
}
|
|
1699
2320
|
}
|
|
@@ -1740,6 +2361,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1740
2361
|
},
|
|
1741
2362
|
tools: opts?.disableTools ? undefined : toolDefs,
|
|
1742
2363
|
cwd: effectiveProjectDir,
|
|
2364
|
+
abortSignal: opts?.abortSignal,
|
|
1743
2365
|
});
|
|
1744
2366
|
// Flush remaining chunk buffer
|
|
1745
2367
|
if (opts?.onWorkerChunk && chunkBuffer) {
|
|
@@ -1764,7 +2386,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1764
2386
|
&& shouldRetrySelectedWorkflowRuntime(err)) {
|
|
1765
2387
|
selectedRuntimeRetryCount++;
|
|
1766
2388
|
console.warn(`[workflow-engine] ${step.agentName} selected runtime failed, retrying the same connection (${selectedRuntimeRetryCount + 1}/${LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT})`);
|
|
1767
|
-
await pauseWorkflowRuntimeRetry(selectedRuntimeRetryCount);
|
|
2389
|
+
await pauseWorkflowRuntimeRetry(selectedRuntimeRetryCount, opts?.abortSignal);
|
|
1768
2390
|
iteration--;
|
|
1769
2391
|
continue;
|
|
1770
2392
|
}
|
|
@@ -1792,29 +2414,43 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1792
2414
|
throw err;
|
|
1793
2415
|
}
|
|
1794
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)'}`);
|
|
1795
2428
|
const nextEpochTurnCount = cliSessionEpochPlan.resumeSessionId
|
|
1796
2429
|
? ((cliSessionEpochPlan.existing?.epoch_turn_count || 0) + 1)
|
|
1797
2430
|
: 1;
|
|
1798
2431
|
data.upsertCliSessionEpoch({
|
|
1799
2432
|
conversationId: conversationId,
|
|
2433
|
+
sessionScopeKey: cliSessionScopeKey,
|
|
1800
2434
|
botId: step.agentId,
|
|
1801
2435
|
provider: activeRuntime.providerName,
|
|
1802
2436
|
sessionId: activeCliSessionId,
|
|
1803
2437
|
epochTurnCount: nextEpochTurnCount,
|
|
1804
2438
|
lastInputTokens: response.usage?.inputTokens ?? null,
|
|
1805
2439
|
lastOutputTokens: response.usage?.outputTokens ?? null,
|
|
1806
|
-
resetReason:
|
|
2440
|
+
resetReason: cliEpochResetReason,
|
|
1807
2441
|
epochStartedAt: cliEpochStartedAt,
|
|
1808
2442
|
lastUsedAt: (0, cli_session_epoch_1.localTimestamp)(),
|
|
1809
2443
|
});
|
|
1810
2444
|
}
|
|
1811
2445
|
if (response.toolCalls && response.toolCalls.length > 0) {
|
|
2446
|
+
throwIfWorkflowAborted(opts?.abortSignal);
|
|
1812
2447
|
messages.push({
|
|
1813
2448
|
role: 'assistant',
|
|
1814
2449
|
content: response.content || '',
|
|
1815
2450
|
toolCalls: response.toolCalls,
|
|
1816
2451
|
});
|
|
1817
2452
|
for (const tc of response.toolCalls) {
|
|
2453
|
+
throwIfWorkflowAborted(opts?.abortSignal);
|
|
1818
2454
|
const permMode = unrestrictedCliProvider
|
|
1819
2455
|
? 'autopilot'
|
|
1820
2456
|
: (profile?.permission_mode || 'autopilot');
|
|
@@ -1838,6 +2474,7 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
1838
2474
|
}
|
|
1839
2475
|
const result = await (0, index_2.executeAndVerify)({ id: tc.id, name: tc.name, arguments: tc.arguments }, toolCtx);
|
|
1840
2476
|
const output = result.success ? result.output : `ERROR: ${result.error || 'Unknown'}`;
|
|
2477
|
+
throwIfWorkflowAborted(opts?.abortSignal);
|
|
1841
2478
|
messages.push({ role: 'tool', content: output, toolCallId: tc.id, toolName: tc.name });
|
|
1842
2479
|
// Emit tool_result event
|
|
1843
2480
|
if (opts?.onWorkerChunk) {
|
|
@@ -2291,15 +2928,38 @@ class WorkflowEngine extends events_1.EventEmitter {
|
|
|
2291
2928
|
return count >= ERROR_LOOP_THRESHOLD;
|
|
2292
2929
|
}
|
|
2293
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) {
|
|
2294
2937
|
const normalized = errorMessage.toLowerCase();
|
|
2295
|
-
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')
|
|
2296
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')
|
|
2297
2957
|
|| normalized.includes('exceeded your current quota')
|
|
2298
2958
|
|| normalized.includes('insufficient_quota')
|
|
2299
|
-
|| normalized.includes('billing details')
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2959
|
+
|| normalized.includes('billing details')) {
|
|
2960
|
+
return { reasonCode: 'provider_quota_limit', exactMessage };
|
|
2961
|
+
}
|
|
2962
|
+
return null;
|
|
2303
2963
|
}
|
|
2304
2964
|
/**
|
|
2305
2965
|
* Replan remaining work when retries are exhausted.
|