@synergenius/flow-weaver-pack-weaver 0.9.159 → 0.9.164
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.d.ts.map +1 -1
- package/dist/ai-chat-provider.js +17 -11
- package/dist/ai-chat-provider.js.map +1 -1
- package/dist/bot/ai-router.js +5 -5
- package/dist/bot/ai-router.js.map +1 -1
- package/dist/bot/assistant-tools.d.ts.map +1 -1
- package/dist/bot/assistant-tools.js +6 -7
- 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 +37 -14
- package/dist/bot/capability-registry.js.map +1 -1
- package/dist/bot/dashboard.js +1 -1
- package/dist/bot/dashboard.js.map +1 -1
- package/dist/bot/index.d.ts +1 -1
- package/dist/bot/index.d.ts.map +1 -1
- package/dist/bot/index.js.map +1 -1
- package/dist/bot/instance-manager.js +3 -3
- package/dist/bot/instance-manager.js.map +1 -1
- package/dist/bot/profile-store.d.ts.map +1 -1
- package/dist/bot/profile-store.js +11 -9
- package/dist/bot/profile-store.js.map +1 -1
- package/dist/bot/profile-types.d.ts +2 -2
- package/dist/bot/profile-types.d.ts.map +1 -1
- package/dist/bot/runner.d.ts +1 -0
- package/dist/bot/runner.d.ts.map +1 -1
- package/dist/bot/runner.js +6 -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 +10 -0
- package/dist/bot/step-executor.js.map +1 -1
- package/dist/bot/swarm-controller.d.ts +3 -5
- package/dist/bot/swarm-controller.d.ts.map +1 -1
- package/dist/bot/swarm-controller.js +157 -74
- package/dist/bot/swarm-controller.js.map +1 -1
- package/dist/bot/task-prompt-builder.d.ts +2 -3
- package/dist/bot/task-prompt-builder.d.ts.map +1 -1
- package/dist/bot/task-prompt-builder.js +81 -67
- package/dist/bot/task-prompt-builder.js.map +1 -1
- package/dist/bot/task-store.d.ts +3 -3
- package/dist/bot/task-store.d.ts.map +1 -1
- package/dist/bot/task-store.js +89 -75
- package/dist/bot/task-store.js.map +1 -1
- package/dist/bot/task-types.d.ts +54 -26
- package/dist/bot/task-types.d.ts.map +1 -1
- package/dist/bot/task-types.js +6 -2
- package/dist/bot/task-types.js.map +1 -1
- package/dist/bot/types.d.ts +2 -0
- package/dist/bot/types.d.ts.map +1 -1
- package/dist/bot/weaver-tools.d.ts.map +1 -1
- package/dist/bot/weaver-tools.js +10 -0
- package/dist/bot/weaver-tools.js.map +1 -1
- package/dist/cli-handlers.d.ts +0 -1
- package/dist/cli-handlers.d.ts.map +1 -1
- package/dist/cli-handlers.js +5 -9
- package/dist/cli-handlers.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/node-types/agent-execute.d.ts.map +1 -1
- package/dist/node-types/agent-execute.js +95 -63
- package/dist/node-types/agent-execute.js.map +1 -1
- package/dist/node-types/plan-task.js +8 -8
- package/dist/node-types/plan-task.js.map +1 -1
- package/dist/ui/bot-panel.js +1 -1
- package/dist/ui/capability-editor.js +37 -14
- package/dist/ui/chat-task-result.js +1 -7
- package/dist/ui/profile-editor.js +37 -14
- package/dist/ui/swarm-controls.js +2 -2
- package/dist/ui/swarm-dashboard.js +72 -109
- package/dist/ui/task-detail-view.js +21 -42
- package/dist/ui/task-editor.js +13 -50
- package/dist/ui/task-pool-list.js +0 -2
- package/flowweaver.manifest.json +1 -1
- package/package.json +1 -1
- package/src/ai-chat-provider.ts +15 -11
- package/src/bot/ai-router.ts +5 -5
- package/src/bot/assistant-tools.ts +6 -7
- package/src/bot/capability-registry.ts +37 -14
- package/src/bot/dashboard.ts +1 -1
- package/src/bot/index.ts +5 -1
- package/src/bot/instance-manager.ts +3 -3
- package/src/bot/profile-store.ts +12 -10
- package/src/bot/profile-types.ts +2 -2
- package/src/bot/runner.ts +6 -2
- package/src/bot/step-executor.ts +11 -0
- package/src/bot/swarm-controller.ts +164 -78
- package/src/bot/task-prompt-builder.ts +86 -71
- package/src/bot/task-store.ts +101 -78
- package/src/bot/task-types.ts +81 -36
- package/src/bot/types.ts +2 -0
- package/src/bot/weaver-tools.ts +11 -0
- package/src/cli-handlers.ts +5 -9
- package/src/index.ts +6 -0
- package/src/node-types/agent-execute.ts +99 -62
- package/src/node-types/plan-task.ts +8 -8
- package/src/ui/bot-panel.tsx +3 -3
- package/src/ui/chat-task-result.tsx +5 -14
- package/src/ui/swarm-controls.tsx +3 -3
- package/src/ui/swarm-dashboard.tsx +3 -3
- package/src/ui/task-detail-view.tsx +29 -52
- package/src/ui/task-editor.tsx +14 -51
- package/src/ui/task-pool-list.tsx +1 -3
|
@@ -27,7 +27,7 @@ import { InstanceManager } from './instance-manager.js';
|
|
|
27
27
|
import { ProfileStore } from './profile-store.js';
|
|
28
28
|
import type { BotProfile, BotInstance, OrchestratorInput, OrchestratorDecision, ProfileBehavior } from './profile-types.js';
|
|
29
29
|
import { buildDefaultBehavior, adjustBehaviorForComplexity } from './behavior-defaults.js';
|
|
30
|
-
import type { Task,
|
|
30
|
+
import type { Task, RunProgress } from './task-types.js';
|
|
31
31
|
import type { WorkflowResult } from './types.js';
|
|
32
32
|
|
|
33
33
|
// ---------------------------------------------------------------------------
|
|
@@ -49,12 +49,9 @@ export interface SwarmState {
|
|
|
49
49
|
|
|
50
50
|
budgets: SwarmBudgets;
|
|
51
51
|
|
|
52
|
-
autoRetry: boolean;
|
|
53
|
-
maxAttemptsDefault: number;
|
|
54
|
-
|
|
55
52
|
// Stats
|
|
56
53
|
tasksCompleted: number;
|
|
57
|
-
|
|
54
|
+
runsIncomplete: number;
|
|
58
55
|
totalTokensUsed: number;
|
|
59
56
|
totalCost: number;
|
|
60
57
|
}
|
|
@@ -65,8 +62,6 @@ export interface SwarmConfig {
|
|
|
65
62
|
sessionBudgetCost?: number;
|
|
66
63
|
workspaceBudgetTokens?: number;
|
|
67
64
|
workspaceBudgetCost?: number;
|
|
68
|
-
autoRetry?: boolean;
|
|
69
|
-
maxAttemptsDefault?: number;
|
|
70
65
|
}
|
|
71
66
|
|
|
72
67
|
export interface SwarmStartConfig {
|
|
@@ -80,7 +75,7 @@ export interface SwarmStartConfig {
|
|
|
80
75
|
// ---------------------------------------------------------------------------
|
|
81
76
|
|
|
82
77
|
const DISPATCH_LOOP_SLEEP_MS = 2000;
|
|
83
|
-
|
|
78
|
+
// No TASK_TIMEOUT_MS — AI call timeout (10min) in the worker is the only boundary.
|
|
84
79
|
const SWARM_STATE_FILE = 'swarm.json';
|
|
85
80
|
|
|
86
81
|
// ---------------------------------------------------------------------------
|
|
@@ -277,8 +272,6 @@ export class SwarmController {
|
|
|
277
272
|
if (config.sessionBudgetCost !== undefined) this.state.budgets.session.limitCost = config.sessionBudgetCost;
|
|
278
273
|
if (config.workspaceBudgetTokens !== undefined) this.state.budgets.workspace.limitTokens = config.workspaceBudgetTokens;
|
|
279
274
|
if (config.workspaceBudgetCost !== undefined) this.state.budgets.workspace.limitCost = config.workspaceBudgetCost;
|
|
280
|
-
if (config.autoRetry !== undefined) this.state.autoRetry = config.autoRetry;
|
|
281
|
-
if (config.maxAttemptsDefault !== undefined) this.state.maxAttemptsDefault = config.maxAttemptsDefault;
|
|
282
275
|
this._persist();
|
|
283
276
|
}
|
|
284
277
|
|
|
@@ -295,20 +288,13 @@ export class SwarmController {
|
|
|
295
288
|
}
|
|
296
289
|
}
|
|
297
290
|
|
|
298
|
-
// Reset in-progress tasks
|
|
291
|
+
// Reset in-progress tasks back to open
|
|
299
292
|
const tasks = await this.taskStore.list({ status: 'in-progress' });
|
|
300
293
|
for (const task of tasks) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
});
|
|
306
|
-
} else {
|
|
307
|
-
await this.taskStore.update(task.id, {
|
|
308
|
-
status: 'open',
|
|
309
|
-
currentBotId: undefined,
|
|
310
|
-
});
|
|
311
|
-
}
|
|
294
|
+
await this.taskStore.update(task.id, {
|
|
295
|
+
status: 'open',
|
|
296
|
+
activeRunId: undefined,
|
|
297
|
+
});
|
|
312
298
|
}
|
|
313
299
|
|
|
314
300
|
// Reset swarm status to idle
|
|
@@ -321,6 +307,66 @@ export class SwarmController {
|
|
|
321
307
|
// Token/cost recording
|
|
322
308
|
// -----------------------------------------------------------------------
|
|
323
309
|
|
|
310
|
+
private _buildBasicRunProgress(
|
|
311
|
+
runId: string, workerId: string, profileId: string,
|
|
312
|
+
result: { success?: boolean; summary?: string },
|
|
313
|
+
tokensUsed: number, costUsed: number, durationMs: number,
|
|
314
|
+
): RunProgress {
|
|
315
|
+
// Determine outcome: zero-work runs (0 tokens, short duration) are stalled, not contributed
|
|
316
|
+
let outcome: 'completed' | 'contributed' | 'stalled' | 'crashed';
|
|
317
|
+
if (result.success) {
|
|
318
|
+
outcome = 'completed';
|
|
319
|
+
} else if (tokensUsed === 0 && durationMs < 10_000) {
|
|
320
|
+
// Ran for less than 10s with no AI tokens — something failed before work started
|
|
321
|
+
outcome = 'stalled';
|
|
322
|
+
} else {
|
|
323
|
+
outcome = 'contributed';
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
runId,
|
|
328
|
+
botId: workerId,
|
|
329
|
+
profileId,
|
|
330
|
+
outcome,
|
|
331
|
+
filesCreated: [],
|
|
332
|
+
filesModified: [],
|
|
333
|
+
summary: result.summary || (outcome === 'completed' ? 'Completed' : outcome === 'stalled' ? 'Stalled (no AI work)' : 'Contributed'),
|
|
334
|
+
tokensUsed,
|
|
335
|
+
cost: costUsed,
|
|
336
|
+
durationMs,
|
|
337
|
+
endedAt: new Date().toISOString(),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private async _checkAcceptance(task: Task): Promise<import('./task-types.js').AcceptanceResult> {
|
|
342
|
+
const criteria = task.acceptance;
|
|
343
|
+
if (!criteria || !criteria.checks.length) {
|
|
344
|
+
return { met: true, results: [], checkedAt: new Date().toISOString() };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const { execSync } = await import('node:child_process');
|
|
348
|
+
const results: Array<{ name: string; pass: boolean; detail?: string }> = [];
|
|
349
|
+
|
|
350
|
+
for (const check of criteria.checks) {
|
|
351
|
+
try {
|
|
352
|
+
execSync(check.command, {
|
|
353
|
+
cwd: this.projectDir,
|
|
354
|
+
encoding: 'utf-8',
|
|
355
|
+
timeout: 60_000,
|
|
356
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
357
|
+
});
|
|
358
|
+
results.push({ name: check.name, pass: true });
|
|
359
|
+
} catch (err: unknown) {
|
|
360
|
+
const e = err as { stdout?: string; stderr?: string; message?: string };
|
|
361
|
+
const detail = (e.stdout ?? e.stderr ?? e.message ?? '').slice(0, 500);
|
|
362
|
+
results.push({ name: check.name, pass: false, detail });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const met = results.every(r => r.pass);
|
|
367
|
+
return { met, results, checkedAt: new Date().toISOString() };
|
|
368
|
+
}
|
|
369
|
+
|
|
324
370
|
recordTokenUsage(workerId: string, _taskId: string, tokensUsed: number, cost: number): void {
|
|
325
371
|
// Update worker in InstanceManager
|
|
326
372
|
try {
|
|
@@ -419,7 +465,7 @@ export class SwarmController {
|
|
|
419
465
|
}
|
|
420
466
|
|
|
421
467
|
// Route unassigned tasks: top-level → orchestrator, subtasks → developer (fallback)
|
|
422
|
-
const unassignedTasks = await this.taskStore.list({ status:
|
|
468
|
+
const unassignedTasks = await this.taskStore.list({ status: 'open' });
|
|
423
469
|
for (const t of unassignedTasks) {
|
|
424
470
|
if (!t.assignedProfile && !t.isParent) {
|
|
425
471
|
// Top-level tasks (no parent) go to orchestrator for decomposition.
|
|
@@ -430,17 +476,26 @@ export class SwarmController {
|
|
|
430
476
|
}
|
|
431
477
|
}
|
|
432
478
|
|
|
433
|
-
// Collect
|
|
434
|
-
const pendingTasks = await this.taskStore.list({ status:
|
|
479
|
+
// Collect open tasks (and all tasks for dependency checks)
|
|
480
|
+
const pendingTasks = await this.taskStore.list({ status: 'open' });
|
|
435
481
|
const allTasks = await this.taskStore.list();
|
|
436
482
|
const routableTasks = pendingTasks.filter((t) => {
|
|
437
483
|
// Skip parent tasks
|
|
438
484
|
if (t.isParent) return false;
|
|
439
|
-
// Skip tasks
|
|
440
|
-
if (t.
|
|
441
|
-
// Skip tasks
|
|
442
|
-
|
|
443
|
-
if (t.
|
|
485
|
+
// Skip tasks with exhausted budget
|
|
486
|
+
if (t.context.budgetExhausted) return false;
|
|
487
|
+
// Skip tasks in rapid-loop cooldown: if last run ended recently AND was zero-work,
|
|
488
|
+
// apply exponential backoff based on stagnation count
|
|
489
|
+
if (t.context.runHistory.length > 0) {
|
|
490
|
+
const lastRun = t.context.runHistory[t.context.runHistory.length - 1];
|
|
491
|
+
if ('endedAt' in lastRun && lastRun.endedAt) {
|
|
492
|
+
const secsSinceLastRun = (Date.now() - new Date(lastRun.endedAt).getTime()) / 1000;
|
|
493
|
+
const stag = t.context.stagnationCount;
|
|
494
|
+
// Exponential backoff: 10s, 20s, 40s, 80s, 160s... based on stagnation
|
|
495
|
+
const cooldownSecs = stag > 0 ? Math.min(10 * Math.pow(2, stag - 1), 300) : 0;
|
|
496
|
+
if (secsSinceLastRun < cooldownSecs) return false;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
444
499
|
// Skip tasks whose dependencies are not all done
|
|
445
500
|
if (t.dependsOn && t.dependsOn.length > 0) {
|
|
446
501
|
const allDepsDone = t.dependsOn.every((depId) => {
|
|
@@ -453,7 +508,7 @@ export class SwarmController {
|
|
|
453
508
|
);
|
|
454
509
|
return true;
|
|
455
510
|
}
|
|
456
|
-
return dep.status === 'done';
|
|
511
|
+
return dep.status === 'done' || dep.status === 'cancelled';
|
|
457
512
|
});
|
|
458
513
|
if (!allDepsDone) return false;
|
|
459
514
|
}
|
|
@@ -484,9 +539,9 @@ export class SwarmController {
|
|
|
484
539
|
complexity: t.complexity ?? 'moderate',
|
|
485
540
|
assignedProfile: t.assignedProfile,
|
|
486
541
|
context: {
|
|
487
|
-
|
|
488
|
-
outcome:
|
|
489
|
-
botId:
|
|
542
|
+
runHistory: t.context.runHistory.map((rp) => ({
|
|
543
|
+
outcome: rp.outcome,
|
|
544
|
+
botId: rp.botId,
|
|
490
545
|
})),
|
|
491
546
|
},
|
|
492
547
|
})),
|
|
@@ -550,7 +605,7 @@ export class SwarmController {
|
|
|
550
605
|
// Mark worker as executing with the task's profileId
|
|
551
606
|
const runId = RunStore.newId();
|
|
552
607
|
this.instanceManager.markExecuting(assignment.instanceId, assignment.taskId, runId, assignment.profileId);
|
|
553
|
-
await this.taskStore.update(assignment.taskId, {
|
|
608
|
+
await this.taskStore.update(assignment.taskId, { activeRunId: runId });
|
|
554
609
|
|
|
555
610
|
// Sync state for dashboard
|
|
556
611
|
this._syncInstancesState();
|
|
@@ -644,9 +699,8 @@ export class SwarmController {
|
|
|
644
699
|
mode: task.context.files.length > 0 ? 'modify' : 'create',
|
|
645
700
|
targets: task.context.files.length > 0 ? task.context.files : undefined,
|
|
646
701
|
options: { autoApprove: true },
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
runSummaries: task.context.runSummaries,
|
|
702
|
+
runHistory: task.context.runHistory,
|
|
703
|
+
stagnationCount: task.context.stagnationCount,
|
|
650
704
|
});
|
|
651
705
|
|
|
652
706
|
// Build behavior config from profile preferences, adjusted for task complexity.
|
|
@@ -664,20 +718,16 @@ export class SwarmController {
|
|
|
664
718
|
const workflowPath = path.isAbsolute(bot.filePath)
|
|
665
719
|
? bot.filePath
|
|
666
720
|
: path.resolve(this.projectDir, bot.filePath);
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
eventLog: runEventLog,
|
|
678
|
-
}),
|
|
679
|
-
timeoutPromise,
|
|
680
|
-
]);
|
|
721
|
+
// No swarm-level timeout race — the AI call timeout (10min) in the worker
|
|
722
|
+
// is the only boundary. This prevents orphaned runs from timeout races.
|
|
723
|
+
result = await runWorkflow(workflowPath, {
|
|
724
|
+
runId,
|
|
725
|
+
taskId,
|
|
726
|
+
botId: workerId,
|
|
727
|
+
config: { provider: 'auto' },
|
|
728
|
+
params: { taskJson, projectDir: this.projectDir, behaviorJson },
|
|
729
|
+
eventLog: runEventLog,
|
|
730
|
+
});
|
|
681
731
|
|
|
682
732
|
runEventLog.done();
|
|
683
733
|
} catch (err) {
|
|
@@ -690,26 +740,64 @@ export class SwarmController {
|
|
|
690
740
|
|
|
691
741
|
const durationMs = Date.now() - startTime;
|
|
692
742
|
|
|
693
|
-
// Extract
|
|
743
|
+
// Extract RunProgress from the workflow context if available (built by agent-execute),
|
|
744
|
+
// otherwise build a basic one from the WorkflowResult.
|
|
694
745
|
const tokensUsed = Math.max(0, (result.cost?.totalInputTokens ?? 0) + (result.cost?.totalOutputTokens ?? 0));
|
|
695
746
|
const costUsed = Math.max(0, result.cost?.totalCost ?? 0);
|
|
696
747
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
748
|
+
let runProgress: RunProgress;
|
|
749
|
+
const ctxRunProgress = result.runProgressJson;
|
|
750
|
+
if (ctxRunProgress) {
|
|
751
|
+
try {
|
|
752
|
+
const parsed = JSON.parse(ctxRunProgress);
|
|
753
|
+
runProgress = {
|
|
754
|
+
runId,
|
|
755
|
+
botId: workerId,
|
|
756
|
+
profileId: profile.id,
|
|
757
|
+
outcome: parsed.outcome ?? (result.success ? 'completed' : 'contributed'),
|
|
758
|
+
filesCreated: parsed.filesCreated ?? [],
|
|
759
|
+
filesModified: parsed.filesModified ?? [],
|
|
760
|
+
summary: parsed.summary ?? result.summary ?? '',
|
|
761
|
+
remainingWork: parsed.remainingWork,
|
|
762
|
+
blockers: parsed.blockers,
|
|
763
|
+
checks: parsed.checks,
|
|
764
|
+
tokensUsed: parsed.tokensUsed ?? tokensUsed,
|
|
765
|
+
cost: parsed.cost ?? costUsed,
|
|
766
|
+
durationMs: parsed.durationMs ?? durationMs,
|
|
767
|
+
endedAt: new Date().toISOString(),
|
|
768
|
+
};
|
|
769
|
+
} catch {
|
|
770
|
+
// JSON parse failed — fall back to basic
|
|
771
|
+
runProgress = this._buildBasicRunProgress(runId, workerId, profile.id, result, tokensUsed, costUsed, durationMs);
|
|
772
|
+
}
|
|
773
|
+
} else {
|
|
774
|
+
runProgress = this._buildBasicRunProgress(runId, workerId, profile.id, result, tokensUsed, costUsed, durationMs);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Determine release status based on convergent model:
|
|
778
|
+
// - completed + no acceptance → done
|
|
779
|
+
// - completed + has acceptance → run acceptance check, done if met, open if not
|
|
780
|
+
// - contributed/stalled/crashed → open (task stays available for next run)
|
|
781
|
+
const task = await this.taskStore.get(taskId);
|
|
782
|
+
let releaseStatus: 'done' | 'open' = 'open';
|
|
783
|
+
|
|
784
|
+
if (runProgress.outcome === 'completed') {
|
|
785
|
+
if (!task?.acceptance) {
|
|
786
|
+
releaseStatus = 'done';
|
|
787
|
+
} else {
|
|
788
|
+
// Run deterministic acceptance check
|
|
789
|
+
const acceptResult = await this._checkAcceptance(task);
|
|
790
|
+
if (acceptResult.met) {
|
|
791
|
+
releaseStatus = 'done';
|
|
792
|
+
}
|
|
793
|
+
// Store the result on the task regardless
|
|
794
|
+
if (task) {
|
|
795
|
+
await this.taskStore.update(taskId, { lastAcceptanceCheck: acceptResult } as Record<string, unknown>);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
709
799
|
|
|
710
|
-
|
|
711
|
-
const releaseStatus = result.success ? 'done' : 'open';
|
|
712
|
-
await this.taskStore.release(taskId, releaseStatus, runSummary);
|
|
800
|
+
await this.taskStore.release(taskId, releaseStatus, runProgress);
|
|
713
801
|
|
|
714
802
|
// Record token usage
|
|
715
803
|
this.recordTokenUsage(workerId, taskId, tokensUsed, costUsed);
|
|
@@ -718,7 +806,7 @@ export class SwarmController {
|
|
|
718
806
|
if (result.success) {
|
|
719
807
|
this.state.tasksCompleted += 1;
|
|
720
808
|
} else {
|
|
721
|
-
this.state.
|
|
809
|
+
this.state.runsIncomplete += 1;
|
|
722
810
|
}
|
|
723
811
|
|
|
724
812
|
// Mark worker as idle (clears profile association)
|
|
@@ -728,9 +816,9 @@ export class SwarmController {
|
|
|
728
816
|
|
|
729
817
|
// Emit swarm-level task event
|
|
730
818
|
this.eventLog.emit({
|
|
731
|
-
type: result.success ? 'task-done' : 'task-
|
|
819
|
+
type: result.success ? 'task-done' : 'task-run-incomplete',
|
|
732
820
|
timestamp: Date.now(),
|
|
733
|
-
data: { botId: workerId, taskId, outcome:
|
|
821
|
+
data: { botId: workerId, taskId, outcome: runProgress.outcome },
|
|
734
822
|
});
|
|
735
823
|
|
|
736
824
|
// Emit hierarchy-scoped event so sibling tasks can see what happened
|
|
@@ -739,13 +827,13 @@ export class SwarmController {
|
|
|
739
827
|
if (task?.parentId) {
|
|
740
828
|
this.hierarchyEventLog.emit({
|
|
741
829
|
parentId: task.parentId,
|
|
742
|
-
type: result.success ? 'task-completed' : 'task-run-
|
|
830
|
+
type: result.success ? 'task-completed' : 'task-run-incomplete',
|
|
743
831
|
taskId,
|
|
744
832
|
data: {
|
|
745
|
-
summary:
|
|
746
|
-
filesModified:
|
|
833
|
+
summary: runProgress.summary,
|
|
834
|
+
filesModified: runProgress.filesModified,
|
|
747
835
|
botId: workerId,
|
|
748
|
-
|
|
836
|
+
runCount: task.context.runHistory.length,
|
|
749
837
|
},
|
|
750
838
|
});
|
|
751
839
|
}
|
|
@@ -851,10 +939,8 @@ export class SwarmController {
|
|
|
851
939
|
workspace: { limitTokens: 0, usedTokens: 0, limitCost: 0, usedCost: 0 },
|
|
852
940
|
session: { limitTokens: 0, usedTokens: 0, limitCost: 0, usedCost: 0 },
|
|
853
941
|
},
|
|
854
|
-
autoRetry: true,
|
|
855
|
-
maxAttemptsDefault: 3,
|
|
856
942
|
tasksCompleted: 0,
|
|
857
|
-
|
|
943
|
+
runsIncomplete: 0,
|
|
858
944
|
totalTokensUsed: 0,
|
|
859
945
|
totalCost: 0,
|
|
860
946
|
};
|
|
@@ -6,25 +6,22 @@
|
|
|
6
6
|
* {description}
|
|
7
7
|
* ### Notes
|
|
8
8
|
* ### Relevant Files
|
|
9
|
-
* ### Previous
|
|
10
|
-
* ### Last Error
|
|
9
|
+
* ### Previous Runs (context decay: last run + acceptance check)
|
|
11
10
|
* ### Parent Context (title + description + sibling status)
|
|
12
11
|
*
|
|
13
12
|
* Truncation cascade when over budget:
|
|
14
|
-
* 1. Older
|
|
13
|
+
* 1. Older runs → one-liners only
|
|
15
14
|
* 2. File list → only files from last 2 runs
|
|
16
15
|
* 3. Parent context → title only
|
|
17
16
|
* 4. Notes → first 500 chars
|
|
18
17
|
*/
|
|
19
18
|
|
|
20
|
-
import type { Task,
|
|
19
|
+
import type { Task, RunProgress } from './task-types.js';
|
|
21
20
|
import type { ProfilePreferences } from './profile-types.js';
|
|
22
21
|
|
|
23
22
|
const MAX_CONTEXT_TOKENS = 4000;
|
|
24
23
|
const CHARS_PER_TOKEN = 4; // rough estimate
|
|
25
24
|
const MAX_CONTEXT_CHARS = MAX_CONTEXT_TOKENS * CHARS_PER_TOKEN;
|
|
26
|
-
const FULL_SUMMARY_COUNT = 3;
|
|
27
|
-
|
|
28
25
|
export function buildTaskPrompt(
|
|
29
26
|
task: Task,
|
|
30
27
|
parentTask?: Task | null,
|
|
@@ -67,28 +64,49 @@ function buildFull(
|
|
|
67
64
|
sections.push(`### Relevant Files\n${task.context.files.join('\n')}`);
|
|
68
65
|
}
|
|
69
66
|
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const lines: string[] = [];
|
|
74
|
-
const recentStart = Math.max(0, summaries.length - FULL_SUMMARY_COUNT);
|
|
67
|
+
// --- Context decay: workspace is the source of truth, not history ---
|
|
68
|
+
// Workers see: last acceptance check, last run's remainingWork/blockers,
|
|
69
|
+
// stagnation count, and a directive to read the workspace.
|
|
75
70
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
71
|
+
// 2.3.2: Last acceptance check result
|
|
72
|
+
if (task.lastAcceptanceCheck) {
|
|
73
|
+
const ac = task.lastAcceptanceCheck;
|
|
74
|
+
const checkLines = ac.results
|
|
75
|
+
.map(r => `- ${r.name}: ${r.pass ? 'PASS' : 'FAIL'}${r.detail ? ` (${r.detail.slice(0, 100)})` : ''}`)
|
|
76
|
+
.join('\n');
|
|
77
|
+
sections.push(`### Acceptance Check (${ac.met ? 'ALL PASS' : 'ISSUES FOUND'})\n${checkLines}`);
|
|
78
|
+
}
|
|
80
79
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
// 2.3.3: Continue from last run's remaining work
|
|
81
|
+
const lastRun = task.context.runHistory.length > 0
|
|
82
|
+
? task.context.runHistory[task.context.runHistory.length - 1]
|
|
83
|
+
: undefined;
|
|
84
|
+
if (lastRun && 'remainingWork' in lastRun && lastRun.remainingWork) {
|
|
85
|
+
sections.push(`### Continue From\n${lastRun.remainingWork}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 2.3.4: Blockers from last stalled run
|
|
89
|
+
if (lastRun && 'blockers' in lastRun && Array.isArray(lastRun.blockers) && lastRun.blockers.length > 0) {
|
|
90
|
+
sections.push(`### Previous Run Blocked By\n${(lastRun.blockers as string[]).map((b: string) => `- ${b}`).join('\n')}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Last run summary (one run only, not full history)
|
|
94
|
+
if (lastRun && 'summary' in lastRun) {
|
|
95
|
+
sections.push(`### Last Run\nOutcome: ${lastRun.outcome} | ${lastRun.summary}`);
|
|
96
|
+
}
|
|
85
97
|
|
|
86
|
-
|
|
98
|
+
// Run count + stagnation
|
|
99
|
+
if (task.context.runHistory.length > 0) {
|
|
100
|
+
let meta = `Total runs: ${task.context.runHistory.length}`;
|
|
101
|
+
if (task.context.stagnationCount > 0) {
|
|
102
|
+
meta += ` | Stagnation: ${task.context.stagnationCount} run(s) with no new changes — try a different approach`;
|
|
103
|
+
}
|
|
104
|
+
sections.push(`### Run History\n${meta}`);
|
|
87
105
|
}
|
|
88
106
|
|
|
89
|
-
//
|
|
90
|
-
if (task.context.
|
|
91
|
-
sections.push(`###
|
|
107
|
+
// Directive: read the workspace, don't rely on stale context
|
|
108
|
+
if (task.context.runHistory.length > 0) {
|
|
109
|
+
sections.push(`### Important\nRead the workspace to understand current state. Do not assume previous run summaries are accurate — files may have changed since then.`);
|
|
92
110
|
}
|
|
93
111
|
|
|
94
112
|
// Execution preferences
|
|
@@ -119,9 +137,9 @@ function truncatePrompt(
|
|
|
119
137
|
siblingTasks?: Task[],
|
|
120
138
|
preferences?: ProfilePreferences,
|
|
121
139
|
): string {
|
|
122
|
-
// Level 1: All
|
|
140
|
+
// Level 1: All runs condensed to one-liners
|
|
123
141
|
let result = buildWithTruncation(task, parentTask, siblingTasks, {
|
|
124
|
-
|
|
142
|
+
allRunsCondensed: true,
|
|
125
143
|
filesFromRecentRunsOnly: false,
|
|
126
144
|
parentTitleOnly: false,
|
|
127
145
|
notesCapped: false,
|
|
@@ -130,7 +148,7 @@ function truncatePrompt(
|
|
|
130
148
|
|
|
131
149
|
// Level 2: File list → only files from last 2 runs
|
|
132
150
|
result = buildWithTruncation(task, parentTask, siblingTasks, {
|
|
133
|
-
|
|
151
|
+
allRunsCondensed: true,
|
|
134
152
|
filesFromRecentRunsOnly: true,
|
|
135
153
|
parentTitleOnly: false,
|
|
136
154
|
notesCapped: false,
|
|
@@ -139,7 +157,7 @@ function truncatePrompt(
|
|
|
139
157
|
|
|
140
158
|
// Level 3: Parent context → title only
|
|
141
159
|
result = buildWithTruncation(task, parentTask, siblingTasks, {
|
|
142
|
-
|
|
160
|
+
allRunsCondensed: true,
|
|
143
161
|
filesFromRecentRunsOnly: true,
|
|
144
162
|
parentTitleOnly: true,
|
|
145
163
|
notesCapped: false,
|
|
@@ -148,7 +166,7 @@ function truncatePrompt(
|
|
|
148
166
|
|
|
149
167
|
// Level 4: Notes → first 500 chars
|
|
150
168
|
result = buildWithTruncation(task, parentTask, siblingTasks, {
|
|
151
|
-
|
|
169
|
+
allRunsCondensed: true,
|
|
152
170
|
filesFromRecentRunsOnly: true,
|
|
153
171
|
parentTitleOnly: true,
|
|
154
172
|
notesCapped: true,
|
|
@@ -163,7 +181,7 @@ function truncatePrompt(
|
|
|
163
181
|
}
|
|
164
182
|
|
|
165
183
|
interface TruncationFlags {
|
|
166
|
-
|
|
184
|
+
allRunsCondensed: boolean;
|
|
167
185
|
filesFromRecentRunsOnly: boolean;
|
|
168
186
|
parentTitleOnly: boolean;
|
|
169
187
|
notesCapped: boolean;
|
|
@@ -193,8 +211,13 @@ function buildWithTruncation(
|
|
|
193
211
|
// Files (potentially only from recent runs)
|
|
194
212
|
if (flags.filesFromRecentRunsOnly) {
|
|
195
213
|
const recentFiles = new Set<string>();
|
|
196
|
-
for (const
|
|
197
|
-
|
|
214
|
+
for (const rp of task.context.runHistory.slice(-2)) {
|
|
215
|
+
if ('filesModified' in rp) {
|
|
216
|
+
for (const f of rp.filesModified) recentFiles.add(f);
|
|
217
|
+
}
|
|
218
|
+
if ('filesCreated' in rp) {
|
|
219
|
+
for (const f of (rp as RunProgress).filesCreated) recentFiles.add(f);
|
|
220
|
+
}
|
|
198
221
|
}
|
|
199
222
|
if (recentFiles.size > 0) {
|
|
200
223
|
sections.push(`### Relevant Files\n${[...recentFiles].join('\n')}`);
|
|
@@ -203,31 +226,39 @@ function buildWithTruncation(
|
|
|
203
226
|
sections.push(`### Relevant Files\n${task.context.files.join('\n')}`);
|
|
204
227
|
}
|
|
205
228
|
|
|
206
|
-
//
|
|
207
|
-
if (task.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
229
|
+
// Context decay: last acceptance check + last run only
|
|
230
|
+
if (task.lastAcceptanceCheck) {
|
|
231
|
+
const ac = task.lastAcceptanceCheck;
|
|
232
|
+
const checkLines = ac.results
|
|
233
|
+
.map(r => `- ${r.name}: ${r.pass ? 'PASS' : 'FAIL'}`)
|
|
234
|
+
.join('\n');
|
|
235
|
+
sections.push(`### Acceptance Check (${ac.met ? 'ALL PASS' : 'ISSUES FOUND'})\n${checkLines}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const lastRunT = task.context.runHistory.length > 0
|
|
239
|
+
? task.context.runHistory[task.context.runHistory.length - 1]
|
|
240
|
+
: undefined;
|
|
241
|
+
if (lastRunT && 'remainingWork' in lastRunT && lastRunT.remainingWork) {
|
|
242
|
+
sections.push(`### Continue From\n${lastRunT.remainingWork}`);
|
|
243
|
+
}
|
|
244
|
+
if (lastRunT && 'blockers' in lastRunT && Array.isArray(lastRunT.blockers) && lastRunT.blockers.length > 0) {
|
|
245
|
+
sections.push(`### Previous Run Blocked By\n${(lastRunT.blockers as string[]).map((b: string) => `- ${b}`).join('\n')}`);
|
|
246
|
+
}
|
|
247
|
+
if (lastRunT && 'summary' in lastRunT) {
|
|
248
|
+
sections.push(`### Last Run\nOutcome: ${lastRunT.outcome} | ${lastRunT.summary}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (task.context.runHistory.length > 0) {
|
|
252
|
+
let meta = `Total runs: ${task.context.runHistory.length}`;
|
|
253
|
+
if (task.context.stagnationCount > 0) {
|
|
254
|
+
meta += ` | Stagnation: ${task.context.stagnationCount} — try a different approach`;
|
|
222
255
|
}
|
|
256
|
+
sections.push(`### Run History\n${meta}`);
|
|
223
257
|
}
|
|
224
258
|
|
|
225
|
-
//
|
|
226
|
-
if (task.context.
|
|
227
|
-
|
|
228
|
-
? task.context.lastError.slice(0, 300)
|
|
229
|
-
: task.context.lastError;
|
|
230
|
-
sections.push(`### Last Error\n${errorText}`);
|
|
259
|
+
// Directive: read the workspace, don't rely on stale context
|
|
260
|
+
if (task.context.runHistory.length > 0) {
|
|
261
|
+
sections.push(`### Important\nRead the workspace to understand current state. Do not assume previous run summaries are accurate — files may have changed since then.`);
|
|
231
262
|
}
|
|
232
263
|
|
|
233
264
|
// Execution preferences
|
|
@@ -256,27 +287,11 @@ function buildWithTruncation(
|
|
|
256
287
|
// Formatting helpers
|
|
257
288
|
// ---------------------------------------------------------------------------
|
|
258
289
|
|
|
259
|
-
function condenseSummary(s: RunSummary): string {
|
|
260
|
-
return `- Run ${s.runId.slice(0, 8)} (${s.botId}): ${s.outcome} — ${s.summary.slice(0, 80)} (${Math.round(s.durationMs / 1000)}s, ${s.tokensUsed} tok)`;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function fullSummary(s: RunSummary): string {
|
|
264
|
-
const lines: string[] = [];
|
|
265
|
-
lines.push(`\n**Run ${s.runId.slice(0, 8)} (${s.botId}) — ${s.outcome}**`);
|
|
266
|
-
lines.push(s.summary);
|
|
267
|
-
if (s.filesModified.length > 0) lines.push(`Files: ${s.filesModified.join(', ')}`);
|
|
268
|
-
if (s.error) lines.push(`Error: ${s.error}`);
|
|
269
|
-
lines.push(
|
|
270
|
-
`Duration: ${Math.round(s.durationMs / 1000)}s · Tokens: ${s.tokensUsed} · Cost: $${s.cost.toFixed(3)}`,
|
|
271
|
-
);
|
|
272
|
-
return lines.join('\n');
|
|
273
|
-
}
|
|
274
|
-
|
|
275
290
|
function buildParentSection(
|
|
276
291
|
parentTask: Task,
|
|
277
292
|
siblingTasks?: Task[],
|
|
278
293
|
): string {
|
|
279
|
-
let section = `### Parent Context\nPart of: "${parentTask.title}"
|
|
294
|
+
let section = `### Parent Context\nPart of: "${parentTask.title}" \u2014 ${parentTask.description}`;
|
|
280
295
|
if (siblingTasks && siblingTasks.length > 0) {
|
|
281
296
|
const siblingLines = siblingTasks
|
|
282
297
|
.map(
|
|
@@ -309,7 +324,7 @@ function statusIcon(status: string): string {
|
|
|
309
324
|
switch (status) {
|
|
310
325
|
case 'done':
|
|
311
326
|
return '\u2705';
|
|
312
|
-
case '
|
|
327
|
+
case 'cancelled':
|
|
313
328
|
return '\u274C';
|
|
314
329
|
case 'in-progress':
|
|
315
330
|
return '\u25CF';
|