@pixelbyte-software/pixcode 1.35.2 → 1.35.4

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 (228) hide show
  1. package/LICENSE +718 -718
  2. package/README.de.md +248 -248
  3. package/README.ja.md +240 -240
  4. package/README.ko.md +240 -240
  5. package/README.md +303 -303
  6. package/README.ru.md +248 -248
  7. package/README.tr.md +250 -250
  8. package/README.zh-CN.md +240 -240
  9. package/dist/api-docs.html +548 -548
  10. package/dist/assets/index-BwmhA_le.css +32 -0
  11. package/dist/assets/{index-D1-AIL_5.js → index-CyxRiNt0.js} +182 -182
  12. package/dist/clear-cache.html +85 -85
  13. package/dist/convert-icons.md +52 -52
  14. package/dist/favicon.svg +8 -8
  15. package/dist/generate-icons.js +48 -48
  16. package/dist/icons/codex-white.svg +3 -3
  17. package/dist/icons/codex.svg +3 -3
  18. package/dist/icons/cursor-white.svg +11 -11
  19. package/dist/icons/icon-128x128.svg +9 -9
  20. package/dist/icons/icon-144x144.svg +9 -9
  21. package/dist/icons/icon-152x152.svg +9 -9
  22. package/dist/icons/icon-192x192.svg +9 -9
  23. package/dist/icons/icon-384x384.svg +9 -9
  24. package/dist/icons/icon-512x512.svg +9 -9
  25. package/dist/icons/icon-72x72.svg +9 -9
  26. package/dist/icons/icon-96x96.svg +9 -9
  27. package/dist/icons/icon-template.svg +9 -9
  28. package/dist/icons/qwen-logo.svg +14 -14
  29. package/dist/index.html +59 -59
  30. package/dist/logo.svg +12 -12
  31. package/dist/manifest.json +60 -60
  32. package/dist/openapi.yaml +1693 -1693
  33. package/dist/sw.js +124 -124
  34. package/dist-server/server/cli.js +96 -96
  35. package/dist-server/server/cli.js.map +1 -1
  36. package/dist-server/server/cursor-cli.js.map +1 -1
  37. package/dist-server/server/daemon/manager.js +33 -33
  38. package/dist-server/server/daemon-manager.js +64 -64
  39. package/dist-server/server/gemini-cli.js +4 -4
  40. package/dist-server/server/gemini-cli.js.map +1 -1
  41. package/dist-server/server/index.js +11 -11
  42. package/dist-server/server/index.js.map +1 -1
  43. package/dist-server/server/load-env.js.map +1 -1
  44. package/dist-server/server/middleware/auth.js.map +1 -1
  45. package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js.map +1 -1
  46. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +1 -1
  47. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  48. package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js +1 -1
  49. package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js.map +1 -1
  50. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +1 -1
  51. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +1 -1
  52. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js +1 -1
  53. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js.map +1 -1
  54. package/dist-server/server/modules/providers/list/qwen/qwen-auth.provider.js +1 -1
  55. package/dist-server/server/modules/providers/list/qwen/qwen-auth.provider.js.map +1 -1
  56. package/dist-server/server/modules/providers/provider.routes.js +3 -6
  57. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  58. package/dist-server/server/opencode-cli.js +1 -1
  59. package/dist-server/server/opencode-cli.js.map +1 -1
  60. package/dist-server/server/projects.js +2 -3
  61. package/dist-server/server/projects.js.map +1 -1
  62. package/dist-server/server/qwen-code-cli.js +1 -1
  63. package/dist-server/server/qwen-code-cli.js.map +1 -1
  64. package/dist-server/server/routes/agent.js +3 -3
  65. package/dist-server/server/routes/agent.js.map +1 -1
  66. package/dist-server/server/routes/auth.js.map +1 -1
  67. package/dist-server/server/routes/codex.js.map +1 -1
  68. package/dist-server/server/routes/commands.js +26 -26
  69. package/dist-server/server/routes/commands.js.map +1 -1
  70. package/dist-server/server/routes/cursor.js +1 -1
  71. package/dist-server/server/routes/cursor.js.map +1 -1
  72. package/dist-server/server/routes/gemini.js.map +1 -1
  73. package/dist-server/server/routes/git.js +18 -18
  74. package/dist-server/server/routes/git.js.map +1 -1
  75. package/dist-server/server/routes/mcp-utils.js.map +1 -1
  76. package/dist-server/server/routes/messages.js.map +1 -1
  77. package/dist-server/server/routes/network.js +1 -1
  78. package/dist-server/server/routes/network.js.map +1 -1
  79. package/dist-server/server/routes/plugins.js +2 -2
  80. package/dist-server/server/routes/plugins.js.map +1 -1
  81. package/dist-server/server/routes/projects.js +1 -1
  82. package/dist-server/server/routes/projects.js.map +1 -1
  83. package/dist-server/server/routes/settings.js.map +1 -1
  84. package/dist-server/server/routes/taskmaster.js +423 -424
  85. package/dist-server/server/routes/taskmaster.js.map +1 -1
  86. package/dist-server/server/routes/user.js +1 -1
  87. package/dist-server/server/routes/user.js.map +1 -1
  88. package/dist-server/server/services/external-access.js +0 -1
  89. package/dist-server/server/services/external-access.js.map +1 -1
  90. package/dist-server/server/services/notification-orchestrator.js.map +1 -1
  91. package/dist-server/server/utils/commandParser.js.map +1 -1
  92. package/dist-server/server/utils/plugin-process-manager.js.map +1 -1
  93. package/dist-server/server/vite-daemon.js.map +1 -1
  94. package/package.json +180 -180
  95. package/scripts/fix-node-pty.js +67 -67
  96. package/scripts/smoke/a2a-roundtrip.mjs +167 -167
  97. package/scripts/smoke/orchestration-api.mjs +172 -172
  98. package/scripts/smoke/orchestration-live-run.mjs +176 -176
  99. package/server/claude-sdk.js +898 -898
  100. package/server/cli.js +936 -935
  101. package/server/constants/config.js +4 -4
  102. package/server/cursor-cli.js +344 -342
  103. package/server/daemon/manager.js +564 -564
  104. package/server/daemon-manager.js +959 -959
  105. package/server/database/db.js +794 -794
  106. package/server/database/json-store.js +197 -197
  107. package/server/gemini-cli.js +536 -535
  108. package/server/gemini-response-handler.js +79 -79
  109. package/server/index.js +3138 -3135
  110. package/server/load-env.js +35 -34
  111. package/server/middleware/auth.js +174 -173
  112. package/server/modules/orchestration/a2a/adapter-registry.ts +108 -108
  113. package/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts +55 -55
  114. package/server/modules/orchestration/a2a/adapters/claude-code.adapter.ts +284 -284
  115. package/server/modules/orchestration/a2a/adapters/codex.adapter.ts +244 -244
  116. package/server/modules/orchestration/a2a/adapters/cursor.adapter.ts +249 -249
  117. package/server/modules/orchestration/a2a/adapters/gemini.adapter.ts +248 -248
  118. package/server/modules/orchestration/a2a/adapters/opencode.adapter.ts +248 -248
  119. package/server/modules/orchestration/a2a/adapters/qwen.adapter.ts +248 -248
  120. package/server/modules/orchestration/a2a/routes.ts +577 -577
  121. package/server/modules/orchestration/a2a/task-store.ts +178 -178
  122. package/server/modules/orchestration/a2a/types.ts +125 -125
  123. package/server/modules/orchestration/a2a/validator.ts +113 -113
  124. package/server/modules/orchestration/index.ts +66 -66
  125. package/server/modules/orchestration/preview/port-watcher.ts +112 -112
  126. package/server/modules/orchestration/preview/preview-proxy.ts +60 -60
  127. package/server/modules/orchestration/preview/types.ts +19 -19
  128. package/server/modules/orchestration/tasks/orchestration-task-store.ts +45 -45
  129. package/server/modules/orchestration/tasks/orchestration-task.routes.ts +74 -73
  130. package/server/modules/orchestration/tasks/orchestration-task.service.ts +145 -145
  131. package/server/modules/orchestration/tasks/orchestration-task.types.ts +29 -29
  132. package/server/modules/orchestration/workflows/built-in-workflows.ts +127 -127
  133. package/server/modules/orchestration/workflows/workflow-runner.ts +1206 -1206
  134. package/server/modules/orchestration/workflows/workflow-store.ts +97 -97
  135. package/server/modules/orchestration/workflows/workflow.routes.ts +169 -169
  136. package/server/modules/orchestration/workflows/workflow.types.ts +70 -70
  137. package/server/modules/orchestration/workflows/workspace-target.ts +120 -120
  138. package/server/modules/orchestration/workspace/docker-workspace.ts +135 -135
  139. package/server/modules/orchestration/workspace/path-safety.ts +55 -55
  140. package/server/modules/orchestration/workspace/types.ts +52 -52
  141. package/server/modules/orchestration/workspace/workspace-manager.ts +97 -97
  142. package/server/modules/orchestration/workspace/worktree-workspace.ts +125 -125
  143. package/server/modules/providers/index.ts +2 -2
  144. package/server/modules/providers/list/claude/claude-auth.provider.ts +146 -145
  145. package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -135
  146. package/server/modules/providers/list/claude/claude-sessions.provider.ts +306 -306
  147. package/server/modules/providers/list/claude/claude.provider.ts +15 -15
  148. package/server/modules/providers/list/codex/codex-auth.provider.ts +116 -115
  149. package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -135
  150. package/server/modules/providers/list/codex/codex-sessions.provider.ts +319 -319
  151. package/server/modules/providers/list/codex/codex.provider.ts +15 -15
  152. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +143 -143
  153. package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -108
  154. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +421 -421
  155. package/server/modules/providers/list/cursor/cursor.provider.ts +15 -15
  156. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +164 -163
  157. package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +110 -110
  158. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +227 -227
  159. package/server/modules/providers/list/gemini/gemini.provider.ts +15 -15
  160. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +131 -130
  161. package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +126 -126
  162. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +232 -232
  163. package/server/modules/providers/list/opencode/opencode.provider.ts +29 -29
  164. package/server/modules/providers/list/qwen/qwen-auth.provider.ts +146 -145
  165. package/server/modules/providers/list/qwen/qwen-mcp.provider.ts +114 -114
  166. package/server/modules/providers/list/qwen/qwen-sessions.provider.ts +265 -265
  167. package/server/modules/providers/list/qwen/qwen.provider.ts +21 -21
  168. package/server/modules/providers/provider.registry.ts +40 -40
  169. package/server/modules/providers/provider.routes.ts +822 -819
  170. package/server/modules/providers/services/mcp.service.ts +86 -86
  171. package/server/modules/providers/services/provider-auth.service.ts +26 -26
  172. package/server/modules/providers/services/sessions.service.ts +45 -45
  173. package/server/modules/providers/shared/base/abstract.provider.ts +20 -20
  174. package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -151
  175. package/server/modules/providers/shared/provider-configs.ts +142 -142
  176. package/server/modules/providers/tests/mcp.test.ts +293 -293
  177. package/server/openai-codex.js +462 -462
  178. package/server/opencode-cli.js +460 -459
  179. package/server/opencode-response-handler.js +107 -107
  180. package/server/projects.js +3106 -3105
  181. package/server/qwen-code-cli.js +396 -395
  182. package/server/qwen-response-handler.js +73 -73
  183. package/server/routes/agent.js +1367 -1365
  184. package/server/routes/auth.js +139 -138
  185. package/server/routes/codex.js +20 -19
  186. package/server/routes/commands.js +556 -554
  187. package/server/routes/cursor.js +54 -52
  188. package/server/routes/gemini.js +25 -24
  189. package/server/routes/git.js +1490 -1488
  190. package/server/routes/mcp-utils.js +32 -31
  191. package/server/routes/messages.js +62 -61
  192. package/server/routes/network.js +121 -120
  193. package/server/routes/plugins.js +320 -318
  194. package/server/routes/projects.js +917 -915
  195. package/server/routes/qwen.js +27 -27
  196. package/server/routes/settings.js +287 -286
  197. package/server/routes/taskmaster.js +1498 -1496
  198. package/server/routes/telegram.js +125 -125
  199. package/server/routes/user.js +125 -123
  200. package/server/services/external-access.js +171 -171
  201. package/server/services/install-jobs.js +571 -571
  202. package/server/services/notification-orchestrator.js +244 -242
  203. package/server/services/provider-credentials.js +189 -189
  204. package/server/services/provider-models.js +381 -381
  205. package/server/services/telegram/bot.js +279 -279
  206. package/server/services/telegram/telegram-http-client.js +130 -130
  207. package/server/services/telegram/translations.js +170 -170
  208. package/server/services/vapid-keys.js +36 -36
  209. package/server/sessionManager.js +225 -225
  210. package/server/shared/interfaces.ts +54 -54
  211. package/server/shared/types.ts +172 -172
  212. package/server/shared/utils.ts +193 -193
  213. package/server/tsconfig.json +36 -36
  214. package/server/utils/colors.js +21 -21
  215. package/server/utils/commandParser.js +305 -303
  216. package/server/utils/frontmatter.js +18 -18
  217. package/server/utils/gitConfig.js +34 -34
  218. package/server/utils/mcp-detector.js +147 -147
  219. package/server/utils/plugin-loader.js +457 -457
  220. package/server/utils/plugin-process-manager.js +185 -184
  221. package/server/utils/port-access.js +209 -209
  222. package/server/utils/runtime-paths.js +37 -37
  223. package/server/utils/taskmaster-websocket.js +128 -128
  224. package/server/utils/url-detection.js +71 -71
  225. package/server/vite-daemon.js +79 -78
  226. package/shared/modelConstants.js +162 -162
  227. package/shared/networkHosts.js +22 -22
  228. package/dist/assets/index-B8w57E1r.css +0 -32
