@synergenius/flow-weaver-pack-weaver 0.9.151 → 0.9.153

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 (93) hide show
  1. package/dist/ai-chat-provider.js +4 -4
  2. package/dist/ai-chat-provider.js.map +1 -1
  3. package/dist/bot/ai-client.d.ts +30 -0
  4. package/dist/bot/ai-client.d.ts.map +1 -1
  5. package/dist/bot/ai-client.js +37 -0
  6. package/dist/bot/ai-client.js.map +1 -1
  7. package/dist/bot/behavior-defaults.d.ts.map +1 -1
  8. package/dist/bot/behavior-defaults.js +7 -2
  9. package/dist/bot/behavior-defaults.js.map +1 -1
  10. package/dist/bot/capability-registry.d.ts.map +1 -1
  11. package/dist/bot/capability-registry.js +48 -30
  12. package/dist/bot/capability-registry.js.map +1 -1
  13. package/dist/bot/file-validator.d.ts +7 -0
  14. package/dist/bot/file-validator.d.ts.map +1 -1
  15. package/dist/bot/file-validator.js +76 -0
  16. package/dist/bot/file-validator.js.map +1 -1
  17. package/dist/bot/instance-manager.d.ts +22 -7
  18. package/dist/bot/instance-manager.d.ts.map +1 -1
  19. package/dist/bot/instance-manager.js +69 -7
  20. package/dist/bot/instance-manager.js.map +1 -1
  21. package/dist/bot/orchestrator.d.ts +11 -9
  22. package/dist/bot/orchestrator.d.ts.map +1 -1
  23. package/dist/bot/orchestrator.js +56 -107
  24. package/dist/bot/orchestrator.js.map +1 -1
  25. package/dist/bot/runner.d.ts +29 -0
  26. package/dist/bot/runner.d.ts.map +1 -1
  27. package/dist/bot/runner.js +114 -73
  28. package/dist/bot/runner.js.map +1 -1
  29. package/dist/bot/step-executor.d.ts.map +1 -1
  30. package/dist/bot/step-executor.js +106 -25
  31. package/dist/bot/step-executor.js.map +1 -1
  32. package/dist/bot/swarm-controller.d.ts +7 -6
  33. package/dist/bot/swarm-controller.d.ts.map +1 -1
  34. package/dist/bot/swarm-controller.js +64 -74
  35. package/dist/bot/swarm-controller.js.map +1 -1
  36. package/dist/bot/task-types.d.ts +1 -0
  37. package/dist/bot/task-types.d.ts.map +1 -1
  38. package/dist/bot/weaver-tools.d.ts +1 -1
  39. package/dist/bot/weaver-tools.d.ts.map +1 -1
  40. package/dist/bot/weaver-tools.js +6 -1
  41. package/dist/bot/weaver-tools.js.map +1 -1
  42. package/dist/node-types/agent-execute.js +2 -2
  43. package/dist/node-types/agent-execute.js.map +1 -1
  44. package/dist/node-types/bot-report.d.ts.map +1 -1
  45. package/dist/node-types/bot-report.js +5 -2
  46. package/dist/node-types/bot-report.js.map +1 -1
  47. package/dist/node-types/build-context.js +2 -1
  48. package/dist/node-types/build-context.js.map +1 -1
  49. package/dist/node-types/exec-validate-retry.d.ts +3 -3
  50. package/dist/node-types/exec-validate-retry.d.ts.map +1 -1
  51. package/dist/node-types/exec-validate-retry.js +13 -184
  52. package/dist/node-types/exec-validate-retry.js.map +1 -1
  53. package/dist/node-types/load-config.d.ts +1 -0
  54. package/dist/node-types/load-config.d.ts.map +1 -1
  55. package/dist/node-types/load-config.js +1 -0
  56. package/dist/node-types/load-config.js.map +1 -1
  57. package/dist/node-types/plan-task.d.ts +7 -5
  58. package/dist/node-types/plan-task.d.ts.map +1 -1
  59. package/dist/node-types/plan-task.js +282 -83
  60. package/dist/node-types/plan-task.js.map +1 -1
  61. package/dist/ui/bot-panel.js +1 -1
  62. package/dist/ui/capability-editor.js +48 -30
  63. package/dist/ui/profile-editor.js +46 -28
  64. package/dist/ui/swarm-dashboard.js +71 -33
  65. package/dist/ui/task-detail-view.js +22 -2
  66. package/dist/workflows/weaver-bot.d.ts +2 -2
  67. package/dist/workflows/weaver-bot.d.ts.map +1 -1
  68. package/dist/workflows/weaver-bot.js +5 -4
  69. package/dist/workflows/weaver-bot.js.map +1 -1
  70. package/flowweaver.manifest.json +1 -1
  71. package/package.json +1 -1
  72. package/src/ai-chat-provider.ts +4 -4
  73. package/src/bot/ai-client.ts +65 -0
  74. package/src/bot/behavior-defaults.ts +5 -2
  75. package/src/bot/capability-registry.ts +48 -30
  76. package/src/bot/file-validator.ts +97 -0
  77. package/src/bot/instance-manager.ts +77 -7
  78. package/src/bot/orchestrator.ts +63 -126
  79. package/src/bot/runner.ts +124 -70
  80. package/src/bot/step-executor.ts +115 -25
  81. package/src/bot/swarm-controller.ts +65 -76
  82. package/src/bot/task-types.ts +1 -0
  83. package/src/bot/weaver-tools.ts +7 -1
  84. package/src/node-types/agent-execute.ts +2 -2
  85. package/src/node-types/bot-report.ts +5 -2
  86. package/src/node-types/build-context.ts +2 -1
  87. package/src/node-types/exec-validate-retry.ts +14 -203
  88. package/src/node-types/load-config.ts +1 -0
  89. package/src/node-types/plan-task.ts +313 -88
  90. package/src/ui/bot-panel.tsx +1 -1
  91. package/src/ui/swarm-dashboard.tsx +3 -3
  92. package/src/ui/task-detail-view.tsx +25 -2
  93. package/src/workflows/weaver-bot.ts +5 -4
