@synergenius/flow-weaver-pack-weaver 0.9.138 → 0.9.142
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/bot/assistant-tools.d.ts.map +1 -1
- package/dist/bot/assistant-tools.js +5 -11
- package/dist/bot/assistant-tools.js.map +1 -1
- package/dist/bot/capability-registry.d.ts.map +1 -1
- package/dist/bot/capability-registry.js +185 -15
- package/dist/bot/capability-registry.js.map +1 -1
- package/dist/bot/dashboard.js +3 -3
- package/dist/bot/dashboard.js.map +1 -1
- package/dist/bot/hierarchy-event-log.d.ts +37 -0
- package/dist/bot/hierarchy-event-log.d.ts.map +1 -0
- package/dist/bot/hierarchy-event-log.js +58 -0
- package/dist/bot/hierarchy-event-log.js.map +1 -0
- package/dist/bot/operations.d.ts +2 -0
- package/dist/bot/operations.d.ts.map +1 -1
- package/dist/bot/operations.js +5 -0
- package/dist/bot/operations.js.map +1 -1
- package/dist/bot/profile-store.d.ts.map +1 -1
- package/dist/bot/profile-store.js +39 -0
- package/dist/bot/profile-store.js.map +1 -1
- package/dist/bot/runner.d.ts.map +1 -1
- package/dist/bot/runner.js +7 -2
- package/dist/bot/runner.js.map +1 -1
- package/dist/bot/step-executor.d.ts.map +1 -1
- package/dist/bot/step-executor.js +33 -1
- package/dist/bot/step-executor.js.map +1 -1
- package/dist/bot/swarm-controller.d.ts +1 -0
- package/dist/bot/swarm-controller.d.ts.map +1 -1
- package/dist/bot/swarm-controller.js +59 -5
- package/dist/bot/swarm-controller.js.map +1 -1
- package/dist/bot/task-store.d.ts +1 -1
- package/dist/bot/task-store.d.ts.map +1 -1
- package/dist/bot/task-store.js +25 -37
- package/dist/bot/task-store.js.map +1 -1
- package/dist/bot/task-types.d.ts +5 -1
- package/dist/bot/task-types.d.ts.map +1 -1
- package/dist/node-types/bot-report.d.ts +2 -1
- package/dist/node-types/bot-report.d.ts.map +1 -1
- package/dist/node-types/bot-report.js +9 -4
- 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 +32 -0
- package/dist/node-types/build-context.js.map +1 -1
- package/dist/node-types/plan-task.d.ts.map +1 -1
- package/dist/node-types/plan-task.js +5 -1
- package/dist/node-types/plan-task.js.map +1 -1
- package/dist/node-types/report.d.ts +1 -1
- package/dist/node-types/report.d.ts.map +1 -1
- package/dist/node-types/report.js +58 -8
- package/dist/node-types/report.js.map +1 -1
- package/dist/ui/capability-editor.js +184 -15
- package/dist/ui/profile-editor.js +184 -15
- package/dist/ui/swarm-dashboard.js +244 -44
- package/dist/ui/task-detail-view.js +60 -29
- package/dist/ui/use-stream-timeline.d.ts.map +1 -1
- package/dist/ui/use-stream-timeline.js +69 -29
- package/dist/ui/use-stream-timeline.js.map +1 -1
- package/dist/workflows/weaver-bot.d.ts +1 -0
- package/dist/workflows/weaver-bot.d.ts.map +1 -1
- package/dist/workflows/weaver-bot.js +239 -4
- package/dist/workflows/weaver-bot.js.map +1 -1
- package/flowweaver.manifest.json +1 -1
- package/package.json +1 -1
- package/src/bot/assistant-tools.ts +5 -11
- package/src/bot/capability-registry.ts +196 -18
- package/src/bot/dashboard.ts +3 -3
- package/src/bot/hierarchy-event-log.ts +64 -0
- package/src/bot/operations.ts +7 -0
- package/src/bot/profile-store.ts +39 -0
- package/src/bot/runner.ts +8 -4
- package/src/bot/step-executor.ts +29 -1
- package/src/bot/swarm-controller.ts +62 -5
- package/src/bot/task-store.ts +26 -39
- package/src/bot/task-types.ts +7 -1
- package/src/node-types/bot-report.ts +8 -3
- package/src/node-types/build-context.ts +32 -0
- package/src/node-types/plan-task.ts +5 -1
- package/src/node-types/report.ts +56 -8
- package/src/ui/use-stream-timeline.ts +73 -33
- package/src/workflows/weaver-bot.ts +398 -3
package/src/bot/task-store.ts
CHANGED
|
@@ -38,7 +38,7 @@ export class TaskStore {
|
|
|
38
38
|
id: parentId,
|
|
39
39
|
title: input.title,
|
|
40
40
|
description: input.description,
|
|
41
|
-
status: '
|
|
41
|
+
status: 'open',
|
|
42
42
|
priority: input.priority ?? 0,
|
|
43
43
|
isParent: true,
|
|
44
44
|
parentId: input.parentId,
|
|
@@ -77,7 +77,7 @@ export class TaskStore {
|
|
|
77
77
|
id: subId,
|
|
78
78
|
title: sub.title,
|
|
79
79
|
description: sub.description ?? '',
|
|
80
|
-
status: '
|
|
80
|
+
status: 'open',
|
|
81
81
|
priority: sub.priority ?? input.priority ?? 0,
|
|
82
82
|
isParent: false,
|
|
83
83
|
parentId: parentId,
|
|
@@ -115,7 +115,7 @@ export class TaskStore {
|
|
|
115
115
|
id: crypto.randomUUID().slice(0, 8),
|
|
116
116
|
title: input.title,
|
|
117
117
|
description: input.description,
|
|
118
|
-
status: '
|
|
118
|
+
status: 'open',
|
|
119
119
|
priority: input.priority ?? 0,
|
|
120
120
|
isParent: false,
|
|
121
121
|
parentId: input.parentId,
|
|
@@ -233,8 +233,8 @@ export class TaskStore {
|
|
|
233
233
|
|
|
234
234
|
const task = tasks[idx];
|
|
235
235
|
const assignable =
|
|
236
|
-
task.status === 'pending'
|
|
237
|
-
|
|
236
|
+
(task.status === 'open' || task.status === 'pending') &&
|
|
237
|
+
task.attempt < task.maxAttempts;
|
|
238
238
|
if (!assignable) {
|
|
239
239
|
throw new Error(`Task ${taskId} is not assignable (status: ${task.status}, attempt: ${task.attempt}/${task.maxAttempts})`);
|
|
240
240
|
}
|
|
@@ -254,7 +254,7 @@ export class TaskStore {
|
|
|
254
254
|
// Release
|
|
255
255
|
// ---------------------------------------------------------------------------
|
|
256
256
|
|
|
257
|
-
async release(taskId: string, status: 'done' | 'failed', runSummary: RunSummary): Promise<Task> {
|
|
257
|
+
async release(taskId: string, status: 'done' | 'failed' | 'open', runSummary: RunSummary): Promise<Task> {
|
|
258
258
|
return this.mutex.runExclusive(async () => {
|
|
259
259
|
const tasks = this._readAll();
|
|
260
260
|
const idx = tasks.findIndex(t => t.id === taskId);
|
|
@@ -262,8 +262,11 @@ export class TaskStore {
|
|
|
262
262
|
|
|
263
263
|
const task = tasks[idx];
|
|
264
264
|
|
|
265
|
-
// Append run summary
|
|
265
|
+
// Append run summary and track runId
|
|
266
266
|
task.context.runSummaries.push(runSummary);
|
|
267
|
+
if (runSummary.runId && !task.runs.includes(runSummary.runId)) {
|
|
268
|
+
task.runs.push(runSummary.runId);
|
|
269
|
+
}
|
|
267
270
|
if (runSummary.outcome === 'failed' || runSummary.outcome === 'error') {
|
|
268
271
|
task.context.lastError = runSummary.error;
|
|
269
272
|
} else {
|
|
@@ -276,7 +279,8 @@ export class TaskStore {
|
|
|
276
279
|
|
|
277
280
|
// Update execution tracking
|
|
278
281
|
task.attempt += 1;
|
|
279
|
-
|
|
282
|
+
// Tasks don't fail — runs fail. Map 'failed' to 'open' for backward compat.
|
|
283
|
+
task.status = status === 'failed' ? 'open' : status;
|
|
280
284
|
task.currentBotId = undefined;
|
|
281
285
|
task.currentRunId = undefined;
|
|
282
286
|
task.updatedAt = new Date().toISOString();
|
|
@@ -284,9 +288,7 @@ export class TaskStore {
|
|
|
284
288
|
if (status === 'done') {
|
|
285
289
|
task.completedAt = new Date().toISOString();
|
|
286
290
|
}
|
|
287
|
-
|
|
288
|
-
task.completedAt = new Date().toISOString();
|
|
289
|
-
}
|
|
291
|
+
// 'open' and 'failed' → task stays open, no completedAt
|
|
290
292
|
|
|
291
293
|
tasks[idx] = task;
|
|
292
294
|
|
|
@@ -305,28 +307,19 @@ export class TaskStore {
|
|
|
305
307
|
|
|
306
308
|
private _handleDependencyEffects(tasks: Task[], changedTask: Task): void {
|
|
307
309
|
if (changedTask.status === 'done') {
|
|
308
|
-
// Unblock dependents: set blocked tasks to
|
|
310
|
+
// Unblock dependents: set blocked tasks to open if all deps are now done
|
|
309
311
|
const doneIds = new Set(tasks.filter(t => t.status === 'done').map(t => t.id));
|
|
310
312
|
for (const t of tasks) {
|
|
311
313
|
if (t.status === 'blocked' && t.dependsOn.includes(changedTask.id)) {
|
|
312
314
|
if (t.dependsOn.every(depId => doneIds.has(depId))) {
|
|
313
|
-
t.status = '
|
|
314
|
-
t.updatedAt = new Date().toISOString();
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
} else if (changedTask.status === 'failed') {
|
|
319
|
-
// Check if failure is terminal (no retries left)
|
|
320
|
-
if (changedTask.attempt >= changedTask.maxAttempts) {
|
|
321
|
-
// Block dependents
|
|
322
|
-
for (const t of tasks) {
|
|
323
|
-
if (t.status === 'pending' && t.dependsOn.includes(changedTask.id)) {
|
|
324
|
-
t.status = 'blocked';
|
|
315
|
+
t.status = 'open';
|
|
325
316
|
t.updatedAt = new Date().toISOString();
|
|
326
317
|
}
|
|
327
318
|
}
|
|
328
319
|
}
|
|
329
320
|
}
|
|
321
|
+
// Tasks don't fail — runs fail. Dependents stay blocked until dep
|
|
322
|
+
// succeeds (status=done). No terminal 'failed' state to handle.
|
|
330
323
|
}
|
|
331
324
|
|
|
332
325
|
private _handleParentEffects(tasks: Task[], changedTask: Task): void {
|
|
@@ -348,14 +341,8 @@ export class TaskStore {
|
|
|
348
341
|
return;
|
|
349
342
|
}
|
|
350
343
|
|
|
351
|
-
//
|
|
352
|
-
|
|
353
|
-
s => s.status === 'failed' && s.attempt >= s.maxAttempts,
|
|
354
|
-
);
|
|
355
|
-
if (hasTerminalFailure) {
|
|
356
|
-
parent.status = 'failed';
|
|
357
|
-
parent.updatedAt = new Date().toISOString();
|
|
358
|
-
}
|
|
344
|
+
// Tasks don't fail. Parent stays open until all subtasks are done
|
|
345
|
+
// or user cancels. No terminal 'failed' state for parent.
|
|
359
346
|
}
|
|
360
347
|
|
|
361
348
|
// ---------------------------------------------------------------------------
|
|
@@ -366,7 +353,7 @@ export class TaskStore {
|
|
|
366
353
|
return this.mutex.runExclusive(async () => {
|
|
367
354
|
const tasks = this._readAll();
|
|
368
355
|
const before = tasks.length;
|
|
369
|
-
const kept = tasks.filter(t => t.status !== 'done' && t.status !== '
|
|
356
|
+
const kept = tasks.filter(t => t.status !== 'done' && t.status !== 'cancelled');
|
|
370
357
|
this._writeAll(kept);
|
|
371
358
|
return before - kept.length;
|
|
372
359
|
});
|
|
@@ -386,17 +373,17 @@ export class TaskStore {
|
|
|
386
373
|
// ---------------------------------------------------------------------------
|
|
387
374
|
|
|
388
375
|
private _findDuplicate(tasks: Task[], title: string, description: string): Task | null {
|
|
389
|
-
// Check pending duplicates
|
|
390
|
-
const
|
|
391
|
-
t => t.status === 'pending' && t.title === title && t.description === description,
|
|
376
|
+
// Check open/pending duplicates
|
|
377
|
+
const openDup = tasks.find(
|
|
378
|
+
t => (t.status === 'open' || t.status === 'pending') && t.title === title && t.description === description,
|
|
392
379
|
);
|
|
393
|
-
if (
|
|
380
|
+
if (openDup) return openDup;
|
|
394
381
|
|
|
395
|
-
// Check recently completed
|
|
382
|
+
// Check recently completed (within dedup window)
|
|
396
383
|
const now = Date.now();
|
|
397
384
|
const recentDup = tasks.find(
|
|
398
385
|
t =>
|
|
399
|
-
|
|
386
|
+
t.status === 'done' &&
|
|
400
387
|
t.title === title &&
|
|
401
388
|
t.description === description &&
|
|
402
389
|
t.completedAt !== undefined &&
|
package/src/bot/task-types.ts
CHANGED
|
@@ -22,7 +22,13 @@ export interface TaskContext {
|
|
|
22
22
|
lastError?: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Task status. Tasks NEVER go to 'failed' — runs fail, tasks stay open.
|
|
27
|
+
* 'open' replaces 'pending'. A task is open until done or cancelled.
|
|
28
|
+
*/
|
|
29
|
+
export type TaskStatus = 'open' | 'pending' | 'in-progress' | 'blocked' | 'done' | 'cancelled';
|
|
30
|
+
// NOTE: 'pending' kept for backward compatibility with existing data.
|
|
31
|
+
// New code should use 'open'. Both are treated as assignable.
|
|
26
32
|
|
|
27
33
|
export interface Task {
|
|
28
34
|
id: string;
|
|
@@ -14,6 +14,7 @@ import { TaskStore } from '../bot/task-store.js';
|
|
|
14
14
|
* @input [mainCtx] [order:0] - Context from main path (JSON, optional)
|
|
15
15
|
* @input [readCtx] [order:1] - Context from read-only path (JSON, optional)
|
|
16
16
|
* @input [abortCtx] [order:2] - Context from abort path (JSON, optional)
|
|
17
|
+
* @input [failCtx] [order:3] - Context from failure paths (exec/plan fail, JSON, optional)
|
|
17
18
|
* @output summary [order:0] - Summary text
|
|
18
19
|
* @output reportJson [order:1] [hidden] - Full report (JSON)
|
|
19
20
|
* @output onFailure [hidden]
|
|
@@ -23,8 +24,9 @@ export async function weaverBotReport(
|
|
|
23
24
|
mainCtx?: string,
|
|
24
25
|
readCtx?: string,
|
|
25
26
|
abortCtx?: string,
|
|
27
|
+
failCtx?: string,
|
|
26
28
|
): Promise<{ onSuccess: boolean; onFailure: boolean; summary: string; reportJson: string }> {
|
|
27
|
-
const ctxStr = mainCtx ?? readCtx ?? abortCtx;
|
|
29
|
+
const ctxStr = mainCtx ?? readCtx ?? abortCtx ?? failCtx;
|
|
28
30
|
|
|
29
31
|
if (!execute) {
|
|
30
32
|
const report = { task: {}, path: 'unknown', result: null, filesModified: [], gitResult: null, timestamp: Date.now() };
|
|
@@ -66,6 +68,9 @@ export async function weaverBotReport(
|
|
|
66
68
|
} else if (abortCtx) {
|
|
67
69
|
result = safeJsonParse<ResultShape>(context.resultJson, null);
|
|
68
70
|
pathName = 'abort';
|
|
71
|
+
} else if (failCtx) {
|
|
72
|
+
result = safeJsonParse<ResultShape>(context.resultJson, null);
|
|
73
|
+
pathName = 'failed';
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
const parts: string[] = [];
|
|
@@ -115,8 +120,8 @@ export async function weaverBotReport(
|
|
|
115
120
|
if (task.queueId && context.env?.projectDir) {
|
|
116
121
|
try {
|
|
117
122
|
const store = new TaskStore(context.env.projectDir);
|
|
118
|
-
await store.update(task.queueId, { status: success ? 'done' : '
|
|
119
|
-
console.log(`\x1b[36m→ Queue task ${task.queueId}: ${success ? 'done' : '
|
|
123
|
+
await store.update(task.queueId, { status: success ? 'done' : 'open' });
|
|
124
|
+
console.log(`\x1b[36m→ Queue task ${task.queueId}: ${success ? 'done' : 'open'}\x1b[0m`);
|
|
120
125
|
} catch (err) { if (process.env.WEAVER_VERBOSE) console.error('[bot-report] queue update failed:', err); }
|
|
121
126
|
}
|
|
122
127
|
|
|
@@ -2,6 +2,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import type { WeaverContext } from '../bot/types.js';
|
|
5
|
+
import { HierarchyEventLog } from '../bot/hierarchy-event-log.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Builds the knowledge bundle the AI needs for planning.
|
|
@@ -31,6 +32,37 @@ export function weaverBuildContext(ctx: string): { ctx: string } {
|
|
|
31
32
|
sections.push(...buildFullContext(projectDir, task.mode));
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
// Auto-recall project memory (conventions persisted by previous bots)
|
|
36
|
+
try {
|
|
37
|
+
const memPath = path.join(projectDir, '.weaver', 'project-memory.json');
|
|
38
|
+
if (fs.existsSync(memPath)) {
|
|
39
|
+
const memory = JSON.parse(fs.readFileSync(memPath, 'utf-8')) as Record<string, string>;
|
|
40
|
+
const entries = Object.entries(memory);
|
|
41
|
+
if (entries.length > 0) {
|
|
42
|
+
const memLines = entries.map(([key, value]) => `- **${key}**: ${value}`);
|
|
43
|
+
sections.push(`## Project Conventions (from memory)\n\nFollow these established patterns:\n${memLines.join('\n')}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch { /* non-fatal — memory is best-effort */ }
|
|
47
|
+
|
|
48
|
+
// Include sibling context from hierarchy events (previous task completions in same hierarchy)
|
|
49
|
+
try {
|
|
50
|
+
const parsedTask = JSON.parse(context.taskJson!) as { parentId?: string };
|
|
51
|
+
const parentId = parsedTask.parentId;
|
|
52
|
+
if (parentId) {
|
|
53
|
+
const hierarchyLog = new HierarchyEventLog(projectDir);
|
|
54
|
+
const siblingEvents = hierarchyLog.tailByParent(parentId);
|
|
55
|
+
if (siblingEvents.length > 0) {
|
|
56
|
+
const siblingLines = siblingEvents.map(e => {
|
|
57
|
+
const d = e.data ?? {};
|
|
58
|
+
const files = (d.filesModified as string[])?.join(', ') || 'none';
|
|
59
|
+
return `- ${e.type}: ${d.summary ?? e.taskId} (files: ${files})`;
|
|
60
|
+
});
|
|
61
|
+
sections.push(`## Previous Task Completions\n\nYour sibling tasks in this hierarchy have completed:\n${siblingLines.join('\n')}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch { /* non-fatal — sibling context is best-effort */ }
|
|
65
|
+
|
|
34
66
|
const bundle = sections.join('\n\n---\n\n');
|
|
35
67
|
// Output handled by session renderer; keep a dim line for debugging
|
|
36
68
|
if (process.env.WEAVER_VERBOSE) process.stderr.write(`\x1b[2m Context: ${bundle.length} chars\x1b[0m\n`);
|
|
@@ -102,7 +102,11 @@ export async function weaverPlanTask(
|
|
|
102
102
|
try {
|
|
103
103
|
const mod = await import('../bot/system-prompt.js');
|
|
104
104
|
const basePrompt = mod.buildPromptFromCapabilities(selectedCaps);
|
|
105
|
-
|
|
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
|
+
const selectedCapNames = new Set(selectedCaps.map(c => c.name));
|
|
108
|
+
const contextBundle = selectedCapNames.has('context') ? context.contextBundle : undefined;
|
|
109
|
+
const botPrompt = mod.buildBotSystemPrompt(contextBundle, undefined, context.env?.projectDir);
|
|
106
110
|
systemPrompt = basePrompt + '\n\n' + botPrompt;
|
|
107
111
|
} catch (err) {
|
|
108
112
|
if (process.env.WEAVER_VERBOSE) console.error('[plan-task] system prompt build failed:', err);
|
package/src/node-types/report.ts
CHANGED
|
@@ -10,18 +10,66 @@ import type { WeaverContext } from '../bot/types.js';
|
|
|
10
10
|
* @icon summarize
|
|
11
11
|
* @color cyan
|
|
12
12
|
* @input ctx [order:0] - Weaver context (JSON)
|
|
13
|
-
* @output summary [order:0] - Summary
|
|
13
|
+
* @output summary [order:0, format:md] - Summary
|
|
14
14
|
* @output onFailure [hidden]
|
|
15
15
|
*/
|
|
16
16
|
export function weaverReport(ctx: string): { summary: string } {
|
|
17
17
|
const context = JSON.parse(ctx) as WeaverContext;
|
|
18
18
|
const result = JSON.parse(context.resultJson!);
|
|
19
19
|
const relPath = path.relative(context.env.projectDir, context.targetPath!);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
|
|
21
|
+
console.log(`\x1b[32m✓ Weaver: ${result.outcome} (${relPath})\x1b[0m`);
|
|
22
|
+
|
|
23
|
+
const md: string[] = [];
|
|
24
|
+
md.push(`## ${result.outcome === 'success' ? 'Task Completed' : 'Task Failed'}`);
|
|
25
|
+
md.push('');
|
|
26
|
+
if (result.summary) md.push(result.summary);
|
|
27
|
+
md.push('');
|
|
28
|
+
|
|
29
|
+
// Steps
|
|
30
|
+
try {
|
|
31
|
+
const steps = JSON.parse(context.stepLogJson ?? '[]') as Array<{ step: string; status: string; detail?: string }>;
|
|
32
|
+
if (steps.length > 0) {
|
|
33
|
+
md.push('### Steps');
|
|
34
|
+
md.push('');
|
|
35
|
+
for (const s of steps) {
|
|
36
|
+
const icon = s.status === 'ok' ? '**ok**' : s.status === 'error' ? '**error**' : '**blocked**';
|
|
37
|
+
md.push(`- ${s.step} (${icon})${s.detail ? `: ${s.detail}` : ''}`);
|
|
38
|
+
}
|
|
39
|
+
md.push('');
|
|
40
|
+
}
|
|
41
|
+
} catch { /* skip */ }
|
|
42
|
+
|
|
43
|
+
// Files
|
|
44
|
+
try {
|
|
45
|
+
const files = JSON.parse(context.filesModified ?? '[]') as string[];
|
|
46
|
+
if (files.length > 0) {
|
|
47
|
+
md.push('### Files Modified');
|
|
48
|
+
md.push('');
|
|
49
|
+
for (const f of files) md.push(`- \`${f}\``);
|
|
50
|
+
md.push('');
|
|
51
|
+
}
|
|
52
|
+
} catch { /* skip */ }
|
|
53
|
+
|
|
54
|
+
// Review
|
|
55
|
+
try {
|
|
56
|
+
const review = JSON.parse(context.reviewJson ?? '{}') as Record<string, string>;
|
|
57
|
+
if (review.intent || review.execution || review.result || review.completeness) {
|
|
58
|
+
md.push('### Review');
|
|
59
|
+
md.push('');
|
|
60
|
+
for (const key of ['intent', 'execution', 'result', 'completeness']) {
|
|
61
|
+
if (review[key]) md.push(`- **${key}:** ${review[key]}`);
|
|
62
|
+
}
|
|
63
|
+
if (review.reason) md.push(`\n${review.reason}`);
|
|
64
|
+
md.push('');
|
|
65
|
+
}
|
|
66
|
+
} catch { /* skip */ }
|
|
67
|
+
|
|
68
|
+
// Meta
|
|
69
|
+
const meta: string[] = [];
|
|
70
|
+
if (result.executionTime) meta.push(`**Duration:** ${result.executionTime}s`);
|
|
71
|
+
if (context.env.providerInfo?.model) meta.push(`**Model:** ${context.env.providerInfo.model}`);
|
|
72
|
+
if (meta.length > 0) md.push(meta.join(' | '));
|
|
73
|
+
|
|
74
|
+
return { summary: md.join('\n') };
|
|
27
75
|
}
|
|
@@ -76,6 +76,9 @@ export function useStreamTimeline(events: StreamEvent[], isDone: boolean): Strea
|
|
|
76
76
|
// Map nodeId → index in entries[] so we can replace start with complete
|
|
77
77
|
const nodeEntryIndex = new Map<string, number>();
|
|
78
78
|
const nodeStarts = new Map<string, number>();
|
|
79
|
+
// Track completed/failed nodes to skip duplicate completion events
|
|
80
|
+
// (FW runtime emits 2x STATUS_CHANGED per node transition)
|
|
81
|
+
const completedNodes = new Set<string>();
|
|
79
82
|
let idCounter = 0;
|
|
80
83
|
|
|
81
84
|
for (const event of events) {
|
|
@@ -95,6 +98,17 @@ export function useStreamTimeline(events: StreamEvent[], isDone: boolean): Strea
|
|
|
95
98
|
case 'node-start': {
|
|
96
99
|
const nodeId = d.nodeId as string;
|
|
97
100
|
if (nodeId) nodeStarts.set(nodeId, event.timestamp);
|
|
101
|
+
|
|
102
|
+
// If we already have an in-flight entry for this node (duplicate
|
|
103
|
+
// STATUS_CHANGED from the FW runtime), skip the duplicate.
|
|
104
|
+
const existingStartIdx = nodeEntryIndex.get(nodeId);
|
|
105
|
+
if (existingStartIdx != null && entries[existingStartIdx]?.type === 'node-started') {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Clear completed tracking so retry loops work (node can re-enter)
|
|
110
|
+
completedNodes.delete(nodeId);
|
|
111
|
+
|
|
98
112
|
const idx = entries.length;
|
|
99
113
|
nodeEntryIndex.set(nodeId, idx);
|
|
100
114
|
entries.push({
|
|
@@ -111,65 +125,91 @@ export function useStreamTimeline(events: StreamEvent[], isDone: boolean): Strea
|
|
|
111
125
|
|
|
112
126
|
case 'node-complete': {
|
|
113
127
|
const nodeId = d.nodeId as string;
|
|
128
|
+
// Skip duplicate completion events for the same node
|
|
129
|
+
if (completedNodes.has(nodeId)) break;
|
|
130
|
+
|
|
114
131
|
const startTs = nodeStarts.get(nodeId);
|
|
115
132
|
const duration =
|
|
116
133
|
(d.durationMs as number) ?? (startTs ? event.timestamp - startTs : undefined);
|
|
117
134
|
if (nodeId) nodeStarts.delete(nodeId);
|
|
118
|
-
|
|
119
|
-
const rawOutputs = d.outputs as Array<{ portLabel: string; value: unknown }> | undefined;
|
|
120
|
-
|
|
121
|
-
const completed: TimelineEntry = {
|
|
122
|
-
id: `s-${idCounter++}`,
|
|
123
|
-
timestamp: new Date(startTs ?? event.timestamp),
|
|
124
|
-
type: 'node-completed',
|
|
125
|
-
nodeId,
|
|
126
|
-
label: (d.label as string) ?? (d.nodeType as string) ?? nodeId ?? 'Node',
|
|
127
|
-
duration,
|
|
128
|
-
color: d.color as string | undefined,
|
|
129
|
-
icon: d.icon as string | undefined,
|
|
130
|
-
outputs: rawOutputs && rawOutputs.length > 0 ? rawOutputs : undefined,
|
|
131
|
-
};
|
|
135
|
+
completedNodes.add(nodeId);
|
|
132
136
|
|
|
133
137
|
// Replace the node-started entry in-place
|
|
134
138
|
const existingIdx = nodeEntryIndex.get(nodeId);
|
|
135
139
|
if (existingIdx != null && entries[existingIdx]) {
|
|
136
|
-
|
|
140
|
+
const rawOutputs = d.outputs as Array<{ portLabel: string; value: unknown }> | undefined;
|
|
141
|
+
entries[existingIdx] = {
|
|
142
|
+
id: entries[existingIdx]!.id,
|
|
143
|
+
timestamp: new Date(startTs ?? event.timestamp),
|
|
144
|
+
type: 'node-completed',
|
|
145
|
+
nodeId,
|
|
146
|
+
label: (d.label as string) ?? (d.nodeType as string) ?? nodeId ?? 'Node',
|
|
147
|
+
duration,
|
|
148
|
+
color: d.color as string | undefined,
|
|
149
|
+
icon: d.icon as string | undefined,
|
|
150
|
+
outputs: rawOutputs && rawOutputs.length > 0 ? rawOutputs : undefined,
|
|
151
|
+
};
|
|
137
152
|
nodeEntryIndex.delete(nodeId);
|
|
138
153
|
} else {
|
|
139
|
-
|
|
154
|
+
// No matching start entry — standalone complete (e.g. Start node)
|
|
155
|
+
const rawOutputs = d.outputs as Array<{ portLabel: string; value: unknown }> | undefined;
|
|
156
|
+
entries.push({
|
|
157
|
+
id: `s-${idCounter++}`,
|
|
158
|
+
timestamp: new Date(startTs ?? event.timestamp),
|
|
159
|
+
type: 'node-completed',
|
|
160
|
+
nodeId,
|
|
161
|
+
label: (d.label as string) ?? (d.nodeType as string) ?? nodeId ?? 'Node',
|
|
162
|
+
duration,
|
|
163
|
+
color: d.color as string | undefined,
|
|
164
|
+
icon: d.icon as string | undefined,
|
|
165
|
+
outputs: rawOutputs && rawOutputs.length > 0 ? rawOutputs : undefined,
|
|
166
|
+
});
|
|
140
167
|
}
|
|
141
168
|
break;
|
|
142
169
|
}
|
|
143
170
|
|
|
144
171
|
case 'node-error': {
|
|
145
172
|
const nodeId = d.nodeId as string;
|
|
173
|
+
if (completedNodes.has(nodeId)) break; // skip duplicate
|
|
174
|
+
|
|
146
175
|
const startTs = nodeStarts.get(nodeId);
|
|
147
176
|
const duration =
|
|
148
177
|
(d.durationMs as number) ?? (startTs ? event.timestamp - startTs : undefined);
|
|
149
178
|
if (nodeId) nodeStarts.delete(nodeId);
|
|
150
|
-
|
|
151
|
-
const rawOutputs = d.outputs as Array<{ portLabel: string; value: unknown }> | undefined;
|
|
152
|
-
|
|
153
|
-
const failed: TimelineEntry = {
|
|
154
|
-
id: `s-${idCounter++}`,
|
|
155
|
-
timestamp: new Date(startTs ?? event.timestamp),
|
|
156
|
-
type: 'node-failed',
|
|
157
|
-
nodeId,
|
|
158
|
-
label: (d.label as string) ?? (d.nodeType as string) ?? nodeId ?? 'Node',
|
|
159
|
-
detail: d.error as string | undefined,
|
|
160
|
-
duration,
|
|
161
|
-
color: d.color as string | undefined,
|
|
162
|
-
icon: d.icon as string | undefined,
|
|
163
|
-
outputs: rawOutputs && rawOutputs.length > 0 ? rawOutputs : undefined,
|
|
164
|
-
};
|
|
179
|
+
completedNodes.add(nodeId);
|
|
165
180
|
|
|
166
181
|
// Replace the node-started entry in-place
|
|
167
182
|
const existingIdx = nodeEntryIndex.get(nodeId);
|
|
168
183
|
if (existingIdx != null && entries[existingIdx]) {
|
|
169
|
-
|
|
184
|
+
|
|
185
|
+
const rawOutputs = d.outputs as Array<{ portLabel: string; value: unknown }> | undefined;
|
|
186
|
+
entries[existingIdx] = {
|
|
187
|
+
id: entries[existingIdx]!.id,
|
|
188
|
+
timestamp: new Date(startTs ?? event.timestamp),
|
|
189
|
+
type: 'node-failed',
|
|
190
|
+
nodeId,
|
|
191
|
+
label: (d.label as string) ?? (d.nodeType as string) ?? nodeId ?? 'Node',
|
|
192
|
+
detail: d.error as string | undefined,
|
|
193
|
+
duration,
|
|
194
|
+
color: d.color as string | undefined,
|
|
195
|
+
icon: d.icon as string | undefined,
|
|
196
|
+
outputs: rawOutputs && rawOutputs.length > 0 ? rawOutputs : undefined,
|
|
197
|
+
};
|
|
170
198
|
nodeEntryIndex.delete(nodeId);
|
|
171
199
|
} else {
|
|
172
|
-
|
|
200
|
+
const rawOutputs = d.outputs as Array<{ portLabel: string; value: unknown }> | undefined;
|
|
201
|
+
entries.push({
|
|
202
|
+
id: `s-${idCounter++}`,
|
|
203
|
+
timestamp: new Date(startTs ?? event.timestamp),
|
|
204
|
+
type: 'node-failed',
|
|
205
|
+
nodeId,
|
|
206
|
+
label: (d.label as string) ?? (d.nodeType as string) ?? nodeId ?? 'Node',
|
|
207
|
+
detail: d.error as string | undefined,
|
|
208
|
+
duration,
|
|
209
|
+
color: d.color as string | undefined,
|
|
210
|
+
icon: d.icon as string | undefined,
|
|
211
|
+
outputs: rawOutputs && rawOutputs.length > 0 ? rawOutputs : undefined,
|
|
212
|
+
});
|
|
173
213
|
}
|
|
174
214
|
break;
|
|
175
215
|
}
|