@@ -1,1206 +1,1206 @@
1
- import crypto from 'node:crypto';
2
-
3
- import type {
4
- Workflow,
5
- WorkflowNode,
6
- WorkflowNodeRun,
7
- WorkflowRun,
8
- } from '@/modules/orchestration/workflows/workflow.types.js';
9
- import {
10
- resolveWorkflowWorkspace,
11
- workspaceContextPrompt,
12
- workspaceTargetMetadata,
13
- } from '@/modules/orchestration/workflows/workspace-target.js';
14
- import { workflowStore } from '@/modules/orchestration/workflows/workflow-store.js';
15
-
16
- const TERMINAL = new Set(['completed', 'failed', 'canceled']);
17
- const SKIPPED = 'skipped';
18
- const BACKEND_HANDOFF_TIMEOUT_MS = 120_000;
19
- const MAX_OUTPUT_CONTEXT_CHARS = 12_000;
20
- const DEFAULT_MAX_REPAIR_CYCLES = 1;
21
- const MAX_REPAIR_CYCLES = 5;
22
- const KNOWN_AGENT_ROLES = [
23
- 'backend',
24
- 'frontend',
25
- 'review',
26
- 'implementation',
27
- 'proposal',
28
- 'critique',
29
- 'response',
30
- 'decision',
31
- 'report',
32
- ] as const;
33
-
34
- class WorkflowCanceledError extends Error {
35
- constructor() {
36
- super('Workflow canceled.');
37
- this.name = 'WorkflowCanceledError';
38
- }
39
- }
40
-
41
- class WorkflowNodeTimeoutError extends Error {
42
- constructor(readonly timeoutMs: number) {
43
- super(`Workflow node timed out after ${Math.round(timeoutMs / 1000)}s.`);
44
- this.name = 'WorkflowNodeTimeoutError';
45
- }
46
- }
47
-
48
- function newId(prefix: string): string {
49
- return `${prefix}_${crypto.randomBytes(8).toString('hex')}`;
50
- }
51
-
52
- function localA2ABaseUrl(): string {
53
- return `http://127.0.0.1:${process.env.SERVER_PORT ?? process.env.PORT ?? '3001'}/a2a`;
54
- }
55
-
56
- function validateWorkflow(workflow: Workflow): void {
57
- if (workflow.nodes.length > 64) {
58
- throw new Error('Workflow node limit exceeded.');
59
- }
60
- const ids = new Set(workflow.nodes.map((node) => node.id));
61
- for (const node of workflow.nodes) {
62
- for (const input of node.inputs) {
63
- if (!ids.has(input)) {
64
- throw new Error(`Workflow node ${node.id} references missing input ${input}.`);
65
- }
66
- }
67
- }
68
- }
69
-
70
- type TaskResult = {
71
- state: string;
72
- text: string;
73
- error?: string;
74
- messages: Array<{ role: string; text: string }>;
75
- artifacts: Array<{
76
- type: string;
77
- text?: string;
78
- data?: Record<string, unknown>;
79
- metadata?: Record<string, unknown>;
80
- }>;
81
- };
82
-
83
- type RawTask = {
84
- state?: string;
85
- error?: { code?: string; message?: string };
86
- history?: Array<{ role?: string; parts?: Array<{ kind?: string; text?: string; data?: Record<string, unknown> }> }>;
87
- artifacts?: Array<{
88
- type?: string;
89
- parts?: Array<{ kind?: string; text?: string; data?: Record<string, unknown> }>;
90
- metadata?: Record<string, unknown>;
91
- }>;
92
- };
93
-
94
- type AgentAssignment = {
95
- instanceId: string;
96
- adapterId: string;
97
- label: string;
98
- role?: AgentRole;
99
- instruction?: string;
100
- model?: string;
101
- permissionMode?: string;
102
- toolsSettings?: Record<string, unknown>;
103
- order: number;
104
- };
105
-
106
- type KnownAgentRole = typeof KNOWN_AGENT_ROLES[number];
107
- type AgentRole = string;
108
-
109
- function readAgentRole(value: unknown): AgentRole | undefined {
110
- return typeof value === 'string' && value.trim() && value.trim() !== 'auto'
111
- ? value.trim()
112
- : undefined;
113
- }
114
-
115
- function isKnownAgentRole(value: string | undefined): value is KnownAgentRole {
116
- return Boolean(value && (KNOWN_AGENT_ROLES as readonly string[]).includes(value));
117
- }
118
-
119
- function getMetadataRecord(metadata: Record<string, unknown> | undefined, key: string): Record<string, unknown> {
120
- return readRecord(metadata?.[key]) ?? {};
121
- }
122
-
123
- function readRecord(value: unknown): Record<string, unknown> | undefined {
124
- return value && typeof value === 'object' ? value as Record<string, unknown> : undefined;
125
- }
126
-
127
- function readString(value: unknown): string | undefined {
128
- return typeof value === 'string' && value.trim() ? value : undefined;
129
- }
130
-
131
- function readBoolean(value: unknown): boolean | undefined {
132
- return typeof value === 'boolean' ? value : undefined;
133
- }
134
-
135
- function readIsolation(value: unknown): 'host' | 'worktree' | 'docker' | undefined {
136
- return value === 'host' || value === 'worktree' || value === 'docker' ? value : undefined;
137
- }
138
-
139
- function readLegacyEnabledAdapters(metadata?: Record<string, unknown>): string[] {
140
- return Array.isArray(metadata?.enabledAdapters)
141
- ? metadata.enabledAdapters.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
142
- : [];
143
- }
144
-
145
- function readMetadataAgents(metadata?: Record<string, unknown>): AgentAssignment[] {
146
- if (!Array.isArray(metadata?.agents)) return [];
147
-
148
- return metadata.agents
149
- .map((value, index): AgentAssignment | null => {
150
- if (!value || typeof value !== 'object') return null;
151
- const record = value as Record<string, unknown>;
152
- const adapterId = readString(record.adapterId);
153
- if (!adapterId) return null;
154
- if (readBoolean(record.enabled) === false) return null;
155
-
156
- return {
157
- instanceId: readString(record.instanceId) ?? `${adapterId}-${index + 1}`,
158
- adapterId,
159
- label: readString(record.label) ?? `${adapterId} #${index + 1}`,
160
- role: readAgentRole(record.role),
161
- instruction: readString(record.instruction),
162
- model: readString(record.model),
163
- permissionMode: readString(record.permissionMode),
164
- toolsSettings: readRecord(record.toolsSettings),
165
- order: index,
166
- };
167
- })
168
- .filter((value): value is AgentAssignment => Boolean(value))
169
- .slice(0, 16);
170
- }
171
-
172
- function readAgentAssignments(metadata?: Record<string, unknown>): AgentAssignment[] {
173
- const agents = readMetadataAgents(metadata);
174
- if (agents.length > 0) return agents;
175
-
176
- return readLegacyEnabledAdapters(metadata).map((adapterId, index) => ({
177
- instanceId: `${adapterId}-${index + 1}`,
178
- adapterId,
179
- label: `${adapterId} #${index + 1}`,
180
- order: index,
181
- }));
182
- }
183
-
184
- function readEnabledAdapters(metadata?: Record<string, unknown>): string[] {
185
- return [...new Set(readAgentAssignments(metadata).map((agent) => agent.adapterId))];
186
- }
187
-
188
- function readMaxParallelAgents(metadata?: Record<string, unknown>): number {
189
- const settings = getMetadataRecord(metadata, 'settings');
190
- const value = settings.maxParallelAgents;
191
- return typeof value === 'number' && Number.isFinite(value)
192
- ? Math.max(1, Math.min(12, Math.round(value)))
193
- : 3;
194
- }
195
-
196
- function readMaxRepairCycles(metadata?: Record<string, unknown>): number {
197
- const settings = getMetadataRecord(metadata, 'settings');
198
- const value = settings.maxRepairCycles;
199
- return typeof value === 'number' && Number.isFinite(value)
200
- ? Math.max(0, Math.min(MAX_REPAIR_CYCLES, Math.round(value)))
201
- : DEFAULT_MAX_REPAIR_CYCLES;
202
- }
203
-
204
- function safeNodeId(adapterId: string, suffix: string): string {
205
- return `${adapterId.replace(/[^a-zA-Z0-9_]+/g, '_')}_${suffix}`;
206
- }
207
-
208
- function safeAgentNodeId(agent: AgentAssignment, index: number, suffix: string): string {
209
- return `agent_${index + 1}_${safeNodeId(agent.adapterId, suffix)}`;
210
- }
211
-
212
- function agentRoster(agents: AgentAssignment[]): string {
213
- return agents
214
- .map((agent, index) => {
215
- const instruction = agent.instruction
216
- ? `\n User assignment: ${agent.instruction}`
217
- : '';
218
- const role = agent.role ? `\n API role: ${agent.role}` : '';
219
- return `${index + 1}. ${agent.label} (${agent.adapterId})${role}${instruction}`;
220
- })
221
- .join('\n');
222
- }
223
-
224
- function inferAgentRole(agent: AgentAssignment): AgentRole {
225
- if (isKnownAgentRole(agent.role)) return agent.role;
226
-
227
- const text = `${agent.label} ${agent.adapterId} ${agent.role ?? ''} ${agent.instruction ?? ''}`.toLocaleLowerCase('tr');
228
- if (/(test|tester|qa|review|code review|hata|kontrol|onay|incele|doğrula|dogrula)/u.test(text)) {
229
- return 'review';
230
- }
231
- if (/(backend|back-end|api|server|veri|database|db|fapi|endpoint|websocket|ws)/u.test(text)) {
232
- return 'backend';
233
- }
234
- if (/(frontend|front-end|ui|ux|tailwind|tasarım|tasarim|design|chart|tradingview|arayüz|arayuz)/u.test(text)) {
235
- return 'frontend';
236
- }
237
- return 'implementation';
238
- }
239
-
240
- function inferImplementationRole(agent: AgentAssignment): 'backend' | 'frontend' | 'review' | 'implementation' {
241
- const role = inferAgentRole(agent);
242
- return role === 'backend' || role === 'frontend' || role === 'review' || role === 'implementation'
243
- ? role
244
- : 'implementation';
245
- }
246
-
247
- function displayStage(agent: AgentAssignment, fallback: AgentRole): string {
248
- return agent.role && !isKnownAgentRole(agent.role) ? agent.role : fallback;
249
- }
250
-
251
- function rolePrompt(role: AgentRole): string {
252
- if (role === 'backend') {
253
- return 'Backend/API/data work should define stable contracts first. Report endpoints, payload shapes, ports, and any data-source limitations clearly for downstream agents.';
254
- }
255
- if (role === 'frontend') {
256
- return 'Frontend/UI work must use prior backend/data-contract outputs when present. If a dependency is missing, use a minimal mock only as a temporary fallback and report the blocker.';
257
- }
258
- if (role === 'review') {
259
- return 'You are the validation/review stage. Inspect the prior agent outputs and actual project state. Approve only if it works; otherwise return a concrete bug list and required fixes.';
260
- }
261
- if (role === 'proposal') {
262
- return 'You are in the proposal stage. Produce a concrete option with tradeoffs, assumptions, and what should happen next. Do not edit files.';
263
- }
264
- if (role === 'critique') {
265
- return 'You are in the critique stage. Challenge the proposal for risks, missing constraints, and weak assumptions. Do not edit files.';
266
- }
267
- if (role === 'response') {
268
- return 'You are in the response stage. Reconcile the critique with the proposal and refine the practical path forward. Do not edit files.';
269
- }
270
- if (role === 'decision' || role === 'report') {
271
- return 'You are the reporting stage. Produce the final concise decision report and a next prompt for launching an implementation agent team. Do not edit files.';
272
- }
273
- if (role !== 'implementation') {
274
- return `You are assigned to the custom stage "${role}". Follow that user-defined stage literally, avoid duplicating other agents, and report changed files, commands, blockers, and next actions.`;
275
- }
276
- return 'Implementation work should avoid duplicating other agents and should report changed files, commands, blockers, and next actions.';
277
- }
278
-
279
- function handoffPrompt(agent: AgentAssignment, role: AgentRole): string {
280
- return [
281
- `You are ${agent.label} in a Pixcode CLI team.`,
282
- `Your inferred stage is: ${role}.`,
283
- 'This is a bounded A2A handoff task, not the full implementation.',
284
- 'Read the original user goal and coordinator plan, then publish a compact contract for downstream agents.',
285
- agent.instruction ? `Your explicit assignment from the user is: ${agent.instruction}` : '',
286
- 'Output only the handoff contract:',
287
- '- owned scope',
288
- '- files/modules you expect to touch',
289
- '- API/data contracts, ports, payload shapes, and limitations',
290
- '- dependencies/blockers for the next agents',
291
- '- concrete next action for your full implementation task',
292
- 'Do not install dependencies, edit files, run long commands, or start servers in this handoff task.',
293
- 'Stop after the contract. Keep it concise and respond in the same language as the user request.',
294
- ].filter(Boolean).join('\n');
295
- }
296
-
297
- function compactOutputForContext(text: string): string {
298
- if (text.length <= MAX_OUTPUT_CONTEXT_CHARS) {
299
- return text;
300
- }
301
-
302
- const edge = Math.floor(MAX_OUTPUT_CONTEXT_CHARS / 2);
303
- return [
304
- text.slice(0, edge),
305
- `\n\n[...${text.length - MAX_OUTPUT_CONTEXT_CHARS} characters omitted from prior agent output...]\n\n`,
306
- text.slice(-edge),
307
- ].join('');
308
- }
309
-
310
- function expandAgentTeamWorkflow(workflow: Workflow, metadata?: Record<string, unknown>): Workflow {
311
- const agents = readAgentAssignments(metadata);
312
- if (agents.length === 0) {
313
- throw new Error('Select at least one CLI agent.');
314
- }
315
-
316
- const coordinator = agents.find((agent) => agent.adapterId === 'claude-code') ?? agents[0];
317
- const roster = agentRoster(agents);
318
- const workerSpecs = agents.map((agent, index) => ({
319
- agent,
320
- role: inferImplementationRole(agent),
321
- stage: displayStage(agent, inferImplementationRole(agent)),
322
- nodeId: safeAgentNodeId(agent, index, 'work'),
323
- handoffNodeId: safeAgentNodeId(agent, index, 'handoff'),
324
- }));
325
- const backendHandoffNodeIds = workerSpecs
326
- .filter((spec) => spec.role === 'backend')
327
- .map((spec) => spec.handoffNodeId);
328
- const implementationNodeIds = workerSpecs
329
- .filter((spec) => spec.role !== 'review')
330
- .map((spec) => spec.nodeId);
331
- const handoffNodes: WorkflowNode[] = workerSpecs
332
- .filter((spec) => spec.role === 'backend')
333
- .map(({ agent, role, handoffNodeId }) => ({
334
- id: handoffNodeId,
335
- adapterId: agent.adapterId,
336
- agentInstanceId: agent.instanceId,
337
- agentLabel: `${agent.label} Handoff`,
338
- assignment: agent.instruction,
339
- stage: 'handoff',
340
- model: agent.model,
341
- permissionMode: agent.permissionMode,
342
- toolsSettings: agent.toolsSettings,
343
- prompt: handoffPrompt(agent, role),
344
- inputs: ['coordinator'],
345
- output: 'message',
346
- onFail: 'continue',
347
- timeoutMs: BACKEND_HANDOFF_TIMEOUT_MS,
348
- }));
349
- const workerNodes: WorkflowNode[] = workerSpecs.map(({ agent, role, stage, nodeId, handoffNodeId }) => {
350
- const inputs = role === 'review'
351
- ? (implementationNodeIds.length > 0 ? implementationNodeIds : ['coordinator'])
352
- : role === 'frontend' && backendHandoffNodeIds.length > 0
353
- ? ['coordinator', ...backendHandoffNodeIds]
354
- : role === 'backend'
355
- ? ['coordinator', handoffNodeId]
356
- : ['coordinator'];
357
-
358
- return {
359
- id: nodeId,
360
- adapterId: agent.adapterId,
361
- agentInstanceId: agent.instanceId,
362
- agentLabel: agent.label,
363
- assignment: agent.instruction,
364
- stage,
365
- model: agent.model,
366
- permissionMode: agent.permissionMode,
367
- toolsSettings: agent.toolsSettings,
368
- prompt: [
369
- `You are ${agent.label} in a Pixcode CLI team.`,
370
- `Your stage is: ${stage}.`,
371
- stage !== role ? `Runtime routing category: ${role}.` : '',
372
- 'The coordinator plan and any dependency outputs are included above. Use them together with the original user goal.',
373
- agent.instruction
374
- ? `Your explicit assignment from the user is: ${agent.instruction}`
375
- : 'No fixed per-agent assignment was set. Take the part assigned to you by the coordinator; if none is named, choose useful work that fits this CLI.',
376
- rolePrompt(stage),
377
- 'Respond in the same language as the user request.',
378
- ].filter(Boolean).join('\n'),
379
- inputs,
380
- output: 'both',
381
- onFail: 'continue',
382
- };
383
- });
384
-
385
- return {
386
- ...workflow,
387
- nodes: [
388
- {
389
- id: 'coordinator',
390
- adapterId: coordinator.adapterId,
391
- agentInstanceId: coordinator.instanceId,
392
- agentLabel: coordinator.label,
393
- stage: 'coordinator',
394
- model: coordinator.model,
395
- permissionMode: coordinator.permissionMode,
396
- toolsSettings: coordinator.toolsSettings,
397
- prompt: [
398
- 'You are the coordinator for a Pixcode CLI agent team.',
399
- 'Read the user goal, active CLI roster, and any per-agent assignments. Create a compact execution plan for the selected agents.',
400
- 'If the user directly names a CLI, honor that. Do not invent permanent roles; assign work only from the goal, active agents, and explicit assignment text.',
401
- `Active roster:\n${roster}`,
402
- 'Respond in the same language as the user request.',
403
- ].join('\n'),
404
- inputs: [],
405
- output: 'message',
406
- onFail: 'abort',
407
- },
408
- ...handoffNodes,
409
- ...workerNodes,
410
- {
411
- id: 'final_report',
412
- adapterId: coordinator.adapterId,
413
- agentInstanceId: coordinator.instanceId,
414
- agentLabel: coordinator.label,
415
- stage: 'final_report',
416
- model: coordinator.model,
417
- permissionMode: coordinator.permissionMode,
418
- toolsSettings: coordinator.toolsSettings,
419
- prompt: [
420
- 'Collect the worker outputs into one user-facing result.',
421
- 'Show what each CLI did, which parts failed, what changed, and the next action if work remains.',
422
- 'Respond in the same language as the user request.',
423
- ].join('\n'),
424
- inputs: workerNodes.map((node) => node.id),
425
- output: 'message',
426
- onFail: 'abort',
427
- },
428
- ],
429
- };
430
- }
431
-
432
- function stagePrompt(agent: AgentAssignment, stage: AgentRole): string {
433
- return [
434
- `You are ${agent.label} in a Pixcode decision workflow.`,
435
- `Your stage is: ${stage}.`,
436
- agent.role && agent.role !== stage ? `User custom stage label: ${agent.role}.` : '',
437
- agent.instruction ? `User assignment for you: ${agent.instruction}` : '',
438
- rolePrompt(stage),
439
- 'Keep the answer concise, structured, and useful for the next stage.',
440
- 'Respond in the same language as the user request.',
441
- ].filter(Boolean).join('\n');
442
- }
443
-
444
- function agentsWithRole(agents: AgentAssignment[], role: AgentRole): AgentAssignment[] {
445
- return agents.filter((agent) => agent.role === role);
446
- }
447
-
448
- function autoAssignDebateAgents(agents: AgentAssignment[]): {
449
- proposalAgents: AgentAssignment[];
450
- critiqueAgents: AgentAssignment[];
451
- responseAgents: AgentAssignment[];
452
- reportAgent: AgentAssignment;
453
- } {
454
- const assigned = new Set<string>();
455
- const markAssigned = (items: AgentAssignment[]) => {
456
- for (const item of items) assigned.add(item.instanceId);
457
- };
458
- const pickNext = () =>
459
- agents.find((agent) => !assigned.has(agent.instanceId) && agent.role !== 'decision' && agent.role !== 'report')
460
- ?? agents.find((agent) => !assigned.has(agent.instanceId))
461
- ?? agents[0];
462
-
463
- const proposalAgents = agentsWithRole(agents, 'proposal');
464
- if (proposalAgents.length === 0) proposalAgents.push(pickNext());
465
- markAssigned(proposalAgents);
466
-
467
- const critiqueAgents = agentsWithRole(agents, 'critique');
468
- if (critiqueAgents.length === 0) critiqueAgents.push(pickNext());
469
- markAssigned(critiqueAgents);
470
-
471
- const responseAgents = agentsWithRole(agents, 'response');
472
- if (responseAgents.length === 0 && agents.length > 2) {
473
- responseAgents.push(...agents.filter((agent) =>
474
- !assigned.has(agent.instanceId) && agent.role !== 'decision' && agent.role !== 'report',
475
- ));
476
- }
477
- markAssigned(responseAgents);
478
-
479
- const reportAgent = agentsWithRole(agents, 'decision')[0]
480
- ?? agentsWithRole(agents, 'report')[0]
481
- ?? agents[0];
482
-
483
- return { proposalAgents, critiqueAgents, responseAgents, reportAgent };
484
- }
485
-
486
- function expandAdversarialDebateWorkflow(workflow: Workflow, metadata?: Record<string, unknown>): Workflow {
487
- const agents = readAgentAssignments(metadata);
488
- if (agents.length === 0) {
489
- throw new Error('Select at least one CLI agent.');
490
- }
491
-
492
- const {
493
- proposalAgents,
494
- critiqueAgents,
495
- responseAgents,
496
- reportAgent,
497
- } = autoAssignDebateAgents(agents);
498
-
499
- const proposalNodes: WorkflowNode[] = proposalAgents.map((agent, index) => ({
500
- id: safeAgentNodeId(agent, index, 'proposal'),
501
- adapterId: agent.adapterId,
502
- agentInstanceId: agent.instanceId,
503
- agentLabel: agent.label,
504
- assignment: agent.instruction || 'Proposal stage',
505
- stage: 'proposal',
506
- model: agent.model,
507
- permissionMode: agent.permissionMode,
508
- toolsSettings: agent.toolsSettings,
509
- prompt: stagePrompt(agent, 'proposal'),
510
- inputs: [],
511
- output: 'message',
512
- onFail: 'continue',
513
- }));
514
- const critiqueNodes: WorkflowNode[] = critiqueAgents.map((agent, index) => ({
515
- id: safeAgentNodeId(agent, index, 'critique'),
516
- adapterId: agent.adapterId,
517
- agentInstanceId: agent.instanceId,
518
- agentLabel: agent.label,
519
- assignment: agent.instruction || 'Critique stage',
520
- stage: 'critique',
521
- model: agent.model,
522
- permissionMode: agent.permissionMode,
523
- toolsSettings: agent.toolsSettings,
524
- prompt: stagePrompt(agent, 'critique'),
525
- inputs: proposalNodes.map((node) => node.id),
526
- output: 'message',
527
- onFail: 'continue',
528
- }));
529
- const responseNodes: WorkflowNode[] = responseAgents.map((agent, index) => ({
530
- id: safeAgentNodeId(agent, index, 'response'),
531
- adapterId: agent.adapterId,
532
- agentInstanceId: agent.instanceId,
533
- agentLabel: agent.label,
534
- assignment: agent.instruction || 'Response stage',
535
- stage: 'response',
536
- model: agent.model,
537
- permissionMode: agent.permissionMode,
538
- toolsSettings: agent.toolsSettings,
539
- prompt: stagePrompt(agent, 'response'),
540
- inputs: critiqueNodes.map((node) => node.id),
541
- output: 'message',
542
- onFail: 'continue',
543
- }));
544
- const finalInputs = responseNodes.length > 0
545
- ? responseNodes.map((node) => node.id)
546
- : critiqueNodes.map((node) => node.id);
547
-
548
- return {
549
- ...workflow,
550
- nodes: [
551
- ...proposalNodes,
552
- ...critiqueNodes,
553
- ...responseNodes,
554
- {
555
- id: 'final_report',
556
- adapterId: reportAgent.adapterId,
557
- agentInstanceId: reportAgent.instanceId,
558
- agentLabel: reportAgent.label,
559
- assignment: reportAgent.instruction || 'Final decision report',
560
- stage: 'final_report',
561
- model: reportAgent.model,
562
- permissionMode: reportAgent.permissionMode,
563
- toolsSettings: reportAgent.toolsSettings,
564
- prompt: [
565
- 'Produce the final decision report from the debate.',
566
- 'Use this exact structure:',
567
- '1. Short decision',
568
- '2. Why',
569
- '3. Risks',
570
- '4. Suggested next prompt',
571
- '5. Proposed agent team and assignments',
572
- 'The next prompt should be ready to paste into Pixcode Agent Team mode.',
573
- 'Do not edit files. Respond in the same language as the user request.',
574
- ].join('\n'),
575
- inputs: finalInputs,
576
- output: 'message',
577
- onFail: 'abort',
578
- },
579
- ],
580
- };
581
- }
582
-
583
- function expandSequentialHandoffWorkflow(workflow: Workflow, metadata?: Record<string, unknown>): Workflow {
584
- const agents = readAgentAssignments(metadata);
585
- if (agents.length === 0) {
586
- throw new Error('Select at least one CLI agent.');
587
- }
588
-
589
- return {
590
- ...workflow,
591
- nodes: agents.map((agent, index): WorkflowNode => ({
592
- id: safeAgentNodeId(agent, index, 'handoff'),
593
- adapterId: agent.adapterId,
594
- agentInstanceId: agent.instanceId,
595
- agentLabel: agent.label,
596
- assignment: agent.instruction,
597
- stage: agent.role ?? 'implementation',
598
- model: agent.model,
599
- permissionMode: agent.permissionMode,
600
- toolsSettings: agent.toolsSettings,
601
- prompt: [
602
- `You are ${agent.label} in a sequential Pixcode handoff.`,
603
- `This is step ${index + 1} of ${agents.length}.`,
604
- agent.instruction
605
- ? `Your explicit assignment from the user is: ${agent.instruction}`
606
- : 'Use the prior step output and do the next most useful handoff step for the user goal.',
607
- 'Report changed files, commands, blockers, and the next handoff requirement.',
608
- 'Respond in the same language as the user request.',
609
- ].filter(Boolean).join('\n'),
610
- inputs: index === 0 ? [] : [safeAgentNodeId(agents[index - 1], index - 1, 'handoff')],
611
- output: 'both',
612
- onFail: 'abort',
613
- })),
614
- };
615
- }
616
-
617
- function expandWorkflowForRun(workflow: Workflow, metadata?: Record<string, unknown>): Workflow {
618
- if (workflow.id === 'agent_team') {
619
- return expandAgentTeamWorkflow(workflow, metadata);
620
- }
621
-
622
- const agents = readAgentAssignments(metadata);
623
- if (workflow.id === 'adversarial_debate') {
624
- return expandAdversarialDebateWorkflow(workflow, metadata);
625
- }
626
- if (workflow.id === 'sequential_handoff') {
627
- return expandSequentialHandoffWorkflow(workflow, metadata);
628
- }
629
- if (workflow.id !== 'multi_model_review' || agents.length === 0) {
630
- return workflow;
631
- }
632
-
633
- const reportAgent = agentsWithRole(agents, 'report')[0] ?? agentsWithRole(agents, 'decision')[0] ?? agents[0];
634
- const reviewAgents = agents.filter((agent) => agent.instanceId !== reportAgent.instanceId || agents.length === 1);
635
- const reviewNodes: WorkflowNode[] = reviewAgents.map((agent, index) => ({
636
- id: safeAgentNodeId(agent, index, 'review'),
637
- adapterId: agent.adapterId,
638
- agentInstanceId: agent.instanceId,
639
- agentLabel: agent.label,
640
- assignment: agent.instruction,
641
- stage: 'review',
642
- model: agent.model,
643
- permissionMode: agent.permissionMode,
644
- toolsSettings: agent.toolsSettings,
645
- prompt: [
646
- `You are ${agent.label}.`,
647
- 'Review the requested change for bugs, regressions, missing validation, security, scale, and user-experience risks.',
648
- agent.instruction ? `Focus on this user assignment: ${agent.instruction}` : '',
649
- 'Respond in the same language as the user request.',
650
- ].filter(Boolean).join('\n'),
651
- inputs: [],
652
- output: 'both',
653
- onFail: 'continue',
654
- }));
655
-
656
- return {
657
- ...workflow,
658
- nodes: [
659
- ...reviewNodes,
660
- {
661
- id: 'aggregate',
662
- adapterId: reportAgent.adapterId,
663
- agentInstanceId: reportAgent.instanceId,
664
- agentLabel: reportAgent.label,
665
- stage: 'report',
666
- model: reportAgent.model,
667
- permissionMode: reportAgent.permissionMode,
668
- toolsSettings: reportAgent.toolsSettings,
669
- prompt: 'Aggregate the prior agent reviews into a concise prioritized report. Respond in the same language as the user request.',
670
- inputs: reviewNodes.map((node) => node.id),
671
- output: 'message',
672
- onFail: 'abort',
673
- },
674
- ],
675
- };
676
- }
677
-
678
- async function cancelA2ATask(taskId: string): Promise<void> {
679
- await fetch(`${localA2ABaseUrl()}/tasks/${taskId}/cancel`, { method: 'POST' }).catch(() => undefined);
680
- }
681
-
682
- function readTaskResult(task: RawTask): TaskResult {
683
- const messages = (task.history ?? []).map((message) => ({
684
- role: typeof message.role === 'string' ? message.role : 'agent',
685
- text: (message.parts ?? [])
686
- .filter((part) => part.kind === 'text' && typeof part.text === 'string')
687
- .map((part) => part.text)
688
- .join('\n'),
689
- })).filter((message) => message.text.trim());
690
- const artifacts = (task.artifacts ?? []).map((artifact) => {
691
- const text = (artifact.parts ?? [])
692
- .filter((part) => part.kind === 'text' && typeof part.text === 'string')
693
- .map((part) => part.text)
694
- .join('\n');
695
- const data = (artifact.parts ?? []).find((part) => part.kind === 'data')?.data;
696
- return {
697
- type: artifact.type ?? 'data',
698
- text: text || undefined,
699
- data,
700
- metadata: artifact.metadata,
701
- };
702
- });
703
- const outputMessages = messages.filter((message) => message.role !== 'user');
704
- const text = outputMessages.map((message) => `${message.role}: ${message.text}`).join('\n\n');
705
- const error = task.error?.message
706
- ? `${task.error.code ? `${task.error.code}: ` : ''}${task.error.message}`
707
- : undefined;
708
- return {
709
- state: task.state ?? 'submitted',
710
- text,
711
- error,
712
- messages,
713
- artifacts,
714
- };
715
- }
716
-
717
- async function waitForTask(
718
- taskId: string,
719
- shouldCancel?: () => boolean,
720
- onSnapshot?: (result: TaskResult) => void,
721
- timeoutMs?: number,
722
- ): Promise<TaskResult> {
723
- const timeout = timeoutMs && timeoutMs > 0 ? timeoutMs : undefined;
724
- const deadline = timeout ? Date.now() + timeout : undefined;
725
- for (;;) {
726
- if (shouldCancel?.()) {
727
- throw new WorkflowCanceledError();
728
- }
729
- if (deadline && Date.now() >= deadline) {
730
- throw new WorkflowNodeTimeoutError(timeout ?? 0);
731
- }
732
- const response = await fetch(`${localA2ABaseUrl()}/tasks/${taskId}`);
733
- const task = await response.json() as RawTask;
734
- const snapshot = readTaskResult(task);
735
- onSnapshot?.(snapshot);
736
- if (task.state && TERMINAL.has(task.state)) {
737
- return snapshot;
738
- }
739
- await new Promise((resolve) => setTimeout(resolve, 1000));
740
- }
741
- }
742
-
743
- function readyNodes(workflow: Workflow, completed: Set<string>, started: Set<string>): WorkflowNode[] {
744
- return workflow.nodes.filter((node) =>
745
- !started.has(node.id) && node.inputs.every((input) => completed.has(input)),
746
- );
747
- }
748
-
749
- function nodeRunFromNode(node: WorkflowNode): WorkflowNodeRun {
750
- return {
751
- nodeId: node.id,
752
- adapterId: node.adapterId,
753
- agentInstanceId: node.agentInstanceId,
754
- agentLabel: node.agentLabel,
755
- assignment: node.assignment,
756
- model: node.model,
757
- permissionMode: node.permissionMode,
758
- timeoutMs: node.timeoutMs,
759
- stage: node.stage,
760
- status: 'queued',
761
- };
762
- }
763
-
764
- function uniqueInputs(inputs: string[]): string[] {
765
- return [...new Set(inputs.filter(Boolean))];
766
- }
767
-
768
- function isReviewNode(node: WorkflowNode): boolean {
769
- return node.stage === 'review';
770
- }
771
-
772
- function isImplementationNode(node: WorkflowNode): boolean {
773
- return node.stage === 'backend' || node.stage === 'frontend' || node.stage === 'implementation' || node.stage === 'repair';
774
- }
775
-
776
- function reviewRequiresRepair(text: string): boolean {
777
- const normalized = text.toLocaleLowerCase('tr').replace(/\s+/g, ' ').trim();
778
- if (!normalized) return false;
779
-
780
- const approvalPatterns = [
781
- /hata yok/u,
782
- /sorun yok/u,
783
- /problem yok/u,
784
- /bulgu yok/u,
785
- /kritik bulgu yok/u,
786
- /temiz/u,
787
- /onaylı/u,
788
- /onayli/u,
789
- /approved/u,
790
- /lgtm/u,
791
- /no issues/u,
792
- /no findings/u,
793
- /looks good/u,
794
- /pass(?:ed)?/u,
795
- ];
796
- const actionableText = approvalPatterns.reduce((current, pattern) => current.replace(pattern, ' '), normalized);
797
- const issuePatterns = [
798
- /hata/u,
799
- /bug/u,
800
- /kritik/u,
801
- /critical/u,
802
- /blocker/u,
803
- /regression/u,
804
- /failed/u,
805
- /failure/u,
806
- /fail/u,
807
- /eksik/u,
808
- /düzelt/u,
809
- /duzelt/u,
810
- /fix required/u,
811
- /needs fix/u,
812
- /sorun/u,
813
- /risk/u,
814
- /güvenlik/u,
815
- /guvenlik/u,
816
- /security/u,
817
- /çalışmıyor/u,
818
- /calismiyor/u,
819
- ];
820
-
821
- return issuePatterns.some((pattern) => pattern.test(actionableText));
822
- }
823
-
824
- function findRepairFixer(workflow: Workflow, reviewNode: WorkflowNode): WorkflowNode | undefined {
825
- return reviewNode.inputs
826
- .map((input) => workflow.nodes.find((node) => node.id === input))
827
- .find((node): node is WorkflowNode => Boolean(node && isImplementationNode(node)))
828
- ?? workflow.nodes.find((node) => isImplementationNode(node))
829
- ?? workflow.nodes.find((node) => node.stage === 'coordinator');
830
- }
831
-
832
- class WorkflowRunner {
833
- private readonly cancelingRuns = new Set<string>();
834
-
835
- preview(workflow: Workflow, metadata?: Record<string, unknown>): Workflow {
836
- const runtimeWorkflow = expandWorkflowForRun(workflow, metadata);
837
- validateWorkflow(runtimeWorkflow);
838
- return runtimeWorkflow;
839
- }
840
-
841
- start(workflow: Workflow, input = '', metadata?: Record<string, unknown>): WorkflowRun {
842
- const runtimeWorkflow = expandWorkflowForRun(workflow, metadata);
843
- validateWorkflow(runtimeWorkflow);
844
- const workspaceTarget = resolveWorkflowWorkspace(metadata);
845
- const runMetadata = {
846
- ...metadata,
847
- projectPath: workspaceTarget.projectPath,
848
- selectedProjectPath: workspaceTarget.selectedProjectPath,
849
- workspaceTarget: workspaceTargetMetadata(workspaceTarget),
850
- };
851
- const run: WorkflowRun = {
852
- id: newId('wrun'),
853
- workflowId: runtimeWorkflow.id,
854
- contextId: newId('ctx'),
855
- status: 'queued',
856
- input,
857
- nodeRuns: runtimeWorkflow.nodes.map(nodeRunFromNode),
858
- startedAt: Date.now(),
859
- metadata: runMetadata,
860
- };
861
- workflowStore.setRun(run);
862
- void this.execute(runtimeWorkflow, run);
863
- return run;
864
- }
865
-
866
- async cancel(runId: string): Promise<WorkflowRun | undefined> {
867
- const run = workflowStore.getRun(runId);
868
- if (!run) return undefined;
869
- if (TERMINAL.has(run.status)) return run;
870
-
871
- this.cancelingRuns.add(run.id);
872
- const taskIds = run.nodeRuns
873
- .filter((node) => node.a2aTaskId && (node.status === 'running' || node.status === 'queued'))
874
- .map((node) => node.a2aTaskId as string);
875
-
876
- this.markCanceled(run);
877
- workflowStore.setRun(run);
878
-
879
- await Promise.all(taskIds.map((taskId) => cancelA2ATask(taskId)));
880
-
881
- return workflowStore.getRun(run.id) ?? run;
882
- }
883
-
884
- private isCanceling(runId: string): boolean {
885
- return this.cancelingRuns.has(runId) || workflowStore.getRun(runId)?.status === 'canceled';
886
- }
887
-
888
- private markCanceled(run: WorkflowRun): void {
889
- run.status = 'canceled';
890
- run.finishedAt = run.finishedAt ?? Date.now();
891
- for (const nodeRun of run.nodeRuns) {
892
- if (!TERMINAL.has(nodeRun.status) && nodeRun.status !== SKIPPED) {
893
- nodeRun.status = 'canceled';
894
- nodeRun.finishedAt = nodeRun.finishedAt ?? Date.now();
895
- }
896
- }
897
- }
898
-
899
- private maybeAddRepairCycle(
900
- node: WorkflowNode,
901
- workflow: Workflow,
902
- run: WorkflowRun,
903
- result: TaskResult,
904
- ): void {
905
- if (workflow.id !== 'agent_team') return;
906
- if (!isReviewNode(node) || node.id.startsWith('repair_') || node.id.startsWith('recheck_')) return;
907
- if (!reviewRequiresRepair(`${result.text}\n${result.error ?? ''}`)) return;
908
-
909
- const maxRepairCycles = readMaxRepairCycles(run.metadata);
910
- if (maxRepairCycles <= 0) return;
911
-
912
- const existingCycles = workflow.nodes.filter((candidate) => candidate.id.startsWith(`repair_${node.id}_`)).length;
913
- if (existingCycles >= maxRepairCycles) return;
914
-
915
- if (workflow.nodes.length + 2 > 64) {
916
- run.metadata = {
917
- ...run.metadata,
918
- dynamicRepairSkipped: `Workflow node limit reached after ${node.id}.`,
919
- };
920
- workflowStore.setRun(run);
921
- return;
922
- }
923
-
924
- const fixer = findRepairFixer(workflow, node);
925
- if (!fixer || fixer.id === node.id) return;
926
-
927
- const cycle = existingCycles + 1;
928
- const repairNode: WorkflowNode = {
929
- id: `repair_${node.id}_${cycle}`,
930
- adapterId: fixer.adapterId,
931
- agentInstanceId: fixer.agentInstanceId,
932
- agentLabel: fixer.agentLabel ? `${fixer.agentLabel} Repair` : undefined,
933
- assignment: `Automatic repair from ${node.agentLabel || node.id} review findings`,
934
- stage: 'repair',
935
- model: fixer.model,
936
- permissionMode: fixer.permissionMode,
937
- toolsSettings: fixer.toolsSettings,
938
- prompt: [
939
- 'A review stage found actionable issues in the prior work.',
940
- 'Use the original user goal, prior implementation outputs, and review output included above.',
941
- 'Fix only the reported issues; do not restart the whole project or duplicate unrelated work.',
942
- 'Report changed files, commands, verification, and any remaining blockers.',
943
- 'Respond in the same language as the user request.',
944
- ].join('\n'),
945
- inputs: uniqueInputs([...node.inputs, fixer.id, node.id]),
946
- output: 'both',
947
- onFail: 'continue',
948
- };
949
- const recheckNode: WorkflowNode = {
950
- id: `recheck_${node.id}_${cycle}`,
951
- adapterId: node.adapterId,
952
- agentInstanceId: node.agentInstanceId,
953
- agentLabel: node.agentLabel ? `${node.agentLabel} Recheck` : undefined,
954
- assignment: 'Automatic validation after repair',
955
- stage: 'review',
956
- model: node.model,
957
- permissionMode: node.permissionMode,
958
- toolsSettings: node.toolsSettings,
959
- prompt: [
960
- 'Validate the automatic repair against the original review findings.',
961
- 'Approve only if the reported issues are fixed.',
962
- 'If anything remains, list the remaining blockers clearly and do not invent new unrelated scope.',
963
- 'Respond in the same language as the user request.',
964
- ].join('\n'),
965
- inputs: uniqueInputs([node.id, repairNode.id]),
966
- output: 'message',
967
- onFail: 'continue',
968
- };
969
-
970
- const finalIndex = workflow.nodes.findIndex((candidate) =>
971
- candidate.id === 'final_report' || candidate.stage === 'final_report' || candidate.stage === 'report',
972
- );
973
- if (finalIndex >= 0) {
974
- workflow.nodes.splice(finalIndex, 0, repairNode, recheckNode);
975
- run.nodeRuns.splice(finalIndex, 0, nodeRunFromNode(repairNode), nodeRunFromNode(recheckNode));
976
- } else {
977
- workflow.nodes.push(repairNode, recheckNode);
978
- run.nodeRuns.push(nodeRunFromNode(repairNode), nodeRunFromNode(recheckNode));
979
- }
980
-
981
- for (const finalNode of workflow.nodes) {
982
- if (finalNode.id === 'final_report' || finalNode.stage === 'final_report' || finalNode.stage === 'report') {
983
- finalNode.inputs = uniqueInputs([...finalNode.inputs, recheckNode.id]);
984
- }
985
- }
986
-
987
- const repairCycles = Array.isArray(run.metadata?.dynamicRepairCycles)
988
- ? run.metadata.dynamicRepairCycles
989
- : [];
990
- run.metadata = {
991
- ...run.metadata,
992
- dynamicRepairCycles: [
993
- ...repairCycles,
994
- {
995
- reviewNodeId: node.id,
996
- repairNodeId: repairNode.id,
997
- recheckNodeId: recheckNode.id,
998
- fixerNodeId: fixer.id,
999
- },
1000
- ],
1001
- };
1002
- workflowStore.setRun(run);
1003
- }
1004
-
1005
- private async execute(workflow: Workflow, run: WorkflowRun): Promise<void> {
1006
- run.status = 'running';
1007
- workflowStore.setRun(run);
1008
- const completed = new Set<string>();
1009
- const started = new Set<string>();
1010
- const outputs = new Map<string, string>();
1011
- const maxParallelAgents = readMaxParallelAgents(run.metadata);
1012
-
1013
- try {
1014
- while (completed.size < workflow.nodes.length) {
1015
- if (this.isCanceling(run.id)) {
1016
- throw new WorkflowCanceledError();
1017
- }
1018
- const batch = readyNodes(workflow, completed, started);
1019
- if (batch.length === 0) {
1020
- throw new Error('Workflow stalled; no ready nodes remain.');
1021
- }
1022
- for (let index = 0; index < batch.length; index += maxParallelAgents) {
1023
- if (this.isCanceling(run.id)) {
1024
- throw new WorkflowCanceledError();
1025
- }
1026
- const slice = batch.slice(index, index + maxParallelAgents);
1027
- await Promise.all(slice.map((node) => this.executeNode(node, workflow, run, outputs, started, completed)));
1028
- }
1029
- }
1030
- if (this.isCanceling(run.id)) {
1031
- throw new WorkflowCanceledError();
1032
- }
1033
- run.status = 'completed';
1034
- } catch (error) {
1035
- if (error instanceof WorkflowCanceledError || this.isCanceling(run.id)) {
1036
- this.markCanceled(run);
1037
- } else {
1038
- run.status = 'failed';
1039
- run.metadata = {
1040
- ...run.metadata,
1041
- error: error instanceof Error ? error.message : String(error),
1042
- };
1043
- }
1044
- } finally {
1045
- run.finishedAt = run.finishedAt ?? Date.now();
1046
- workflowStore.setRun(run);
1047
- this.cancelingRuns.delete(run.id);
1048
- }
1049
- }
1050
-
1051
- private async executeNode(
1052
- node: WorkflowNode,
1053
- workflow: Workflow,
1054
- run: WorkflowRun,
1055
- outputs: Map<string, string>,
1056
- started: Set<string>,
1057
- completed: Set<string>,
1058
- ): Promise<void> {
1059
- started.add(node.id);
1060
- const nodeRun = run.nodeRuns.find((candidate) => candidate.nodeId === node.id) as WorkflowNodeRun;
1061
- const enabledAdapters = readEnabledAdapters(run.metadata);
1062
- if (enabledAdapters.length > 0 && !enabledAdapters.includes(node.adapterId)) {
1063
- nodeRun.status = SKIPPED;
1064
- nodeRun.finishedAt = Date.now();
1065
- completed.add(node.id);
1066
- workflowStore.setRun(run);
1067
- return;
1068
- }
1069
- if (this.isCanceling(run.id)) {
1070
- nodeRun.status = 'canceled';
1071
- nodeRun.finishedAt = Date.now();
1072
- workflowStore.setRun(run);
1073
- throw new WorkflowCanceledError();
1074
- }
1075
-
1076
- nodeRun.status = 'running';
1077
- nodeRun.startedAt = Date.now();
1078
- workflowStore.setRun(run);
1079
-
1080
- const inputContext = node.inputs.map((input) => outputs.get(input)).filter(Boolean).join('\n\n');
1081
- const workspaceTarget = resolveWorkflowWorkspace(run.metadata);
1082
- const prompt = [workspaceContextPrompt(workspaceTarget), run.input, inputContext, node.prompt]
1083
- .filter(Boolean)
1084
- .join('\n\n');
1085
- const settings = getMetadataRecord(run.metadata, 'settings');
1086
- const projectPath = workspaceTarget.projectPath;
1087
- const isolation = readIsolation(settings.isolation) ?? node.isolation ?? 'host';
1088
- const keepAfterCompletion = readBoolean(settings.keepWorkspace) ?? true;
1089
- const baseRef = readString(settings.baseRef) ?? 'HEAD';
1090
- const submit = await fetch(`${localA2ABaseUrl()}/tasks`, {
1091
- method: 'POST',
1092
- headers: { 'content-type': 'application/json' },
1093
- body: JSON.stringify({
1094
- adapterId: node.adapterId,
1095
- contextId: run.contextId,
1096
- message: {
1097
- messageId: newId('msg'),
1098
- role: 'user',
1099
- parts: [{ kind: 'text', text: prompt }],
1100
- },
1101
- metadata: {
1102
- workflowRunId: run.id,
1103
- workflowNodeId: node.id,
1104
- agentInstanceId: node.agentInstanceId,
1105
- agentLabel: node.agentLabel,
1106
- assignment: node.assignment,
1107
- model: node.model,
1108
- permissionMode: node.permissionMode,
1109
- toolsSettings: node.toolsSettings,
1110
- projectPath,
1111
- workspaceTarget: workspaceTargetMetadata(workspaceTarget),
1112
- workspace: {
1113
- kind: isolation,
1114
- projectPath,
1115
- baseRef,
1116
- keepAfterCompletion,
1117
- },
1118
- },
1119
- }),
1120
- });
1121
- const body = await submit.json() as { id?: string; error?: { message?: string } };
1122
- if (!submit.ok || !body.id) {
1123
- throw new Error(body.error?.message ?? `Workflow node ${node.id} submit failed.`);
1124
- }
1125
- nodeRun.a2aTaskId = body.id;
1126
- workflowStore.setRun(run);
1127
-
1128
- if (this.isCanceling(run.id)) {
1129
- await cancelA2ATask(body.id);
1130
- nodeRun.status = 'canceled';
1131
- nodeRun.finishedAt = Date.now();
1132
- workflowStore.setRun(run);
1133
- throw new WorkflowCanceledError();
1134
- }
1135
-
1136
- let result: TaskResult;
1137
- try {
1138
- result = await waitForTask(
1139
- body.id,
1140
- () => this.isCanceling(run.id),
1141
- (snapshot) => {
1142
- nodeRun.outputText = snapshot.text || nodeRun.outputText;
1143
- nodeRun.messages = snapshot.messages;
1144
- nodeRun.artifacts = snapshot.artifacts;
1145
- nodeRun.error = snapshot.error;
1146
- workflowStore.setRun(run);
1147
- },
1148
- node.timeoutMs,
1149
- );
1150
- } catch (error) {
1151
- if (!(error instanceof WorkflowNodeTimeoutError)) {
1152
- throw error;
1153
- }
1154
-
1155
- await cancelA2ATask(body.id);
1156
- nodeRun.finishedAt = Date.now();
1157
- nodeRun.status = 'failed';
1158
- nodeRun.error = error.message;
1159
- if (nodeRun.outputText) {
1160
- outputs.set(node.id, compactOutputForContext(nodeRun.outputText));
1161
- }
1162
- workflowStore.setRun(run);
1163
- if (node.onFail === 'continue') {
1164
- completed.add(node.id);
1165
- return;
1166
- }
1167
- throw error;
1168
- }
1169
- nodeRun.finishedAt = Date.now();
1170
- nodeRun.outputText = result.text;
1171
- nodeRun.messages = result.messages;
1172
- nodeRun.artifacts = result.artifacts;
1173
- if (this.isCanceling(run.id)) {
1174
- nodeRun.status = 'canceled';
1175
- workflowStore.setRun(run);
1176
- throw new WorkflowCanceledError();
1177
- }
1178
- if (result.state === 'completed') {
1179
- outputs.set(node.id, compactOutputForContext(result.text));
1180
- completed.add(node.id);
1181
- nodeRun.status = 'completed';
1182
- workflowStore.setRun(run);
1183
- this.maybeAddRepairCycle(node, workflow, run, result);
1184
- return;
1185
- }
1186
- if (result.state === 'canceled') {
1187
- nodeRun.status = 'canceled';
1188
- workflowStore.setRun(run);
1189
- throw new WorkflowCanceledError();
1190
- }
1191
-
1192
- nodeRun.status = 'failed';
1193
- nodeRun.error = result.error ?? `A2A task ended with ${result.state}`;
1194
- workflowStore.setRun(run);
1195
- if (node.onFail === 'continue') {
1196
- if (nodeRun.outputText) {
1197
- outputs.set(node.id, compactOutputForContext(nodeRun.outputText));
1198
- }
1199
- completed.add(node.id);
1200
- return;
1201
- }
1202
- throw new Error(nodeRun.error);
1203
- }
1204
- }
1205
-
1206
- export const workflowRunner = new WorkflowRunner();
1
+ import crypto from 'node:crypto';
2
+
3
+ import type {
4
+ Workflow,
5
+ WorkflowNode,
6
+ WorkflowNodeRun,
7
+ WorkflowRun,
8
+ } from '@/modules/orchestration/workflows/workflow.types.js';
9
+ import {
10
+ resolveWorkflowWorkspace,
11
+ workspaceContextPrompt,
12
+ workspaceTargetMetadata,
13
+ } from '@/modules/orchestration/workflows/workspace-target.js';
14
+ import { workflowStore } from '@/modules/orchestration/workflows/workflow-store.js';
15
+
16
+ const TERMINAL = new Set(['completed', 'failed', 'canceled']);
17
+ const SKIPPED = 'skipped';
18
+ const BACKEND_HANDOFF_TIMEOUT_MS = 120_000;
19
+ const MAX_OUTPUT_CONTEXT_CHARS = 12_000;
20
+ const DEFAULT_MAX_REPAIR_CYCLES = 1;
21
+ const MAX_REPAIR_CYCLES = 5;
22
+ const KNOWN_AGENT_ROLES = [
23
+ 'backend',
24
+ 'frontend',
25
+ 'review',
26
+ 'implementation',
27
+ 'proposal',
28
+ 'critique',
29
+ 'response',
30
+ 'decision',
31
+ 'report',
32
+ ] as const;
33
+
34
+ class WorkflowCanceledError extends Error {
35
+ constructor() {
36
+ super('Workflow canceled.');
37
+ this.name = 'WorkflowCanceledError';
38
+ }
39
+ }
40
+
41
+ class WorkflowNodeTimeoutError extends Error {
42
+ constructor(readonly timeoutMs: number) {
43
+ super(`Workflow node timed out after ${Math.round(timeoutMs / 1000)}s.`);
44
+ this.name = 'WorkflowNodeTimeoutError';
45
+ }
46
+ }
47
+
48
+ function newId(prefix: string): string {
49
+ return `${prefix}_${crypto.randomBytes(8).toString('hex')}`;
50
+ }
51
+
52
+ function localA2ABaseUrl(): string {
53
+ return `http://127.0.0.1:${process.env.SERVER_PORT ?? process.env.PORT ?? '3001'}/a2a`;
54
+ }
55
+
56
+ function validateWorkflow(workflow: Workflow): void {
57
+ if (workflow.nodes.length > 64) {
58
+ throw new Error('Workflow node limit exceeded.');
59
+ }
60
+ const ids = new Set(workflow.nodes.map((node) => node.id));
61
+ for (const node of workflow.nodes) {
62
+ for (const input of node.inputs) {
63
+ if (!ids.has(input)) {
64
+ throw new Error(`Workflow node ${node.id} references missing input ${input}.`);
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ type TaskResult = {
71
+ state: string;
72
+ text: string;
73
+ error?: string;
74
+ messages: Array<{ role: string; text: string }>;
75
+ artifacts: Array<{
76
+ type: string;
77
+ text?: string;
78
+ data?: Record<string, unknown>;
79
+ metadata?: Record<string, unknown>;
80
+ }>;
81
+ };
82
+
83
+ type RawTask = {
84
+ state?: string;
85
+ error?: { code?: string; message?: string };
86
+ history?: Array<{ role?: string; parts?: Array<{ kind?: string; text?: string; data?: Record<string, unknown> }> }>;
87
+ artifacts?: Array<{
88
+ type?: string;
89
+ parts?: Array<{ kind?: string; text?: string; data?: Record<string, unknown> }>;
90
+ metadata?: Record<string, unknown>;
91
+ }>;
92
+ };
93
+
94
+ type AgentAssignment = {
95
+ instanceId: string;
96
+ adapterId: string;
97
+ label: string;
98
+ role?: AgentRole;
99
+ instruction?: string;
100
+ model?: string;
101
+ permissionMode?: string;
102
+ toolsSettings?: Record<string, unknown>;
103
+ order: number;
104
+ };
105
+
106
+ type KnownAgentRole = typeof KNOWN_AGENT_ROLES[number];
107
+ type AgentRole = string;
108
+
109
+ function readAgentRole(value: unknown): AgentRole | undefined {
110
+ return typeof value === 'string' && value.trim() && value.trim() !== 'auto'
111
+ ? value.trim()
112
+ : undefined;
113
+ }
114
+
115
+ function isKnownAgentRole(value: string | undefined): value is KnownAgentRole {
116
+ return Boolean(value && (KNOWN_AGENT_ROLES as readonly string[]).includes(value));
117
+ }
118
+
119
+ function getMetadataRecord(metadata: Record<string, unknown> | undefined, key: string): Record<string, unknown> {
120
+ return readRecord(metadata?.[key]) ?? {};
121
+ }
122
+
123
+ function readRecord(value: unknown): Record<string, unknown> | undefined {
124
+ return value && typeof value === 'object' ? value as Record<string, unknown> : undefined;
125
+ }
126
+
127
+ function readString(value: unknown): string | undefined {
128
+ return typeof value === 'string' && value.trim() ? value : undefined;
129
+ }
130
+
131
+ function readBoolean(value: unknown): boolean | undefined {
132
+ return typeof value === 'boolean' ? value : undefined;
133
+ }
134
+
135
+ function readIsolation(value: unknown): 'host' | 'worktree' | 'docker' | undefined {
136
+ return value === 'host' || value === 'worktree' || value === 'docker' ? value : undefined;
137
+ }
138
+
139
+ function readLegacyEnabledAdapters(metadata?: Record<string, unknown>): string[] {
140
+ return Array.isArray(metadata?.enabledAdapters)
141
+ ? metadata.enabledAdapters.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
142
+ : [];
143
+ }
144
+
145
+ function readMetadataAgents(metadata?: Record<string, unknown>): AgentAssignment[] {
146
+ if (!Array.isArray(metadata?.agents)) return [];
147
+
148
+ return metadata.agents
149
+ .map((value, index): AgentAssignment | null => {
150
+ if (!value || typeof value !== 'object') return null;
151
+ const record = value as Record<string, unknown>;
152
+ const adapterId = readString(record.adapterId);
153
+ if (!adapterId) return null;
154
+ if (readBoolean(record.enabled) === false) return null;
155
+
156
+ return {
157
+ instanceId: readString(record.instanceId) ?? `${adapterId}-${index + 1}`,
158
+ adapterId,
159
+ label: readString(record.label) ?? `${adapterId} #${index + 1}`,
160
+ role: readAgentRole(record.role),
161
+ instruction: readString(record.instruction),
162
+ model: readString(record.model),
163
+ permissionMode: readString(record.permissionMode),
164
+ toolsSettings: readRecord(record.toolsSettings),
165
+ order: index,
166
+ };
167
+ })
168
+ .filter((value): value is AgentAssignment => Boolean(value))
169
+ .slice(0, 16);
170
+ }
171
+
172
+ function readAgentAssignments(metadata?: Record<string, unknown>): AgentAssignment[] {
173
+ const agents = readMetadataAgents(metadata);
174
+ if (agents.length > 0) return agents;
175
+
176
+ return readLegacyEnabledAdapters(metadata).map((adapterId, index) => ({
177
+ instanceId: `${adapterId}-${index + 1}`,
178
+ adapterId,
179
+ label: `${adapterId} #${index + 1}`,
180
+ order: index,
181
+ }));
182
+ }
183
+
184
+ function readEnabledAdapters(metadata?: Record<string, unknown>): string[] {
185
+ return [...new Set(readAgentAssignments(metadata).map((agent) => agent.adapterId))];
186
+ }
187
+
188
+ function readMaxParallelAgents(metadata?: Record<string, unknown>): number {
189
+ const settings = getMetadataRecord(metadata, 'settings');
190
+ const value = settings.maxParallelAgents;
191
+ return typeof value === 'number' && Number.isFinite(value)
192
+ ? Math.max(1, Math.min(12, Math.round(value)))
193
+ : 3;
194
+ }
195
+
196
+ function readMaxRepairCycles(metadata?: Record<string, unknown>): number {
197
+ const settings = getMetadataRecord(metadata, 'settings');
198
+ const value = settings.maxRepairCycles;
199
+ return typeof value === 'number' && Number.isFinite(value)
200
+ ? Math.max(0, Math.min(MAX_REPAIR_CYCLES, Math.round(value)))
201
+ : DEFAULT_MAX_REPAIR_CYCLES;
202
+ }
203
+
204
+ function safeNodeId(adapterId: string, suffix: string): string {
205
+ return `${adapterId.replace(/[^a-zA-Z0-9_]+/g, '_')}_${suffix}`;
206
+ }
207
+
208
+ function safeAgentNodeId(agent: AgentAssignment, index: number, suffix: string): string {
209
+ return `agent_${index + 1}_${safeNodeId(agent.adapterId, suffix)}`;
210
+ }
211
+
212
+ function agentRoster(agents: AgentAssignment[]): string {
213
+ return agents
214
+ .map((agent, index) => {
215
+ const instruction = agent.instruction
216
+ ? `\n User assignment: ${agent.instruction}`
217
+ : '';
218
+ const role = agent.role ? `\n API role: ${agent.role}` : '';
219
+ return `${index + 1}. ${agent.label} (${agent.adapterId})${role}${instruction}`;
220
+ })
221
+ .join('\n');
222
+ }
223
+
224
+ function inferAgentRole(agent: AgentAssignment): AgentRole {
225
+ if (isKnownAgentRole(agent.role)) return agent.role;
226
+
227
+ const text = `${agent.label} ${agent.adapterId} ${agent.role ?? ''} ${agent.instruction ?? ''}`.toLocaleLowerCase('tr');
228
+ if (/(test|tester|qa|review|code review|hata|kontrol|onay|incele|doğrula|dogrula)/u.test(text)) {
229
+ return 'review';
230
+ }
231
+ if (/(backend|back-end|api|server|veri|database|db|fapi|endpoint|websocket|ws)/u.test(text)) {
232
+ return 'backend';
233
+ }
234
+ if (/(frontend|front-end|ui|ux|tailwind|tasarım|tasarim|design|chart|tradingview|arayüz|arayuz)/u.test(text)) {
235
+ return 'frontend';
236
+ }
237
+ return 'implementation';
238
+ }
239
+
240
+ function inferImplementationRole(agent: AgentAssignment): 'backend' | 'frontend' | 'review' | 'implementation' {
241
+ const role = inferAgentRole(agent);
242
+ return role === 'backend' || role === 'frontend' || role === 'review' || role === 'implementation'
243
+ ? role
244
+ : 'implementation';
245
+ }
246
+
247
+ function displayStage(agent: AgentAssignment, fallback: AgentRole): string {
248
+ return agent.role && !isKnownAgentRole(agent.role) ? agent.role : fallback;
249
+ }
250
+
251
+ function rolePrompt(role: AgentRole): string {
252
+ if (role === 'backend') {
253
+ return 'Backend/API/data work should define stable contracts first. Report endpoints, payload shapes, ports, and any data-source limitations clearly for downstream agents.';
254
+ }
255
+ if (role === 'frontend') {
256
+ return 'Frontend/UI work must use prior backend/data-contract outputs when present. If a dependency is missing, use a minimal mock only as a temporary fallback and report the blocker.';
257
+ }
258
+ if (role === 'review') {
259
+ return 'You are the validation/review stage. Inspect the prior agent outputs and actual project state. Approve only if it works; otherwise return a concrete bug list and required fixes.';
260
+ }
261
+ if (role === 'proposal') {
262
+ return 'You are in the proposal stage. Produce a concrete option with tradeoffs, assumptions, and what should happen next. Do not edit files.';
263
+ }
264
+ if (role === 'critique') {
265
+ return 'You are in the critique stage. Challenge the proposal for risks, missing constraints, and weak assumptions. Do not edit files.';
266
+ }
267
+ if (role === 'response') {
268
+ return 'You are in the response stage. Reconcile the critique with the proposal and refine the practical path forward. Do not edit files.';
269
+ }
270
+ if (role === 'decision' || role === 'report') {
271
+ return 'You are the reporting stage. Produce the final concise decision report and a next prompt for launching an implementation agent team. Do not edit files.';
272
+ }
273
+ if (role !== 'implementation') {
274
+ return `You are assigned to the custom stage "${role}". Follow that user-defined stage literally, avoid duplicating other agents, and report changed files, commands, blockers, and next actions.`;
275
+ }
276
+ return 'Implementation work should avoid duplicating other agents and should report changed files, commands, blockers, and next actions.';
277
+ }
278
+
279
+ function handoffPrompt(agent: AgentAssignment, role: AgentRole): string {
280
+ return [
281
+ `You are ${agent.label} in a Pixcode CLI team.`,
282
+ `Your inferred stage is: ${role}.`,
283
+ 'This is a bounded A2A handoff task, not the full implementation.',
284
+ 'Read the original user goal and coordinator plan, then publish a compact contract for downstream agents.',
285
+ agent.instruction ? `Your explicit assignment from the user is: ${agent.instruction}` : '',
286
+ 'Output only the handoff contract:',
287
+ '- owned scope',
288
+ '- files/modules you expect to touch',
289
+ '- API/data contracts, ports, payload shapes, and limitations',
290
+ '- dependencies/blockers for the next agents',
291
+ '- concrete next action for your full implementation task',
292
+ 'Do not install dependencies, edit files, run long commands, or start servers in this handoff task.',
293
+ 'Stop after the contract. Keep it concise and respond in the same language as the user request.',
294
+ ].filter(Boolean).join('\n');
295
+ }
296
+
297
+ function compactOutputForContext(text: string): string {
298
+ if (text.length <= MAX_OUTPUT_CONTEXT_CHARS) {
299
+ return text;
300
+ }
301
+
302
+ const edge = Math.floor(MAX_OUTPUT_CONTEXT_CHARS / 2);
303
+ return [
304
+ text.slice(0, edge),
305
+ `\n\n[...${text.length - MAX_OUTPUT_CONTEXT_CHARS} characters omitted from prior agent output...]\n\n`,
306
+ text.slice(-edge),
307
+ ].join('');
308
+ }
309
+
310
+ function expandAgentTeamWorkflow(workflow: Workflow, metadata?: Record<string, unknown>): Workflow {
311
+ const agents = readAgentAssignments(metadata);
312
+ if (agents.length === 0) {
313
+ throw new Error('Select at least one CLI agent.');
314
+ }
315
+
316
+ const coordinator = agents.find((agent) => agent.adapterId === 'claude-code') ?? agents[0];
317
+ const roster = agentRoster(agents);
318
+ const workerSpecs = agents.map((agent, index) => ({
319
+ agent,
320
+ role: inferImplementationRole(agent),
321
+ stage: displayStage(agent, inferImplementationRole(agent)),
322
+ nodeId: safeAgentNodeId(agent, index, 'work'),
323
+ handoffNodeId: safeAgentNodeId(agent, index, 'handoff'),
324
+ }));
325
+ const backendHandoffNodeIds = workerSpecs
326
+ .filter((spec) => spec.role === 'backend')
327
+ .map((spec) => spec.handoffNodeId);
328
+ const implementationNodeIds = workerSpecs
329
+ .filter((spec) => spec.role !== 'review')
330
+ .map((spec) => spec.nodeId);
331
+ const handoffNodes: WorkflowNode[] = workerSpecs
332
+ .filter((spec) => spec.role === 'backend')
333
+ .map(({ agent, role, handoffNodeId }) => ({
334
+ id: handoffNodeId,
335
+ adapterId: agent.adapterId,
336
+ agentInstanceId: agent.instanceId,
337
+ agentLabel: `${agent.label} Handoff`,
338
+ assignment: agent.instruction,
339
+ stage: 'handoff',
340
+ model: agent.model,
341
+ permissionMode: agent.permissionMode,
342
+ toolsSettings: agent.toolsSettings,
343
+ prompt: handoffPrompt(agent, role),
344
+ inputs: ['coordinator'],
345
+ output: 'message',
346
+ onFail: 'continue',
347
+ timeoutMs: BACKEND_HANDOFF_TIMEOUT_MS,
348
+ }));
349
+ const workerNodes: WorkflowNode[] = workerSpecs.map(({ agent, role, stage, nodeId, handoffNodeId }) => {
350
+ const inputs = role === 'review'
351
+ ? (implementationNodeIds.length > 0 ? implementationNodeIds : ['coordinator'])
352
+ : role === 'frontend' && backendHandoffNodeIds.length > 0
353
+ ? ['coordinator', ...backendHandoffNodeIds]
354
+ : role === 'backend'
355
+ ? ['coordinator', handoffNodeId]
356
+ : ['coordinator'];
357
+
358
+ return {
359
+ id: nodeId,
360
+ adapterId: agent.adapterId,
361
+ agentInstanceId: agent.instanceId,
362
+ agentLabel: agent.label,
363
+ assignment: agent.instruction,
364
+ stage,
365
+ model: agent.model,
366
+ permissionMode: agent.permissionMode,
367
+ toolsSettings: agent.toolsSettings,
368
+ prompt: [
369
+ `You are ${agent.label} in a Pixcode CLI team.`,
370
+ `Your stage is: ${stage}.`,
371
+ stage !== role ? `Runtime routing category: ${role}.` : '',
372
+ 'The coordinator plan and any dependency outputs are included above. Use them together with the original user goal.',
373
+ agent.instruction
374
+ ? `Your explicit assignment from the user is: ${agent.instruction}`
375
+ : 'No fixed per-agent assignment was set. Take the part assigned to you by the coordinator; if none is named, choose useful work that fits this CLI.',
376
+ rolePrompt(stage),
377
+ 'Respond in the same language as the user request.',
378
+ ].filter(Boolean).join('\n'),
379
+ inputs,
380
+ output: 'both',
381
+ onFail: 'continue',
382
+ };
383
+ });
384
+
385
+ return {
386
+ ...workflow,
387
+ nodes: [
388
+ {
389
+ id: 'coordinator',
390
+ adapterId: coordinator.adapterId,
391
+ agentInstanceId: coordinator.instanceId,
392
+ agentLabel: coordinator.label,
393
+ stage: 'coordinator',
394
+ model: coordinator.model,
395
+ permissionMode: coordinator.permissionMode,
396
+ toolsSettings: coordinator.toolsSettings,
397
+ prompt: [
398
+ 'You are the coordinator for a Pixcode CLI agent team.',
399
+ 'Read the user goal, active CLI roster, and any per-agent assignments. Create a compact execution plan for the selected agents.',
400
+ 'If the user directly names a CLI, honor that. Do not invent permanent roles; assign work only from the goal, active agents, and explicit assignment text.',
401
+ `Active roster:\n${roster}`,
402
+ 'Respond in the same language as the user request.',
403
+ ].join('\n'),
404
+ inputs: [],
405
+ output: 'message',
406
+ onFail: 'abort',
407
+ },
408
+ ...handoffNodes,
409
+ ...workerNodes,
410
+ {
411
+ id: 'final_report',
412
+ adapterId: coordinator.adapterId,
413
+ agentInstanceId: coordinator.instanceId,
414
+ agentLabel: coordinator.label,
415
+ stage: 'final_report',
416
+ model: coordinator.model,
417
+ permissionMode: coordinator.permissionMode,
418
+ toolsSettings: coordinator.toolsSettings,
419
+ prompt: [
420
+ 'Collect the worker outputs into one user-facing result.',
421
+ 'Show what each CLI did, which parts failed, what changed, and the next action if work remains.',
422
+ 'Respond in the same language as the user request.',
423
+ ].join('\n'),
424
+ inputs: workerNodes.map((node) => node.id),
425
+ output: 'message',
426
+ onFail: 'abort',
427
+ },
428
+ ],
429
+ };
430
+ }
431
+
432
+ function stagePrompt(agent: AgentAssignment, stage: AgentRole): string {
433
+ return [
434
+ `You are ${agent.label} in a Pixcode decision workflow.`,
435
+ `Your stage is: ${stage}.`,
436
+ agent.role && agent.role !== stage ? `User custom stage label: ${agent.role}.` : '',
437
+ agent.instruction ? `User assignment for you: ${agent.instruction}` : '',
438
+ rolePrompt(stage),
439
+ 'Keep the answer concise, structured, and useful for the next stage.',
440
+ 'Respond in the same language as the user request.',
441
+ ].filter(Boolean).join('\n');
442
+ }
443
+
444
+ function agentsWithRole(agents: AgentAssignment[], role: AgentRole): AgentAssignment[] {
445
+ return agents.filter((agent) => agent.role === role);
446
+ }
447
+
448
+ function autoAssignDebateAgents(agents: AgentAssignment[]): {
449
+ proposalAgents: AgentAssignment[];
450
+ critiqueAgents: AgentAssignment[];
451
+ responseAgents: AgentAssignment[];
452
+ reportAgent: AgentAssignment;
453
+ } {
454
+ const assigned = new Set<string>();
455
+ const markAssigned = (items: AgentAssignment[]) => {
456
+ for (const item of items) assigned.add(item.instanceId);
457
+ };
458
+ const pickNext = () =>
459
+ agents.find((agent) => !assigned.has(agent.instanceId) && agent.role !== 'decision' && agent.role !== 'report')
460
+ ?? agents.find((agent) => !assigned.has(agent.instanceId))
461
+ ?? agents[0];
462
+
463
+ const proposalAgents = agentsWithRole(agents, 'proposal');
464
+ if (proposalAgents.length === 0) proposalAgents.push(pickNext());
465
+ markAssigned(proposalAgents);
466
+
467
+ const critiqueAgents = agentsWithRole(agents, 'critique');
468
+ if (critiqueAgents.length === 0) critiqueAgents.push(pickNext());
469
+ markAssigned(critiqueAgents);
470
+
471
+ const responseAgents = agentsWithRole(agents, 'response');
472
+ if (responseAgents.length === 0 && agents.length > 2) {
473
+ responseAgents.push(...agents.filter((agent) =>
474
+ !assigned.has(agent.instanceId) && agent.role !== 'decision' && agent.role !== 'report',
475
+ ));
476
+ }
477
+ markAssigned(responseAgents);
478
+
479
+ const reportAgent = agentsWithRole(agents, 'decision')[0]
480
+ ?? agentsWithRole(agents, 'report')[0]
481
+ ?? agents[0];
482
+
483
+ return { proposalAgents, critiqueAgents, responseAgents, reportAgent };
484
+ }
485
+
486
+ function expandAdversarialDebateWorkflow(workflow: Workflow, metadata?: Record<string, unknown>): Workflow {
487
+ const agents = readAgentAssignments(metadata);
488
+ if (agents.length === 0) {
489
+ throw new Error('Select at least one CLI agent.');
490
+ }
491
+
492
+ const {
493
+ proposalAgents,
494
+ critiqueAgents,
495
+ responseAgents,
496
+ reportAgent,
497
+ } = autoAssignDebateAgents(agents);
498
+
499
+ const proposalNodes: WorkflowNode[] = proposalAgents.map((agent, index) => ({
500
+ id: safeAgentNodeId(agent, index, 'proposal'),
501
+ adapterId: agent.adapterId,
502
+ agentInstanceId: agent.instanceId,
503
+ agentLabel: agent.label,
504
+ assignment: agent.instruction || 'Proposal stage',
505
+ stage: 'proposal',
506
+ model: agent.model,
507
+ permissionMode: agent.permissionMode,
508
+ toolsSettings: agent.toolsSettings,
509
+ prompt: stagePrompt(agent, 'proposal'),
510
+ inputs: [],
511
+ output: 'message',
512
+ onFail: 'continue',
513
+ }));
514
+ const critiqueNodes: WorkflowNode[] = critiqueAgents.map((agent, index) => ({
515
+ id: safeAgentNodeId(agent, index, 'critique'),
516
+ adapterId: agent.adapterId,
517
+ agentInstanceId: agent.instanceId,
518
+ agentLabel: agent.label,
519
+ assignment: agent.instruction || 'Critique stage',
520
+ stage: 'critique',
521
+ model: agent.model,
522
+ permissionMode: agent.permissionMode,
523
+ toolsSettings: agent.toolsSettings,
524
+ prompt: stagePrompt(agent, 'critique'),
525
+ inputs: proposalNodes.map((node) => node.id),
526
+ output: 'message',
527
+ onFail: 'continue',
528
+ }));
529
+ const responseNodes: WorkflowNode[] = responseAgents.map((agent, index) => ({
530
+ id: safeAgentNodeId(agent, index, 'response'),
531
+ adapterId: agent.adapterId,
532
+ agentInstanceId: agent.instanceId,
533
+ agentLabel: agent.label,
534
+ assignment: agent.instruction || 'Response stage',
535
+ stage: 'response',
536
+ model: agent.model,
537
+ permissionMode: agent.permissionMode,
538
+ toolsSettings: agent.toolsSettings,
539
+ prompt: stagePrompt(agent, 'response'),
540
+ inputs: critiqueNodes.map((node) => node.id),
541
+ output: 'message',
542
+ onFail: 'continue',
543
+ }));
544
+ const finalInputs = responseNodes.length > 0
545
+ ? responseNodes.map((node) => node.id)
546
+ : critiqueNodes.map((node) => node.id);
547
+
548
+ return {
549
+ ...workflow,
550
+ nodes: [
551
+ ...proposalNodes,
552
+ ...critiqueNodes,
553
+ ...responseNodes,
554
+ {
555
+ id: 'final_report',
556
+ adapterId: reportAgent.adapterId,
557
+ agentInstanceId: reportAgent.instanceId,
558
+ agentLabel: reportAgent.label,
559
+ assignment: reportAgent.instruction || 'Final decision report',
560
+ stage: 'final_report',
561
+ model: reportAgent.model,
562
+ permissionMode: reportAgent.permissionMode,
563
+ toolsSettings: reportAgent.toolsSettings,
564
+ prompt: [
565
+ 'Produce the final decision report from the debate.',
566
+ 'Use this exact structure:',
567
+ '1. Short decision',
568
+ '2. Why',
569
+ '3. Risks',
570
+ '4. Suggested next prompt',
571
+ '5. Proposed agent team and assignments',
572
+ 'The next prompt should be ready to paste into Pixcode Agent Team mode.',
573
+ 'Do not edit files. Respond in the same language as the user request.',
574
+ ].join('\n'),
575
+ inputs: finalInputs,
576
+ output: 'message',
577
+ onFail: 'abort',
578
+ },
579
+ ],
580
+ };
581
+ }
582
+
583
+ function expandSequentialHandoffWorkflow(workflow: Workflow, metadata?: Record<string, unknown>): Workflow {
584
+ const agents = readAgentAssignments(metadata);
585
+ if (agents.length === 0) {
586
+ throw new Error('Select at least one CLI agent.');
587
+ }
588
+
589
+ return {
590
+ ...workflow,
591
+ nodes: agents.map((agent, index): WorkflowNode => ({
592
+ id: safeAgentNodeId(agent, index, 'handoff'),
593
+ adapterId: agent.adapterId,
594
+ agentInstanceId: agent.instanceId,
595
+ agentLabel: agent.label,
596
+ assignment: agent.instruction,
597
+ stage: agent.role ?? 'implementation',
598
+ model: agent.model,
599
+ permissionMode: agent.permissionMode,
600
+ toolsSettings: agent.toolsSettings,
601
+ prompt: [
602
+ `You are ${agent.label} in a sequential Pixcode handoff.`,
603
+ `This is step ${index + 1} of ${agents.length}.`,
604
+ agent.instruction
605
+ ? `Your explicit assignment from the user is: ${agent.instruction}`
606
+ : 'Use the prior step output and do the next most useful handoff step for the user goal.',
607
+ 'Report changed files, commands, blockers, and the next handoff requirement.',
608
+ 'Respond in the same language as the user request.',
609
+ ].filter(Boolean).join('\n'),
610
+ inputs: index === 0 ? [] : [safeAgentNodeId(agents[index - 1], index - 1, 'handoff')],
611
+ output: 'both',
612
+ onFail: 'abort',
613
+ })),
614
+ };
615
+ }
616
+
617
+ function expandWorkflowForRun(workflow: Workflow, metadata?: Record<string, unknown>): Workflow {
618
+ if (workflow.id === 'agent_team') {
619
+ return expandAgentTeamWorkflow(workflow, metadata);
620
+ }
621
+
622
+ const agents = readAgentAssignments(metadata);
623
+ if (workflow.id === 'adversarial_debate') {
624
+ return expandAdversarialDebateWorkflow(workflow, metadata);
625
+ }
626
+ if (workflow.id === 'sequential_handoff') {
627
+ return expandSequentialHandoffWorkflow(workflow, metadata);
628
+ }
629
+ if (workflow.id !== 'multi_model_review' || agents.length === 0) {
630
+ return workflow;
631
+ }
632
+
633
+ const reportAgent = agentsWithRole(agents, 'report')[0] ?? agentsWithRole(agents, 'decision')[0] ?? agents[0];
634
+ const reviewAgents = agents.filter((agent) => agent.instanceId !== reportAgent.instanceId || agents.length === 1);
635
+ const reviewNodes: WorkflowNode[] = reviewAgents.map((agent, index) => ({
636
+ id: safeAgentNodeId(agent, index, 'review'),
637
+ adapterId: agent.adapterId,
638
+ agentInstanceId: agent.instanceId,
639
+ agentLabel: agent.label,
640
+ assignment: agent.instruction,
641
+ stage: 'review',
642
+ model: agent.model,
643
+ permissionMode: agent.permissionMode,
644
+ toolsSettings: agent.toolsSettings,
645
+ prompt: [
646
+ `You are ${agent.label}.`,
647
+ 'Review the requested change for bugs, regressions, missing validation, security, scale, and user-experience risks.',
648
+ agent.instruction ? `Focus on this user assignment: ${agent.instruction}` : '',
649
+ 'Respond in the same language as the user request.',
650
+ ].filter(Boolean).join('\n'),
651
+ inputs: [],
652
+ output: 'both',
653
+ onFail: 'continue',
654
+ }));
655
+
656
+ return {
657
+ ...workflow,
658
+ nodes: [
659
+ ...reviewNodes,
660
+ {
661
+ id: 'aggregate',
662
+ adapterId: reportAgent.adapterId,
663
+ agentInstanceId: reportAgent.instanceId,
664
+ agentLabel: reportAgent.label,
665
+ stage: 'report',
666
+ model: reportAgent.model,
667
+ permissionMode: reportAgent.permissionMode,
668
+ toolsSettings: reportAgent.toolsSettings,
669
+ prompt: 'Aggregate the prior agent reviews into a concise prioritized report. Respond in the same language as the user request.',
670
+ inputs: reviewNodes.map((node) => node.id),
671
+ output: 'message',
672
+ onFail: 'abort',
673
+ },
674
+ ],
675
+ };
676
+ }
677
+
678
+ async function cancelA2ATask(taskId: string): Promise<void> {
679
+ await fetch(`${localA2ABaseUrl()}/tasks/${taskId}/cancel`, { method: 'POST' }).catch(() => undefined);
680
+ }
681
+
682
+ function readTaskResult(task: RawTask): TaskResult {
683
+ const messages = (task.history ?? []).map((message) => ({
684
+ role: typeof message.role === 'string' ? message.role : 'agent',
685
+ text: (message.parts ?? [])
686
+ .filter((part) => part.kind === 'text' && typeof part.text === 'string')
687
+ .map((part) => part.text)
688
+ .join('\n'),
689
+ })).filter((message) => message.text.trim());
690
+ const artifacts = (task.artifacts ?? []).map((artifact) => {
691
+ const text = (artifact.parts ?? [])
692
+ .filter((part) => part.kind === 'text' && typeof part.text === 'string')
693
+ .map((part) => part.text)
694
+ .join('\n');
695
+ const data = (artifact.parts ?? []).find((part) => part.kind === 'data')?.data;
696
+ return {
697
+ type: artifact.type ?? 'data',
698
+ text: text || undefined,
699
+ data,
700
+ metadata: artifact.metadata,
701
+ };
702
+ });
703
+ const outputMessages = messages.filter((message) => message.role !== 'user');
704
+ const text = outputMessages.map((message) => `${message.role}: ${message.text}`).join('\n\n');
705
+ const error = task.error?.message
706
+ ? `${task.error.code ? `${task.error.code}: ` : ''}${task.error.message}`
707
+ : undefined;
708
+ return {
709
+ state: task.state ?? 'submitted',
710
+ text,
711
+ error,
712
+ messages,
713
+ artifacts,
714
+ };
715
+ }
716
+
717
+ async function waitForTask(
718
+ taskId: string,
719
+ shouldCancel?: () => boolean,
720
+ onSnapshot?: (result: TaskResult) => void,
721
+ timeoutMs?: number,
722
+ ): Promise<TaskResult> {
723
+ const timeout = timeoutMs && timeoutMs > 0 ? timeoutMs : undefined;
724
+ const deadline = timeout ? Date.now() + timeout : undefined;
725
+ for (;;) {
726
+ if (shouldCancel?.()) {
727
+ throw new WorkflowCanceledError();
728
+ }
729
+ if (deadline && Date.now() >= deadline) {
730
+ throw new WorkflowNodeTimeoutError(timeout ?? 0);
731
+ }
732
+ const response = await fetch(`${localA2ABaseUrl()}/tasks/${taskId}`);
733
+ const task = await response.json() as RawTask;
734
+ const snapshot = readTaskResult(task);
735
+ onSnapshot?.(snapshot);
736
+ if (task.state && TERMINAL.has(task.state)) {
737
+ return snapshot;
738
+ }
739
+ await new Promise((resolve) => setTimeout(resolve, 1000));
740
+ }
741
+ }
742
+
743
+ function readyNodes(workflow: Workflow, completed: Set<string>, started: Set<string>): WorkflowNode[] {
744
+ return workflow.nodes.filter((node) =>
745
+ !started.has(node.id) && node.inputs.every((input) => completed.has(input)),
746
+ );
747
+ }
748
+
749
+ function nodeRunFromNode(node: WorkflowNode): WorkflowNodeRun {
750
+ return {
751
+ nodeId: node.id,
752
+ adapterId: node.adapterId,
753
+ agentInstanceId: node.agentInstanceId,
754
+ agentLabel: node.agentLabel,
755
+ assignment: node.assignment,
756
+ model: node.model,
757
+ permissionMode: node.permissionMode,
758
+ timeoutMs: node.timeoutMs,
759
+ stage: node.stage,
760
+ status: 'queued',
761
+ };
762
+ }
763
+
764
+ function uniqueInputs(inputs: string[]): string[] {
765
+ return [...new Set(inputs.filter(Boolean))];
766
+ }
767
+
768
+ function isReviewNode(node: WorkflowNode): boolean {
769
+ return node.stage === 'review';
770
+ }
771
+
772
+ function isImplementationNode(node: WorkflowNode): boolean {
773
+ return node.stage === 'backend' || node.stage === 'frontend' || node.stage === 'implementation' || node.stage === 'repair';
774
+ }
775
+
776
+ function reviewRequiresRepair(text: string): boolean {
777
+ const normalized = text.toLocaleLowerCase('tr').replace(/\s+/g, ' ').trim();
778
+ if (!normalized) return false;
779
+
780
+ const approvalPatterns = [
781
+ /hata yok/u,
782
+ /sorun yok/u,
783
+ /problem yok/u,
784
+ /bulgu yok/u,
785
+ /kritik bulgu yok/u,
786
+ /temiz/u,
787
+ /onaylı/u,
788
+ /onayli/u,
789
+ /approved/u,
790
+ /lgtm/u,
791
+ /no issues/u,
792
+ /no findings/u,
793
+ /looks good/u,
794
+ /pass(?:ed)?/u,
795
+ ];
796
+ const actionableText = approvalPatterns.reduce((current, pattern) => current.replace(pattern, ' '), normalized);
797
+ const issuePatterns = [
798
+ /hata/u,
799
+ /bug/u,
800
+ /kritik/u,
801
+ /critical/u,
802
+ /blocker/u,
803
+ /regression/u,
804
+ /failed/u,
805
+ /failure/u,
806
+ /fail/u,
807
+ /eksik/u,
808
+ /düzelt/u,
809
+ /duzelt/u,
810
+ /fix required/u,
811
+ /needs fix/u,
812
+ /sorun/u,
813
+ /risk/u,
814
+ /güvenlik/u,
815
+ /guvenlik/u,
816
+ /security/u,
817
+ /çalışmıyor/u,
818
+ /calismiyor/u,
819
+ ];
820
+
821
+ return issuePatterns.some((pattern) => pattern.test(actionableText));
822
+ }
823
+
824
+ function findRepairFixer(workflow: Workflow, reviewNode: WorkflowNode): WorkflowNode | undefined {
825
+ return reviewNode.inputs
826
+ .map((input) => workflow.nodes.find((node) => node.id === input))
827
+ .find((node): node is WorkflowNode => Boolean(node && isImplementationNode(node)))
828
+ ?? workflow.nodes.find((node) => isImplementationNode(node))
829
+ ?? workflow.nodes.find((node) => node.stage === 'coordinator');
830
+ }
831
+
832
+ class WorkflowRunner {
833
+ private readonly cancelingRuns = new Set<string>();
834
+
835
+ preview(workflow: Workflow, metadata?: Record<string, unknown>): Workflow {
836
+ const runtimeWorkflow = expandWorkflowForRun(workflow, metadata);
837
+ validateWorkflow(runtimeWorkflow);
838
+ return runtimeWorkflow;
839
+ }
840
+
841
+ start(workflow: Workflow, input = '', metadata?: Record<string, unknown>): WorkflowRun {
842
+ const runtimeWorkflow = expandWorkflowForRun(workflow, metadata);
843
+ validateWorkflow(runtimeWorkflow);
844
+ const workspaceTarget = resolveWorkflowWorkspace(metadata);
845
+ const runMetadata = {
846
+ ...metadata,
847
+ projectPath: workspaceTarget.projectPath,
848
+ selectedProjectPath: workspaceTarget.selectedProjectPath,
849
+ workspaceTarget: workspaceTargetMetadata(workspaceTarget),
850
+ };
851
+ const run: WorkflowRun = {
852
+ id: newId('wrun'),
853
+ workflowId: runtimeWorkflow.id,
854
+ contextId: newId('ctx'),
855
+ status: 'queued',
856
+ input,
857
+ nodeRuns: runtimeWorkflow.nodes.map(nodeRunFromNode),
858
+ startedAt: Date.now(),
859
+ metadata: runMetadata,
860
+ };
861
+ workflowStore.setRun(run);
862
+ void this.execute(runtimeWorkflow, run);
863
+ return run;
864
+ }
865
+
866
+ async cancel(runId: string): Promise<WorkflowRun | undefined> {
867
+ const run = workflowStore.getRun(runId);
868
+ if (!run) return undefined;
869
+ if (TERMINAL.has(run.status)) return run;
870
+
871
+ this.cancelingRuns.add(run.id);
872
+ const taskIds = run.nodeRuns
873
+ .filter((node) => node.a2aTaskId && (node.status === 'running' || node.status === 'queued'))
874
+ .map((node) => node.a2aTaskId as string);
875
+
876
+ this.markCanceled(run);
877
+ workflowStore.setRun(run);
878
+
879
+ await Promise.all(taskIds.map((taskId) => cancelA2ATask(taskId)));
880
+
881
+ return workflowStore.getRun(run.id) ?? run;
882
+ }
883
+
884
+ private isCanceling(runId: string): boolean {
885
+ return this.cancelingRuns.has(runId) || workflowStore.getRun(runId)?.status === 'canceled';
886
+ }
887
+
888
+ private markCanceled(run: WorkflowRun): void {
889
+ run.status = 'canceled';
890
+ run.finishedAt = run.finishedAt ?? Date.now();
891
+ for (const nodeRun of run.nodeRuns) {
892
+ if (!TERMINAL.has(nodeRun.status) && nodeRun.status !== SKIPPED) {
893
+ nodeRun.status = 'canceled';
894
+ nodeRun.finishedAt = nodeRun.finishedAt ?? Date.now();
895
+ }
896
+ }
897
+ }
898
+
899
+ private maybeAddRepairCycle(
900
+ node: WorkflowNode,
901
+ workflow: Workflow,
902
+ run: WorkflowRun,
903
+ result: TaskResult,
904
+ ): void {
905
+ if (workflow.id !== 'agent_team') return;
906
+ if (!isReviewNode(node) || node.id.startsWith('repair_') || node.id.startsWith('recheck_')) return;
907
+ if (!reviewRequiresRepair(`${result.text}\n${result.error ?? ''}`)) return;
908
+
909
+ const maxRepairCycles = readMaxRepairCycles(run.metadata);
910
+ if (maxRepairCycles <= 0) return;
911
+
912
+ const existingCycles = workflow.nodes.filter((candidate) => candidate.id.startsWith(`repair_${node.id}_`)).length;
913
+ if (existingCycles >= maxRepairCycles) return;
914
+
915
+ if (workflow.nodes.length + 2 > 64) {
916
+ run.metadata = {
917
+ ...run.metadata,
918
+ dynamicRepairSkipped: `Workflow node limit reached after ${node.id}.`,
919
+ };
920
+ workflowStore.setRun(run);
921
+ return;
922
+ }
923
+
924
+ const fixer = findRepairFixer(workflow, node);
925
+ if (!fixer || fixer.id === node.id) return;
926
+
927
+ const cycle = existingCycles + 1;
928
+ const repairNode: WorkflowNode = {
929
+ id: `repair_${node.id}_${cycle}`,
930
+ adapterId: fixer.adapterId,
931
+ agentInstanceId: fixer.agentInstanceId,
932
+ agentLabel: fixer.agentLabel ? `${fixer.agentLabel} Repair` : undefined,
933
+ assignment: `Automatic repair from ${node.agentLabel || node.id} review findings`,
934
+ stage: 'repair',
935
+ model: fixer.model,
936
+ permissionMode: fixer.permissionMode,
937
+ toolsSettings: fixer.toolsSettings,
938
+ prompt: [
939
+ 'A review stage found actionable issues in the prior work.',
940
+ 'Use the original user goal, prior implementation outputs, and review output included above.',
941
+ 'Fix only the reported issues; do not restart the whole project or duplicate unrelated work.',
942
+ 'Report changed files, commands, verification, and any remaining blockers.',
943
+ 'Respond in the same language as the user request.',
944
+ ].join('\n'),
945
+ inputs: uniqueInputs([...node.inputs, fixer.id, node.id]),
946
+ output: 'both',
947
+ onFail: 'continue',
948
+ };
949
+ const recheckNode: WorkflowNode = {
950
+ id: `recheck_${node.id}_${cycle}`,
951
+ adapterId: node.adapterId,
952
+ agentInstanceId: node.agentInstanceId,
953
+ agentLabel: node.agentLabel ? `${node.agentLabel} Recheck` : undefined,
954
+ assignment: 'Automatic validation after repair',
955
+ stage: 'review',
956
+ model: node.model,
957
+ permissionMode: node.permissionMode,
958
+ toolsSettings: node.toolsSettings,
959
+ prompt: [
960
+ 'Validate the automatic repair against the original review findings.',
961
+ 'Approve only if the reported issues are fixed.',
962
+ 'If anything remains, list the remaining blockers clearly and do not invent new unrelated scope.',
963
+ 'Respond in the same language as the user request.',
964
+ ].join('\n'),
965
+ inputs: uniqueInputs([node.id, repairNode.id]),
966
+ output: 'message',
967
+ onFail: 'continue',
968
+ };
969
+
970
+ const finalIndex = workflow.nodes.findIndex((candidate) =>
971
+ candidate.id === 'final_report' || candidate.stage === 'final_report' || candidate.stage === 'report',
972
+ );
973
+ if (finalIndex >= 0) {
974
+ workflow.nodes.splice(finalIndex, 0, repairNode, recheckNode);
975
+ run.nodeRuns.splice(finalIndex, 0, nodeRunFromNode(repairNode), nodeRunFromNode(recheckNode));
976
+ } else {
977
+ workflow.nodes.push(repairNode, recheckNode);
978
+ run.nodeRuns.push(nodeRunFromNode(repairNode), nodeRunFromNode(recheckNode));
979
+ }
980
+
981
+ for (const finalNode of workflow.nodes) {
982
+ if (finalNode.id === 'final_report' || finalNode.stage === 'final_report' || finalNode.stage === 'report') {
983
+ finalNode.inputs = uniqueInputs([...finalNode.inputs, recheckNode.id]);
984
+ }
985
+ }
986
+
987
+ const repairCycles = Array.isArray(run.metadata?.dynamicRepairCycles)
988
+ ? run.metadata.dynamicRepairCycles
989
+ : [];
990
+ run.metadata = {
991
+ ...run.metadata,
992
+ dynamicRepairCycles: [
993
+ ...repairCycles,
994
+ {
995
+ reviewNodeId: node.id,
996
+ repairNodeId: repairNode.id,
997
+ recheckNodeId: recheckNode.id,
998
+ fixerNodeId: fixer.id,
999
+ },
1000
+ ],
1001
+ };
1002
+ workflowStore.setRun(run);
1003
+ }
1004
+
1005
+ private async execute(workflow: Workflow, run: WorkflowRun): Promise<void> {
1006
+ run.status = 'running';
1007
+ workflowStore.setRun(run);
1008
+ const completed = new Set<string>();
1009
+ const started = new Set<string>();
1010
+ const outputs = new Map<string, string>();
1011
+ const maxParallelAgents = readMaxParallelAgents(run.metadata);
1012
+
1013
+ try {
1014
+ while (completed.size < workflow.nodes.length) {
1015
+ if (this.isCanceling(run.id)) {
1016
+ throw new WorkflowCanceledError();
1017
+ }
1018
+ const batch = readyNodes(workflow, completed, started);
1019
+ if (batch.length === 0) {
1020
+ throw new Error('Workflow stalled; no ready nodes remain.');
1021
+ }
1022
+ for (let index = 0; index < batch.length; index += maxParallelAgents) {
1023
+ if (this.isCanceling(run.id)) {
1024
+ throw new WorkflowCanceledError();
1025
+ }
1026
+ const slice = batch.slice(index, index + maxParallelAgents);
1027
+ await Promise.all(slice.map((node) => this.executeNode(node, workflow, run, outputs, started, completed)));
1028
+ }
1029
+ }
1030
+ if (this.isCanceling(run.id)) {
1031
+ throw new WorkflowCanceledError();
1032
+ }
1033
+ run.status = 'completed';
1034
+ } catch (error) {
1035
+ if (error instanceof WorkflowCanceledError || this.isCanceling(run.id)) {
1036
+ this.markCanceled(run);
1037
+ } else {
1038
+ run.status = 'failed';
1039
+ run.metadata = {
1040
+ ...run.metadata,
1041
+ error: error instanceof Error ? error.message : String(error),
1042
+ };
1043
+ }
1044
+ } finally {
1045
+ run.finishedAt = run.finishedAt ?? Date.now();
1046
+ workflowStore.setRun(run);
1047
+ this.cancelingRuns.delete(run.id);
1048
+ }
1049
+ }
1050
+
1051
+ private async executeNode(
1052
+ node: WorkflowNode,
1053
+ workflow: Workflow,
1054
+ run: WorkflowRun,
1055
+ outputs: Map<string, string>,
1056
+ started: Set<string>,
1057
+ completed: Set<string>,
1058
+ ): Promise<void> {
1059
+ started.add(node.id);
1060
+ const nodeRun = run.nodeRuns.find((candidate) => candidate.nodeId === node.id) as WorkflowNodeRun;
1061
+ const enabledAdapters = readEnabledAdapters(run.metadata);
1062
+ if (enabledAdapters.length > 0 && !enabledAdapters.includes(node.adapterId)) {
1063
+ nodeRun.status = SKIPPED;
1064
+ nodeRun.finishedAt = Date.now();
1065
+ completed.add(node.id);
1066
+ workflowStore.setRun(run);
1067
+ return;
1068
+ }
1069
+ if (this.isCanceling(run.id)) {
1070
+ nodeRun.status = 'canceled';
1071
+ nodeRun.finishedAt = Date.now();
1072
+ workflowStore.setRun(run);
1073
+ throw new WorkflowCanceledError();
1074
+ }
1075
+
1076
+ nodeRun.status = 'running';
1077
+ nodeRun.startedAt = Date.now();
1078
+ workflowStore.setRun(run);
1079
+
1080
+ const inputContext = node.inputs.map((input) => outputs.get(input)).filter(Boolean).join('\n\n');
1081
+ const workspaceTarget = resolveWorkflowWorkspace(run.metadata);
1082
+ const prompt = [workspaceContextPrompt(workspaceTarget), run.input, inputContext, node.prompt]
1083
+ .filter(Boolean)
1084
+ .join('\n\n');
1085
+ const settings = getMetadataRecord(run.metadata, 'settings');
1086
+ const projectPath = workspaceTarget.projectPath;
1087
+ const isolation = readIsolation(settings.isolation) ?? node.isolation ?? 'host';
1088
+ const keepAfterCompletion = readBoolean(settings.keepWorkspace) ?? true;
1089
+ const baseRef = readString(settings.baseRef) ?? 'HEAD';
1090
+ const submit = await fetch(`${localA2ABaseUrl()}/tasks`, {
1091
+ method: 'POST',
1092
+ headers: { 'content-type': 'application/json' },
1093
+ body: JSON.stringify({
1094
+ adapterId: node.adapterId,
1095
+ contextId: run.contextId,
1096
+ message: {
1097
+ messageId: newId('msg'),
1098
+ role: 'user',
1099
+ parts: [{ kind: 'text', text: prompt }],
1100
+ },
1101
+ metadata: {
1102
+ workflowRunId: run.id,
1103
+ workflowNodeId: node.id,
1104
+ agentInstanceId: node.agentInstanceId,
1105
+ agentLabel: node.agentLabel,
1106
+ assignment: node.assignment,
1107
+ model: node.model,
1108
+ permissionMode: node.permissionMode,
1109
+ toolsSettings: node.toolsSettings,
1110
+ projectPath,
1111
+ workspaceTarget: workspaceTargetMetadata(workspaceTarget),
1112
+ workspace: {
1113
+ kind: isolation,
1114
+ projectPath,
1115
+ baseRef,
1116
+ keepAfterCompletion,
1117
+ },
1118
+ },
1119
+ }),
1120
+ });
1121
+ const body = await submit.json() as { id?: string; error?: { message?: string } };
1122
+ if (!submit.ok || !body.id) {
1123
+ throw new Error(body.error?.message ?? `Workflow node ${node.id} submit failed.`);
1124
+ }
1125
+ nodeRun.a2aTaskId = body.id;
1126
+ workflowStore.setRun(run);
1127
+
1128
+ if (this.isCanceling(run.id)) {
1129
+ await cancelA2ATask(body.id);
1130
+ nodeRun.status = 'canceled';
1131
+ nodeRun.finishedAt = Date.now();
1132
+ workflowStore.setRun(run);
1133
+ throw new WorkflowCanceledError();
1134
+ }
1135
+
1136
+ let result: TaskResult;
1137
+ try {
1138
+ result = await waitForTask(
1139
+ body.id,
1140
+ () => this.isCanceling(run.id),
1141
+ (snapshot) => {
1142
+ nodeRun.outputText = snapshot.text || nodeRun.outputText;
1143
+ nodeRun.messages = snapshot.messages;
1144
+ nodeRun.artifacts = snapshot.artifacts;
1145
+ nodeRun.error = snapshot.error;
1146
+ workflowStore.setRun(run);
1147
+ },
1148
+ node.timeoutMs,
1149
+ );
1150
+ } catch (error) {
1151
+ if (!(error instanceof WorkflowNodeTimeoutError)) {
1152
+ throw error;
1153
+ }
1154
+
1155
+ await cancelA2ATask(body.id);
1156
+ nodeRun.finishedAt = Date.now();
1157
+ nodeRun.status = 'failed';
1158
+ nodeRun.error = error.message;
1159
+ if (nodeRun.outputText) {
1160
+ outputs.set(node.id, compactOutputForContext(nodeRun.outputText));
1161
+ }
1162
+ workflowStore.setRun(run);
1163
+ if (node.onFail === 'continue') {
1164
+ completed.add(node.id);
1165
+ return;
1166
+ }
1167
+ throw error;
1168
+ }
1169
+ nodeRun.finishedAt = Date.now();
1170
+ nodeRun.outputText = result.text;
1171
+ nodeRun.messages = result.messages;
1172
+ nodeRun.artifacts = result.artifacts;
1173
+ if (this.isCanceling(run.id)) {
1174
+ nodeRun.status = 'canceled';
1175
+ workflowStore.setRun(run);
1176
+ throw new WorkflowCanceledError();
1177
+ }
1178
+ if (result.state === 'completed') {
1179
+ outputs.set(node.id, compactOutputForContext(result.text));
1180
+ completed.add(node.id);
1181
+ nodeRun.status = 'completed';
1182
+ workflowStore.setRun(run);
1183
+ this.maybeAddRepairCycle(node, workflow, run, result);
1184
+ return;
1185
+ }
1186
+ if (result.state === 'canceled') {
1187
+ nodeRun.status = 'canceled';
1188
+ workflowStore.setRun(run);
1189
+ throw new WorkflowCanceledError();
1190
+ }
1191
+
1192
+ nodeRun.status = 'failed';
1193
+ nodeRun.error = result.error ?? `A2A task ended with ${result.state}`;
1194
+ workflowStore.setRun(run);
1195
+ if (node.onFail === 'continue') {
1196
+ if (nodeRun.outputText) {
1197
+ outputs.set(node.id, compactOutputForContext(nodeRun.outputText));
1198
+ }
1199
+ completed.add(node.id);
1200
+ return;
1201
+ }
1202
+ throw new Error(nodeRun.error);
1203
+ }
1204
+ }
1205
+
1206
+ export const workflowRunner = new WorkflowRunner();