@@ -7,6 +7,7 @@ import type { BotConfig } from '../bot/types.js';
7
7
  *
8
8
  * @flowWeaver nodeType
9
9
  * @expression
10
+ * @safe
10
11
  * @label Load Config
11
12
  * @icon settings
12
13
  * @color cyan
@@ -1,61 +1,135 @@
1
- import type { WeaverContext } from '../bot/types.js';
2
- import { callAIWithTools, callCapabilityTriage, parseJsonResponse, normalizePlan } from '../bot/ai-client.js';
3
- import type { AiTool } from '../bot/ai-client.js';
1
+ import type { WeaverContext, StepLogEntry } from '../bot/types.js';
2
+ import { callPlatformWithMessages, callCapabilityTriage } from '../bot/ai-client.js';
3
+ import type { AiTool, AiCallResult, ChatMessage } from '../bot/ai-client.js';
4
4
  import { auditEmit } from '../bot/audit-logger.js';
5
- import { PLAN_OPERATIONS } from '../bot/operations.js';
5
+ import { executeStep } from '../bot/step-executor.js';
6
6
  import { getCapabilitiesByNames, BUILT_IN_CAPABILITIES } from '../bot/capability-registry.js';
7
7
 
8
8
  // ---------------------------------------------------------------------------
9
- // Plan tool definition passed via native tool_use so the AI returns
10
- // structured JSON arguments instead of prose. Works across Anthropic and OpenAI.
9
+ // Tool definitionsbuilt dynamically from capability-granted operations
11
10
  // ---------------------------------------------------------------------------
12
11
 
13
- /** Build the create_plan tool definition with dynamic operation list. */
14
- function buildPlanTool(operations: string[]): AiTool {
15
- const ops = operations.length > 0 ? operations : PLAN_OPERATIONS as unknown as string[];
16
- return {
17
- name: 'create_plan',
18
- description: 'Create a structured execution plan for this task. Call this tool with the plan steps and summary.',
12
+ /** All known tool schemas, keyed by operation name. */
13
+ const TOOL_SCHEMAS: Record<string, AiTool> = {
14
+ read_file: {
15
+ name: 'read_file',
16
+ description: 'Read a file and return its content',
17
+ parameters: { type: 'object', properties: { file: { type: 'string' } }, required: ['file'] },
18
+ },
19
+ write_file: {
20
+ name: 'write_file',
21
+ description: 'Write a file (full content)',
22
+ parameters: { type: 'object', properties: { file: { type: 'string' }, content: { type: 'string' } }, required: ['file', 'content'] },
23
+ },
24
+ patch_file: {
25
+ name: 'patch_file',
26
+ description: 'Find and replace in a file',
19
27
  parameters: {
20
28
  type: 'object',
21
29
  properties: {
22
- summary: { type: 'string', description: 'One-line summary of what the plan does' },
23
- steps: {
24
- type: 'array',
25
- description: 'Ordered list of operations to execute',
26
- items: {
27
- type: 'object',
28
- properties: {
29
- operation: {
30
- type: 'string',
31
- description: `Tool to invoke: ${ops.join(', ')}`,
32
- },
33
- description: { type: 'string', description: 'What this step does' },
34
- args: {
35
- type: 'object',
36
- description: 'Arguments for the operation (e.g. {file, content} for write_file, {file, find, replace} for patch_file)',
37
- },
38
- },
39
- required: ['operation', 'description', 'args'],
40
- },
41
- },
30
+ file: { type: 'string' },
31
+ patches: { type: 'array', items: { type: 'object', properties: { find: { type: 'string' }, replace: { type: 'string' } } } },
42
32
  },
43
- required: ['summary', 'steps'],
33
+ required: ['file', 'patches'],
44
34
  },
45
- };
35
+ },
36
+ run_shell: {
37
+ name: 'run_shell',
38
+ description: 'Run a shell command',
39
+ parameters: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] },
40
+ },
41
+ list_files: {
42
+ name: 'list_files',
43
+ description: 'List files in a directory',
44
+ parameters: { type: 'object', properties: { directory: { type: 'string' }, pattern: { type: 'string' } } },
45
+ },
46
+ task_create: {
47
+ name: 'task_create',
48
+ description: 'Create a swarm subtask',
49
+ parameters: {
50
+ type: 'object',
51
+ properties: {
52
+ title: { type: 'string' },
53
+ description: { type: 'string' },
54
+ assignedProfile: { type: 'string' },
55
+ parentId: { type: 'string' },
56
+ dependsOn: { type: 'array', items: { type: 'string' } },
57
+ complexity: { type: 'string' },
58
+ },
59
+ required: ['title'],
60
+ },
61
+ },
62
+ done: {
63
+ name: 'done',
64
+ description: 'Signal task completion. Call this when you have finished the task.',
65
+ parameters: { type: 'object', properties: { summary: { type: 'string' } }, required: ['summary'] },
66
+ },
67
+ remember: {
68
+ name: 'remember',
69
+ description: 'Save a project convention for future sessions',
70
+ parameters: { type: 'object', properties: { key: { type: 'string' }, value: { type: 'string' } }, required: ['key', 'value'] },
71
+ },
72
+ recall: {
73
+ name: 'recall',
74
+ description: 'Recall saved project conventions',
75
+ parameters: { type: 'object', properties: {} },
76
+ },
77
+ validate: {
78
+ name: 'validate',
79
+ description: 'Validate a Flow Weaver workflow file',
80
+ parameters: { type: 'object', properties: { file: { type: 'string' } }, required: ['file'] },
81
+ },
82
+ respond: {
83
+ name: 'respond',
84
+ description: 'Send a text response to the user',
85
+ parameters: { type: 'object', properties: { response: { type: 'string' } }, required: ['response'] },
86
+ },
87
+ };
88
+
89
+ /** Build tool definitions from capability-granted operation names. */
90
+ function buildToolDefinitions(grantedTools: string[]): AiTool[] {
91
+ const tools: AiTool[] = [];
92
+ const seen = new Set<string>();
93
+
94
+ for (const name of grantedTools) {
95
+ if (seen.has(name)) continue;
96
+ seen.add(name);
97
+ const schema = TOOL_SCHEMAS[name];
98
+ if (schema) tools.push(schema);
99
+ }
100
+
101
+ // Always include 'done' so the AI can signal completion
102
+ if (!seen.has('done')) {
103
+ tools.push(TOOL_SCHEMAS.done);
104
+ }
105
+
106
+ return tools;
46
107
  }
