@synergenius/flow-weaver-pack-weaver 0.9.152 → 0.9.154
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-chat-provider.js +4 -4
- package/dist/ai-chat-provider.js.map +1 -1
- package/dist/bot/ai-client.d.ts +30 -0
- package/dist/bot/ai-client.d.ts.map +1 -1
- package/dist/bot/ai-client.js +37 -0
- package/dist/bot/ai-client.js.map +1 -1
- package/dist/bot/behavior-defaults.d.ts.map +1 -1
- package/dist/bot/behavior-defaults.js +7 -2
- package/dist/bot/behavior-defaults.js.map +1 -1
- package/dist/bot/capability-registry.d.ts.map +1 -1
- package/dist/bot/capability-registry.js +46 -33
- package/dist/bot/capability-registry.js.map +1 -1
- package/dist/bot/file-validator.d.ts +7 -0
- package/dist/bot/file-validator.d.ts.map +1 -1
- package/dist/bot/file-validator.js +76 -0
- package/dist/bot/file-validator.js.map +1 -1
- package/dist/bot/instance-manager.d.ts +22 -7
- package/dist/bot/instance-manager.d.ts.map +1 -1
- package/dist/bot/instance-manager.js +69 -7
- package/dist/bot/instance-manager.js.map +1 -1
- package/dist/bot/orchestrator.d.ts +11 -9
- package/dist/bot/orchestrator.d.ts.map +1 -1
- package/dist/bot/orchestrator.js +56 -107
- package/dist/bot/orchestrator.js.map +1 -1
- package/dist/bot/runner.d.ts +29 -0
- package/dist/bot/runner.d.ts.map +1 -1
- package/dist/bot/runner.js +114 -73
- package/dist/bot/runner.js.map +1 -1
- package/dist/bot/step-executor.d.ts.map +1 -1
- package/dist/bot/step-executor.js +28 -9
- package/dist/bot/step-executor.js.map +1 -1
- package/dist/bot/swarm-controller.d.ts +7 -6
- package/dist/bot/swarm-controller.d.ts.map +1 -1
- package/dist/bot/swarm-controller.js +64 -74
- package/dist/bot/swarm-controller.js.map +1 -1
- package/dist/bot/system-prompt.d.ts.map +1 -1
- package/dist/bot/system-prompt.js +2 -0
- package/dist/bot/system-prompt.js.map +1 -1
- package/dist/bot/task-types.d.ts +1 -0
- package/dist/bot/task-types.d.ts.map +1 -1
- package/dist/bot/weaver-tools.d.ts +1 -1
- package/dist/bot/weaver-tools.d.ts.map +1 -1
- package/dist/bot/weaver-tools.js +12 -1
- package/dist/bot/weaver-tools.js.map +1 -1
- package/dist/node-types/agent-execute.js +2 -2
- package/dist/node-types/agent-execute.js.map +1 -1
- package/dist/node-types/bot-report.d.ts.map +1 -1
- package/dist/node-types/bot-report.js +5 -2
- package/dist/node-types/bot-report.js.map +1 -1
- package/dist/node-types/build-context.d.ts.map +1 -1
- package/dist/node-types/build-context.js +13 -1
- package/dist/node-types/build-context.js.map +1 -1
- package/dist/node-types/exec-validate-retry.d.ts +3 -3
- package/dist/node-types/exec-validate-retry.d.ts.map +1 -1
- package/dist/node-types/exec-validate-retry.js +13 -184
- package/dist/node-types/exec-validate-retry.js.map +1 -1
- package/dist/node-types/load-config.d.ts +1 -0
- package/dist/node-types/load-config.d.ts.map +1 -1
- package/dist/node-types/load-config.js +1 -0
- package/dist/node-types/load-config.js.map +1 -1
- package/dist/node-types/plan-task.d.ts +7 -5
- package/dist/node-types/plan-task.d.ts.map +1 -1
- package/dist/node-types/plan-task.js +282 -83
- package/dist/node-types/plan-task.js.map +1 -1
- package/dist/ui/bot-panel.js +1 -1
- package/dist/ui/capability-editor.js +46 -33
- package/dist/ui/chat-task-result.js +7 -7
- package/dist/ui/profile-editor.js +44 -31
- package/dist/ui/swarm-dashboard.js +80 -47
- package/dist/ui/task-detail-view.js +31 -11
- package/dist/ui/task-editor.js +1 -1
- package/dist/ui/task-pool-list.js +1 -1
- package/dist/workflows/weaver-bot.d.ts +5 -4
- package/dist/workflows/weaver-bot.d.ts.map +1 -1
- package/dist/workflows/weaver-bot.js +8 -7
- package/dist/workflows/weaver-bot.js.map +1 -1
- package/flowweaver.manifest.json +1 -1
- package/package.json +1 -1
- package/src/ai-chat-provider.ts +4 -4
- package/src/bot/ai-client.ts +65 -0
- package/src/bot/behavior-defaults.ts +5 -2
- package/src/bot/capability-registry.ts +46 -33
- package/src/bot/file-validator.ts +97 -0
- package/src/bot/instance-manager.ts +77 -7
- package/src/bot/orchestrator.ts +63 -126
- package/src/bot/runner.ts +124 -70
- package/src/bot/step-executor.ts +30 -9
- package/src/bot/swarm-controller.ts +65 -76
- package/src/bot/system-prompt.ts +2 -0
- package/src/bot/task-types.ts +1 -0
- package/src/bot/weaver-tools.ts +14 -1
- package/src/node-types/agent-execute.ts +2 -2
- package/src/node-types/bot-report.ts +5 -2
- package/src/node-types/build-context.ts +13 -1
- package/src/node-types/exec-validate-retry.ts +14 -203
- package/src/node-types/load-config.ts +1 -0
- package/src/node-types/plan-task.ts +313 -88
- package/src/ui/bot-panel.tsx +1 -1
- package/src/ui/chat-task-result.tsx +10 -8
- package/src/ui/swarm-dashboard.tsx +4 -4
- package/src/ui/task-detail-view.tsx +35 -12
- package/src/ui/task-editor.tsx +2 -2
- package/src/ui/task-pool-list.tsx +2 -2
- package/src/workflows/weaver-bot.ts +8 -7
package/src/bot/orchestrator.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Orchestrator — the routing brain that decides which tasks go to which
|
|
2
|
+
* Orchestrator — the routing brain that decides which tasks go to which workers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* In the worker pool model, workers are generic slots. The orchestrator:
|
|
5
|
+
* 1. Finds the profile for each task (via assignedProfile).
|
|
6
|
+
* 2. Assigns the task to any idle worker.
|
|
7
|
+
*
|
|
8
|
+
* Simplified cascade: exact-match profile → any idle worker.
|
|
9
|
+
* No more per-profile instance matching or scale-up actions.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
import type {
|
|
@@ -15,7 +19,6 @@ import type {
|
|
|
15
19
|
|
|
16
20
|
type Task = OrchestratorInput['pendingTasks'][number];
|
|
17
21
|
type Assignment = OrchestratorOutput['assignments'][number];
|
|
18
|
-
type ScaleAction = OrchestratorOutput['scaleActions'][number];
|
|
19
22
|
|
|
20
23
|
// ---------------------------------------------------------------------------
|
|
21
24
|
// AI Router types
|
|
@@ -50,11 +53,10 @@ export class Orchestrator {
|
|
|
50
53
|
|
|
51
54
|
async route(input: OrchestratorInput): Promise<OrchestratorOutput> {
|
|
52
55
|
const assignments: Assignment[] = [];
|
|
53
|
-
const scaleActions: ScaleAction[] = [];
|
|
54
56
|
const skippedTasks: OrchestratorOutput['skippedTasks'] = [];
|
|
55
57
|
|
|
56
|
-
// Track
|
|
57
|
-
const
|
|
58
|
+
// Track workers claimed during this routing cycle to avoid double-assignment.
|
|
59
|
+
const claimedWorkerIds = new Set<string>();
|
|
58
60
|
|
|
59
61
|
// Sort tasks by priority DESC (higher number = higher priority).
|
|
60
62
|
const sorted = [...input.pendingTasks].sort((a, b) => b.priority - a.priority);
|
|
@@ -66,50 +68,43 @@ export class Orchestrator {
|
|
|
66
68
|
continue;
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
71
|
+
// Resolve profile for this task (includes routing method)
|
|
72
|
+
const resolution = await this._resolveProfile(task, input.profiles);
|
|
73
|
+
if (!resolution) {
|
|
72
74
|
skippedTasks.push({ taskId: task.id, reason: 'no-eligible-profile' });
|
|
73
75
|
continue;
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
taskId: task.id,
|
|
82
|
-
profileId: result.profileId,
|
|
83
|
-
instanceId: result.instanceId,
|
|
84
|
-
reason: result.reason,
|
|
85
|
-
method: result.method,
|
|
86
|
-
confidence: result.confidence,
|
|
87
|
-
});
|
|
88
|
-
this._recordDecision(task, result, eligible);
|
|
89
|
-
} else {
|
|
90
|
-
// No idle instance available — request scale-up if possible.
|
|
91
|
-
const scaleProfile = eligible[0];
|
|
92
|
-
const currentCount = input.instances.filter(
|
|
93
|
-
(i) => i.profileId === scaleProfile.id,
|
|
94
|
-
).length;
|
|
95
|
-
|
|
96
|
-
if (currentCount < scaleProfile.maxInstances) {
|
|
97
|
-
// Only add one scale-up action per profile.
|
|
98
|
-
if (!scaleActions.some((sa) => sa.profileId === scaleProfile.id)) {
|
|
99
|
-
scaleActions.push({
|
|
100
|
-
profileId: scaleProfile.id,
|
|
101
|
-
action: 'scale-up',
|
|
102
|
-
targetInstances: currentCount + 1,
|
|
103
|
-
reason: `idle instances exhausted for profile ${scaleProfile.id}`,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
skippedTasks.push({ taskId: task.id, reason: 'no-idle-instance' });
|
|
78
|
+
// Find any idle worker
|
|
79
|
+
const idleWorker = this._findIdleWorker(input.instances, claimedWorkerIds);
|
|
80
|
+
if (!idleWorker) {
|
|
81
|
+
skippedTasks.push({ taskId: task.id, reason: 'no-idle-worker' });
|
|
82
|
+
continue;
|
|
109
83
|
}
|
|
84
|
+
|
|
85
|
+
claimedWorkerIds.add(idleWorker.instanceId);
|
|
86
|
+
|
|
87
|
+
const reason = `${resolution.method}: ${resolution.profileId} → ${idleWorker.instanceId}`;
|
|
88
|
+
assignments.push({
|
|
89
|
+
taskId: task.id,
|
|
90
|
+
profileId: resolution.profileId,
|
|
91
|
+
instanceId: idleWorker.instanceId,
|
|
92
|
+
reason,
|
|
93
|
+
method: resolution.method,
|
|
94
|
+
confidence: resolution.confidence,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
this._recordDecision(task, {
|
|
98
|
+
profileId: resolution.profileId,
|
|
99
|
+
instanceId: idleWorker.instanceId,
|
|
100
|
+
method: resolution.method,
|
|
101
|
+
reason,
|
|
102
|
+
confidence: resolution.confidence,
|
|
103
|
+
}, input.profiles.filter(p => p.id === resolution.profileId));
|
|
110
104
|
}
|
|
111
105
|
|
|
112
|
-
|
|
106
|
+
// No scale actions in pool model — pool size is fixed at maxConcurrent
|
|
107
|
+
return { assignments, scaleActions: [], skippedTasks };
|
|
113
108
|
}
|
|
114
109
|
|
|
115
110
|
getDecisionLog(limit?: number): OrchestratorDecision[] {
|
|
@@ -126,106 +121,48 @@ export class Orchestrator {
|
|
|
126
121
|
// Internal
|
|
127
122
|
// ---------------------------------------------------------------------------
|
|
128
123
|
|
|
129
|
-
/**
|
|
130
|
-
private
|
|
124
|
+
/** Resolve which profileId should handle this task, including routing method. */
|
|
125
|
+
private async _resolveProfile(
|
|
126
|
+
task: Task,
|
|
127
|
+
profiles: BotProfile[],
|
|
128
|
+
): Promise<{ profileId: string; method: OrchestratorDecision['method']; confidence?: number } | null> {
|
|
129
|
+
// Exact match: task has assignedProfile
|
|
131
130
|
if (task.assignedProfile) {
|
|
132
|
-
const match = profiles.
|
|
133
|
-
if (match
|
|
134
|
-
// Assigned profile doesn't exist — return empty to skip this task
|
|
135
|
-
// rather than falling through to all profiles (silent mis-routing).
|
|
131
|
+
const match = profiles.find(p => p.id === task.assignedProfile);
|
|
132
|
+
if (match) return { profileId: match.id, method: 'exact-match' };
|
|
136
133
|
console.warn(
|
|
137
134
|
`[orchestrator] Task "${task.id}" assigned to profile "${task.assignedProfile}" which does not exist — skipping`,
|
|
138
135
|
);
|
|
139
|
-
return
|
|
136
|
+
return null;
|
|
140
137
|
}
|
|
141
|
-
return profiles;
|
|
142
|
-
}
|
|
143
138
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
eligible: BotProfile[],
|
|
148
|
-
instances: BotInstance[],
|
|
149
|
-
claimed: Set<string>,
|
|
150
|
-
): Promise<{ profileId: string; instanceId: string; method: OrchestratorDecision['method']; reason: string; confidence?: number } | null> {
|
|
151
|
-
// Determine routing method
|
|
152
|
-
let method = this._routingMethod(task, eligible);
|
|
153
|
-
|
|
154
|
-
if (eligible.length === 1) {
|
|
155
|
-
const profile = eligible[0];
|
|
156
|
-
const idle = instances.find(
|
|
157
|
-
(i) => i.profileId === profile.id && i.status === 'idle' && !claimed.has(i.instanceId),
|
|
158
|
-
);
|
|
159
|
-
if (!idle) return null;
|
|
160
|
-
return {
|
|
161
|
-
profileId: profile.id,
|
|
162
|
-
instanceId: idle.instanceId,
|
|
163
|
-
method,
|
|
164
|
-
reason: `${method}: ${profile.id}`,
|
|
165
|
-
};
|
|
139
|
+
// Single eligible profile
|
|
140
|
+
if (profiles.length === 1) {
|
|
141
|
+
return { profileId: profiles[0].id, method: 'single-eligible' };
|
|
166
142
|
}
|
|
167
143
|
|
|
168
|
-
//
|
|
169
|
-
if (this._aiRouter &&
|
|
144
|
+
// Multiple eligible profiles — try AI routing
|
|
145
|
+
if (this._aiRouter && profiles.length > 1) {
|
|
170
146
|
try {
|
|
171
|
-
const aiResult = await this._aiRouter.route(task,
|
|
172
|
-
const aiProfile =
|
|
147
|
+
const aiResult = await this._aiRouter.route(task, profiles);
|
|
148
|
+
const aiProfile = profiles.find(p => p.id === aiResult.profileId);
|
|
173
149
|
if (aiProfile) {
|
|
174
|
-
|
|
175
|
-
(i) => i.profileId === aiProfile.id && i.status === 'idle' && !claimed.has(i.instanceId),
|
|
176
|
-
);
|
|
177
|
-
if (idle) {
|
|
178
|
-
return {
|
|
179
|
-
profileId: aiProfile.id,
|
|
180
|
-
instanceId: idle.instanceId,
|
|
181
|
-
method: 'ai-routed',
|
|
182
|
-
reason: `ai-routed: ${aiProfile.id} — ${aiResult.reason}`,
|
|
183
|
-
confidence: aiResult.confidence,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
// AI chose a profile but no idle instance — fall through to round-robin
|
|
150
|
+
return { profileId: aiProfile.id, method: 'ai-routed', confidence: aiResult.confidence };
|
|
187
151
|
}
|
|
188
|
-
// AI returned unknown profile — fall through to round-robin
|
|
189
152
|
} catch {
|
|
190
153
|
// AI call failed — fall through to round-robin
|
|
191
154
|
}
|
|
192
155
|
}
|
|
193
156
|
|
|
194
|
-
//
|
|
195
|
-
method
|
|
196
|
-
let bestProfile: BotProfile | null = null;
|
|
197
|
-
let bestIdleCount = -1;
|
|
198
|
-
let bestInstance: BotInstance | null = null;
|
|
199
|
-
|
|
200
|
-
for (const profile of eligible) {
|
|
201
|
-
const idleInstances = instances.filter(
|
|
202
|
-
(i) => i.profileId === profile.id && i.status === 'idle' && !claimed.has(i.instanceId),
|
|
203
|
-
);
|
|
204
|
-
if (idleInstances.length > bestIdleCount) {
|
|
205
|
-
bestIdleCount = idleInstances.length;
|
|
206
|
-
bestProfile = profile;
|
|
207
|
-
bestInstance = idleInstances[0] ?? null;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (!bestProfile || !bestInstance) return null;
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
profileId: bestProfile.id,
|
|
215
|
-
instanceId: bestInstance.instanceId,
|
|
216
|
-
method,
|
|
217
|
-
reason: `${method}: ${bestProfile.id}`,
|
|
218
|
-
};
|
|
157
|
+
// Fallback: round-robin (first profile)
|
|
158
|
+
return profiles.length > 0 ? { profileId: profiles[0].id, method: 'round-robin' } : null;
|
|
219
159
|
}
|
|
220
160
|
|
|
221
|
-
/**
|
|
222
|
-
private
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (task.assignedProfile) return 'exact-match';
|
|
227
|
-
if (eligible.length === 1) return 'single-eligible';
|
|
228
|
-
return 'round-robin';
|
|
161
|
+
/** Find any idle worker that hasn't been claimed this cycle. */
|
|
162
|
+
private _findIdleWorker(instances: BotInstance[], claimed: Set<string>): BotInstance | null {
|
|
163
|
+
return instances.find(
|
|
164
|
+
(i) => i.status === 'idle' && !claimed.has(i.instanceId),
|
|
165
|
+
) ?? null;
|
|
229
166
|
}
|
|
230
167
|
|
|
231
168
|
/** Record a decision in the log. */
|
package/src/bot/runner.ts
CHANGED
|
@@ -84,7 +84,7 @@ function parseConfigFile(configPath: string): WeaverConfig {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
function buildSummary(result: unknown): string {
|
|
87
|
+
export function buildSummary(result: unknown): string {
|
|
88
88
|
if (!result || typeof result !== 'object') return String(result);
|
|
89
89
|
|
|
90
90
|
const r = result as Record<string, unknown>;
|
|
@@ -101,6 +101,120 @@ function buildSummary(result: unknown): string {
|
|
|
101
101
|
return parts.length > 0 ? parts.join(', ') : 'completed';
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Build a markdown report from the workflow result's ctx field.
|
|
106
|
+
* Extracted for testability — same logic used inside runWorkflow.
|
|
107
|
+
*/
|
|
108
|
+
export function buildReport(
|
|
109
|
+
result: Record<string, unknown> | null,
|
|
110
|
+
success: boolean,
|
|
111
|
+
stepLog?: import('./types.js').StepLogEntry[],
|
|
112
|
+
): string | undefined {
|
|
113
|
+
try {
|
|
114
|
+
const ctxStr = result?.ctx as string | undefined;
|
|
115
|
+
if (!ctxStr) return undefined;
|
|
116
|
+
const ctx = JSON.parse(ctxStr);
|
|
117
|
+
const md: string[] = [];
|
|
118
|
+
md.push(`## ${success ? 'Task Completed' : 'Task Failed'}`);
|
|
119
|
+
md.push('');
|
|
120
|
+
|
|
121
|
+
// Steps
|
|
122
|
+
if (stepLog && stepLog.length > 0) {
|
|
123
|
+
md.push('### Steps');
|
|
124
|
+
md.push('');
|
|
125
|
+
for (const s of stepLog) {
|
|
126
|
+
const icon = s.status === 'ok' ? '**ok**' : s.status === 'error' ? '**error**' : '**blocked**';
|
|
127
|
+
md.push(`- ${s.step} (${icon})${s.detail ? `: ${s.detail}` : ''}`);
|
|
128
|
+
}
|
|
129
|
+
md.push('');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Files
|
|
133
|
+
const files: string[] = ctx.filesModified ? JSON.parse(ctx.filesModified) : [];
|
|
134
|
+
if (files.length > 0) {
|
|
135
|
+
md.push('### Files Modified');
|
|
136
|
+
md.push('');
|
|
137
|
+
for (const f of files) md.push(`- \`${f}\``);
|
|
138
|
+
md.push('');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Review
|
|
142
|
+
if (ctx.reviewJson) {
|
|
143
|
+
const review = JSON.parse(ctx.reviewJson) as Record<string, string>;
|
|
144
|
+
if (review.intent || review.execution || review.result || review.completeness) {
|
|
145
|
+
md.push('### Review');
|
|
146
|
+
md.push('');
|
|
147
|
+
for (const key of ['intent', 'execution', 'result', 'completeness']) {
|
|
148
|
+
if (review[key]) md.push(`- **${key}:** ${review[key]}`);
|
|
149
|
+
}
|
|
150
|
+
if (review.reason) md.push(`\n${review.reason}`);
|
|
151
|
+
md.push('');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return md.length > 2 ? md.join('\n') : undefined;
|
|
156
|
+
} catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Extract stepLog and plan from the result's ctx field.
|
|
163
|
+
* Extracted for testability — same logic used inside runWorkflow.
|
|
164
|
+
*/
|
|
165
|
+
export function extractCtxData(result: Record<string, unknown> | null): {
|
|
166
|
+
stepLog?: import('./types.js').StepLogEntry[];
|
|
167
|
+
plan?: { summary: string; steps: Array<{ id: string; operation: string; description: string; args?: Record<string, unknown> }> };
|
|
168
|
+
} {
|
|
169
|
+
try {
|
|
170
|
+
const ctxStr = result?.ctx as string | undefined;
|
|
171
|
+
if (!ctxStr) return {};
|
|
172
|
+
const ctx = JSON.parse(ctxStr);
|
|
173
|
+
let stepLog: import('./types.js').StepLogEntry[] | undefined;
|
|
174
|
+
let plan: { summary: string; steps: Array<{ id: string; operation: string; description: string; args?: Record<string, unknown> }> } | undefined;
|
|
175
|
+
|
|
176
|
+
if (ctx.stepLogJson) stepLog = JSON.parse(ctx.stepLogJson);
|
|
177
|
+
if (ctx.planJson) {
|
|
178
|
+
const parsed = JSON.parse(ctx.planJson);
|
|
179
|
+
if (parsed?.steps) {
|
|
180
|
+
plan = {
|
|
181
|
+
summary: parsed.summary ?? '',
|
|
182
|
+
steps: (parsed.steps as Array<Record<string, unknown>>).map((s) => ({
|
|
183
|
+
id: String(s.id ?? ''),
|
|
184
|
+
operation: String(s.operation ?? ''),
|
|
185
|
+
description: String(s.description ?? ''),
|
|
186
|
+
...(s.args ? { args: s.args as Record<string, unknown> } : {}),
|
|
187
|
+
})),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return { stepLog, plan };
|
|
192
|
+
} catch {
|
|
193
|
+
return {};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Fallback: extract report from trace outputs (bot-report node's "Markdown report" port).
|
|
199
|
+
*/
|
|
200
|
+
export function extractReportFromTrace(
|
|
201
|
+
trace: Array<{ type: string; outputs?: unknown[] }>,
|
|
202
|
+
): string | undefined {
|
|
203
|
+
for (const te of trace) {
|
|
204
|
+
if (te.type === 'node-complete' && te.outputs) {
|
|
205
|
+
for (const o of te.outputs) {
|
|
206
|
+
if (typeof o === 'object' && o && 'portLabel' in o) {
|
|
207
|
+
const po = o as { portLabel: string; value: unknown };
|
|
208
|
+
if (po.portLabel === 'Markdown report' && typeof po.value === 'string' && po.value.length > 0) {
|
|
209
|
+
return po.value;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
104
218
|
export async function runWorkflow(
|
|
105
219
|
filePath: string,
|
|
106
220
|
options?: {
|
|
@@ -272,7 +386,7 @@ export async function runWorkflow(
|
|
|
272
386
|
}
|
|
273
387
|
}
|
|
274
388
|
}
|
|
275
|
-
for (const inst of parseResult?.workflow?.instances ?? []) {
|
|
389
|
+
for (const inst of (parseResult?.workflows?.[0]?.instances ?? parseResult?.workflow?.instances ?? [])) {
|
|
276
390
|
const label = inst.config?.label ?? nodeLabels.get(inst.nodeType);
|
|
277
391
|
if (label) nodeLabels.set(inst.id, label);
|
|
278
392
|
// Inherit visual meta from node type to instance
|
|
@@ -415,77 +529,17 @@ export async function runWorkflow(
|
|
|
415
529
|
}
|
|
416
530
|
|
|
417
531
|
// Extract stepLog and plan from WeaverContext if available
|
|
418
|
-
|
|
419
|
-
let plan: RunRecord['plan'] | undefined;
|
|
420
|
-
try {
|
|
421
|
-
const ctxStr = result?.ctx as string | undefined;
|
|
422
|
-
if (ctxStr) {
|
|
423
|
-
const ctx = JSON.parse(ctxStr);
|
|
424
|
-
if (ctx.stepLogJson) stepLog = JSON.parse(ctx.stepLogJson);
|
|
425
|
-
if (ctx.planJson) {
|
|
426
|
-
const parsed = JSON.parse(ctx.planJson);
|
|
427
|
-
if (parsed?.steps) {
|
|
428
|
-
plan = {
|
|
429
|
-
summary: parsed.summary ?? '',
|
|
430
|
-
steps: (parsed.steps as Array<Record<string, unknown>>).map((s) => ({
|
|
431
|
-
id: String(s.id ?? ''),
|
|
432
|
-
operation: String(s.operation ?? ''),
|
|
433
|
-
description: String(s.description ?? ''),
|
|
434
|
-
...(s.args ? { args: s.args as Record<string, unknown> } : {}),
|
|
435
|
-
})),
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
} catch { /* extraction is best-effort */ }
|
|
532
|
+
const { stepLog, plan } = extractCtxData(result);
|
|
441
533
|
|
|
442
534
|
// Build markdown report from the extracted context data
|
|
443
|
-
let report
|
|
444
|
-
try {
|
|
445
|
-
const ctxStr = result?.ctx as string | undefined;
|
|
446
|
-
if (ctxStr) {
|
|
447
|
-
const ctx = JSON.parse(ctxStr);
|
|
448
|
-
const md: string[] = [];
|
|
449
|
-
md.push(`## ${success ? 'Task Completed' : 'Task Failed'}`);
|
|
450
|
-
md.push('');
|
|
451
|
-
|
|
452
|
-
// Steps
|
|
453
|
-
if (stepLog && stepLog.length > 0) {
|
|
454
|
-
md.push('### Steps');
|
|
455
|
-
md.push('');
|
|
456
|
-
for (const s of stepLog) {
|
|
457
|
-
const icon = s.status === 'ok' ? '**ok**' : s.status === 'error' ? '**error**' : '**blocked**';
|
|
458
|
-
md.push(`- ${s.step} (${icon})${s.detail ? `: ${s.detail}` : ''}`);
|
|
459
|
-
}
|
|
460
|
-
md.push('');
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// Files
|
|
464
|
-
const files: string[] = ctx.filesModified ? JSON.parse(ctx.filesModified) : [];
|
|
465
|
-
if (files.length > 0) {
|
|
466
|
-
md.push('### Files Modified');
|
|
467
|
-
md.push('');
|
|
468
|
-
for (const f of files) md.push(`- \`${f}\``);
|
|
469
|
-
md.push('');
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// Review
|
|
473
|
-
if (ctx.reviewJson) {
|
|
474
|
-
const review = JSON.parse(ctx.reviewJson) as Record<string, string>;
|
|
475
|
-
if (review.intent || review.execution || review.result || review.completeness) {
|
|
476
|
-
md.push('### Review');
|
|
477
|
-
md.push('');
|
|
478
|
-
for (const key of ['intent', 'execution', 'result', 'completeness']) {
|
|
479
|
-
if (review[key]) md.push(`- **${key}:** ${review[key]}`);
|
|
480
|
-
}
|
|
481
|
-
if (review.reason) md.push(`\n${review.reason}`);
|
|
482
|
-
md.push('');
|
|
483
|
-
}
|
|
484
|
-
}
|
|
535
|
+
let report = buildReport(result, success, stepLog);
|
|
485
536
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
537
|
+
// Fallback: extract report from the bot-report node's trace output.
|
|
538
|
+
// The FW compiled workflow doesn't wire bot-report's report port to END,
|
|
539
|
+
// so result.report is undefined. But the trace captured all node outputs.
|
|
540
|
+
if (!report) {
|
|
541
|
+
report = extractReportFromTrace(collectedTrace);
|
|
542
|
+
}
|
|
489
543
|
|
|
490
544
|
await notifier({
|
|
491
545
|
type: 'workflow-complete',
|
package/src/bot/step-executor.ts
CHANGED
|
@@ -189,6 +189,8 @@ export async function executeStep(
|
|
|
189
189
|
}
|
|
190
190
|
assertSafePath(file, projectDir);
|
|
191
191
|
const filePath = path.resolve(projectDir, file);
|
|
192
|
+
// Use the relative path for reporting — never leak absolute server paths
|
|
193
|
+
const relPath = path.relative(projectDir, filePath);
|
|
192
194
|
const rawContent = args.content ?? args.body ?? '';
|
|
193
195
|
if (typeof rawContent !== 'string') {
|
|
194
196
|
return { blocked: true, blockReason: `${step.operation}: "content" must be a string, got ${typeof rawContent}.` };
|
|
@@ -197,14 +199,14 @@ export async function executeStep(
|
|
|
197
199
|
|
|
198
200
|
const guard = checkWriteSafety(filePath, content);
|
|
199
201
|
if (!guard.allowed) {
|
|
200
|
-
return { file:
|
|
202
|
+
return { file: relPath, blocked: true, blockReason: guard.reason };
|
|
201
203
|
}
|
|
202
204
|
|
|
203
205
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
204
206
|
const existed = fs.existsSync(filePath);
|
|
205
207
|
fs.writeFileSync(filePath, content, 'utf-8');
|
|
206
208
|
filesWrittenThisPlan++;
|
|
207
|
-
return { file:
|
|
209
|
+
return { file: relPath, created: !existed };
|
|
208
210
|
}
|
|
209
211
|
|
|
210
212
|
// -----------------------------------------------------------------
|
|
@@ -216,6 +218,7 @@ export async function executeStep(
|
|
|
216
218
|
}
|
|
217
219
|
assertSafePath(file, projectDir);
|
|
218
220
|
const filePath = path.resolve(projectDir, file);
|
|
221
|
+
const relPath = path.relative(projectDir, filePath);
|
|
219
222
|
|
|
220
223
|
if (!fs.existsSync(filePath)) {
|
|
221
224
|
return { blocked: true, blockReason: `File not found: ${file}` };
|
|
@@ -263,7 +266,7 @@ export async function executeStep(
|
|
|
263
266
|
|
|
264
267
|
if (applied === 0) {
|
|
265
268
|
return {
|
|
266
|
-
file:
|
|
269
|
+
file: relPath,
|
|
267
270
|
output: `No patches applied. Search strings not found: ${notFound.join('; ')}`,
|
|
268
271
|
};
|
|
269
272
|
}
|
|
@@ -274,7 +277,7 @@ export async function executeStep(
|
|
|
274
277
|
if (originalSize > SHRINK_GUARD_MIN_SIZE && newSize < originalSize * MAX_SHRINK_RATIO) {
|
|
275
278
|
const shrinkPct = Math.round((1 - newSize / originalSize) * 100);
|
|
276
279
|
return {
|
|
277
|
-
file:
|
|
280
|
+
file: relPath,
|
|
278
281
|
blocked: true,
|
|
279
282
|
blockReason:
|
|
280
283
|
`Refusing to patch ${path.basename(filePath)}: result (${newSize}B) ` +
|
|
@@ -288,7 +291,7 @@ export async function executeStep(
|
|
|
288
291
|
|
|
289
292
|
const summary = `Applied ${applied}/${patches.length} patches` +
|
|
290
293
|
(notFound.length ? `. Not found: ${notFound.join('; ')}` : '');
|
|
291
|
-
return { file:
|
|
294
|
+
return { file: relPath, output: summary };
|
|
292
295
|
}
|
|
293
296
|
|
|
294
297
|
// -----------------------------------------------------------------
|
|
@@ -300,6 +303,7 @@ export async function executeStep(
|
|
|
300
303
|
}
|
|
301
304
|
assertSafePath(file, projectDir);
|
|
302
305
|
const filePath = path.resolve(projectDir, file);
|
|
306
|
+
const relPath = path.relative(projectDir, filePath);
|
|
303
307
|
if (!fs.existsSync(filePath)) {
|
|
304
308
|
return { output: `File not found: ${file}` };
|
|
305
309
|
}
|
|
@@ -313,9 +317,9 @@ export async function executeStep(
|
|
|
313
317
|
return { output: `File too large to read (${stat.size} bytes, max ${MAX_READ_SIZE}). Use run_shell with head/tail instead.` };
|
|
314
318
|
}
|
|
315
319
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
316
|
-
return { file:
|
|
320
|
+
return { file: relPath, output: content };
|
|
317
321
|
} catch (err: unknown) {
|
|
318
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
322
|
+
const msg = (err instanceof Error ? err.message : String(err)).replaceAll(projectDir + '/', '');
|
|
319
323
|
return { output: `Error reading file "${file}": ${msg}` };
|
|
320
324
|
}
|
|
321
325
|
}
|
|
@@ -356,8 +360,9 @@ export async function executeStep(
|
|
|
356
360
|
const stdout = (execErr.stdout ?? '').trim();
|
|
357
361
|
const stderr = (execErr.stderr ?? '').trim();
|
|
358
362
|
const combined = [stdout, stderr].filter(Boolean).join('\n');
|
|
363
|
+
const raw = combined || (err instanceof Error ? err.message : String(err));
|
|
359
364
|
return {
|
|
360
|
-
output:
|
|
365
|
+
output: raw.replaceAll(projectDir + '/', ''),
|
|
361
366
|
};
|
|
362
367
|
}
|
|
363
368
|
}
|
|
@@ -379,7 +384,7 @@ export async function executeStep(
|
|
|
379
384
|
try {
|
|
380
385
|
entries = fs.readdirSync(targetDir, { recursive: true, encoding: 'utf-8' }) as string[];
|
|
381
386
|
} catch (err: unknown) {
|
|
382
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
387
|
+
const msg = (err instanceof Error ? err.message : String(err)).replaceAll(projectDir + '/', '');
|
|
383
388
|
return { output: `Error listing directory "${dir}": ${msg}` };
|
|
384
389
|
}
|
|
385
390
|
let files = entries
|
|
@@ -456,6 +461,22 @@ export async function executeStep(
|
|
|
456
461
|
parentId = resolved;
|
|
457
462
|
}
|
|
458
463
|
|
|
464
|
+
// Idempotent: if a task with the same title and parentId already exists, return it
|
|
465
|
+
// instead of creating a duplicate. This prevents retry loops from producing duplicates.
|
|
466
|
+
if (parentId) {
|
|
467
|
+
const existing = (await store.list()).find(
|
|
468
|
+
t => t.parentId === parentId && t.title.toLowerCase() === title.toLowerCase(),
|
|
469
|
+
);
|
|
470
|
+
if (existing) {
|
|
471
|
+
if (symbolicIdMap) {
|
|
472
|
+
const symbolicKey = (args.symbolicId as string) ?? (args.id as string);
|
|
473
|
+
if (symbolicKey) symbolicIdMap[symbolicKey] = existing.id;
|
|
474
|
+
symbolicIdMap[title] = existing.id;
|
|
475
|
+
}
|
|
476
|
+
return { output: `Task "${title}" already exists (${existing.id}), skipped duplicate.` };
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
459
480
|
// Resolve symbolic IDs in dependsOn through the map
|
|
460
481
|
const rawDeps = (args.dependsOn as string[]) ?? [];
|
|
461
482
|
const resolvedDeps = symbolicIdMap
|