@synergenius/flow-weaver-pack-weaver 0.9.193 → 0.9.196
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/assistant-core.js +2 -2
- package/dist/bot/assistant-core.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 +3 -3
- package/dist/bot/capability-registry.js.map +1 -1
- package/dist/bot/context-compactor.d.ts +35 -0
- package/dist/bot/context-compactor.d.ts.map +1 -0
- package/dist/bot/context-compactor.js +130 -0
- package/dist/bot/context-compactor.js.map +1 -0
- 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/memory-extraction-worker.d.ts +14 -0
- package/dist/bot/memory-extraction-worker.d.ts.map +1 -0
- package/dist/bot/memory-extraction-worker.js +42 -0
- package/dist/bot/memory-extraction-worker.js.map +1 -0
- package/dist/bot/memory-extractor.d.ts +27 -0
- package/dist/bot/memory-extractor.d.ts.map +1 -0
- package/dist/bot/memory-extractor.js +155 -0
- package/dist/bot/memory-extractor.js.map +1 -0
- package/dist/bot/operations.d.ts +3 -1
- package/dist/bot/operations.d.ts.map +1 -1
- package/dist/bot/operations.js +3 -1
- package/dist/bot/operations.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 +7 -0
- package/dist/bot/swarm-controller.d.ts.map +1 -1
- package/dist/bot/swarm-controller.js +121 -1
- package/dist/bot/swarm-controller.js.map +1 -1
- package/dist/bot/task-prompt-builder.js +35 -21
- package/dist/bot/task-prompt-builder.js.map +1 -1
- package/dist/bot/task-types.d.ts +13 -0
- package/dist/bot/task-types.d.ts.map +1 -1
- package/dist/bot/tool-registry.d.ts +13 -0
- package/dist/bot/tool-registry.d.ts.map +1 -1
- package/dist/bot/tool-registry.js +80 -0
- package/dist/bot/tool-registry.js.map +1 -1
- package/dist/bot/types.d.ts +2 -0
- package/dist/bot/types.d.ts.map +1 -1
- package/dist/node-types/agent-execute.d.ts.map +1 -1
- package/dist/node-types/agent-execute.js +38 -17
- 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 +37 -6
- package/dist/node-types/build-context.js.map +1 -1
- package/dist/node-types/receive-task.d.ts +2 -1
- package/dist/node-types/receive-task.d.ts.map +1 -1
- package/dist/node-types/receive-task.js +4 -1
- package/dist/node-types/receive-task.js.map +1 -1
- package/dist/node-types/review-result.d.ts +9 -0
- package/dist/node-types/review-result.d.ts.map +1 -1
- package/dist/node-types/review-result.js +20 -5
- package/dist/node-types/review-result.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 +3 -3
- package/dist/ui/profile-editor.js +3 -3
- package/dist/ui/swarm-dashboard.js +3 -3
- 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/assistant-core.ts +2 -2
- package/src/bot/behavior-defaults.ts +9 -1
- package/src/bot/capability-registry.ts +3 -3
- package/src/bot/context-compactor.ts +147 -0
- package/src/bot/dream-task.ts +167 -0
- package/src/bot/knowledge-store.ts +27 -0
- package/src/bot/memory-extraction-worker.ts +58 -0
- package/src/bot/memory-extractor.ts +213 -0
- package/src/bot/operations.ts +3 -1
- package/src/bot/post-turn-hooks.ts +137 -0
- package/src/bot/profile-types.ts +17 -0
- package/src/bot/swarm-controller.ts +129 -2
- package/src/bot/task-prompt-builder.ts +37 -21
- package/src/bot/task-types.ts +21 -0
- package/src/bot/tool-registry.ts +89 -0
- package/src/bot/types.ts +2 -0
- package/src/node-types/agent-execute.ts +44 -17
- package/src/node-types/build-context.ts +45 -7
- package/src/node-types/receive-task.ts +3 -0
- package/src/node-types/review-result.ts +22 -5
- 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,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM-based context compaction for task retries.
|
|
3
|
+
*
|
|
4
|
+
* When a task has 3+ runs, calls a fast-tier model to produce a structured
|
|
5
|
+
* summary of all attempts. The summary replaces verbose run history in
|
|
6
|
+
* the prompt builder, preserving semantic signal (intent, errors, approaches)
|
|
7
|
+
* that cascading truncation would destroy.
|
|
8
|
+
*
|
|
9
|
+
* Uses the <analysis> scratchpad pattern — the model reasons in tags that
|
|
10
|
+
* are stripped before storing the summary.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Task, RunProgress, CompactedRun } from './task-types.js';
|
|
14
|
+
import type { ProviderInfo } from './types.js';
|
|
15
|
+
import type { CostStrategy } from './profile-types.js';
|
|
16
|
+
|
|
17
|
+
/** Minimum runs before compaction triggers, indexed by cost strategy. */
|
|
18
|
+
const COMPACTION_THRESHOLDS: Record<CostStrategy, number> = {
|
|
19
|
+
frugal: 5,
|
|
20
|
+
balanced: 3,
|
|
21
|
+
performance: 2,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const COMPACTION_SYSTEM_PROMPT = `You are a context compactor for an AI task execution system. Your job is to
|
|
25
|
+
summarize the history of a task's execution runs into a concise briefing for
|
|
26
|
+
the next AI worker that will continue the task.
|
|
27
|
+
|
|
28
|
+
The next worker will read the workspace files directly. Your summary should
|
|
29
|
+
focus on WHAT WAS TRIED and WHY IT DIDN'T FULLY SUCCEED — not on describing
|
|
30
|
+
file contents (the worker will read those).
|
|
31
|
+
|
|
32
|
+
CRITICAL: Respond with TEXT ONLY. Do NOT call any tools. Tool calls will be
|
|
33
|
+
REJECTED and will waste your only turn.
|
|
34
|
+
|
|
35
|
+
Produce a structured summary with these sections. Be terse — total output
|
|
36
|
+
must be under 800 tokens.
|
|
37
|
+
|
|
38
|
+
<analysis>
|
|
39
|
+
Think about what patterns emerge across the runs. What keeps failing? What
|
|
40
|
+
approaches were tried? Is there stagnation?
|
|
41
|
+
</analysis>
|
|
42
|
+
|
|
43
|
+
## Task Intent
|
|
44
|
+
One sentence: what is the task trying to achieve?
|
|
45
|
+
|
|
46
|
+
## Approaches Tried
|
|
47
|
+
Bullet list of distinct approaches attempted across runs, with outcomes.
|
|
48
|
+
|
|
49
|
+
## Key Errors
|
|
50
|
+
Specific error messages or test failures that recurred. Include exact names.
|
|
51
|
+
|
|
52
|
+
## Files Touched
|
|
53
|
+
Files created or modified across all runs (deduplicated).
|
|
54
|
+
|
|
55
|
+
## What Works
|
|
56
|
+
Any partial progress that should be preserved (tests passing, files correct).
|
|
57
|
+
|
|
58
|
+
## Recommended Next Step
|
|
59
|
+
Based on the pattern of failures, what should the next run try differently?`;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Strip `<analysis>...</analysis>` scratchpad from LLM response.
|
|
63
|
+
* Reuses the same pattern as review-result.ts stripAnalysis.
|
|
64
|
+
*/
|
|
65
|
+
function stripAnalysis(text: string): string {
|
|
66
|
+
return text.replace(/<analysis>[\s\S]*?<\/analysis>/g, '').trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build the user prompt from task + run history.
|
|
71
|
+
*/
|
|
72
|
+
export function buildCompactionPrompt(task: Task): string {
|
|
73
|
+
const lines: string[] = [];
|
|
74
|
+
lines.push(`Task: ${task.title}`);
|
|
75
|
+
lines.push(`Description: ${task.description}`);
|
|
76
|
+
lines.push('');
|
|
77
|
+
lines.push('Run history:');
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < task.context.runHistory.length; i++) {
|
|
80
|
+
const run = task.context.runHistory[i];
|
|
81
|
+
const rp = run as RunProgress;
|
|
82
|
+
lines.push(`Run ${i + 1} (${rp.outcome}):`);
|
|
83
|
+
if (rp.summary) lines.push(` Summary: ${rp.summary}`);
|
|
84
|
+
if (rp.filesCreated?.length) lines.push(` Files created: ${rp.filesCreated.join(', ')}`);
|
|
85
|
+
if (rp.filesModified?.length) lines.push(` Files modified: ${rp.filesModified.join(', ')}`);
|
|
86
|
+
if (rp.remainingWork) lines.push(` Remaining work: ${rp.remainingWork}`);
|
|
87
|
+
if (rp.blockers?.length) lines.push(` Blockers: ${rp.blockers.join(', ')}`);
|
|
88
|
+
lines.push('');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (task.lastAcceptanceCheck) {
|
|
92
|
+
const ac = task.lastAcceptanceCheck;
|
|
93
|
+
const checkLines = ac.results
|
|
94
|
+
.map(r => ` ${r.name}: ${r.pass ? 'PASS' : 'FAIL'}${r.detail ? ` (${r.detail.slice(0, 100)})` : ''}`)
|
|
95
|
+
.join('\n');
|
|
96
|
+
lines.push(`Acceptance check results:\n${checkLines}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
lines.push(`Stagnation count: ${task.context.stagnationCount}`);
|
|
100
|
+
|
|
101
|
+
return lines.join('\n');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Determine if compaction should run for this task.
|
|
106
|
+
*/
|
|
107
|
+
export function shouldCompact(
|
|
108
|
+
task: Task,
|
|
109
|
+
costStrategy: CostStrategy = 'balanced',
|
|
110
|
+
): boolean {
|
|
111
|
+
if (task.context.compactedSummary) return false;
|
|
112
|
+
const threshold = COMPACTION_THRESHOLDS[costStrategy] ?? 3;
|
|
113
|
+
return task.context.runHistory.length >= threshold;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Compact a task's run history into a structured summary via LLM.
|
|
118
|
+
*
|
|
119
|
+
* Returns the compacted summary string, or null if the LLM call fails.
|
|
120
|
+
* Failures are non-fatal — the prompt builder falls back to the existing
|
|
121
|
+
* context decay approach.
|
|
122
|
+
*
|
|
123
|
+
* @param task - The task to compact
|
|
124
|
+
* @param pInfo - Provider info for making the LLM call
|
|
125
|
+
* @param callAIFn - The callAI function (injected for testability)
|
|
126
|
+
*/
|
|
127
|
+
export async function compactRunHistory(
|
|
128
|
+
task: Task,
|
|
129
|
+
pInfo: Pick<ProviderInfo, 'type' | 'apiKey' | 'model' | 'cliBinPath'>,
|
|
130
|
+
callAIFn: (
|
|
131
|
+
pInfo: Pick<ProviderInfo, 'type' | 'apiKey' | 'model' | 'cliBinPath'>,
|
|
132
|
+
systemPrompt: string,
|
|
133
|
+
userPrompt: string,
|
|
134
|
+
maxTokens: number,
|
|
135
|
+
) => Promise<string>,
|
|
136
|
+
): Promise<string | null> {
|
|
137
|
+
try {
|
|
138
|
+
const userPrompt = buildCompactionPrompt(task);
|
|
139
|
+
const response = await callAIFn(pInfo, COMPACTION_SYSTEM_PROMPT, userPrompt, 1024);
|
|
140
|
+
const cleaned = stripAnalysis(response);
|
|
141
|
+
// Sanity check: the cleaned response should have meaningful content
|
|
142
|
+
if (cleaned.length < 20) return null;
|
|
143
|
+
return cleaned;
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -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,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fire-and-forget memory extraction worker.
|
|
3
|
+
*
|
|
4
|
+
* Schedules heuristic extraction from completed runs into the KnowledgeStore.
|
|
5
|
+
* Uses AsyncMutex to serialize concurrent writes from parallel bot slots.
|
|
6
|
+
* Errors are caught and logged — never blocks task release.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { AsyncMutex } from './async-mutex.js';
|
|
10
|
+
import { KnowledgeStore } from './knowledge-store.js';
|
|
11
|
+
import { extractMemoryFacts } from './memory-extractor.js';
|
|
12
|
+
import type { Task, RunProgress } from './task-types.js';
|
|
13
|
+
|
|
14
|
+
/** Maximum total knowledge entries before skipping pattern:* writes. */
|
|
15
|
+
const MAX_TOTAL_ENTRIES = 100;
|
|
16
|
+
|
|
17
|
+
const mutex = new AsyncMutex();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Schedule a fire-and-forget memory extraction.
|
|
21
|
+
* Returns immediately — extraction runs async in the background.
|
|
22
|
+
*/
|
|
23
|
+
export function scheduleMemoryExtraction(
|
|
24
|
+
projectDir: string,
|
|
25
|
+
task: Task,
|
|
26
|
+
runProgress: RunProgress,
|
|
27
|
+
): void {
|
|
28
|
+
_doExtract(projectDir, task, runProgress).catch(err => {
|
|
29
|
+
if (process.env.WEAVER_VERBOSE) {
|
|
30
|
+
console.warn('[swarm] memory extraction failed:', err);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function _doExtract(
|
|
36
|
+
projectDir: string,
|
|
37
|
+
task: Task,
|
|
38
|
+
runProgress: RunProgress,
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
await mutex.runExclusive(async () => {
|
|
41
|
+
const store = new KnowledgeStore(projectDir);
|
|
42
|
+
const existing = store.list();
|
|
43
|
+
const existingKeys = new Set(existing.map(e => e.key));
|
|
44
|
+
|
|
45
|
+
// If over the cap, only allow project:* overwrites (no new pattern:* entries)
|
|
46
|
+
const overCap = existing.length >= MAX_TOTAL_ENTRIES;
|
|
47
|
+
|
|
48
|
+
const facts = extractMemoryFacts(task, runProgress, existingKeys);
|
|
49
|
+
|
|
50
|
+
for (const fact of facts) {
|
|
51
|
+
// Skip pattern:* entries when over cap (project:* overwrites are always allowed)
|
|
52
|
+
if (overCap && fact.key.startsWith('pattern:') && !existingKeys.has(fact.key)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
store.learn(fact.key, fact.value, `run:${runProgress.runId}`);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic memory extraction from completed bot runs.
|
|
3
|
+
*
|
|
4
|
+
* Pure function — takes structured run data and returns candidate
|
|
5
|
+
* knowledge entries. No I/O, no LLM calls, no side effects.
|
|
6
|
+
*
|
|
7
|
+
* Key namespaces:
|
|
8
|
+
* - project:* — canonical project facts (test runner, build tool, linter, pkg manager)
|
|
9
|
+
* - pattern:blocker:* — recurring blockers
|
|
10
|
+
* - pattern:stagnation:* — stalled task patterns
|
|
11
|
+
* - pattern:success:* — first-run success patterns
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Task, RunProgress } from './task-types.js';
|
|
15
|
+
|
|
16
|
+
export interface MemoryFact {
|
|
17
|
+
key: string;
|
|
18
|
+
value: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Maximum facts to extract per run. */
|
|
22
|
+
const MAX_FACTS_PER_RUN = 5;
|
|
23
|
+
|
|
24
|
+
/** Maximum key length. */
|
|
25
|
+
const MAX_KEY_LENGTH = 80;
|
|
26
|
+
|
|
27
|
+
/** Maximum value length. */
|
|
28
|
+
const MAX_VALUE_LENGTH = 500;
|
|
29
|
+
|
|
30
|
+
// ── Detection patterns ──────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
interface DetectionRule {
|
|
33
|
+
key: string;
|
|
34
|
+
pattern: RegExp;
|
|
35
|
+
value: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const TEST_RUNNERS: DetectionRule[] = [
|
|
39
|
+
{ key: 'project:test-runner', pattern: /\bvitest\b/i, value: 'vitest' },
|
|
40
|
+
{ key: 'project:test-runner', pattern: /\bjest\b/i, value: 'jest' },
|
|
41
|
+
{ key: 'project:test-runner', pattern: /\bmocha\b/i, value: 'mocha' },
|
|
42
|
+
{ key: 'project:test-runner', pattern: /\bplaywright\b/i, value: 'playwright' },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const BUILD_TOOLS: DetectionRule[] = [
|
|
46
|
+
{ key: 'project:build-tool', pattern: /\bvite\b/i, value: 'vite' },
|
|
47
|
+
{ key: 'project:build-tool', pattern: /\bwebpack\b/i, value: 'webpack' },
|
|
48
|
+
{ key: 'project:build-tool', pattern: /\besbuild\b/i, value: 'esbuild' },
|
|
49
|
+
{ key: 'project:build-tool', pattern: /\btsc\b/i, value: 'tsc' },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const PKG_MANAGERS: DetectionRule[] = [
|
|
53
|
+
{ key: 'project:pkg-manager', pattern: /\bpnpm\b/i, value: 'pnpm' },
|
|
54
|
+
{ key: 'project:pkg-manager', pattern: /\byarn\b/i, value: 'yarn' },
|
|
55
|
+
{ key: 'project:pkg-manager', pattern: /\bbun\b/i, value: 'bun' },
|
|
56
|
+
{ key: 'project:pkg-manager', pattern: /\bnpm\b/i, value: 'npm' },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const LINTERS: DetectionRule[] = [
|
|
60
|
+
{ key: 'project:linter', pattern: /\bbiome\b/i, value: 'biome' },
|
|
61
|
+
{ key: 'project:linter', pattern: /\beslint\b/i, value: 'eslint' },
|
|
62
|
+
{ key: 'project:linter', pattern: /\bprettier\b/i, value: 'prettier' },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
function truncate(s: string, max: number): string {
|
|
68
|
+
return s.length <= max ? s : s.slice(0, max - 3) + '...';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeBlockerKey(text: string): string {
|
|
72
|
+
return text
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
75
|
+
.trim()
|
|
76
|
+
.replace(/\s+/g, '-')
|
|
77
|
+
.slice(0, 40);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function searchableText(rp: RunProgress): string {
|
|
81
|
+
const parts = [rp.summary ?? ''];
|
|
82
|
+
if (rp.checks) {
|
|
83
|
+
for (const v of Object.values(rp.checks)) {
|
|
84
|
+
parts.push(String(v));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return parts.join(' ');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function detectFirst(
|
|
91
|
+
text: string,
|
|
92
|
+
rules: DetectionRule[],
|
|
93
|
+
): MemoryFact | undefined {
|
|
94
|
+
for (const rule of rules) {
|
|
95
|
+
if (rule.pattern.test(text)) {
|
|
96
|
+
return { key: rule.key, value: rule.value };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function countExtensions(files: string[]): Record<string, number> {
|
|
103
|
+
const counts: Record<string, number> = {};
|
|
104
|
+
for (const f of files) {
|
|
105
|
+
const ext = f.includes('.') ? '.' + f.split('.').pop() : '(none)';
|
|
106
|
+
counts[ext] = (counts[ext] ?? 0) + 1;
|
|
107
|
+
}
|
|
108
|
+
return counts;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Main extraction ─────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Extract generalizable facts from a completed run.
|
|
115
|
+
*
|
|
116
|
+
* @param task - The task that was executed
|
|
117
|
+
* @param rp - The run's progress/result data
|
|
118
|
+
* @param existingKeys - Keys already in the KnowledgeStore (for dedup)
|
|
119
|
+
* @returns Array of facts to persist (max MAX_FACTS_PER_RUN)
|
|
120
|
+
*/
|
|
121
|
+
export function extractMemoryFacts(
|
|
122
|
+
task: Task,
|
|
123
|
+
rp: RunProgress,
|
|
124
|
+
existingKeys: Set<string>,
|
|
125
|
+
): MemoryFact[] {
|
|
126
|
+
const facts: MemoryFact[] = [];
|
|
127
|
+
const text = searchableText(rp);
|
|
128
|
+
const allFiles = [...rp.filesCreated, ...rp.filesModified];
|
|
129
|
+
|
|
130
|
+
// ── Project-level facts (overwrite existing) ──
|
|
131
|
+
|
|
132
|
+
const testRunner = detectFirst(text, TEST_RUNNERS);
|
|
133
|
+
if (testRunner) facts.push(testRunner);
|
|
134
|
+
|
|
135
|
+
const buildTool = detectFirst(text, BUILD_TOOLS);
|
|
136
|
+
if (buildTool) facts.push(buildTool);
|
|
137
|
+
|
|
138
|
+
const pkgManager = detectFirst(text, PKG_MANAGERS);
|
|
139
|
+
if (pkgManager) facts.push(pkgManager);
|
|
140
|
+
|
|
141
|
+
const linter = detectFirst(text, LINTERS);
|
|
142
|
+
if (linter) facts.push(linter);
|
|
143
|
+
|
|
144
|
+
// ── Source file pattern ──
|
|
145
|
+
|
|
146
|
+
if (allFiles.length >= 3 && !existingKeys.has('project:source-pattern')) {
|
|
147
|
+
const extCounts = countExtensions(allFiles);
|
|
148
|
+
const dominant = Object.entries(extCounts)
|
|
149
|
+
.sort(([, a], [, b]) => b - a)
|
|
150
|
+
.slice(0, 3)
|
|
151
|
+
.map(([ext, count]) => `${ext} (${count})`)
|
|
152
|
+
.join(', ');
|
|
153
|
+
facts.push({
|
|
154
|
+
key: 'project:source-pattern',
|
|
155
|
+
value: truncate(dominant, MAX_VALUE_LENGTH),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Blockers ──
|
|
160
|
+
|
|
161
|
+
if (rp.blockers && rp.blockers.length > 0) {
|
|
162
|
+
for (const blocker of rp.blockers.slice(0, 2)) {
|
|
163
|
+
const normalized = normalizeBlockerKey(blocker);
|
|
164
|
+
if (!normalized) continue;
|
|
165
|
+
const key = truncate(`pattern:blocker:${normalized}`, MAX_KEY_LENGTH);
|
|
166
|
+
if (!existingKeys.has(key)) {
|
|
167
|
+
facts.push({ key, value: truncate(blocker, MAX_VALUE_LENGTH) });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Stagnation pattern ──
|
|
173
|
+
|
|
174
|
+
if (
|
|
175
|
+
rp.outcome === 'stalled' &&
|
|
176
|
+
task.context.stagnationCount > 1
|
|
177
|
+
) {
|
|
178
|
+
const key = truncate(`pattern:stagnation:${task.id}`, MAX_KEY_LENGTH);
|
|
179
|
+
if (!existingKeys.has(key)) {
|
|
180
|
+
facts.push({
|
|
181
|
+
key,
|
|
182
|
+
value: truncate(
|
|
183
|
+
`Task "${task.title}" stalled ${task.context.stagnationCount}x. Last: ${rp.summary}`,
|
|
184
|
+
MAX_VALUE_LENGTH,
|
|
185
|
+
),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── First-run success pattern ──
|
|
191
|
+
|
|
192
|
+
if (
|
|
193
|
+
rp.outcome === 'completed' &&
|
|
194
|
+
task.context.runHistory.length <= 1
|
|
195
|
+
) {
|
|
196
|
+
const key = truncate(
|
|
197
|
+
`pattern:success:${rp.profileId}:${task.complexity ?? 'unknown'}`,
|
|
198
|
+
MAX_KEY_LENGTH,
|
|
199
|
+
);
|
|
200
|
+
if (!existingKeys.has(key)) {
|
|
201
|
+
facts.push({
|
|
202
|
+
key,
|
|
203
|
+
value: truncate(
|
|
204
|
+
`Profile ${rp.profileId} completed ${task.complexity ?? 'unknown'}-complexity task on first run`,
|
|
205
|
+
MAX_VALUE_LENGTH,
|
|
206
|
+
),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Enforce max facts per run
|
|
212
|
+
return facts.slice(0, MAX_FACTS_PER_RUN);
|
|
213
|
+
}
|
package/src/bot/operations.ts
CHANGED
|
@@ -38,8 +38,10 @@ export const OP_TASK_CREATE = 'task_create';
|
|
|
38
38
|
// Memory
|
|
39
39
|
// ---------------------------------------------------------------------------
|
|
40
40
|
|
|
41
|
-
export const
|
|
41
|
+
export const OP_LEARN = 'learn';
|
|
42
42
|
export const OP_RECALL = 'recall';
|
|
43
|
+
/** @deprecated Use OP_LEARN. Kept for step-executor backward compat. */
|
|
44
|
+
export const OP_REMEMBER = 'remember';
|
|
43
45
|
|
|
44
46
|
// ---------------------------------------------------------------------------
|
|
45
47
|
// Passthrough (no execution needed)
|