47
108
 
109
+ // ---------------------------------------------------------------------------
110
+ // Safety limits
111
+ // ---------------------------------------------------------------------------
112
+
113
+ const MAX_TOOL_CALLS = 30;
114
+ const AGENT_TIMEOUT_MS = 180_000; // 3 minutes
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Agent Loop — replaces Plan Task + Execute & Validate
118
+ // ---------------------------------------------------------------------------
119
+
48
120
  /**
49
- * Sends task + context to the AI provider and gets back a structured
50
- * execution plan. The core AI planning node.
121
+ * Agent loop: sends task + tools to the AI, executes tool calls iteratively
122
+ * until the AI signals completion or a safety limit is reached.
123
+ *
124
+ * Replaces the old Plan Task → Execute & Validate two-phase workflow.
51
125
  *
52
126
  * @flowWeaver nodeType
53
- * @label Plan Task
54
- * @icon task
127
+ * @label Agent Loop
128
+ * @icon psychology
55
129
  * @color blue
56
130
  * @input ctx [order:0] - Weaver context (JSON)
57
131
  * @input [modelOverride] [order:1] - Model ID override from profile behavior
58
- * @output ctx [order:0] - Weaver context with planJson (JSON)
132
+ * @output ctx [order:0] - Weaver context with results (JSON)
59
133
  * @output onSuccess [order:-2] - On Success
60
134
  * @output onFailure [order:-1] [hidden] - On Failure
61
135
  */
