dual-brain 7.1.21 → 7.1.22
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/bin/dual-brain.mjs +2580 -717
- package/hooks/budget-balancer.mjs +104 -266
- package/hooks/wave-orchestrator.mjs +29 -26
- package/package.json +13 -3
- package/scripts/verify-publish.mjs +26 -0
- package/src/context.mjs +389 -0
- package/src/decide.mjs +283 -60
- package/src/detect.mjs +133 -1
- package/src/dispatch.mjs +175 -30
- package/src/doctor.mjs +577 -0
- package/src/failure-memory.mjs +178 -0
- package/src/nextstep.mjs +100 -0
- package/src/observer.mjs +241 -0
- package/src/outcome.mjs +256 -0
- package/src/pipeline.mjs +759 -0
- package/src/profile.mjs +357 -485
- package/src/receipt.mjs +131 -0
- package/src/session.mjs +358 -10
package/src/outcome.mjs
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { mkdirSync, appendFileSync, readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
|
|
5
|
+
const STOP_WORDS = new Set([
|
|
6
|
+
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'to', 'from',
|
|
7
|
+
'in', 'on', 'for', 'with', 'and', 'or', 'but', 'not', 'this', 'that', 'it',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
function outcomesDir(cwd) {
|
|
11
|
+
return join(cwd, '.dualbrain', 'outcomes');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function todayFile(cwd) {
|
|
15
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
16
|
+
return join(outcomesDir(cwd), `outcomes-${date}.jsonl`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function ensureDir(cwd) {
|
|
20
|
+
mkdirSync(outcomesDir(cwd), { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readOutcomeFile(filePath) {
|
|
24
|
+
try {
|
|
25
|
+
return readFileSync(filePath, 'utf8')
|
|
26
|
+
.split('\n')
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.flatMap(line => {
|
|
29
|
+
try { return [JSON.parse(line)]; } catch { return []; }
|
|
30
|
+
});
|
|
31
|
+
} catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function last7DaysFiles(cwd) {
|
|
37
|
+
const dir = outcomesDir(cwd);
|
|
38
|
+
const files = [];
|
|
39
|
+
for (let i = 0; i < 7; i++) {
|
|
40
|
+
const d = new Date(Date.now() - i * 86_400_000).toISOString().slice(0, 10);
|
|
41
|
+
const f = join(dir, `outcomes-${d}.jsonl`);
|
|
42
|
+
if (existsSync(f)) files.push(f);
|
|
43
|
+
}
|
|
44
|
+
return files;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function computeRoutingScore(plan, result, verification) {
|
|
48
|
+
let score = 3;
|
|
49
|
+
if (result.success && result.duration < 60_000) score += 1;
|
|
50
|
+
if (verification.filesVerified && verification.testsPassed === true) score += 1;
|
|
51
|
+
if (result.error) score -= 1;
|
|
52
|
+
if (result.duration > 180_000) score -= 1;
|
|
53
|
+
if ((plan.challengerPolicy === 'none' || !plan.challengerPolicy) && !result.success) score -= 2;
|
|
54
|
+
return Math.max(1, Math.min(5, score));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function generateLessons(plan, result, verification) {
|
|
58
|
+
const lessons = [];
|
|
59
|
+
const noChallenger = !plan.challengerPolicy || plan.challengerPolicy === 'none';
|
|
60
|
+
|
|
61
|
+
if (noChallenger && !result.success) {
|
|
62
|
+
lessons.push('Task failed without challenger — consider escalating similar tasks');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
plan.reasoningDepth === 'ultra' &&
|
|
67
|
+
result.duration < 60_000 &&
|
|
68
|
+
(plan.complexity === 'simple' || plan.complexity === 'low')
|
|
69
|
+
) {
|
|
70
|
+
lessons.push('Ultra reasoning unnecessary — task completed quickly at low complexity');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!result.success) {
|
|
74
|
+
const keywords = (plan.prompt || '')
|
|
75
|
+
.toLowerCase()
|
|
76
|
+
.split(/\s+/)
|
|
77
|
+
.filter(w => w.length > 3 && !STOP_WORDS.has(w))
|
|
78
|
+
.slice(0, 4)
|
|
79
|
+
.join(' ');
|
|
80
|
+
if (keywords) {
|
|
81
|
+
lessons.push(`Prior failure pattern: ${keywords} on ${plan.tier}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!noChallenger && result.success && verification.filesVerified) {
|
|
86
|
+
lessons.push(`Challenger caught issues — keep challenger policy for ${plan.risk} risk`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return lessons;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function recordOutcome(plan, result, verification, cwd) {
|
|
93
|
+
try {
|
|
94
|
+
ensureDir(cwd);
|
|
95
|
+
|
|
96
|
+
const routingScore = computeRoutingScore(plan, result, verification);
|
|
97
|
+
const lessons = generateLessons(plan, result, verification);
|
|
98
|
+
|
|
99
|
+
const record = {
|
|
100
|
+
id: randomUUID(),
|
|
101
|
+
timestamp: Date.now(),
|
|
102
|
+
prompt: plan.prompt ?? '',
|
|
103
|
+
tier: plan.tier ?? '',
|
|
104
|
+
primaryModel: plan.primaryModel ?? '',
|
|
105
|
+
reasoningDepth: plan.reasoningDepth ?? '',
|
|
106
|
+
challengerPolicy: plan.challengerPolicy ?? 'none',
|
|
107
|
+
risk: plan.risk ?? '',
|
|
108
|
+
result: {
|
|
109
|
+
success: result.success ?? false,
|
|
110
|
+
filesChanged: result.filesChanged ?? [],
|
|
111
|
+
duration: result.duration ?? 0,
|
|
112
|
+
error: result.error ?? null,
|
|
113
|
+
},
|
|
114
|
+
verification: {
|
|
115
|
+
filesVerified: verification.filesVerified ?? false,
|
|
116
|
+
testsRun: verification.testsRun ?? false,
|
|
117
|
+
testsPassed: verification.testsPassed ?? null,
|
|
118
|
+
},
|
|
119
|
+
routingScore,
|
|
120
|
+
lessons,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
appendFileSync(todayFile(cwd), JSON.stringify(record) + '\n', 'utf8');
|
|
124
|
+
return record;
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function tokenize(text) {
|
|
131
|
+
return (text || '')
|
|
132
|
+
.toLowerCase()
|
|
133
|
+
.split(/\W+/)
|
|
134
|
+
.filter(w => w.length > 3 && !STOP_WORDS.has(w));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function promptOverlap(a, b) {
|
|
138
|
+
const wordsA = new Set(tokenize(a));
|
|
139
|
+
const wordsB = tokenize(b);
|
|
140
|
+
return wordsB.filter(w => wordsA.has(w)).length;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function fileOverlap(filesA = [], filesB = []) {
|
|
144
|
+
const setA = new Set(filesA.map(f => f.split('/').pop()));
|
|
145
|
+
return filesB.map(f => f.split('/').pop()).filter(f => setA.has(f)).length;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function getRelevantOutcomes(prompt, files = [], cwd, options = {}) {
|
|
149
|
+
try {
|
|
150
|
+
const allFiles = last7DaysFiles(cwd);
|
|
151
|
+
const outcomes = allFiles.flatMap(readOutcomeFile);
|
|
152
|
+
|
|
153
|
+
const scored = outcomes.map(o => {
|
|
154
|
+
let score = promptOverlap(prompt, o.prompt);
|
|
155
|
+
score += fileOverlap(files, o.result?.filesChanged ?? []);
|
|
156
|
+
return { o, score };
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return scored
|
|
160
|
+
.filter(({ score }) => score >= 2)
|
|
161
|
+
.sort((a, b) => b.score - a.score)
|
|
162
|
+
.slice(0, 5)
|
|
163
|
+
.map(({ o, score }) => ({
|
|
164
|
+
id: o.id,
|
|
165
|
+
timestamp: o.timestamp,
|
|
166
|
+
prompt: o.prompt,
|
|
167
|
+
success: o.result?.success ?? false,
|
|
168
|
+
routingScore: o.routingScore,
|
|
169
|
+
lessons: o.lessons,
|
|
170
|
+
relevanceScore: score,
|
|
171
|
+
}));
|
|
172
|
+
} catch {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function getOutcomeStats(cwd, days = 7) {
|
|
178
|
+
try {
|
|
179
|
+
const allFiles = last7DaysFiles(cwd).slice(0, days);
|
|
180
|
+
const outcomes = allFiles.flatMap(readOutcomeFile);
|
|
181
|
+
|
|
182
|
+
if (outcomes.length === 0) {
|
|
183
|
+
return {
|
|
184
|
+
totalTasks: 0,
|
|
185
|
+
successRate: 0,
|
|
186
|
+
avgRoutingScore: 0,
|
|
187
|
+
avgDuration: 0,
|
|
188
|
+
challengerHelpRate: 0,
|
|
189
|
+
topLessons: [],
|
|
190
|
+
modelBreakdown: {},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const totalTasks = outcomes.length;
|
|
195
|
+
const successes = outcomes.filter(o => o.result?.success).length;
|
|
196
|
+
const successRate = successes / totalTasks;
|
|
197
|
+
|
|
198
|
+
const avgRoutingScore =
|
|
199
|
+
outcomes.reduce((sum, o) => sum + (o.routingScore ?? 3), 0) / totalTasks;
|
|
200
|
+
|
|
201
|
+
const avgDuration =
|
|
202
|
+
outcomes.reduce((sum, o) => sum + (o.result?.duration ?? 0), 0) / totalTasks;
|
|
203
|
+
|
|
204
|
+
const challengerUsed = outcomes.filter(
|
|
205
|
+
o => o.challengerPolicy && o.challengerPolicy !== 'none'
|
|
206
|
+
);
|
|
207
|
+
const challengerHelped = challengerUsed.filter(o => o.result?.success);
|
|
208
|
+
const challengerHelpRate =
|
|
209
|
+
challengerUsed.length > 0 ? challengerHelped.length / challengerUsed.length : 0;
|
|
210
|
+
|
|
211
|
+
const lessonCounts = {};
|
|
212
|
+
for (const o of outcomes) {
|
|
213
|
+
for (const lesson of o.lessons ?? []) {
|
|
214
|
+
lessonCounts[lesson] = (lessonCounts[lesson] ?? 0) + 1;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const topLessons = Object.entries(lessonCounts)
|
|
218
|
+
.sort((a, b) => b[1] - a[1])
|
|
219
|
+
.slice(0, 5)
|
|
220
|
+
.map(([lesson]) => lesson);
|
|
221
|
+
|
|
222
|
+
const modelBreakdown = {};
|
|
223
|
+
for (const o of outcomes) {
|
|
224
|
+
const model = o.primaryModel;
|
|
225
|
+
if (!model) continue;
|
|
226
|
+
if (!modelBreakdown[model]) modelBreakdown[model] = { count: 0, successCount: 0 };
|
|
227
|
+
modelBreakdown[model].count += 1;
|
|
228
|
+
if (o.result?.success) modelBreakdown[model].successCount += 1;
|
|
229
|
+
}
|
|
230
|
+
for (const model of Object.keys(modelBreakdown)) {
|
|
231
|
+
const { count, successCount } = modelBreakdown[model];
|
|
232
|
+
modelBreakdown[model].successRate = count > 0 ? successCount / count : 0;
|
|
233
|
+
delete modelBreakdown[model].successCount;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
totalTasks,
|
|
238
|
+
successRate,
|
|
239
|
+
avgRoutingScore,
|
|
240
|
+
avgDuration,
|
|
241
|
+
challengerHelpRate,
|
|
242
|
+
topLessons,
|
|
243
|
+
modelBreakdown,
|
|
244
|
+
};
|
|
245
|
+
} catch {
|
|
246
|
+
return {
|
|
247
|
+
totalTasks: 0,
|
|
248
|
+
successRate: 0,
|
|
249
|
+
avgRoutingScore: 0,
|
|
250
|
+
avgDuration: 0,
|
|
251
|
+
challengerHelpRate: 0,
|
|
252
|
+
topLessons: [],
|
|
253
|
+
modelBreakdown: {},
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|