@synergenius/flow-weaver-pack-weaver 0.9.195 → 0.9.197
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/ai-client.d.ts +5 -0
- package/dist/bot/ai-client.d.ts.map +1 -1
- package/dist/bot/ai-client.js +43 -0
- package/dist/bot/ai-client.js.map +1 -1
- package/dist/bot/behavior-defaults.d.ts +3 -1
- package/dist/bot/behavior-defaults.d.ts.map +1 -1
- package/dist/bot/behavior-defaults.js +7 -0
- package/dist/bot/behavior-defaults.js.map +1 -1
- package/dist/bot/capability-registry.js +1 -1
- package/dist/bot/dream-task.d.ts +45 -0
- package/dist/bot/dream-task.d.ts.map +1 -0
- package/dist/bot/dream-task.js +125 -0
- package/dist/bot/dream-task.js.map +1 -0
- package/dist/bot/knowledge-store.d.ts +9 -0
- package/dist/bot/knowledge-store.d.ts.map +1 -1
- package/dist/bot/knowledge-store.js +21 -0
- package/dist/bot/knowledge-store.js.map +1 -1
- package/dist/bot/post-turn-hooks.d.ts +57 -0
- package/dist/bot/post-turn-hooks.d.ts.map +1 -0
- package/dist/bot/post-turn-hooks.js +108 -0
- package/dist/bot/post-turn-hooks.js.map +1 -0
- package/dist/bot/profile-types.d.ts +16 -0
- package/dist/bot/profile-types.d.ts.map +1 -1
- package/dist/bot/swarm-controller.d.ts +5 -0
- package/dist/bot/swarm-controller.d.ts.map +1 -1
- package/dist/bot/swarm-controller.js +92 -9
- package/dist/bot/swarm-controller.js.map +1 -1
- package/dist/bot/task-types.d.ts +11 -0
- package/dist/bot/task-types.d.ts.map +1 -1
- package/dist/node-types/agent-execute.d.ts.map +1 -1
- package/dist/node-types/agent-execute.js +18 -2
- package/dist/node-types/agent-execute.js.map +1 -1
- package/dist/node-types/build-context.d.ts +4 -3
- package/dist/node-types/build-context.d.ts.map +1 -1
- package/dist/node-types/build-context.js +21 -5
- package/dist/node-types/build-context.js.map +1 -1
- package/dist/node-types/verify-task.d.ts +22 -0
- package/dist/node-types/verify-task.d.ts.map +1 -0
- package/dist/node-types/verify-task.js +143 -0
- package/dist/node-types/verify-task.js.map +1 -0
- package/dist/ui/capability-editor.js +1 -1
- package/dist/ui/profile-editor.js +1 -1
- package/dist/ui/swarm-dashboard.js +1 -1
- package/dist/workflows/weaver-agent.d.ts +3 -3
- package/dist/workflows/weaver-agent.d.ts.map +1 -1
- package/dist/workflows/weaver-agent.js +267 -18
- package/dist/workflows/weaver-agent.js.map +1 -1
- package/dist/workflows/weaver-bot-batch.d.ts +3 -3
- package/dist/workflows/weaver-bot-batch.d.ts.map +1 -1
- package/dist/workflows/weaver-bot-batch.js +280 -24
- package/dist/workflows/weaver-bot-batch.js.map +1 -1
- package/dist/workflows/weaver-bot.d.ts +2 -0
- package/dist/workflows/weaver-bot.d.ts.map +1 -1
- package/dist/workflows/weaver-bot.js +15 -10
- package/dist/workflows/weaver-bot.js.map +1 -1
- package/flowweaver.manifest.json +1 -1
- package/package.json +3 -3
- package/src/bot/ai-client.ts +54 -0
- package/src/bot/behavior-defaults.ts +9 -1
- package/src/bot/capability-registry.ts +1 -1
- package/src/bot/dream-task.ts +167 -0
- package/src/bot/knowledge-store.ts +27 -0
- package/src/bot/post-turn-hooks.ts +137 -0
- package/src/bot/profile-types.ts +17 -0
- package/src/bot/swarm-controller.ts +103 -13
- package/src/bot/task-types.ts +19 -0
- package/src/node-types/agent-execute.ts +19 -2
- package/src/node-types/build-context.ts +28 -6
- package/src/node-types/verify-task.ts +181 -0
- package/src/workflows/weaver-agent.ts +429 -18
- package/src/workflows/weaver-bot-batch.ts +443 -24
- package/src/workflows/weaver-bot.ts +16 -11
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DreamTask — idle-time knowledge consolidation.
|
|
3
|
+
*
|
|
4
|
+
* Runs during swarm dispatch loop idle periods to extract cross-task
|
|
5
|
+
* patterns, detect recurring failures, clean stale knowledge, and
|
|
6
|
+
* convert InsightEngine findings into knowledge entries.
|
|
7
|
+
*
|
|
8
|
+
* Purely heuristic — no LLM calls. Designed to run within 500ms.
|
|
9
|
+
* Key namespaces (non-overlapping with memory-extractor's project:*):
|
|
10
|
+
* - pattern:hot-file:* — files modified by multiple tasks (24h TTL)
|
|
11
|
+
* - warning:recurring-failure:* — recurring failures (7d TTL)
|
|
12
|
+
* - insight:* — converted InsightEngine insights (7d TTL)
|
|
13
|
+
* - session:stats — rolling session statistics (overwritten each cycle)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { KnowledgeStore, type KnowledgeEntry } from './knowledge-store.js';
|
|
17
|
+
import { RunStore } from './run-store.js';
|
|
18
|
+
import type { Insight, ProjectModel } from './types.js';
|
|
19
|
+
|
|
20
|
+
/** Minimum time between consolidation runs (ms). */
|
|
21
|
+
const COOLDOWN_MS = 60_000;
|
|
22
|
+
|
|
23
|
+
/** TTL for dream-sourced entries (ms). */
|
|
24
|
+
const STALE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000;
|
|
25
|
+
|
|
26
|
+
/** TTL for hot-file patterns (ms). */
|
|
27
|
+
const HOT_FILE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
28
|
+
|
|
29
|
+
/** Minimum confidence for converting insights to knowledge. */
|
|
30
|
+
const MIN_CONFIDENCE = 0.6;
|
|
31
|
+
|
|
32
|
+
export interface DreamTaskOptions {
|
|
33
|
+
projectDir: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DreamResult {
|
|
37
|
+
warningsStored: number;
|
|
38
|
+
staleEntriesCleaned: number;
|
|
39
|
+
insightsConverted: number;
|
|
40
|
+
durationMs: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class DreamTask {
|
|
44
|
+
private lastConsolidatedAt = 0;
|
|
45
|
+
private readonly projectDir: string;
|
|
46
|
+
|
|
47
|
+
constructor(opts: DreamTaskOptions) {
|
|
48
|
+
this.projectDir = opts.projectDir;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
shouldRun(): boolean {
|
|
52
|
+
return Date.now() - this.lastConsolidatedAt >= COOLDOWN_MS;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async consolidate(): Promise<DreamResult> {
|
|
56
|
+
const start = Date.now();
|
|
57
|
+
this.lastConsolidatedAt = start;
|
|
58
|
+
|
|
59
|
+
const store = new KnowledgeStore(this.projectDir);
|
|
60
|
+
let warningsStored = 0;
|
|
61
|
+
let staleEntriesCleaned = 0;
|
|
62
|
+
let insightsConverted = 0;
|
|
63
|
+
|
|
64
|
+
// Phase 1: Clean stale dream-sourced entries
|
|
65
|
+
staleEntriesCleaned = this.cleanStaleEntries(store);
|
|
66
|
+
|
|
67
|
+
// Phase 2: Convert InsightEngine findings to knowledge
|
|
68
|
+
try {
|
|
69
|
+
insightsConverted = await this.convertInsights(store);
|
|
70
|
+
} catch { /* non-fatal */ }
|
|
71
|
+
|
|
72
|
+
// Phase 3: Session stats
|
|
73
|
+
try {
|
|
74
|
+
this.updateSessionStats(store);
|
|
75
|
+
} catch { /* non-fatal */ }
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
warningsStored,
|
|
79
|
+
staleEntriesCleaned,
|
|
80
|
+
insightsConverted,
|
|
81
|
+
durationMs: Date.now() - start,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Phase 1: Remove stale dream-sourced entries.
|
|
87
|
+
* Only cleans entries with source starting with 'dream-task'.
|
|
88
|
+
*/
|
|
89
|
+
private cleanStaleEntries(store: KnowledgeStore): number {
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
const entries = store.list();
|
|
92
|
+
let cleaned = 0;
|
|
93
|
+
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
if (!entry.source?.startsWith('dream-task')) continue;
|
|
96
|
+
|
|
97
|
+
const age = now - entry.createdAt;
|
|
98
|
+
const isHotFile = entry.key.startsWith('pattern:hot-file:');
|
|
99
|
+
const ttl = isHotFile ? HOT_FILE_TTL_MS : STALE_THRESHOLD_MS;
|
|
100
|
+
|
|
101
|
+
if (age > ttl) {
|
|
102
|
+
store.forget(entry.key);
|
|
103
|
+
cleaned++;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return cleaned;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Phase 2: Convert high-confidence InsightEngine insights to knowledge.
|
|
112
|
+
* Imports ProjectModel and InsightEngine dynamically to avoid circular deps.
|
|
113
|
+
*/
|
|
114
|
+
private async convertInsights(store: KnowledgeStore): Promise<number> {
|
|
115
|
+
const { ProjectModelStore } = await import('./project-model.js');
|
|
116
|
+
const { InsightEngine } = await import('./insight-engine.js');
|
|
117
|
+
|
|
118
|
+
const pms = new ProjectModelStore(this.projectDir);
|
|
119
|
+
const model = await pms.getOrBuild();
|
|
120
|
+
if (!model) return 0;
|
|
121
|
+
|
|
122
|
+
const engine = new InsightEngine();
|
|
123
|
+
const insights = engine.analyze(model);
|
|
124
|
+
const existingKeys = new Set(store.list().map(e => e.key));
|
|
125
|
+
let converted = 0;
|
|
126
|
+
|
|
127
|
+
for (const insight of insights) {
|
|
128
|
+
if (insight.confidence < MIN_CONFIDENCE) continue;
|
|
129
|
+
if (insight.severity !== 'warning' && insight.severity !== 'critical') continue;
|
|
130
|
+
|
|
131
|
+
const key = `insight:${insight.type}:${insight.id}`;
|
|
132
|
+
if (existingKeys.has(key)) continue;
|
|
133
|
+
|
|
134
|
+
store.learn(
|
|
135
|
+
key,
|
|
136
|
+
`${insight.title}: ${insight.description}. Suggestion: ${insight.suggestion}`,
|
|
137
|
+
'dream-task:insight-engine',
|
|
138
|
+
);
|
|
139
|
+
converted++;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return converted;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Phase 3: Update rolling session statistics.
|
|
147
|
+
*/
|
|
148
|
+
private updateSessionStats(store: KnowledgeStore): void {
|
|
149
|
+
try {
|
|
150
|
+
const runStore = new RunStore();
|
|
151
|
+
const recentRuns = runStore.list({ limit: 50 });
|
|
152
|
+
|
|
153
|
+
if (recentRuns.length === 0) return;
|
|
154
|
+
|
|
155
|
+
const successCount = recentRuns.filter(r => r.success).length;
|
|
156
|
+
const avgDuration = Math.round(
|
|
157
|
+
recentRuns.reduce((sum, r) => sum + (r.durationMs ?? 0), 0) / recentRuns.length,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
store.learn(
|
|
161
|
+
'session:stats',
|
|
162
|
+
`${recentRuns.length} recent runs, ${Math.round(successCount / recentRuns.length * 100)}% success, avg ${Math.round(avgDuration / 1000)}s`,
|
|
163
|
+
'dream-task',
|
|
164
|
+
);
|
|
165
|
+
} catch { /* non-fatal */ }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -42,6 +42,33 @@ export class KnowledgeStore {
|
|
|
42
42
|
return this.readAll();
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Build a compact manifest of knowledge entries for LLM relevance selection.
|
|
47
|
+
* Returns entries sorted newest-first, capped at maxEntries.
|
|
48
|
+
* Format per line: `- [N] key (Xd ago): truncated_value`
|
|
49
|
+
*/
|
|
50
|
+
static buildManifest(
|
|
51
|
+
entries: KnowledgeEntry[],
|
|
52
|
+
maxEntries = 200,
|
|
53
|
+
): { manifest: string; entries: KnowledgeEntry[] } {
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
56
|
+
const sorted = [...entries]
|
|
57
|
+
.sort((a, b) => b.createdAt - a.createdAt)
|
|
58
|
+
.slice(0, maxEntries);
|
|
59
|
+
|
|
60
|
+
const lines = sorted.map((e, i) => {
|
|
61
|
+
const ageMs = now - e.createdAt;
|
|
62
|
+
const age = ageMs < DAY_MS
|
|
63
|
+
? `${Math.round(ageMs / (60 * 60 * 1000))}h ago`
|
|
64
|
+
: `${Math.floor(ageMs / DAY_MS)}d ago`;
|
|
65
|
+
const value = e.value.length > 80 ? e.value.slice(0, 77) + '...' : e.value;
|
|
66
|
+
return `- [${i}] ${e.key} (${age}): ${value}`;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return { manifest: lines.join('\n'), entries: sorted };
|
|
70
|
+
}
|
|
71
|
+
|
|
45
72
|
private readAll(): KnowledgeEntry[] {
|
|
46
73
|
if (!fs.existsSync(this.filePath)) return [];
|
|
47
74
|
const content = fs.readFileSync(this.filePath, 'utf-8').trim();
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-turn hook system for the agent loop.
|
|
3
|
+
*
|
|
4
|
+
* Hooks run after each iteration of the agent loop, enabling cost checks,
|
|
5
|
+
* steering, progress reporting, and knowledge extraction between LLM turns.
|
|
6
|
+
*
|
|
7
|
+
* Hooks run sequentially (not parallel) so abort hooks fire before
|
|
8
|
+
* subsequent hooks. Errors are caught per-hook — a failing hook does
|
|
9
|
+
* not block other hooks or the agent loop.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { TurnEndContext, TurnEndResult } from '@synergenius/flow-weaver/agent';
|
|
13
|
+
import { CostTracker } from './cost-tracker.js';
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Hook interface
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
export type HookTiming = 'every' | 'final' | 'between';
|
|
20
|
+
|
|
21
|
+
export interface PostTurnHook {
|
|
22
|
+
name: string;
|
|
23
|
+
/** When to run: 'every' = every turn, 'final' = only final turn, 'between' = only between turns */
|
|
24
|
+
timing: HookTiming;
|
|
25
|
+
execute(context: TurnEndContext): Promise<PostTurnHookResult>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PostTurnHookResult {
|
|
29
|
+
/** If false, abort the agent loop. Default: true. */
|
|
30
|
+
continue?: boolean;
|
|
31
|
+
/** Optional message to inject into conversation (steering nudge). */
|
|
32
|
+
injectMessage?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Hook runner
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
export class PostTurnHookRunner {
|
|
40
|
+
private hooks: PostTurnHook[] = [];
|
|
41
|
+
|
|
42
|
+
register(hook: PostTurnHook): void {
|
|
43
|
+
this.hooks.push(hook);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Returns the onTurnEnd callback to pass to runAgentLoop options. */
|
|
47
|
+
createCallback(): (ctx: TurnEndContext) => Promise<TurnEndResult | void> {
|
|
48
|
+
return async (ctx: TurnEndContext): Promise<TurnEndResult | void> => {
|
|
49
|
+
let injectMessage: string | undefined;
|
|
50
|
+
|
|
51
|
+
for (const hook of this.hooks) {
|
|
52
|
+
// Check timing
|
|
53
|
+
if (hook.timing === 'final' && !ctx.isFinalTurn) continue;
|
|
54
|
+
if (hook.timing === 'between' && ctx.isFinalTurn) continue;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const result = await hook.execute(ctx);
|
|
58
|
+
if (result.continue === false) {
|
|
59
|
+
return { continue: false, injectMessage: result.injectMessage };
|
|
60
|
+
}
|
|
61
|
+
if (result.injectMessage) {
|
|
62
|
+
injectMessage = injectMessage
|
|
63
|
+
? injectMessage + '\n' + result.injectMessage
|
|
64
|
+
: result.injectMessage;
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
// Hook failure is non-fatal — log and continue
|
|
68
|
+
if (process.env.WEAVER_VERBOSE) {
|
|
69
|
+
console.error(`[post-turn-hook] ${hook.name} failed:`, err);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (injectMessage) return { injectMessage };
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Built-in hooks
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Cost checkpoint — aborts the loop when cumulative cost exceeds budget.
|
|
85
|
+
* Subsumes the standalone #6 per-turn budget enforcement approach.
|
|
86
|
+
*/
|
|
87
|
+
export class CostCheckpointHook implements PostTurnHook {
|
|
88
|
+
name = 'cost-checkpoint';
|
|
89
|
+
timing: HookTiming = 'between';
|
|
90
|
+
|
|
91
|
+
constructor(
|
|
92
|
+
private maxCost: number,
|
|
93
|
+
private model: string,
|
|
94
|
+
) {}
|
|
95
|
+
|
|
96
|
+
async execute(ctx: TurnEndContext): Promise<PostTurnHookResult> {
|
|
97
|
+
const cost = CostTracker.estimateCost(this.model, {
|
|
98
|
+
inputTokens: ctx.usage.promptTokens,
|
|
99
|
+
outputTokens: ctx.usage.completionTokens,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (cost >= this.maxCost) {
|
|
103
|
+
return {
|
|
104
|
+
continue: false,
|
|
105
|
+
injectMessage: `Budget exceeded: $${cost.toFixed(4)} >= $${this.maxCost.toFixed(4)}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Progress report — emits a stream event with turn progress for UI updates.
|
|
114
|
+
*/
|
|
115
|
+
export class ProgressReportHook implements PostTurnHook {
|
|
116
|
+
name = 'progress-report';
|
|
117
|
+
timing: HookTiming = 'every';
|
|
118
|
+
|
|
119
|
+
constructor(
|
|
120
|
+
private emitEvent: (event: { type: string; timestamp: number; data: Record<string, unknown> }) => void,
|
|
121
|
+
) {}
|
|
122
|
+
|
|
123
|
+
async execute(ctx: TurnEndContext): Promise<PostTurnHookResult> {
|
|
124
|
+
this.emitEvent({
|
|
125
|
+
type: 'turn-progress',
|
|
126
|
+
timestamp: Date.now(),
|
|
127
|
+
data: {
|
|
128
|
+
iteration: ctx.iteration,
|
|
129
|
+
maxIterations: ctx.maxIterations,
|
|
130
|
+
toolCallCount: ctx.toolCallCount,
|
|
131
|
+
isFinalTurn: ctx.isFinalTurn,
|
|
132
|
+
usage: ctx.usage,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
return {};
|
|
136
|
+
}
|
|
137
|
+
}
|
package/src/bot/profile-types.ts
CHANGED
|
@@ -92,6 +92,21 @@ export interface PhaseDescriptor {
|
|
|
92
92
|
* not hardcoded. Each workflow can have different phases (e.g. a review
|
|
93
93
|
* bot might have analyze/report/suggest instead of plan/execute/review).
|
|
94
94
|
*/
|
|
95
|
+
/** Post-run verification config — independent review of completed work. */
|
|
96
|
+
export interface VerificationConfig {
|
|
97
|
+
/** Whether verification is enabled. Default: false. */
|
|
98
|
+
enabled: boolean;
|
|
99
|
+
/** Model tier for the verification agent. Default: 'standard'. */
|
|
100
|
+
tier: ModelTier;
|
|
101
|
+
/**
|
|
102
|
+
* How often to run verification (1 = every completed task, 2 = every other, etc.).
|
|
103
|
+
* Default: 1 (always verify).
|
|
104
|
+
*/
|
|
105
|
+
frequency: number;
|
|
106
|
+
/** Re-open the task if verification fails. Default: true. */
|
|
107
|
+
reopenOnFail: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
95
110
|
export interface ProfileBehavior {
|
|
96
111
|
/** Capability names this profile can use (e.g. ['core', 'file-ops', 'shell']). */
|
|
97
112
|
capabilities?: string[];
|
|
@@ -107,6 +122,8 @@ export interface ProfileBehavior {
|
|
|
107
122
|
exitProtocol: ExitProtocol;
|
|
108
123
|
/** Structured output requirements. */
|
|
109
124
|
outputContract?: OutputContract;
|
|
125
|
+
/** Post-run verification by an independent agent. */
|
|
126
|
+
verification?: VerificationConfig;
|
|
110
127
|
}
|
|
111
128
|
|
|
112
129
|
/** Structured exit status from a bot run. */
|
|
@@ -32,9 +32,10 @@ import { ProfileStore } from './profile-store.js';
|
|
|
32
32
|
import type { BotProfile, BotInstance, OrchestratorInput, OrchestratorDecision, ProfileBehavior } from './profile-types.js';
|
|
33
33
|
import { buildDefaultBehavior, adjustBehaviorForComplexity } from './behavior-defaults.js';
|
|
34
34
|
import type { Task, RunProgress } from './task-types.js';
|
|
35
|
-
import type { WorkflowResult } from './types.js';
|
|
35
|
+
import type { WorkflowResult, ProviderInfo } from './types.js';
|
|
36
36
|
import { scheduleMemoryExtraction } from './memory-extraction-worker.js';
|
|
37
37
|
import { shouldCompact, compactRunHistory } from './context-compactor.js';
|
|
38
|
+
import { DreamTask } from './dream-task.js';
|
|
38
39
|
import { callAI } from './ai-client.js';
|
|
39
40
|
|
|
40
41
|
// ---------------------------------------------------------------------------
|
|
@@ -87,6 +88,11 @@ const DISPATCH_LOOP_SLEEP_MS = 2000;
|
|
|
87
88
|
// No TASK_TIMEOUT_MS — AI call timeout (10min) in the worker is the only boundary.
|
|
88
89
|
const SWARM_STATE_FILE = 'swarm.json';
|
|
89
90
|
|
|
91
|
+
/** Safely access runHistory — older tasks may have `runSummaries` instead. */
|
|
92
|
+
function getRunHistory(ctx: { runHistory?: unknown; runSummaries?: unknown }): Array<Record<string, unknown>> {
|
|
93
|
+
return (ctx.runHistory ?? ctx.runSummaries ?? []) as Array<Record<string, unknown>>;
|
|
94
|
+
}
|
|
95
|
+
|
|
90
96
|
// ---------------------------------------------------------------------------
|
|
91
97
|
// SwarmController
|
|
92
98
|
// ---------------------------------------------------------------------------
|
|
@@ -124,6 +130,12 @@ export class SwarmController {
|
|
|
124
130
|
/** Frozen system prompt prefix for cross-slot Anthropic cache sharing. */
|
|
125
131
|
private frozenPromptPrefix: string | null = null;
|
|
126
132
|
|
|
133
|
+
/** Background knowledge consolidation during idle periods. */
|
|
134
|
+
private dreamTask: DreamTask;
|
|
135
|
+
|
|
136
|
+
/** Counter for verification frequency gating (incremented per completed task). */
|
|
137
|
+
private verificationCounter = 0;
|
|
138
|
+
|
|
127
139
|
// -----------------------------------------------------------------------
|
|
128
140
|
// Singleton
|
|
129
141
|
// -----------------------------------------------------------------------
|
|
@@ -154,6 +166,7 @@ export class SwarmController {
|
|
|
154
166
|
this.orchestrator = new Orchestrator({ aiRouter: new AIRouterImpl(projectDir) });
|
|
155
167
|
this.instanceManager = new InstanceManager();
|
|
156
168
|
this.profileStore = new ProfileStore(projectDir);
|
|
169
|
+
this.dreamTask = new DreamTask({ projectDir });
|
|
157
170
|
|
|
158
171
|
// Load persisted state or create default
|
|
159
172
|
this.state = this._loadState();
|
|
@@ -550,10 +563,11 @@ export class SwarmController {
|
|
|
550
563
|
if (t.context.budgetExhausted) return false;
|
|
551
564
|
// Skip tasks in rapid-loop cooldown: if last run ended recently AND was zero-work,
|
|
552
565
|
// apply exponential backoff based on stagnation count
|
|
553
|
-
|
|
554
|
-
|
|
566
|
+
const taskRunHistory = getRunHistory(t.context);
|
|
567
|
+
if (taskRunHistory.length > 0) {
|
|
568
|
+
const lastRun = taskRunHistory[taskRunHistory.length - 1];
|
|
555
569
|
if ('endedAt' in lastRun && lastRun.endedAt) {
|
|
556
|
-
const secsSinceLastRun = (Date.now() - new Date(lastRun.endedAt).getTime()) / 1000;
|
|
570
|
+
const secsSinceLastRun = (Date.now() - new Date(lastRun.endedAt as string).getTime()) / 1000;
|
|
557
571
|
const stag = t.context.stagnationCount;
|
|
558
572
|
// Exponential backoff: 10s, 20s, 40s, 80s, 160s... based on stagnation
|
|
559
573
|
const cooldownSecs = stag > 0 ? Math.min(10 * Math.pow(2, stag - 1), 300) : 0;
|
|
@@ -609,6 +623,7 @@ export class SwarmController {
|
|
|
609
623
|
return !routableTasks.includes(t);
|
|
610
624
|
}).length;
|
|
611
625
|
_dl(`[dispatch] 0 routable from ${pendingTasks.length} open. parent=${pendingTasks.filter(t => t.isParent).length} budget=${pendingTasks.filter(t => t.context.budgetExhausted).length} deps-blocked=${depsBlocked}`);
|
|
626
|
+
await this._maybeDream();
|
|
612
627
|
await this._sleep(DISPATCH_LOOP_SLEEP_MS, signal);
|
|
613
628
|
continue;
|
|
614
629
|
}
|
|
@@ -650,9 +665,9 @@ export class SwarmController {
|
|
|
650
665
|
complexity: t.complexity ?? 'moderate',
|
|
651
666
|
assignedProfile: t.assignedProfile,
|
|
652
667
|
context: {
|
|
653
|
-
runHistory: t.context.
|
|
654
|
-
outcome: rp.outcome,
|
|
655
|
-
botId: rp.botId,
|
|
668
|
+
runHistory: getRunHistory(t.context).map((rp) => ({
|
|
669
|
+
outcome: rp.outcome as string,
|
|
670
|
+
botId: rp.botId as string,
|
|
656
671
|
})),
|
|
657
672
|
},
|
|
658
673
|
})),
|
|
@@ -791,6 +806,26 @@ export class SwarmController {
|
|
|
791
806
|
}
|
|
792
807
|
}
|
|
793
808
|
|
|
809
|
+
// -----------------------------------------------------------------------
|
|
810
|
+
// Idle-time knowledge consolidation
|
|
811
|
+
// -----------------------------------------------------------------------
|
|
812
|
+
|
|
813
|
+
private async _maybeDream(): Promise<void> {
|
|
814
|
+
if (!this.dreamTask.shouldRun()) return;
|
|
815
|
+
try {
|
|
816
|
+
const result = await this.dreamTask.consolidate();
|
|
817
|
+
if (result.staleEntriesCleaned + result.insightsConverted > 0) {
|
|
818
|
+
this.eventLog.emit({
|
|
819
|
+
type: 'dream-consolidation',
|
|
820
|
+
timestamp: Date.now(),
|
|
821
|
+
data: result as unknown as Record<string, unknown>,
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
} catch (err) {
|
|
825
|
+
if (process.env.WEAVER_VERBOSE) console.warn('[swarm] dream-task failed:', err);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
794
829
|
// -----------------------------------------------------------------------
|
|
795
830
|
// Task execution
|
|
796
831
|
// -----------------------------------------------------------------------
|
|
@@ -815,10 +850,13 @@ export class SwarmController {
|
|
|
815
850
|
if (shouldCompact(task, profile.preferences?.costStrategy)) {
|
|
816
851
|
try {
|
|
817
852
|
const { resolveModelTier } = await import('./behavior-defaults.js');
|
|
818
|
-
const
|
|
853
|
+
const { resolveProviderConfig } = await import('./agent-provider.js');
|
|
854
|
+
const detected = resolveProviderConfig('auto');
|
|
855
|
+
const providerType = detected.name;
|
|
856
|
+
const compactModel = detected.model ?? resolveModelTier('fast', providerType);
|
|
819
857
|
const compactPInfo = {
|
|
820
|
-
type:
|
|
821
|
-
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
858
|
+
type: providerType as ProviderInfo['type'],
|
|
859
|
+
apiKey: process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY,
|
|
822
860
|
model: compactModel,
|
|
823
861
|
};
|
|
824
862
|
const summary = await compactRunHistory(task, compactPInfo, callAI);
|
|
@@ -846,7 +884,7 @@ export class SwarmController {
|
|
|
846
884
|
mode: task.context.files.length > 0 ? 'modify' : 'create',
|
|
847
885
|
targets: task.context.files.length > 0 ? task.context.files : undefined,
|
|
848
886
|
options: { autoApprove: true },
|
|
849
|
-
runHistory: task.context
|
|
887
|
+
runHistory: getRunHistory(task.context),
|
|
850
888
|
stagnationCount: task.context.stagnationCount,
|
|
851
889
|
});
|
|
852
890
|
|
|
@@ -872,7 +910,12 @@ export class SwarmController {
|
|
|
872
910
|
taskId,
|
|
873
911
|
botId: workerId,
|
|
874
912
|
config: { provider: 'auto' },
|
|
875
|
-
params: {
|
|
913
|
+
params: {
|
|
914
|
+
taskJson,
|
|
915
|
+
projectDir: this.projectDir,
|
|
916
|
+
behaviorJson,
|
|
917
|
+
...(this.frozenPromptPrefix ? { frozenPromptPrefix: this.frozenPromptPrefix } : {}),
|
|
918
|
+
},
|
|
876
919
|
eventLog: runEventLog,
|
|
877
920
|
});
|
|
878
921
|
|
|
@@ -949,6 +992,53 @@ export class SwarmController {
|
|
|
949
992
|
}
|
|
950
993
|
}
|
|
951
994
|
|
|
995
|
+
// Independent verification — runs after acceptance passes, before release.
|
|
996
|
+
// Uses a fresh LLM session with a potentially different model tier.
|
|
997
|
+
if (releaseStatus === 'done' && task) {
|
|
998
|
+
const behavior: import('./profile-types.js').ProfileBehavior | undefined =
|
|
999
|
+
profile.preferences?.behavior;
|
|
1000
|
+
const vConfig = behavior?.verification;
|
|
1001
|
+
if (vConfig?.enabled) {
|
|
1002
|
+
this.verificationCounter++;
|
|
1003
|
+
const shouldVerify = this.verificationCounter % vConfig.frequency === 0;
|
|
1004
|
+
if (shouldVerify) {
|
|
1005
|
+
try {
|
|
1006
|
+
const { runVerification } = await import('../node-types/verify-task.js');
|
|
1007
|
+
const { resolveProviderConfig } = await import('./agent-provider.js');
|
|
1008
|
+
const { resolveModelTier } = await import('./behavior-defaults.js');
|
|
1009
|
+
const detected = resolveProviderConfig('auto');
|
|
1010
|
+
const verifyModel = resolveModelTier(vConfig.tier, detected.name);
|
|
1011
|
+
const verifyResult = await runVerification(
|
|
1012
|
+
{
|
|
1013
|
+
taskTitle: task.title,
|
|
1014
|
+
taskDescription: task.description,
|
|
1015
|
+
filesCreated: runProgress.filesCreated,
|
|
1016
|
+
filesModified: runProgress.filesModified,
|
|
1017
|
+
summary: runProgress.summary,
|
|
1018
|
+
checks: runProgress.checks,
|
|
1019
|
+
},
|
|
1020
|
+
detected.name,
|
|
1021
|
+
verifyModel,
|
|
1022
|
+
process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY,
|
|
1023
|
+
);
|
|
1024
|
+
await this.taskStore.update(taskId, { lastVerification: verifyResult } as Record<string, unknown>);
|
|
1025
|
+
this.eventLog.emit({
|
|
1026
|
+
type: 'verification',
|
|
1027
|
+
timestamp: Date.now(),
|
|
1028
|
+
data: { taskId, runId, verdict: verifyResult.verdict, summary: verifyResult.summary, cost: verifyResult.cost } as unknown as Record<string, unknown>,
|
|
1029
|
+
});
|
|
1030
|
+
if (verifyResult.verdict === 'fail' && vConfig.reopenOnFail) {
|
|
1031
|
+
releaseStatus = 'open';
|
|
1032
|
+
console.log(`\x1b[33m[swarm] verification FAILED for task ${taskId}: ${verifyResult.summary}\x1b[0m`);
|
|
1033
|
+
}
|
|
1034
|
+
} catch (verifyErr) {
|
|
1035
|
+
if (process.env.WEAVER_VERBOSE) console.warn('[swarm] verification failed:', verifyErr);
|
|
1036
|
+
// Verification failure is non-fatal — release as originally planned
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
952
1042
|
// Health checks — detect suspicious runs before releasing
|
|
953
1043
|
if (tokensUsed === 0 && runProgress.outcome === 'completed') {
|
|
954
1044
|
console.warn(`[swarm] HEALTH: zero-token completion task=${taskId} worker=${workerId} duration=${durationMs}ms`);
|
|
@@ -1002,7 +1092,7 @@ export class SwarmController {
|
|
|
1002
1092
|
summary: runProgress.summary,
|
|
1003
1093
|
filesModified: runProgress.filesModified,
|
|
1004
1094
|
botId: workerId,
|
|
1005
|
-
runCount: task.context.
|
|
1095
|
+
runCount: getRunHistory(task.context).length,
|
|
1006
1096
|
},
|
|
1007
1097
|
});
|
|
1008
1098
|
}
|
package/src/bot/task-types.ts
CHANGED
|
@@ -60,6 +60,22 @@ export interface AcceptanceResult {
|
|
|
60
60
|
checkedAt: string;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Verification result — independent post-run review
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
export type VerificationVerdict = 'pass' | 'fail' | 'inconclusive';
|
|
68
|
+
|
|
69
|
+
export interface VerificationResult {
|
|
70
|
+
verdict: VerificationVerdict;
|
|
71
|
+
summary: string;
|
|
72
|
+
issues: string[];
|
|
73
|
+
filesReviewed: string[];
|
|
74
|
+
verifiedAt: string;
|
|
75
|
+
/** Cost of the verification run in USD. */
|
|
76
|
+
cost: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
63
79
|
// ---------------------------------------------------------------------------
|
|
64
80
|
// Task context
|
|
65
81
|
// ---------------------------------------------------------------------------
|
|
@@ -105,6 +121,9 @@ export interface Task {
|
|
|
105
121
|
acceptance?: AcceptanceCriteria;
|
|
106
122
|
lastAcceptanceCheck?: AcceptanceResult;
|
|
107
123
|
|
|
124
|
+
// Verification
|
|
125
|
+
lastVerification?: VerificationResult;
|
|
126
|
+
|
|
108
127
|
// Context
|
|
109
128
|
context: TaskContext;
|
|
110
129
|
|
|
@@ -18,6 +18,7 @@ import { resolveToolsForTask } from '../bot/tool-registry.js';
|
|
|
18
18
|
import { auditEmit } from '../bot/audit-logger.js';
|
|
19
19
|
import { withRetry, getErrorGuidance } from '../bot/error-classifier.js';
|
|
20
20
|
import { CostTracker } from '../bot/cost-tracker.js';
|
|
21
|
+
import { PostTurnHookRunner, CostCheckpointHook, ProgressReportHook } from '../bot/post-turn-hooks.js';
|
|
21
22
|
|
|
22
23
|
// Clean up persistent sessions on process exit
|
|
23
24
|
let cleanupRegistered = false;
|
|
@@ -238,13 +239,30 @@ export async function weaverAgentExecute(
|
|
|
238
239
|
);
|
|
239
240
|
const tools = WEAVER_TOOLS.filter(t => grantedToolNames.has(t.name));
|
|
240
241
|
|
|
242
|
+
// Set up post-turn hooks — cost checkpoint + progress reporting.
|
|
243
|
+
// CostCheckpointHook aborts the loop when cumulative cost exceeds budget.
|
|
244
|
+
// ProgressReportHook emits turn-progress events for UI updates.
|
|
245
|
+
const hookRunner = new PostTurnHookRunner();
|
|
246
|
+
const model = pInfo.model ?? 'claude-sonnet-4-6';
|
|
247
|
+
const budget = behavior?.budget;
|
|
248
|
+
if (budget != null && budget > 0) {
|
|
249
|
+
hookRunner.register(new CostCheckpointHook(budget, model));
|
|
250
|
+
}
|
|
251
|
+
hookRunner.register(new ProgressReportHook((event) => {
|
|
252
|
+
renderer.onStreamEvent?.({ type: 'text_delta', text: '' }); // keep renderer alive
|
|
253
|
+
if (process.env.WEAVER_VERBOSE) {
|
|
254
|
+
console.log(`[post-turn] ${event.type}: iter=${event.data.iteration} tools=${event.data.toolCallCount}`);
|
|
255
|
+
}
|
|
256
|
+
}));
|
|
257
|
+
const onTurnEnd = hookRunner.createCallback();
|
|
258
|
+
|
|
241
259
|
const result = await withRetry(
|
|
242
260
|
() => runAgentLoop(
|
|
243
261
|
provider,
|
|
244
262
|
tools,
|
|
245
263
|
executor,
|
|
246
264
|
[{ role: 'user', content: taskPrompt }],
|
|
247
|
-
{ systemPrompt, maxIterations: 15, onToolEvent, onStreamEvent },
|
|
265
|
+
{ systemPrompt, maxIterations: 15, onToolEvent, onStreamEvent, onTurnEnd },
|
|
248
266
|
),
|
|
249
267
|
{
|
|
250
268
|
maxRetries: 3,
|
|
@@ -256,7 +274,6 @@ export async function weaverAgentExecute(
|
|
|
256
274
|
);
|
|
257
275
|
|
|
258
276
|
const usage = result.usage;
|
|
259
|
-
const model = pInfo.model ?? 'claude-sonnet-4-6';
|
|
260
277
|
const estimatedCost = CostTracker.estimateCost(model, {
|
|
261
278
|
inputTokens: usage.promptTokens,
|
|
262
279
|
outputTokens: usage.completionTokens,
|