@@ -72,15 +146,24 @@ export async function weaverPlanTask(
72
146
 
73
147
  if (!execute) {
74
148
  context.planJson = '{"steps":[],"summary":"dry run"}';
149
+ context.resultJson = JSON.stringify({ success: true, toolCallCount: 0 });
150
+ context.filesModified = '[]';
151
+ context.stepLogJson = '[]';
152
+ context.allValid = true;
75
153
  return { onSuccess: true, onFailure: false, ctx: JSON.stringify(context) };
76
154
  }
77
155
 
78
156
  const pInfo = modelOverride
79
157
  ? { ...env.providerInfo, model: modelOverride }
80
158
  : env.providerInfo;
159
+ const { projectDir } = env;
81
160
 
82
161
  if (!context.taskJson) {
83
- context.planJson = JSON.stringify({ steps: [], summary: 'Planning failed: missing taskJson' });
162
+ context.planJson = JSON.stringify({ steps: [], summary: 'Agent loop failed: missing taskJson' });
163
+ context.resultJson = JSON.stringify({ success: false, error: 'missing taskJson' });
164
+ context.filesModified = '[]';
165
+ context.stepLogJson = '[]';
166
+ context.allValid = false;
84
167
  return { onSuccess: false, onFailure: true, ctx: JSON.stringify(context) };
85
168
  }
86
169
  const task = JSON.parse(context.taskJson);
@@ -91,80 +174,222 @@ export async function weaverPlanTask(
91
174
  const availableCaps = getCapabilitiesByNames(availableCapNames);
92
175
 
93
176
  // Capability triage: cheap Haiku call selects which capabilities this task needs
94
- let selectedCaps = availableCaps; // fallback: all available
177
+ let selectedCaps = availableCaps;
95
178
  const triageResult = await callCapabilityTriage(pInfo, task.instruction, availableCaps);
96
179
  if (triageResult) {
97
180
  selectedCaps = getCapabilitiesByNames(triageResult);
98
181
  }
99
- // If triage failed, selectedCaps = all available (no degradation)
100
182
 
183
+ // Build system prompt from capabilities
101
184
  let systemPrompt: string;
102
185
  try {
103
186
  const mod = await import('../bot/system-prompt.js');
104
187
  const basePrompt = mod.buildPromptFromCapabilities(selectedCaps);
105
- // Only include the context bundle if the 'context' capability was selected.
106
- // This prevents sending 10k+ tokens of FW authoring docs for simple file creation tasks.
107
188
  const selectedCapNames = new Set(selectedCaps.map(c => c.name));
108
189
  const contextBundle = selectedCapNames.has('context') ? context.contextBundle : undefined;
109
190
  const botPrompt = mod.buildBotSystemPrompt(contextBundle, undefined, context.env?.projectDir);
110
191
  systemPrompt = basePrompt + '\n\n' + botPrompt;
111
192
  } catch (err) {
112
- if (process.env.WEAVER_VERBOSE) console.error('[plan-task] system prompt build failed:', err);
113
- systemPrompt = 'You are Weaver, an AI workflow bot. Return ONLY valid JSON with a plan.';
193
+ if (process.env.WEAVER_VERBOSE) console.error('[agent-loop] system prompt build failed:', err);
194
+ systemPrompt = 'You are Weaver, an AI workflow bot. Use the provided tools to complete tasks.';
114
195
  }
115
196
 
116
- const userPrompt = `Task: ${task.instruction}\nMode: ${task.mode ?? 'create'}\n${task.id ? `Task ID: ${task.id}` : ''}\n${task.targets ? 'Targets: ' + task.targets.join(', ') : ''}
197
+ // Build tool definitions from capability-granted operations
198
+ let tools: AiTool[];
199
+ try {
200
+ const mod2 = await import('../bot/system-prompt.js');
201
+ const grantedTools = mod2.collectToolsFromCapabilities(selectedCaps);
202
+ tools = buildToolDefinitions(grantedTools);
203
+ } catch {
204
+ tools = buildToolDefinitions(['read_file', 'write_file', 'patch_file', 'run_shell', 'list_files', 'task_create']);
205
+ }
206
+
207
+ // Build initial user prompt
208
+ let userPrompt = `Task: ${task.instruction}\nMode: ${task.mode ?? 'create'}\n${task.id ? `Your Task ID is ${task.id}. Use parentId: "@self" in task_create to create subtasks under this task.` : ''}\n${task.targets ? 'Targets: ' + task.targets.join(', ') : ''}
117
209
 
118
- Plan this task by calling the create_plan tool with concrete steps.
210
+ Execute this task step by step using the available tools.
211
+ When you are done, call the "done" tool with a summary of what you accomplished.
119
212
  Rules:
120
- 1. Every step MUST have complete, concrete args no empty patches, no placeholders.
121
- 2. For file creation tasks, use write_file with full file content in args.
122
- 3. If you need to discover file contents first, plan ONLY discovery steps (read_file, list_files).
123
- 4. Do NOT plan patch_file unless you know the exact find/replace strings.`;
213
+ 1. Read files before modifying them (use read_file to get exact content for patches).
214
+ 2. Use patch_file for modifications, write_file only for new files.
215
+ 3. Verify your work by running tests or tsc when appropriate.`;
216
+
217
+ // Append retry context when this is a retry attempt (attempt > 0)
218
+ const attempt = task.attempt ?? 0;
219
+ if (attempt > 0) {
220
+ const retryParts: string[] = ['\n\n--- RETRY CONTEXT (attempt ' + (attempt + 1) + ') ---'];
221
+ if (task.lastError) {
222
+ retryParts.push('Previous attempt failed with error: ' + task.lastError);
223
+ }
224
+ const summaries: Array<{ outcome?: string; filesModified?: string[]; summary?: string }> = task.runSummaries ?? [];
225
+ const lastSummary = summaries.length > 0 ? summaries[summaries.length - 1] : undefined;
226
+ if (lastSummary) {
227
+ if (lastSummary.outcome) retryParts.push('Last outcome: ' + lastSummary.outcome);
228
+ if (lastSummary.summary) retryParts.push('Last summary: ' + lastSummary.summary);
229
+ if (lastSummary.filesModified && lastSummary.filesModified.length > 0) {
230
+ retryParts.push('Files already created/modified: ' + lastSummary.filesModified.join(', '));
231
+ }
232
+ }
233
+ retryParts.push('Do NOT recreate files that already exist. Read them first and continue from where the previous attempt left off.');
234
+ retryParts.push('---');
235
+ userPrompt += retryParts.join('\n');
236
+ }
237
+
238
+ // Seed symbolic ID map for task references
239
+ const symbolicIdMap: Record<string, string> = {};
240
+ if (task.id) symbolicIdMap['@self'] = task.id;
241
+ if (task.parentId) symbolicIdMap['@parent'] = task.parentId;
242
+
243
+ // State tracking
244
+ const filesModified: string[] = [];
245
+ const stepLog: StepLogEntry[] = [];
246
+ let toolCallCount = 0;
247
+ const deadline = Date.now() + AGENT_TIMEOUT_MS;
248
+
249
+ // Messages array for multi-turn conversation
250
+ const messages: ChatMessage[] = [
251
+ { role: 'system', content: systemPrompt },
252
+ { role: 'user', content: userPrompt },
253
+ ];
254
+
255
+ auditEmit('run-start', { task: task.instruction, mode: 'agent-loop' });
124
256
 
125
257
  try {
126
- // Build plan tool with operations scoped to selected capabilities
127
- const mod2 = await import('../bot/system-prompt.js');
128
- const grantedTools = mod2.collectToolsFromCapabilities(selectedCaps);
129
- // Always include respond in the available operations
130
- const planOps = grantedTools.length > 0 ? [...grantedTools, 'respond'] : PLAN_OPERATIONS as unknown as string[];
131
- const planTool = buildPlanTool(planOps);
132
-
133
- // Use native tool_use when available (Anthropic, OpenAI).
134
- // The AI calls create_plan with structured args instead of returning prose.
135
- const result = await callAIWithTools(pInfo, systemPrompt, userPrompt, [planTool], 8192);
136
-
137
- let plan: { steps: Array<{ id: string; operation: string; description: string; args: Record<string, unknown> }>; summary: string };
138
-
139
- // Check if AI used the create_plan tool (structured response)
140
- const planCall = result.toolCalls.find(tc => tc.name === 'create_plan');
141
- if (planCall) {
142
- plan = normalizePlan(planCall.arguments);
143
- } else {
144
- // Fallback: parse JSON from text content (CLI providers, or AI chose not to use tool)
145
- const text = result.content;
146
- try {
147
- const raw = parseJsonResponse(text);
148
- plan = normalizePlan(raw);
149
- } catch {
150
- // LLM returned plain text — wrap as a text-response plan.
151
- const trimmed = text.trim();
152
- const firstLine = trimmed.split('\n')[0].slice(0, 120);
153
- plan = {
154
- steps: [{ id: 'step-1', operation: 'respond', description: firstLine, args: { response: trimmed } }],
155
- summary: firstLine,
156
- };
258
+ // Agent loop
259
+ let done = false;
260
+ while (!done && toolCallCount < MAX_TOOL_CALLS && Date.now() < deadline) {
261
+ const result: AiCallResult = await callPlatformWithMessages(messages, tools, pInfo.model, pInfo.maxTokens ?? 8192);
262
+
263
+ // If AI returns text with no tool calls, we're done
264
+ if (!result.toolCalls || result.toolCalls.length === 0) {
265
+ if (result.content) {
266
+ console.log(`\x1b[36m→ Agent: ${result.content.slice(0, 200)}\x1b[0m`);
267
+ }
268
+ done = true;
269
+ break;
270
+ }
271
+
272
+ // Process each tool call
273
+ for (const tc of result.toolCalls) {
274
+ if (toolCallCount >= MAX_TOOL_CALLS || Date.now() >= deadline) {
275
+ done = true;
276
+ break;
277
+ }
278
+
279
+ toolCallCount++;
280
+ const toolName = tc.name;
281
+ const toolArgs = tc.arguments;
282
+
283
+ // Check for done signal
284
+ if (toolName === 'done' || toolName === 'complete') {
285
+ console.log(`\x1b[36m→ Agent done: ${(toolArgs as Record<string, string>).summary ?? 'completed'}\x1b[0m`);
286
+
287
+ // Execute through step-executor for consistent handling
288
+ try {
289
+ const stepResult = await executeStep(
290
+ { operation: toolName, args: toolArgs },
291
+ projectDir,
292
+ symbolicIdMap,
293
+ );
294
+ stepLog.push({ step: `${toolCallCount}:${toolName}`, status: 'ok', detail: stepResult.output ?? 'done' });
295
+ } catch {
296
+ stepLog.push({ step: `${toolCallCount}:${toolName}`, status: 'ok', detail: 'done' });
297
+ }
298
+
299
+ // Add assistant message with tool use and tool result to messages
300
+ messages.push({
301
+ role: 'assistant',
302
+ content: result.content || undefined,
303
+ tool_use: { id: tc.id, name: toolName, input: toolArgs },
304
+ });
305
+ messages.push({
306
+ role: 'user',
307
+ tool_use_id: tc.id,
308
+ content: 'Task completed.',
309
+ });
310
+
311
+ done = true;
312
+ break;
313
+ }
314
+
315
+ // Execute the tool via step-executor
316
+ let toolOutput: string;
317
+ try {
318
+ const stepResult = await executeStep(
319
+ { operation: toolName, args: toolArgs },
320
+ projectDir,
321
+ symbolicIdMap,
322
+ );
323
+
324
+ // Track files modified — only from write/patch operations, not reads/listings
325
+ const isWriteOp = toolName === 'write_file' || toolName === 'patch_file' || toolName === 'create_workflow' || toolName === 'modify_source' || toolName === 'implement_node';
326
+ if (isWriteOp && stepResult.file) filesModified.push(stepResult.file);
327
+
328
+ // Build output for AI
329
+ if (stepResult.blocked) {
330
+ toolOutput = `BLOCKED: ${stepResult.blockReason}`;
331
+ stepLog.push({ step: `${toolCallCount}:${toolName}`, status: 'blocked', detail: stepResult.blockReason });
332
+ } else {
333
+ toolOutput = stepResult.output ?? 'OK';
334
+ stepLog.push({ step: `${toolCallCount}:${toolName}`, status: 'ok', detail: toolName });
335
+ }
336
+
337
+ console.log(`\x1b[32m + ${toolCallCount}: ${toolName}\x1b[0m`);
338
+ } catch (err: unknown) {
339
+ const msg = err instanceof Error ? err.message : String(err);
340
+ toolOutput = `ERROR: ${msg}`;
341
+ stepLog.push({ step: `${toolCallCount}:${toolName}`, status: 'error', detail: msg });
342
+ console.error(`\x1b[31m x ${toolCallCount}: ${toolName}: ${msg}\x1b[0m`);
343
+ }
344
+
345
+ // Append assistant tool_use + tool result to messages for next iteration
346
+ messages.push({
347
+ role: 'assistant',
348
+ content: result.content || undefined,
349
+ tool_use: { id: tc.id, name: toolName, input: toolArgs },
350
+ });
351
+ messages.push({
352
+ role: 'user',
353
+ tool_use_id: tc.id,
354
+ content: toolOutput,
355
+ });
157
356
  }
158
357
  }
159
- console.log(`\x1b[36m→ Plan: ${plan.summary ?? 'generated'}\x1b[0m`);
160
- auditEmit('plan-created', { summary: plan.summary, stepCount: plan.steps?.length ?? 0 });
161
358
 
162
- context.planJson = JSON.stringify(plan);
359
+ // Deduplicate files
360
+ const uniqueFiles = [...new Set(filesModified)];
361
+
362
+ // Store results in context (compatible with exec-validate-retry output format)
363
+ context.resultJson = JSON.stringify({
364
+ success: true,
365
+ toolCallCount,
366
+ filesModified: uniqueFiles,
367
+ stepsCompleted: toolCallCount,
368
+ stepsTotal: toolCallCount,
369
+ });
370
+ context.planJson = JSON.stringify({ steps: [], summary: `Agent loop: ${toolCallCount} tool calls` });
371
+ context.filesModified = JSON.stringify(uniqueFiles);
372
+ context.stepLogJson = JSON.stringify(stepLog);
373
+ context.allValid = true;
374
+
375
+ auditEmit('run-complete', {
376
+ success: true,
377
+ toolCalls: toolCallCount,
378
+ filesModified: uniqueFiles.length,
379
+ });
380
+
381
+ console.log(`\x1b[36m→ Agent loop: ${toolCallCount} tool calls, ${uniqueFiles.length} files modified\x1b[0m`);
163
382
  return { onSuccess: true, onFailure: false, ctx: JSON.stringify(context) };
164
383
  } catch (err: unknown) {
165
384
  const msg = err instanceof Error ? err.message : String(err);
166
- console.error(`\x1b[31m→ Planning failed: ${msg}\x1b[0m`);
167
- context.planJson = JSON.stringify({ steps: [], summary: `Planning failed: ${msg}` });
385
+ console.error(`\x1b[31m→ Agent loop failed: ${msg}\x1b[0m`);
386
+
387
+ context.resultJson = JSON.stringify({ success: false, error: msg });
388
+ context.planJson = JSON.stringify({ steps: [], summary: `Agent loop failed: ${msg}` });
389
+ context.filesModified = JSON.stringify([...new Set(filesModified)]);
390
+ context.stepLogJson = JSON.stringify(stepLog);
391
+ context.allValid = false;
392
+
168
393
  return { onSuccess: false, onFailure: true, ctx: JSON.stringify(context) };
169
394
  }
170
395
  }
@@ -286,7 +286,7 @@ function BotPanel() {
286
286
  React.createElement(Flex, { variant: 'row-center-start-nowrap-4' },
287
287
  React.createElement(Typography, {
288
288
  variant: 'caption-thick', color: 'color-text-high',
289
- }, `${inst.profileId} #${inst.index}`),
289
+ }, inst.profileId ? `${inst.instanceId} (${inst.profileId})` : inst.instanceId),
290
290
  React.createElement(StatusIcon, {
291
291
  status: statusToIconStatus(inst.status),
292
292
  size: 'sm',
@@ -357,7 +357,7 @@ function SwarmDashboard() {
357
357
  React.createElement(Tabs, {
358
358
  tabs: [
359
359
  { id: 'tasks', title: `Tasks (${tasks.length})` },
360
- { id: 'bots', title: hasSwarmInstances ? `Instances (${swarmInstanceEntries.length})` : `Bots (${registeredBots.length})` },
360
+ { id: 'bots', title: hasSwarmInstances ? `Workers (${swarmInstanceEntries.length})` : `Bots (${registeredBots.length})` },
361
361
  { id: 'profiles', title: `Profiles (${profiles.length})` },
362
362
  { id: 'config', title: 'Config' },
363
363
  ],
@@ -475,7 +475,7 @@ function SwarmDashboard() {
475
475
  React.createElement(Typography, {
476
476
  variant: 'smallCaption-regular', color: 'color-text-subtle',
477
477
  style: { width: '120px', flexShrink: 0 },
478
- }, 'Instance'),
478
+ }, 'Worker'),
479
479
  React.createElement(Typography, {
480
480
  variant: 'smallCaption-regular', color: 'color-text-subtle',
481
481
  style: { width: '110px', flexShrink: 0 },
@@ -511,7 +511,7 @@ function SwarmDashboard() {
511
511
  key: inst.instanceId,
512
512
  bot: {
513
513
  botId: inst.instanceId,
514
- botName: `${inst.profileId} #${inst.index}`,
514
+ botName: inst.profileId ? `${inst.instanceId} (${inst.profileId})` : inst.instanceId,
515
515
  status: inst.status,
516
516
  currentTaskId: inst.currentTaskId,
517
517
  currentRunId: inst.currentRunId,
@@ -223,6 +223,27 @@ function TaskDetailView({ taskId, onBack, onEdit }: TaskDetailViewProps) {
223
223
  const t = (data.task ?? data) as Task;
224
224
  setTask(t);
225
225
 
226
+ // Populate run history from task's runSummaries (RunStore may be empty for swarm tasks)
227
+ const rs = (t as unknown as Record<string, unknown>).context as Record<string, unknown> | undefined;
228
+ const summaries = rs?.runSummaries as Array<Record<string, unknown>> | undefined;
229
+ if (summaries?.length) {
230
+ setHistory(prev => {
231
+ if (prev.length > 0) return prev; // Don't overwrite if RunStore data exists
232
+ return summaries.map((s) => ({
233
+ id: s.runId as string,
234
+ runId: s.runId as string,
235
+ outcome: s.outcome as string,
236
+ summary: s.summary as string,
237
+ report: s.report as string | undefined,
238
+ filesModified: s.filesModified as string[],
239
+ durationMs: s.durationMs as number,
240
+ cost: s.cost as number,
241
+ tokensUsed: s.tokensUsed as number,
242
+ error: s.error as string | undefined,
243
+ } as unknown as HistoricalRun));
244
+ });
245
+ }
246
+
226
247
  // Subtasks from response or fetch separately
227
248
  if (data.subtasks && Array.isArray(data.subtasks)) {
228
249
  setSubtasks(data.subtasks as Subtask[]);
@@ -242,13 +263,14 @@ function TaskDetailView({ taskId, onBack, onEdit }: TaskDetailViewProps) {
242
263
  try {
243
264
  const raw = await callTool('fw_weaver_history', { taskId });
244
265
  const data = typeof raw === 'string' ? JSON.parse(raw) : raw;
245
- if (Array.isArray(data)) {
266
+ if (Array.isArray(data) && data.length > 0) {
246
267
  setHistory(
247
268
  data.map((r: Record<string, unknown>) => {
248
269
  const costObj = r.cost && typeof r.cost === 'object' ? (r.cost as Record<string, unknown>) : undefined;
249
270
  return { ...r, costDetail: costObj, cost: costObj?.totalCost } as unknown as HistoricalRun;
250
271
  }),
251
272
  );
273
+ return;
252
274
  }
253
275
  } catch {
254
276
  // Non-fatal
@@ -261,6 +283,7 @@ function TaskDetailView({ taskId, onBack, onEdit }: TaskDetailViewProps) {
261
283
  Promise.all([fetchTask(), fetchHistory()]).finally(() => setLoading(false));
262
284
  }, [fetchTask, fetchHistory]);
263
285
 
286
+
264
287
  // Poll while in-progress (every 5s)
265
288
  useEffect(() => {
266
289
  if (!task || task.status !== 'in-progress') return;
@@ -673,7 +696,7 @@ function TaskDetailView({ taskId, onBack, onEdit }: TaskDetailViewProps) {
673
696
  React.createElement(Flex, { variant: 'column-stretch-start-nowrap-8' },
674
697
  React.createElement(Typography, { variant: 'caption-thick', color: 'color-text-medium' }, 'Status'),
675
698
  React.createElement(Flex, { variant: 'row-center-start-nowrap-6' },
676
- task.status === 'failed' && React.createElement(Button, {
699
+ (task.status === 'failed' || (task.status === 'open' && task.attempt >= task.maxAttempts)) && React.createElement(Button, {
677
700
  size: 'xs', variant: 'fill', color: 'primary',
678
701
  onClick: handleRetry,
679
702
  loading: actionLoading === 'retry',
@@ -465,10 +465,10 @@ class GeneratedExecutionContext {
465
465
  * @connect readWf.onSuccess -> report.execute
466
466
  * @connect abort.onSuccess -> report.execute
467
467
  * @connect notify.onSuccess -> report.execute
468
- * @connect gitOps.ctx -> report.mainCtx
468
+ * @connect mergePlan.ctx -> report.mainCtx
469
469
  * @connect readWf.ctx -> report.readCtx
470
470
  * @connect abort.ctx -> report.abortCtx
471
- * @connect execRetry.ctx -> report.failCtx
471
+ * @connect plan.ctx -> report.failCtx
472
472
  * @connect report.summary -> Exit.summary
473
473
  * @param execute [order:-1] - Execute
474
474
  * @param taskJson [order:0] - TaskJson
@@ -2118,16 +2118,17 @@ export async function weaverBot(
2118
2118
  try {
2119
2119
  const report_execute = (readWfIdx !== undefined ? await ctx.getVariable({ id: 'readWf', portName: 'onSuccess', executionIndex: readWfIdx }) as boolean : false) || (abortIdx !== undefined ? await ctx.getVariable({ id: 'abort', portName: 'onSuccess', executionIndex: abortIdx }) as boolean : false) || (notifyIdx !== undefined ? await ctx.getVariable({ id: 'notify', portName: 'onSuccess', executionIndex: notifyIdx }) as boolean : false) || (gitOpsIdx !== undefined ? await ctx.getVariable({ id: 'gitOps', portName: 'onSuccess', executionIndex: gitOpsIdx }) as boolean : false) || (receiveIdx !== undefined ? await ctx.getVariable({ id: 'receive', portName: 'onFailure', executionIndex: receiveIdx }) as boolean : false) || (planIdx !== undefined ? await ctx.getVariable({ id: 'plan', portName: 'onFailure', executionIndex: planIdx }) as boolean : false) || (execRetryIdx !== undefined ? await ctx.getVariable({ id: 'execRetry', portName: 'onFailure', executionIndex: execRetryIdx }) as boolean : false);
2120
2120
  await ctx.setVariable({ id: 'report', portName: 'execute', executionIndex: reportIdx, nodeTypeName: 'weaverBotReport' }, report_execute);
2121
- const report_mainCtx = gitOpsIdx !== undefined ? await ctx.getVariable({ id: 'gitOps', portName: 'ctx', executionIndex: gitOpsIdx }) as string : undefined;
2121
+ const report_mainCtx = mergePlanIdx !== undefined ? await ctx.getVariable({ id: 'mergePlan', portName: 'ctx', executionIndex: mergePlanIdx }) as string : undefined;
2122
2122
  await ctx.setVariable({ id: 'report', portName: 'mainCtx', executionIndex: reportIdx, nodeTypeName: 'weaverBotReport' }, report_mainCtx);
2123
2123
  const report_readCtx = readWfIdx !== undefined ? await ctx.getVariable({ id: 'readWf', portName: 'ctx', executionIndex: readWfIdx }) as string : undefined;
2124
2124
  await ctx.setVariable({ id: 'report', portName: 'readCtx', executionIndex: reportIdx, nodeTypeName: 'weaverBotReport' }, report_readCtx);
2125
2125
  const report_abortCtx = abortIdx !== undefined ? await ctx.getVariable({ id: 'abort', portName: 'ctx', executionIndex: abortIdx }) as string : undefined;
2126
2126
  await ctx.setVariable({ id: 'report', portName: 'abortCtx', executionIndex: reportIdx, nodeTypeName: 'weaverBotReport' }, report_abortCtx);
2127
- const report_failCtx = execRetryIdx !== undefined ? await ctx.getVariable({ id: 'execRetry', portName: 'ctx', executionIndex: execRetryIdx }) as string : undefined;
2127
+ const report_failCtx = planIdx !== undefined ? await ctx.getVariable({ id: 'plan', portName: 'ctx', executionIndex: planIdx }) as string : undefined;
2128
2128
  await ctx.setVariable({ id: 'report', portName: 'failCtx', executionIndex: reportIdx, nodeTypeName: 'weaverBotReport' }, report_failCtx);
2129
2129
  const reportResult = await weaverBotReport(report_execute, report_mainCtx, report_readCtx, report_abortCtx, report_failCtx);
2130
2130
  await ctx.setVariable({ id: 'report', portName: 'summary', executionIndex: reportIdx, nodeTypeName: 'weaverBotReport' }, reportResult.summary);
2131
+ await ctx.setVariable({ id: 'report', portName: 'report', executionIndex: reportIdx, nodeTypeName: 'weaverBotReport' }, reportResult.report);
2131
2132
  await ctx.setVariable({ id: 'report', portName: 'reportJson', executionIndex: reportIdx, nodeTypeName: 'weaverBotReport' }, reportResult.reportJson);
2132
2133
  await ctx.setVariable({ id: 'report', portName: 'onFailure', executionIndex: reportIdx, nodeTypeName: 'weaverBotReport' }, reportResult.onFailure);
2133
2134
  await ctx.setVariable({ id: 'report', portName: 'onSuccess', executionIndex: reportIdx, nodeTypeName: 'weaverBotReport' }, reportResult.onSuccess);