funolio-agent 1.0.7 → 1.0.48

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.
Files changed (239) hide show
  1. package/dist/agent-config.d.ts +9 -1
  2. package/dist/agent-config.d.ts.map +1 -1
  3. package/dist/agent-config.js +4 -1
  4. package/dist/agent-config.js.map +1 -1
  5. package/dist/approval.d.ts +1 -0
  6. package/dist/approval.d.ts.map +1 -1
  7. package/dist/approval.js +12 -0
  8. package/dist/approval.js.map +1 -1
  9. package/dist/auth/auto-detect.d.ts +11 -3
  10. package/dist/auth/auto-detect.d.ts.map +1 -1
  11. package/dist/auth/auto-detect.js +136 -168
  12. package/dist/auth/auto-detect.js.map +1 -1
  13. package/dist/auth/subscription-runtime.js +1 -1
  14. package/dist/auth/subscription-runtime.js.map +1 -1
  15. package/dist/auto-organizer.d.ts.map +1 -1
  16. package/dist/auto-organizer.js +4 -3
  17. package/dist/auto-organizer.js.map +1 -1
  18. package/dist/backfill.d.ts.map +1 -1
  19. package/dist/backfill.js +34 -30
  20. package/dist/backfill.js.map +1 -1
  21. package/dist/bot-manager.d.ts +4 -8
  22. package/dist/bot-manager.d.ts.map +1 -1
  23. package/dist/bot-manager.js +31 -160
  24. package/dist/bot-manager.js.map +1 -1
  25. package/dist/clerk-model.d.ts +15 -7
  26. package/dist/clerk-model.d.ts.map +1 -1
  27. package/dist/clerk-model.js +78 -43
  28. package/dist/clerk-model.js.map +1 -1
  29. package/dist/cli-session-epoch.d.ts +10 -0
  30. package/dist/cli-session-epoch.d.ts.map +1 -0
  31. package/dist/cli-session-epoch.js +61 -0
  32. package/dist/cli-session-epoch.js.map +1 -0
  33. package/dist/cli.js +7 -2
  34. package/dist/cli.js.map +1 -1
  35. package/dist/commands/import-history.js +5 -1
  36. package/dist/commands/import-history.js.map +1 -1
  37. package/dist/commands/init.d.ts.map +1 -1
  38. package/dist/commands/init.js +30 -1
  39. package/dist/commands/init.js.map +1 -1
  40. package/dist/commands/pool.js +1 -1
  41. package/dist/commands/pool.js.map +1 -1
  42. package/dist/commands/setup.d.ts +37 -0
  43. package/dist/commands/setup.d.ts.map +1 -1
  44. package/dist/commands/setup.js +146 -43
  45. package/dist/commands/setup.js.map +1 -1
  46. package/dist/commands/start.d.ts.map +1 -1
  47. package/dist/commands/start.js +117 -255
  48. package/dist/commands/start.js.map +1 -1
  49. package/dist/config-cleanup.d.ts.map +1 -1
  50. package/dist/config-cleanup.js +2 -1
  51. package/dist/config-cleanup.js.map +1 -1
  52. package/dist/config.d.ts +6 -9
  53. package/dist/config.d.ts.map +1 -1
  54. package/dist/config.js +7 -18
  55. package/dist/config.js.map +1 -1
  56. package/dist/context-window.d.ts +33 -5
  57. package/dist/context-window.d.ts.map +1 -1
  58. package/dist/context-window.js +122 -21
  59. package/dist/context-window.js.map +1 -1
  60. package/dist/eval/orchestrator-front-door-replay.js +1 -1
  61. package/dist/eval/orchestrator-front-door-replay.js.map +1 -1
  62. package/dist/eval/policy-detection-replay.js +1 -1
  63. package/dist/eval/policy-detection-replay.js.map +1 -1
  64. package/dist/import-parser-core.d.ts.map +1 -1
  65. package/dist/import-parser-core.js +74 -8
  66. package/dist/import-parser-core.js.map +1 -1
  67. package/dist/integration-tokens.d.ts +1 -6
  68. package/dist/integration-tokens.d.ts.map +1 -1
  69. package/dist/integration-tokens.js +38 -40
  70. package/dist/integration-tokens.js.map +1 -1
  71. package/dist/local-cli-pty-manager.d.ts +50 -0
  72. package/dist/local-cli-pty-manager.d.ts.map +1 -0
  73. package/dist/local-cli-pty-manager.js +645 -0
  74. package/dist/local-cli-pty-manager.js.map +1 -0
  75. package/dist/local-data.d.ts +89 -6
  76. package/dist/local-data.d.ts.map +1 -1
  77. package/dist/local-data.js +600 -63
  78. package/dist/local-data.js.map +1 -1
  79. package/dist/local-db.d.ts.map +1 -1
  80. package/dist/local-db.js +74 -1
  81. package/dist/local-db.js.map +1 -1
  82. package/dist/local-funnel.d.ts +0 -7
  83. package/dist/local-funnel.d.ts.map +1 -1
  84. package/dist/local-funnel.js +22 -30
  85. package/dist/local-funnel.js.map +1 -1
  86. package/dist/local-import-worker.d.ts.map +1 -1
  87. package/dist/local-import-worker.js +49 -4
  88. package/dist/local-import-worker.js.map +1 -1
  89. package/dist/local-memory-search.d.ts +1 -0
  90. package/dist/local-memory-search.d.ts.map +1 -1
  91. package/dist/local-memory-search.js +107 -21
  92. package/dist/local-memory-search.js.map +1 -1
  93. package/dist/local-server.d.ts +21 -0
  94. package/dist/local-server.d.ts.map +1 -1
  95. package/dist/local-server.js +1057 -501
  96. package/dist/local-server.js.map +1 -1
  97. package/dist/mcp/bridge-server.d.ts.map +1 -1
  98. package/dist/mcp/bridge-server.js +2 -1
  99. package/dist/mcp/bridge-server.js.map +1 -1
  100. package/dist/mcp/local-memory-server.d.ts +6 -1
  101. package/dist/mcp/local-memory-server.d.ts.map +1 -1
  102. package/dist/mcp/local-memory-server.js +38 -13
  103. package/dist/mcp/local-memory-server.js.map +1 -1
  104. package/dist/mcp/manager.d.ts +3 -22
  105. package/dist/mcp/manager.d.ts.map +1 -1
  106. package/dist/mcp/manager.js +66 -320
  107. package/dist/mcp/manager.js.map +1 -1
  108. package/dist/memory-extraction.d.ts +2 -0
  109. package/dist/memory-extraction.d.ts.map +1 -1
  110. package/dist/memory-extraction.js +3 -1
  111. package/dist/memory-extraction.js.map +1 -1
  112. package/dist/message-loop.d.ts +1 -3
  113. package/dist/message-loop.d.ts.map +1 -1
  114. package/dist/message-loop.js +220 -437
  115. package/dist/message-loop.js.map +1 -1
  116. package/dist/mqtt-client.d.ts +2 -28
  117. package/dist/mqtt-client.d.ts.map +1 -1
  118. package/dist/mqtt-client.js +2 -2
  119. package/dist/mqtt-client.js.map +1 -1
  120. package/dist/oauth.d.ts +6 -0
  121. package/dist/oauth.d.ts.map +1 -1
  122. package/dist/oauth.js +91 -0
  123. package/dist/oauth.js.map +1 -1
  124. package/dist/orchestration/front-door-policy.d.ts +5 -2
  125. package/dist/orchestration/front-door-policy.d.ts.map +1 -1
  126. package/dist/orchestration/front-door-policy.js +25 -28
  127. package/dist/orchestration/front-door-policy.js.map +1 -1
  128. package/dist/orchestration/orchestrator-blocked-prompt.d.ts +2 -1
  129. package/dist/orchestration/orchestrator-blocked-prompt.d.ts.map +1 -1
  130. package/dist/orchestration/orchestrator-blocked-prompt.js +12 -1
  131. package/dist/orchestration/orchestrator-blocked-prompt.js.map +1 -1
  132. package/dist/orchestration/orchestrator-final-response-prompt.d.ts +4 -1
  133. package/dist/orchestration/orchestrator-final-response-prompt.d.ts.map +1 -1
  134. package/dist/orchestration/orchestrator-final-response-prompt.js +9 -7
  135. package/dist/orchestration/orchestrator-final-response-prompt.js.map +1 -1
  136. package/dist/orchestration/orchestrator-operating-prompt.d.ts +11 -0
  137. package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
  138. package/dist/orchestration/orchestrator-operating-prompt.js +67 -44
  139. package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
  140. package/dist/orchestration/worker-operating-prompt.d.ts +2 -0
  141. package/dist/orchestration/worker-operating-prompt.d.ts.map +1 -1
  142. package/dist/orchestration/worker-operating-prompt.js +41 -2
  143. package/dist/orchestration/worker-operating-prompt.js.map +1 -1
  144. package/dist/orchestrator.d.ts +17 -0
  145. package/dist/orchestrator.d.ts.map +1 -1
  146. package/dist/orchestrator.js +328 -166
  147. package/dist/orchestrator.js.map +1 -1
  148. package/dist/prompt-template.js +3 -3
  149. package/dist/prompt-template.js.map +1 -1
  150. package/dist/providers/anthropic.d.ts +0 -5
  151. package/dist/providers/anthropic.d.ts.map +1 -1
  152. package/dist/providers/anthropic.js +29 -75
  153. package/dist/providers/anthropic.js.map +1 -1
  154. package/dist/providers/claude-cli-prompt.d.ts.map +1 -1
  155. package/dist/providers/claude-cli-prompt.js +22 -6
  156. package/dist/providers/claude-cli-prompt.js.map +1 -1
  157. package/dist/providers/claude-cli.d.ts.map +1 -1
  158. package/dist/providers/claude-cli.js +36 -142
  159. package/dist/providers/claude-cli.js.map +1 -1
  160. package/dist/providers/codex-cli.d.ts.map +1 -1
  161. package/dist/providers/codex-cli.js +148 -74
  162. package/dist/providers/codex-cli.js.map +1 -1
  163. package/dist/providers/google.d.ts.map +1 -1
  164. package/dist/providers/google.js +4 -2
  165. package/dist/providers/google.js.map +1 -1
  166. package/dist/providers/index.d.ts +13 -0
  167. package/dist/providers/index.d.ts.map +1 -1
  168. package/dist/providers/index.js.map +1 -1
  169. package/dist/providers/openai.d.ts.map +1 -1
  170. package/dist/providers/openai.js +27 -2
  171. package/dist/providers/openai.js.map +1 -1
  172. package/dist/runtime-context.d.ts +10 -0
  173. package/dist/runtime-context.d.ts.map +1 -0
  174. package/dist/runtime-context.js +30 -0
  175. package/dist/runtime-context.js.map +1 -0
  176. package/dist/storage-mode.d.ts +5 -0
  177. package/dist/storage-mode.d.ts.map +1 -0
  178. package/dist/storage-mode.js +21 -0
  179. package/dist/storage-mode.js.map +1 -0
  180. package/dist/subagent/queue.d.ts.map +1 -1
  181. package/dist/subagent/queue.js +1 -0
  182. package/dist/subagent/queue.js.map +1 -1
  183. package/dist/summarization-pipeline.d.ts +10 -0
  184. package/dist/summarization-pipeline.d.ts.map +1 -1
  185. package/dist/summarization-pipeline.js +147 -34
  186. package/dist/summarization-pipeline.js.map +1 -1
  187. package/dist/tool-permissions.d.ts +2 -0
  188. package/dist/tool-permissions.d.ts.map +1 -0
  189. package/dist/tool-permissions.js +25 -0
  190. package/dist/tool-permissions.js.map +1 -0
  191. package/dist/tools/analyze-image.js +2 -2
  192. package/dist/tools/analyze-image.js.map +1 -1
  193. package/dist/tools/edit-file.js +3 -3
  194. package/dist/tools/edit-file.js.map +1 -1
  195. package/dist/tools/index.d.ts +7 -8
  196. package/dist/tools/index.d.ts.map +1 -1
  197. package/dist/tools/index.js +106 -60
  198. package/dist/tools/index.js.map +1 -1
  199. package/dist/tools/list-directory.js +7 -4
  200. package/dist/tools/list-directory.js.map +1 -1
  201. package/dist/tools/read-file.js +3 -3
  202. package/dist/tools/read-file.js.map +1 -1
  203. package/dist/tools/run-command.js +3 -3
  204. package/dist/tools/run-command.js.map +1 -1
  205. package/dist/tools/sandbox.d.ts +10 -5
  206. package/dist/tools/sandbox.d.ts.map +1 -1
  207. package/dist/tools/sandbox.js +41 -13
  208. package/dist/tools/sandbox.js.map +1 -1
  209. package/dist/tools/search-codebase.js +2 -2
  210. package/dist/tools/search-codebase.js.map +1 -1
  211. package/dist/tools/search-local-memory.d.ts.map +1 -1
  212. package/dist/tools/search-local-memory.js +19 -8
  213. package/dist/tools/search-local-memory.js.map +1 -1
  214. package/dist/tools/search-memory.d.ts.map +1 -1
  215. package/dist/tools/search-memory.js +9 -3
  216. package/dist/tools/search-memory.js.map +1 -1
  217. package/dist/tools/spawn-subagent.d.ts.map +1 -1
  218. package/dist/tools/spawn-subagent.js +1 -0
  219. package/dist/tools/spawn-subagent.js.map +1 -1
  220. package/dist/tools/write-file.js +3 -3
  221. package/dist/tools/write-file.js.map +1 -1
  222. package/dist/types.d.ts +5 -0
  223. package/dist/types.d.ts.map +1 -1
  224. package/dist/types.js +0 -3
  225. package/dist/types.js.map +1 -1
  226. package/dist/verification/index.js +2 -2
  227. package/dist/verification/index.js.map +1 -1
  228. package/dist/wizard-state.d.ts.map +1 -1
  229. package/dist/wizard-state.js +16 -2
  230. package/dist/wizard-state.js.map +1 -1
  231. package/dist/wizard-support.d.ts +2 -2
  232. package/dist/wizard-support.d.ts.map +1 -1
  233. package/dist/wizard-support.js +88 -99
  234. package/dist/wizard-support.js.map +1 -1
  235. package/dist/workflow-engine.d.ts +9 -3
  236. package/dist/workflow-engine.d.ts.map +1 -1
  237. package/dist/workflow-engine.js +378 -82
  238. package/dist/workflow-engine.js.map +1 -1
  239. package/package.json +2 -1
@@ -62,18 +62,47 @@ const safeguards_1 = require("./orchestration/safeguards");
62
62
  const data = __importStar(require("./local-data"));
63
63
  const orchestrator_profile_1 = require("./orchestrator-profile");
64
64
  const subscription_runtime_1 = require("./auth/subscription-runtime");
65
+ const runtime_context_1 = require("./runtime-context");
66
+ const local_cli_pty_manager_1 = require("./local-cli-pty-manager");
67
+ const cli_session_epoch_1 = require("./cli-session-epoch");
65
68
  // ─── Workflow Engine ─────────────────────────────────────────────
66
69
  const MAX_STEP_ATTEMPTS = safeguards_1.SAFEGUARDS.MAX_AGENT_ATTEMPTS;
67
70
  const MAX_STEPS = safeguards_1.SAFEGUARDS.MAX_WORKFLOW_STEPS;
68
71
  function isInteractiveAuthFailure(text) {
69
72
  return /\b(not logged in|please run \/login|unauthorized|invalid api key|authentication required)\b/i.test(text);
70
73
  }
74
+ const LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT = 2;
75
+ function shouldRetrySelectedWorkflowRuntime(err) {
76
+ const text = String(err?.message || err || '').toLowerCase();
77
+ if (!text)
78
+ 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)) {
80
+ return false;
81
+ }
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);
83
+ }
84
+ async function pauseWorkflowRuntimeRetry(attempt) {
85
+ const delayMs = attempt <= 1 ? 750 : 1500;
86
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
87
+ }
88
+ function buildWorkflowPtyConversationKey(conversationId, step, workflowContext) {
89
+ if (conversationId)
90
+ return conversationId;
91
+ if (workflowContext?.workflowId)
92
+ return `workflow:${workflowContext.workflowId}`;
93
+ return `adhoc:${step.agentId}:${step.id}`;
94
+ }
71
95
  class WorkflowEngine extends events_1.EventEmitter {
72
96
  projectDir;
97
+ runtimeMode;
73
98
  activeWorkflows = new Map();
74
- constructor(projectDir) {
99
+ constructor(projectDir, runtimeMode) {
75
100
  super();
76
101
  this.projectDir = projectDir;
102
+ this.runtimeMode = (0, runtime_context_1.normalizeRuntimeMode)(runtimeMode || (0, runtime_context_1.getDefaultRuntimeMode)());
103
+ }
104
+ getRuntimeMode() {
105
+ return this.runtimeMode;
77
106
  }
78
107
  /**
79
108
  * Execute a workflow: decompose a prompt into steps, route, and execute.
@@ -82,7 +111,8 @@ class WorkflowEngine extends events_1.EventEmitter {
82
111
  async execute(prompt, conversationId, agentId, opts) {
83
112
  const workflowId = crypto.randomUUID();
84
113
  const startedAt = Date.now();
85
- const clerk = (0, clerk_model_1.getClerk)();
114
+ const effectiveRuntimeMode = (0, runtime_context_1.normalizeRuntimeMode)(opts?.runtimeMode || this.runtimeMode);
115
+ const clerk = (0, clerk_model_1.getClerk)({ runtimeMode: effectiveRuntimeMode });
86
116
  const allProfiles = data.listAgentProfiles();
87
117
  // Filter out orchestrator profiles by default, but allow explicitly assigned specialists
88
118
  // and the selected agent back into the planning pool when needed.
@@ -100,10 +130,12 @@ class WorkflowEngine extends events_1.EventEmitter {
100
130
  }
101
131
  else {
102
132
  // Single agent, single step
133
+ const stepDescription = opts?.stepDescription?.trim()
134
+ || prompt.slice(0, 100);
103
135
  steps = [{
104
136
  id: `${workflowId}-0`,
105
137
  index: 0,
106
- description: prompt.slice(0, 100),
138
+ description: stepDescription,
107
139
  prompt,
108
140
  agentId: profile.id,
109
141
  agentName: profile.name,
@@ -281,7 +313,7 @@ class WorkflowEngine extends events_1.EventEmitter {
281
313
  `TODO ITEM #${task.id}: ${task.title}`,
282
314
  task.details ? `Task details:\n${task.details}` : '',
283
315
  'Work only on this TODO item. Do not mark any other item complete.',
284
- 'When your stage is complete, return the structured footer Funolio requires so routing can continue automatically.',
316
+ 'When your stage is complete, return the structured footer required below so routing can continue automatically.',
285
317
  ].filter(Boolean).join('\n\n');
286
318
  const steps = template.steps
287
319
  .sort((a, b) => a.order_index - b.order_index)
@@ -633,7 +665,7 @@ class WorkflowEngine extends events_1.EventEmitter {
633
665
  ? 'Include REASON only when STATUS is FAIL.'
634
666
  : 'Include REASON only when STATUS is BLOCKED.';
635
667
  return [
636
- `You are ${profile?.name || 'a Funolio workflow worker'}.`,
668
+ `You are ${profile?.name || 'a workflow worker'}.`,
637
669
  'Complete only the assigned workflow step.',
638
670
  roleLine,
639
671
  'Use available tools when the task requires creating, changing, or verifying files.',
@@ -654,8 +686,8 @@ class WorkflowEngine extends events_1.EventEmitter {
654
686
  ? 'Your role is QA. Verify the delivered work, then complete the TODO through the worker tool or create the next fix handoff.'
655
687
  : 'Your role is implementer. Do the assigned work, then complete the TODO through the worker tool.';
656
688
  return [
657
- `You are ${profile?.name || 'a Funolio workflow worker'}.`,
658
- 'You are executing one orchestrated TODO task inside Funolio.',
689
+ `You are ${profile?.name || 'a workflow worker'}.`,
690
+ 'You are executing one orchestrated TODO task.',
659
691
  roleLine,
660
692
  'Follow the assignment in the user message exactly.',
661
693
  'Use available tools when needed.',
@@ -831,6 +863,7 @@ class WorkflowEngine extends events_1.EventEmitter {
831
863
  * Handles checkpoint loop-back and footer-driven retry/failure logic.
832
864
  */
833
865
  async executeSteps(workflowId, steps, conversationId, opts) {
866
+ const effectiveRuntimeMode = (0, runtime_context_1.normalizeRuntimeMode)(opts?.runtimeMode || this.runtimeMode);
834
867
  const completed = new Set();
835
868
  const failed = new Set();
836
869
  const allowMissingFooterForDirectStep = !opts?.isOrchestrated && !!opts?.disableDecomposition && steps.length === 1;
@@ -902,6 +935,53 @@ class WorkflowEngine extends events_1.EventEmitter {
902
935
  loopCount: step.loopCount,
903
936
  };
904
937
  const result = await this.executeStep(step, fullPrompt, conversationId, opts, opts?.apiKey, wfContext);
938
+ if (opts?.isOrchestrated && opts.taskId) {
939
+ const completedTodo = data.getTodoTask(opts.taskId, 'completed');
940
+ if (completedTodo) {
941
+ step.status = 'completed';
942
+ step.result = completedTodo.output_summary || completedTodo.handoff_prompt || result;
943
+ step.completedAt = Date.now();
944
+ completed.add(step.index);
945
+ this.emitProgress(workflowId, step, steps, 'step-completed', opts?.onProgress, undefined, opts?.onWorkerChunk);
946
+ try {
947
+ const execId = step._execId;
948
+ if (execId) {
949
+ data.updateStepExecution(execId, {
950
+ status: 'completed',
951
+ attempts: step.attempts,
952
+ resultSummary: step.result.length > 200 ? step.result.slice(0, 200) + '...' : step.result,
953
+ completedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
954
+ });
955
+ data.updateWorkflowExecution(workflowId, { completedSteps: completed.size });
956
+ }
957
+ }
958
+ catch { /* best effort */ }
959
+ return;
960
+ }
961
+ const blockedTodo = data.getTodoTask(opts.taskId, 'active');
962
+ if (blockedTodo?.blocker_summary) {
963
+ step.status = 'failed';
964
+ step.result = blockedTodo.blocker_summary;
965
+ step.error = blockedTodo.blocker_summary;
966
+ step.completedAt = Date.now();
967
+ failed.add(step.index);
968
+ this.emitProgress(workflowId, step, steps, 'step-failed', opts?.onProgress, undefined, opts?.onWorkerChunk);
969
+ try {
970
+ const execId = step._execId;
971
+ if (execId) {
972
+ data.updateStepExecution(execId, {
973
+ status: 'failed',
974
+ attempts: step.attempts,
975
+ error: step.error,
976
+ resultSummary: blockedTodo.blocker_summary.slice(0, 200),
977
+ completedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
978
+ });
979
+ }
980
+ }
981
+ catch { /* best effort */ }
982
+ return;
983
+ }
984
+ }
905
985
  const footer = (0, status_parser_1.parseStructuredFooter)(result);
906
986
  const stepRole = this.inferRoleForStep(step);
907
987
  if (stepRole !== 'qa' && (footer.status === 'PASS' || footer.status === 'FAIL')) {
@@ -1066,7 +1146,7 @@ class WorkflowEngine extends events_1.EventEmitter {
1066
1146
  && !hasFutureQaStep
1067
1147
  && !opts?.isOrchestrated;
1068
1148
  if (shouldRunClerkVerification) {
1069
- const verifyClerk = (0, clerk_model_1.getClerk)();
1149
+ const verifyClerk = (0, clerk_model_1.getClerk)({ runtimeMode: effectiveRuntimeMode });
1070
1150
  if (verifyClerk) {
1071
1151
  try {
1072
1152
  const verification = await verifyClerk.verifyStepOutput(step.description, step.expectedOutput, result);
@@ -1237,7 +1317,7 @@ class WorkflowEngine extends events_1.EventEmitter {
1237
1317
  }
1238
1318
  else if (step.attempts < step.maxAttempts) {
1239
1319
  // Nudge on retry — generate adjusted prompt
1240
- const nudgeClerk = (0, clerk_model_1.getClerk)();
1320
+ const nudgeClerk = (0, clerk_model_1.getClerk)({ runtimeMode: effectiveRuntimeMode });
1241
1321
  if (nudgeClerk) {
1242
1322
  try {
1243
1323
  const nudge = await nudgeClerk.generateNudge(step.description, step.prompt, err.message);
@@ -1278,12 +1358,14 @@ class WorkflowEngine extends events_1.EventEmitter {
1278
1358
  * Execute a single workflow step: call LLM with tools, agentic loop.
1279
1359
  */
1280
1360
  async executeStep(step, prompt, conversationId, opts, apiKey, workflowContext) {
1361
+ const effectiveRuntimeMode = (0, runtime_context_1.normalizeRuntimeMode)(opts?.runtimeMode || this.runtimeMode);
1281
1362
  const profile = data.getAgentProfile(step.agentId);
1282
- const runtime = await this.resolveStepRuntime(step, profile, apiKey);
1363
+ const runtime = await this.resolveStepRuntime(step, profile, apiKey, opts);
1283
1364
  let activeRuntime = runtime;
1284
1365
  let llm = (0, index_1.createProvider)(activeRuntime.providerName, {
1285
1366
  apiKey: activeRuntime.apiKey,
1286
1367
  model: activeRuntime.model,
1368
+ runtimeMode: effectiveRuntimeMode,
1287
1369
  ...(activeRuntime.requestShape ? { requestShape: activeRuntime.requestShape } : {}),
1288
1370
  ...(activeRuntime.baseUrl ? { baseUrl: activeRuntime.baseUrl } : {}),
1289
1371
  ...(activeRuntime.apiQuery ? { apiQuery: activeRuntime.apiQuery } : {}),
@@ -1295,19 +1377,22 @@ class WorkflowEngine extends events_1.EventEmitter {
1295
1377
  ...(opts?.isOrchestrated ? { toolProjectId: opts?.projectId ?? null } : {}),
1296
1378
  ...(opts?.isOrchestrated ? { currentTodoTaskId: opts?.taskId } : {}),
1297
1379
  });
1298
- const toolDefs = (0, index_2.getAllToolDefinitions)();
1380
+ const toolDefs = (0, index_2.getAllToolDefinitions)(effectiveRuntimeMode);
1299
1381
  const workspacePath = opts?.workspacePath?.trim();
1300
1382
  const effectiveProjectDir = workspacePath || this.projectDir;
1383
+ const unrestrictedCliProvider = index_1.CLI_PROVIDERS.has(step.provider);
1301
1384
  const toolCtx = (0, index_2.createToolContext)(effectiveProjectDir, {
1302
1385
  projectId: opts?.projectId ?? null,
1303
1386
  currentTodoTaskId: opts?.taskId,
1304
1387
  actorType: 'llm',
1305
1388
  actorId: step.agentName || step.agentId,
1389
+ runtimeMode: effectiveRuntimeMode,
1390
+ restrictFileAccessToProject: unrestrictedCliProvider ? false : undefined,
1306
1391
  });
1307
1392
  const isWorkflowWorker = opts?.workerMode !== false;
1308
1393
  let systemPrompt = profile?.soul_md
1309
- || 'You are a Funolio AI agent working on a specific task step.';
1310
- const clerk = (0, clerk_model_1.getClerk)();
1394
+ || 'You are an AI assistant working on a specific task step.';
1395
+ const clerk = (0, clerk_model_1.getClerk)({ runtimeMode: effectiveRuntimeMode });
1311
1396
  if (opts?.systemPromptOverride?.trim()) {
1312
1397
  systemPrompt = opts.systemPromptOverride.trim();
1313
1398
  }
@@ -1342,12 +1427,27 @@ class WorkflowEngine extends events_1.EventEmitter {
1342
1427
  const messages = [{ role: 'user', content: effectivePrompt }];
1343
1428
  const promptChars = effectivePrompt.length;
1344
1429
  const systemChars = systemPrompt.length;
1430
+ const isLocalCliSession = effectiveRuntimeMode === 'local_desktop'
1431
+ && (activeRuntime.providerName === 'claude-cli' || activeRuntime.providerName === 'codex-cli');
1432
+ const hasPersistentConversation = isLocalCliSession && !!conversationId;
1433
+ const cliSessionEpochPlan = hasPersistentConversation
1434
+ ? (0, cli_session_epoch_1.selectCliSessionEpoch)(conversationId, step.agentId, activeRuntime.providerName)
1435
+ : { existing: undefined, resumeSessionId: null, resetReason: null };
1436
+ let activeCliSessionId = cliSessionEpochPlan.resumeSessionId;
1437
+ const cliEpochStartedAt = cliSessionEpochPlan.resumeSessionId
1438
+ ? (cliSessionEpochPlan.existing?.epoch_started_at || (0, cli_session_epoch_1.localTimestamp)())
1439
+ : (0, cli_session_epoch_1.localTimestamp)();
1440
+ const ptyConversationKey = buildWorkflowPtyConversationKey(conversationId, step, workflowContext);
1345
1441
  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}`);
1346
1442
  // Agentic loop
1347
1443
  let iteration = 0;
1348
1444
  const MAX_ITERATIONS = safeguards_1.SAFEGUARDS.MAX_WORKFLOW_ITERATIONS;
1349
1445
  let incompleteExecutionRecoveries = 0;
1446
+ let selectedRuntimeRetryCount = 0;
1350
1447
  while (iteration < MAX_ITERATIONS) {
1448
+ if (opts?.isOrchestrated && this.orchestratedTodoHandled(opts.taskId)) {
1449
+ return this.buildOrchestratedTodoHandledResult(opts.taskId);
1450
+ }
1351
1451
  iteration++;
1352
1452
  let streamedContent = '';
1353
1453
  let lastChunkProgressAt = 0;
@@ -1364,70 +1464,155 @@ class WorkflowEngine extends events_1.EventEmitter {
1364
1464
  : 'auto';
1365
1465
  let response;
1366
1466
  try {
1367
- response = await llm.chat({
1368
- messages,
1369
- system: systemPrompt,
1370
- stream: true,
1371
- toolChoice,
1372
- onChunk: async (chunk) => {
1373
- streamedContent += chunk;
1374
- // Forward chunks to onWorkerChunk with 150ms/80-char buffer
1375
- if (opts?.onWorkerChunk) {
1376
- chunkBuffer += chunk;
1377
- const now2 = Date.now();
1378
- if (chunkBuffer.length >= 80 || now2 - lastChunkForwardAt >= 150) {
1379
- opts.onWorkerChunk({
1380
- type: 'chunk',
1381
- stepId: step.id,
1382
- agentName: step.agentName,
1383
- description: step.description,
1384
- stepIndex: workflowContext?.stepIndex ?? 0,
1385
- totalSteps: workflowContext?.totalSteps ?? 1,
1386
- text: chunkBuffer,
1387
- });
1388
- chunkBuffer = '';
1389
- lastChunkForwardAt = now2;
1467
+ if (isLocalCliSession) {
1468
+ if (cliSessionEpochPlan.resetReason && iteration === 1 && opts?.onWorkerChunk) {
1469
+ const resetDetail = cliSessionEpochPlan.resetReason === 'turn_limit'
1470
+ ? 'Resetting CLI session after reaching the turn limit.'
1471
+ : cliSessionEpochPlan.resetReason === 'token_limit'
1472
+ ? 'Resetting CLI session after reaching the context budget.'
1473
+ : 'Resetting CLI session because the runtime changed.';
1474
+ opts.onWorkerChunk({
1475
+ type: opts?.isOrchestrated ? 'worker_chunk' : 'chunk',
1476
+ stepId: step.id,
1477
+ agentName: step.agentName,
1478
+ description: step.description,
1479
+ stepIndex: workflowContext?.stepIndex ?? 0,
1480
+ totalSteps: workflowContext?.totalSteps ?? 1,
1481
+ text: resetDetail,
1482
+ });
1483
+ }
1484
+ const ptyManager = (0, local_cli_pty_manager_1.getLocalCliPtySessionManager)();
1485
+ let ptyAttempt = 0;
1486
+ while (true) {
1487
+ ptyAttempt++;
1488
+ try {
1489
+ const ptyResult = await ptyManager.runTurn({
1490
+ conversationId: ptyConversationKey,
1491
+ botId: step.agentId,
1492
+ provider: activeRuntime.providerName,
1493
+ cwd: effectiveProjectDir,
1494
+ systemPrompt,
1495
+ messages,
1496
+ forceFreshSession: !hasPersistentConversation || (iteration === 1 && !cliSessionEpochPlan.resumeSessionId),
1497
+ onDetail: async (detail) => {
1498
+ streamedContent += streamedContent ? `\n${detail}` : detail;
1499
+ if (opts?.onWorkerChunk) {
1500
+ opts.onWorkerChunk({
1501
+ type: opts?.isOrchestrated ? 'worker_chunk' : 'chunk',
1502
+ stepId: step.id,
1503
+ agentName: step.agentName,
1504
+ description: step.description,
1505
+ stepIndex: workflowContext?.stepIndex ?? 0,
1506
+ totalSteps: workflowContext?.totalSteps ?? 1,
1507
+ text: detail,
1508
+ });
1509
+ }
1510
+ const now = Date.now();
1511
+ if (now - lastChunkProgressAt < 8000)
1512
+ return;
1513
+ lastChunkProgressAt = now;
1514
+ const activeSteps = workflowContext?.workflowId
1515
+ ? this.getActiveWorkflows().find((w) => w.id === workflowContext.workflowId)?.steps || [step]
1516
+ : [step];
1517
+ this.emitProgress(workflowContext?.workflowId || 'workflow', step, activeSteps, 'step-progress', opts?.onProgress, `${step.agentName} is still working on ${step.description}`);
1518
+ },
1519
+ });
1520
+ if (ptyResult.sessionId) {
1521
+ activeCliSessionId = ptyResult.sessionId;
1390
1522
  }
1523
+ response = {
1524
+ content: ptyResult.content || '',
1525
+ usage: ptyResult.usage,
1526
+ };
1527
+ break;
1391
1528
  }
1392
- // Existing 8-second heartbeat for backward compat
1393
- const now = Date.now();
1394
- if (now - lastChunkProgressAt < 8000)
1395
- return;
1396
- lastChunkProgressAt = now;
1397
- const compact = chunk.replace(/\s+/g, ' ').trim();
1398
- if (!compact && emittedChunkHeartbeat)
1399
- return;
1400
- emittedChunkHeartbeat = true;
1401
- const preview = `${step.agentName} is still working on ${step.description}`;
1402
- const activeSteps = workflowContext?.workflowId
1403
- ? this.getActiveWorkflows().find((w) => w.id === workflowContext.workflowId)?.steps || [step]
1404
- : [step];
1405
- this.emitProgress(workflowContext?.workflowId || 'workflow', step, activeSteps, 'step-progress', opts?.onProgress, preview);
1406
- },
1407
- tools: opts?.disableTools ? undefined : toolDefs,
1408
- cwd: effectiveProjectDir,
1409
- });
1410
- // Flush remaining chunk buffer
1411
- if (opts?.onWorkerChunk && chunkBuffer) {
1412
- opts.onWorkerChunk({
1413
- type: 'chunk',
1414
- stepId: step.id,
1415
- agentName: step.agentName,
1416
- description: step.description,
1417
- stepIndex: workflowContext?.stepIndex ?? 0,
1418
- totalSteps: workflowContext?.totalSteps ?? 1,
1419
- text: chunkBuffer,
1529
+ catch (ptyErr) {
1530
+ if (ptyAttempt >= LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT
1531
+ || !shouldRetrySelectedWorkflowRuntime(ptyErr)) {
1532
+ throw ptyErr;
1533
+ }
1534
+ console.warn(`[workflow-engine] ${step.agentName} selected runtime failed, retrying the same connection (${ptyAttempt + 1}/${LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT})`);
1535
+ await pauseWorkflowRuntimeRetry(ptyAttempt);
1536
+ }
1537
+ }
1538
+ }
1539
+ else {
1540
+ response = await llm.chat({
1541
+ messages,
1542
+ system: systemPrompt,
1543
+ stream: true,
1544
+ toolChoice,
1545
+ onChunk: async (chunk) => {
1546
+ streamedContent += chunk;
1547
+ // Forward chunks to onWorkerChunk with 150ms/80-char buffer
1548
+ if (opts?.onWorkerChunk) {
1549
+ chunkBuffer += chunk;
1550
+ const now2 = Date.now();
1551
+ if (chunkBuffer.length >= 80 || now2 - lastChunkForwardAt >= 150) {
1552
+ opts.onWorkerChunk({
1553
+ type: opts?.isOrchestrated ? 'worker_chunk' : 'chunk',
1554
+ stepId: step.id,
1555
+ agentName: step.agentName,
1556
+ description: step.description,
1557
+ stepIndex: workflowContext?.stepIndex ?? 0,
1558
+ totalSteps: workflowContext?.totalSteps ?? 1,
1559
+ text: chunkBuffer,
1560
+ });
1561
+ chunkBuffer = '';
1562
+ lastChunkForwardAt = now2;
1563
+ }
1564
+ }
1565
+ // Existing 8-second heartbeat for backward compat
1566
+ const now = Date.now();
1567
+ if (now - lastChunkProgressAt < 8000)
1568
+ return;
1569
+ lastChunkProgressAt = now;
1570
+ const compact = chunk.replace(/\s+/g, ' ').trim();
1571
+ if (!compact && emittedChunkHeartbeat)
1572
+ return;
1573
+ emittedChunkHeartbeat = true;
1574
+ const preview = `${step.agentName} is still working on ${step.description}`;
1575
+ const activeSteps = workflowContext?.workflowId
1576
+ ? this.getActiveWorkflows().find((w) => w.id === workflowContext.workflowId)?.steps || [step]
1577
+ : [step];
1578
+ this.emitProgress(workflowContext?.workflowId || 'workflow', step, activeSteps, 'step-progress', opts?.onProgress, preview);
1579
+ },
1580
+ tools: opts?.disableTools ? undefined : toolDefs,
1581
+ cwd: effectiveProjectDir,
1420
1582
  });
1421
- chunkBuffer = '';
1583
+ // Flush remaining chunk buffer
1584
+ if (opts?.onWorkerChunk && chunkBuffer) {
1585
+ opts.onWorkerChunk({
1586
+ type: opts?.isOrchestrated ? 'worker_chunk' : 'chunk',
1587
+ stepId: step.id,
1588
+ agentName: step.agentName,
1589
+ description: step.description,
1590
+ stepIndex: workflowContext?.stepIndex ?? 0,
1591
+ totalSteps: workflowContext?.totalSteps ?? 1,
1592
+ text: chunkBuffer,
1593
+ });
1594
+ chunkBuffer = '';
1595
+ }
1422
1596
  }
1597
+ selectedRuntimeRetryCount = 0;
1423
1598
  }
1424
1599
  catch (err) {
1425
1600
  console.warn(`[workflow-engine] step error: agent=${step.agentName} provider=${activeRuntime.providerName} model=${activeRuntime.model} runtime=${activeRuntime.runtimeLabel || 'unknown'} iteration=${iteration} message=${err?.message || err}`);
1426
- if (activeRuntime.fallback) {
1601
+ if (effectiveRuntimeMode === 'local_desktop'
1602
+ && selectedRuntimeRetryCount < (LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT - 1)
1603
+ && shouldRetrySelectedWorkflowRuntime(err)) {
1604
+ selectedRuntimeRetryCount++;
1605
+ console.warn(`[workflow-engine] ${step.agentName} selected runtime failed, retrying the same connection (${selectedRuntimeRetryCount + 1}/${LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT})`);
1606
+ await pauseWorkflowRuntimeRetry(selectedRuntimeRetryCount);
1607
+ iteration--;
1608
+ continue;
1609
+ }
1610
+ if (effectiveRuntimeMode !== 'local_desktop' && activeRuntime.fallback) {
1427
1611
  activeRuntime = activeRuntime.fallback;
1428
1612
  llm = (0, index_1.createProvider)(activeRuntime.providerName, {
1429
1613
  apiKey: activeRuntime.apiKey,
1430
1614
  model: activeRuntime.model,
1615
+ runtimeMode: effectiveRuntimeMode,
1431
1616
  ...(activeRuntime.requestShape ? { requestShape: activeRuntime.requestShape } : {}),
1432
1617
  ...(activeRuntime.baseUrl ? { baseUrl: activeRuntime.baseUrl } : {}),
1433
1618
  ...(activeRuntime.apiQuery ? { apiQuery: activeRuntime.apiQuery } : {}),
@@ -1445,6 +1630,23 @@ class WorkflowEngine extends events_1.EventEmitter {
1445
1630
  }
1446
1631
  throw err;
1447
1632
  }
1633
+ if (hasPersistentConversation && activeCliSessionId && !response.toolCalls?.length) {
1634
+ const nextEpochTurnCount = cliSessionEpochPlan.resumeSessionId
1635
+ ? ((cliSessionEpochPlan.existing?.epoch_turn_count || 0) + 1)
1636
+ : 1;
1637
+ data.upsertCliSessionEpoch({
1638
+ conversationId: conversationId,
1639
+ botId: step.agentId,
1640
+ provider: activeRuntime.providerName,
1641
+ sessionId: activeCliSessionId,
1642
+ epochTurnCount: nextEpochTurnCount,
1643
+ lastInputTokens: response.usage?.inputTokens ?? null,
1644
+ lastOutputTokens: response.usage?.outputTokens ?? null,
1645
+ resetReason: cliSessionEpochPlan.resetReason,
1646
+ epochStartedAt: cliEpochStartedAt,
1647
+ lastUsedAt: (0, cli_session_epoch_1.localTimestamp)(),
1648
+ });
1649
+ }
1448
1650
  if (response.toolCalls && response.toolCalls.length > 0) {
1449
1651
  messages.push({
1450
1652
  role: 'assistant',
@@ -1452,7 +1654,9 @@ class WorkflowEngine extends events_1.EventEmitter {
1452
1654
  toolCalls: response.toolCalls,
1453
1655
  });
1454
1656
  for (const tc of response.toolCalls) {
1455
- const permMode = (profile?.permission_mode || 'autopilot');
1657
+ const permMode = unrestrictedCliProvider
1658
+ ? 'autopilot'
1659
+ : (profile?.permission_mode || 'autopilot');
1456
1660
  const approval = (0, approval_1.checkPermission)(tc.name, permMode);
1457
1661
  if (!approval.approved) {
1458
1662
  throw new Error(`APPROVAL_REQUIRED: ${approval.reason}`);
@@ -1460,7 +1664,7 @@ class WorkflowEngine extends events_1.EventEmitter {
1460
1664
  // Emit tool_call event so UI can show what's happening
1461
1665
  if (opts?.onWorkerChunk) {
1462
1666
  opts.onWorkerChunk({
1463
- type: 'tool_call',
1667
+ type: opts?.isOrchestrated ? 'worker_tool_call' : 'tool_call',
1464
1668
  stepId: step.id,
1465
1669
  agentName: step.agentName,
1466
1670
  description: step.description,
@@ -1477,7 +1681,7 @@ class WorkflowEngine extends events_1.EventEmitter {
1477
1681
  // Emit tool_result event
1478
1682
  if (opts?.onWorkerChunk) {
1479
1683
  opts.onWorkerChunk({
1480
- type: 'tool_result',
1684
+ type: opts?.isOrchestrated ? 'worker_tool_result' : 'tool_result',
1481
1685
  stepId: step.id,
1482
1686
  agentName: step.agentName,
1483
1687
  description: step.description,
@@ -1489,6 +1693,9 @@ class WorkflowEngine extends events_1.EventEmitter {
1489
1693
  toolIsError: !result.success,
1490
1694
  });
1491
1695
  }
1696
+ if (opts?.isOrchestrated && this.orchestratedTodoHandled(opts.taskId)) {
1697
+ return this.buildOrchestratedTodoHandledResult(opts.taskId);
1698
+ }
1492
1699
  }
1493
1700
  if (opts?.isOrchestrated && this.orchestratedTodoHandled(opts.taskId)) {
1494
1701
  return this.buildOrchestratedTodoHandledResult(opts.taskId);
@@ -1551,12 +1758,15 @@ class WorkflowEngine extends events_1.EventEmitter {
1551
1758
  this.emitProgress(workflowContext?.workflowId || 'workflow', step, activeSteps, 'step-progress', opts?.onProgress, `${step.agentName} described intended work instead of executing it. Retrying with an explicit tool-use correction.`);
1552
1759
  continue;
1553
1760
  }
1554
- if (activeRuntime.fallback && activeRuntime.providerName !== activeRuntime.fallback.providerName) {
1761
+ if (effectiveRuntimeMode !== 'local_desktop'
1762
+ && activeRuntime.fallback
1763
+ && activeRuntime.providerName !== activeRuntime.fallback.providerName) {
1555
1764
  console.warn(`[workflow-engine] forcing runtime fallback after repeated incomplete execution: agent=${step.agentName} from=${activeRuntime.providerName} to=${activeRuntime.fallback.providerName}`);
1556
1765
  activeRuntime = activeRuntime.fallback;
1557
1766
  llm = (0, index_1.createProvider)(activeRuntime.providerName, {
1558
1767
  apiKey: activeRuntime.apiKey,
1559
1768
  model: activeRuntime.model,
1769
+ runtimeMode: effectiveRuntimeMode,
1560
1770
  ...(activeRuntime.requestShape ? { requestShape: activeRuntime.requestShape } : {}),
1561
1771
  ...(activeRuntime.baseUrl ? { baseUrl: activeRuntime.baseUrl } : {}),
1562
1772
  ...(activeRuntime.apiQuery ? { apiQuery: activeRuntime.apiQuery } : {}),
@@ -1687,12 +1897,13 @@ class WorkflowEngine extends events_1.EventEmitter {
1687
1897
  default: return undefined;
1688
1898
  }
1689
1899
  }
1690
- async resolveStepRuntime(step, profile, apiKey) {
1900
+ async resolveStepRuntime(step, profile, apiKey, opts) {
1901
+ const effectiveRuntimeMode = (0, runtime_context_1.normalizeRuntimeMode)(opts?.runtimeMode || this.runtimeMode);
1691
1902
  if (step.provider === 'claude-cli') {
1692
- return this.resolveClaudeWorkflowRuntime(step, profile);
1903
+ return this.resolveClaudeWorkflowRuntime(step, profile, opts?.disallowCliRuntime === true && effectiveRuntimeMode !== 'local_desktop', effectiveRuntimeMode);
1693
1904
  }
1694
1905
  if (step.provider === 'codex-cli') {
1695
- return this.resolveCodexWorkflowRuntime(step, profile);
1906
+ return this.resolveCodexWorkflowRuntime(step, profile, opts?.disallowCliRuntime === true && effectiveRuntimeMode !== 'local_desktop', effectiveRuntimeMode);
1696
1907
  }
1697
1908
  const profileConnection = profile?.provider_connection_id
1698
1909
  ? data.getProviderConnection(profile.provider_connection_id)
@@ -1701,6 +1912,22 @@ class WorkflowEngine extends events_1.EventEmitter {
1701
1912
  || profileConnection?.api_key_enc
1702
1913
  || apiKey
1703
1914
  || this.resolveApiKey(step.provider);
1915
+ if (effectiveRuntimeMode !== 'local_desktop' && !resolvedKey && step.provider === 'openai') {
1916
+ const preferredModel = (0, subscription_runtime_1.resolveSubscriptionApiModel)(step.model || profile?.model, data.findProviderConnection('openai')?.default_model || undefined) || (step.model || profile?.model || 'default').trim() || 'default';
1917
+ const subscriptionRuntime = await (0, subscription_runtime_1.resolveCodexSubscriptionRuntime)({
1918
+ preferredModel,
1919
+ });
1920
+ if (subscriptionRuntime) {
1921
+ return {
1922
+ providerName: subscriptionRuntime.providerName,
1923
+ apiKey: subscriptionRuntime.apiKey,
1924
+ model: subscriptionRuntime.model,
1925
+ runtimeLabel: 'Subscription API (Token)',
1926
+ ...(subscriptionRuntime.baseUrl ? { baseUrl: subscriptionRuntime.baseUrl } : {}),
1927
+ ...(subscriptionRuntime.apiStyle ? { apiStyle: subscriptionRuntime.apiStyle } : {}),
1928
+ };
1929
+ }
1930
+ }
1704
1931
  if (!resolvedKey) {
1705
1932
  throw new Error(`No API key for provider ${step.provider}`);
1706
1933
  }
@@ -1711,7 +1938,15 @@ class WorkflowEngine extends events_1.EventEmitter {
1711
1938
  runtimeLabel: 'API Key',
1712
1939
  };
1713
1940
  }
1714
- async resolveClaudeWorkflowRuntime(step, profile) {
1941
+ async resolveClaudeWorkflowRuntime(step, profile, disallowCliRuntime = false, runtimeMode = this.runtimeMode) {
1942
+ if (runtimeMode === 'local_desktop') {
1943
+ return {
1944
+ providerName: 'claude-cli',
1945
+ apiKey: 'cli-auth',
1946
+ model: (step.model || profile?.model || 'default').trim() || 'default',
1947
+ runtimeLabel: 'Subscription CLI',
1948
+ };
1949
+ }
1715
1950
  const preferredModel = (0, subscription_runtime_1.resolveSubscriptionApiModel)(step.model || profile?.model, data.findProviderConnection('anthropic')?.default_model || data.findProviderConnection('claude-cli')?.default_model || undefined) || (step.model || profile?.model || 'default').trim() || 'default';
1716
1951
  const apiKeyFallback = data.findProviderConnection('anthropic')?.api_key_enc || process.env.ANTHROPIC_API_KEY;
1717
1952
  const apiFallbackConfig = apiKeyFallback
@@ -1722,6 +1957,26 @@ class WorkflowEngine extends events_1.EventEmitter {
1722
1957
  runtimeLabel: 'API Key',
1723
1958
  }
1724
1959
  : undefined;
1960
+ if (disallowCliRuntime) {
1961
+ const subscriptionRuntime = await (0, subscription_runtime_1.resolveClaudeSubscriptionRuntime)({
1962
+ preferredModel,
1963
+ });
1964
+ if (subscriptionRuntime) {
1965
+ return {
1966
+ providerName: subscriptionRuntime.providerName,
1967
+ apiKey: subscriptionRuntime.apiKey,
1968
+ model: subscriptionRuntime.model,
1969
+ runtimeLabel: 'Subscription API (Token)',
1970
+ ...(subscriptionRuntime.authMode ? { authMode: subscriptionRuntime.authMode } : {}),
1971
+ ...(subscriptionRuntime.baseUrl ? { baseUrl: subscriptionRuntime.baseUrl } : {}),
1972
+ ...(subscriptionRuntime.apiQuery ? { apiQuery: subscriptionRuntime.apiQuery } : {}),
1973
+ ...(subscriptionRuntime.extraHeaders ? { extraHeaders: subscriptionRuntime.extraHeaders } : {}),
1974
+ };
1975
+ }
1976
+ if (apiFallbackConfig)
1977
+ return apiFallbackConfig;
1978
+ throw new Error('No non-CLI Anthropic runtime is available for this workflow step.');
1979
+ }
1725
1980
  const cliFallbackConfig = {
1726
1981
  providerName: 'claude-cli',
1727
1982
  apiKey: 'cli-auth',
@@ -1731,11 +1986,43 @@ class WorkflowEngine extends events_1.EventEmitter {
1731
1986
  };
1732
1987
  return cliFallbackConfig;
1733
1988
  }
1734
- async resolveCodexWorkflowRuntime(step, profile) {
1989
+ async resolveCodexWorkflowRuntime(step, profile, disallowCliRuntime = false, runtimeMode = this.runtimeMode) {
1990
+ if (runtimeMode === 'local_desktop') {
1991
+ return {
1992
+ providerName: 'codex-cli',
1993
+ apiKey: 'cli-auth',
1994
+ model: (step.model || profile?.model || 'default').trim() || 'default',
1995
+ runtimeLabel: 'Subscription CLI',
1996
+ };
1997
+ }
1735
1998
  const preferredModel = (0, subscription_runtime_1.resolveSubscriptionApiModel)(step.model || profile?.model, data.findProviderConnection('openai')?.default_model || data.findProviderConnection('codex-cli')?.default_model || undefined)
1736
1999
  || (step.model || profile?.model || 'default').trim()
1737
2000
  || 'default';
2001
+ const subscriptionRuntime = await (0, subscription_runtime_1.resolveCodexSubscriptionRuntime)({
2002
+ preferredModel,
2003
+ });
2004
+ if (subscriptionRuntime) {
2005
+ return {
2006
+ providerName: subscriptionRuntime.providerName,
2007
+ apiKey: subscriptionRuntime.apiKey,
2008
+ model: subscriptionRuntime.model,
2009
+ runtimeLabel: 'Subscription API (Token)',
2010
+ ...(subscriptionRuntime.baseUrl ? { baseUrl: subscriptionRuntime.baseUrl } : {}),
2011
+ ...(subscriptionRuntime.apiStyle ? { apiStyle: subscriptionRuntime.apiStyle } : {}),
2012
+ };
2013
+ }
1738
2014
  const apiKeyFallback = data.findProviderConnection('openai')?.api_key_enc || process.env.OPENAI_API_KEY;
2015
+ if (disallowCliRuntime) {
2016
+ if (apiKeyFallback) {
2017
+ return {
2018
+ providerName: 'openai',
2019
+ apiKey: apiKeyFallback,
2020
+ model: preferredModel,
2021
+ runtimeLabel: 'API Key',
2022
+ };
2023
+ }
2024
+ throw new Error('No non-CLI OpenAI runtime is available for this workflow step.');
2025
+ }
1739
2026
  return {
1740
2027
  providerName: 'codex-cli',
1741
2028
  apiKey: 'cli-auth',
@@ -1860,7 +2147,7 @@ class WorkflowEngine extends events_1.EventEmitter {
1860
2147
  async replanRemainingSteps(steps, failedStep, agents, defaultProfile) {
1861
2148
  if (steps.length <= 1)
1862
2149
  return null;
1863
- const clerk = (0, clerk_model_1.getClerk)();
2150
+ const clerk = (0, clerk_model_1.getClerk)({ runtimeMode: this.runtimeMode });
1864
2151
  if (!clerk)
1865
2152
  return null;
1866
2153
  const completedSummary = steps
@@ -1951,15 +2238,24 @@ Rules:
1951
2238
  }
1952
2239
  exports.WorkflowEngine = WorkflowEngine;
1953
2240
  // ─── Singleton ───────────────────────────────────────────────────
1954
- let _engine = null;
1955
- function getWorkflowEngine(projectDir) {
1956
- if (!_engine && projectDir) {
1957
- _engine = new WorkflowEngine(projectDir);
2241
+ const _workflowEngines = new Map();
2242
+ function getWorkflowEngine(projectDir, runtimeMode) {
2243
+ const effectiveRuntimeMode = (0, runtime_context_1.normalizeRuntimeMode)(runtimeMode || (0, runtime_context_1.getDefaultRuntimeMode)());
2244
+ const existing = _workflowEngines.get(effectiveRuntimeMode);
2245
+ if (existing) {
2246
+ if (!projectDir || existing.projectDir === projectDir) {
2247
+ return existing;
2248
+ }
2249
+ }
2250
+ if (projectDir) {
2251
+ const engine = new WorkflowEngine(projectDir, effectiveRuntimeMode);
2252
+ _workflowEngines.set(effectiveRuntimeMode, engine);
2253
+ return engine;
1958
2254
  }
1959
- if (!_engine) {
2255
+ if (!_workflowEngines.size) {
1960
2256
  throw new Error('Workflow engine not initialized. Call getWorkflowEngine(projectDir) first.');
1961
2257
  }
1962
- return _engine;
2258
+ throw new Error(`Workflow engine not initialized for runtime ${effectiveRuntimeMode}. Call getWorkflowEngine(projectDir, runtimeMode) first.`);
1963
2259
  }
1964
2260
  // ─── JSON Parsing Helper ─────────────────────────────────────────
1965
2261
  function parseJsonResponse(text) {