portable-agent-layer 0.6.1 → 0.7.0
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/package.json +2 -3
- package/src/cli/index.ts +12 -0
- package/src/hooks/handlers/failure.ts +20 -83
- package/src/hooks/handlers/rating.ts +1 -1
- package/src/hooks/handlers/update-check.ts +189 -0
- package/src/hooks/handlers/work-learning.ts +4 -9
- package/src/hooks/lib/context.ts +24 -150
- package/src/hooks/lib/graduation.ts +199 -336
- package/src/hooks/lib/learning-store.ts +265 -0
- package/src/hooks/lib/security.ts +1 -0
- package/src/hooks/lib/stop.ts +1 -6
- package/src/tools/analyze.ts +118 -0
- package/src/hooks/handlers/synthesis.ts +0 -109
- package/src/tools/graduate.ts +0 -79
- package/src/tools/pattern-synthesis.ts +0 -432
|
@@ -1,432 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* LearningPatternSynthesis — Aggregate ratings into actionable patterns.
|
|
4
|
-
*
|
|
5
|
-
* Analyzes memory/signals/ratings.jsonl to find recurring frustration/success
|
|
6
|
-
* patterns and generates synthesis reports.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* bun run tool:patterns # Analyze last 7 days (default)
|
|
10
|
-
* bun run tool:patterns -- --month # Analyze last 30 days
|
|
11
|
-
* bun run tool:patterns -- --all # Analyze all ratings
|
|
12
|
-
* bun run tool:patterns -- --dry-run # Preview without writing
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
|
-
import { resolve } from "node:path";
|
|
17
|
-
import { parseArgs } from "node:util";
|
|
18
|
-
import { stringify } from "../hooks/lib/frontmatter";
|
|
19
|
-
import { HAIKU_MODEL } from "../hooks/lib/models";
|
|
20
|
-
import { palHome } from "../hooks/lib/paths";
|
|
21
|
-
|
|
22
|
-
// ── Paths ──
|
|
23
|
-
|
|
24
|
-
const RATINGS_FILE = resolve(palHome(), "memory", "signals", "ratings.jsonl");
|
|
25
|
-
const SYNTHESIS_DIR = resolve(palHome(), "memory", "learning", "synthesis");
|
|
26
|
-
|
|
27
|
-
// ── Types ──
|
|
28
|
-
|
|
29
|
-
interface Rating {
|
|
30
|
-
ts: string;
|
|
31
|
-
rating: number;
|
|
32
|
-
context: string;
|
|
33
|
-
source: "explicit" | "implicit";
|
|
34
|
-
response_preview?: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface PatternGroup {
|
|
38
|
-
pattern: string;
|
|
39
|
-
count: number;
|
|
40
|
-
avgRating: number;
|
|
41
|
-
examples: string[];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface SynthesisResult {
|
|
45
|
-
period: string;
|
|
46
|
-
totalRatings: number;
|
|
47
|
-
avgRating: number;
|
|
48
|
-
frustrations: PatternGroup[];
|
|
49
|
-
successes: PatternGroup[];
|
|
50
|
-
topIssues: string[];
|
|
51
|
-
recommendations: string[];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ── Pattern Detection ──
|
|
55
|
-
|
|
56
|
-
const FRUSTRATION_PATTERNS: Record<string, RegExp> = {
|
|
57
|
-
"Time/Performance Issues": /time|slow|delay|hang|wait|long|minutes|hours/i,
|
|
58
|
-
"Incomplete Work": /incomplete|missing|partial|didn't finish|not done/i,
|
|
59
|
-
"Wrong Approach": /wrong|incorrect|not what|misunderstand|mistake/i,
|
|
60
|
-
"Over-engineering": /over-?engineer|too complex|unnecessary|bloat/i,
|
|
61
|
-
"Tool/System Failures": /fail|error|broken|crash|bug|issue/i,
|
|
62
|
-
"Communication Problems": /unclear|confus|didn't ask|should have asked/i,
|
|
63
|
-
"Repetitive Issues": /again|repeat|still|same problem/i,
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const SUCCESS_PATTERNS: Record<string, RegExp> = {
|
|
67
|
-
"Quick Resolution": /quick|fast|efficient|smooth/i,
|
|
68
|
-
"Good Understanding": /understood|clear|exactly|perfect/i,
|
|
69
|
-
"Proactive Help": /proactive|anticipat|helpful|above and beyond/i,
|
|
70
|
-
"Clean Implementation": /clean|simple|elegant|well done/i,
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
function detectPatterns(
|
|
74
|
-
summaries: string[],
|
|
75
|
-
patterns: Record<string, RegExp>
|
|
76
|
-
): Map<string, string[]> {
|
|
77
|
-
const results = new Map<string, string[]>();
|
|
78
|
-
for (const summary of summaries) {
|
|
79
|
-
for (const [name, pattern] of Object.entries(patterns)) {
|
|
80
|
-
if (pattern.test(summary)) {
|
|
81
|
-
const arr = results.get(name) ?? [];
|
|
82
|
-
arr.push(summary);
|
|
83
|
-
results.set(name, arr);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return results;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function toPatternGroups(
|
|
91
|
-
grouped: Map<string, string[]>,
|
|
92
|
-
ratings: Rating[]
|
|
93
|
-
): PatternGroup[] {
|
|
94
|
-
const groups: PatternGroup[] = [];
|
|
95
|
-
|
|
96
|
-
for (const [pattern, examples] of grouped.entries()) {
|
|
97
|
-
const matching = ratings.filter((r) => examples.some((e) => e === r.context));
|
|
98
|
-
const avgRating =
|
|
99
|
-
matching.length > 0
|
|
100
|
-
? matching.reduce((sum, r) => sum + r.rating, 0) / matching.length
|
|
101
|
-
: 5;
|
|
102
|
-
|
|
103
|
-
groups.push({
|
|
104
|
-
pattern,
|
|
105
|
-
count: examples.length,
|
|
106
|
-
avgRating,
|
|
107
|
-
examples: examples.slice(0, 3),
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return groups.sort((a, b) => b.count - a.count);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// ── Analysis ──
|
|
115
|
-
|
|
116
|
-
async function generateRecommendations(
|
|
117
|
-
frustrations: PatternGroup[],
|
|
118
|
-
successes: PatternGroup[],
|
|
119
|
-
avgRating: number
|
|
120
|
-
): Promise<string[]> {
|
|
121
|
-
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
122
|
-
if (!apiKey || frustrations.length === 0) {
|
|
123
|
-
// Fallback: generic recommendations
|
|
124
|
-
if (frustrations.length === 0)
|
|
125
|
-
return ["Continue current patterns - no major issues detected"];
|
|
126
|
-
return frustrations
|
|
127
|
-
.slice(0, 3)
|
|
128
|
-
.map(
|
|
129
|
-
(f) =>
|
|
130
|
-
`Address "${f.pattern}" (${f.count} occurrences, avg ${f.avgRating.toFixed(1)}/10)`
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
try {
|
|
135
|
-
const context = [
|
|
136
|
-
`Average rating: ${avgRating.toFixed(1)}/10`,
|
|
137
|
-
"",
|
|
138
|
-
"Top frustration patterns:",
|
|
139
|
-
...frustrations
|
|
140
|
-
.slice(0, 5)
|
|
141
|
-
.map(
|
|
142
|
-
(f) =>
|
|
143
|
-
`- ${f.pattern} (${f.count}x, avg ${f.avgRating.toFixed(1)}): ${f.examples.slice(0, 2).join("; ")}`
|
|
144
|
-
),
|
|
145
|
-
"",
|
|
146
|
-
successes.length > 0 ? "Success patterns:" : "",
|
|
147
|
-
...successes
|
|
148
|
-
.slice(0, 3)
|
|
149
|
-
.map((s) => `- ${s.pattern} (${s.count}x, avg ${s.avgRating.toFixed(1)})`),
|
|
150
|
-
]
|
|
151
|
-
.filter(Boolean)
|
|
152
|
-
.join("\n");
|
|
153
|
-
|
|
154
|
-
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
155
|
-
method: "POST",
|
|
156
|
-
headers: {
|
|
157
|
-
"x-api-key": apiKey,
|
|
158
|
-
"anthropic-version": "2023-06-01",
|
|
159
|
-
"content-type": "application/json",
|
|
160
|
-
},
|
|
161
|
-
body: JSON.stringify({
|
|
162
|
-
model: HAIKU_MODEL,
|
|
163
|
-
max_tokens: 300,
|
|
164
|
-
messages: [{ role: "user", content: context }],
|
|
165
|
-
system:
|
|
166
|
-
"You analyze AI assistant interaction patterns. Given frustration and success patterns from user ratings, generate 3-5 recommendations. Each MUST reference a specific example from the data — no generic advice like 'ask clarifying questions' or 'communicate better'. Every recommendation should name the concrete situation and the concrete fix. One sentence each. Return a JSON array of strings.",
|
|
167
|
-
output_config: {
|
|
168
|
-
format: {
|
|
169
|
-
type: "json_schema",
|
|
170
|
-
schema: {
|
|
171
|
-
type: "object",
|
|
172
|
-
additionalProperties: false,
|
|
173
|
-
properties: {
|
|
174
|
-
recommendations: {
|
|
175
|
-
type: "array",
|
|
176
|
-
items: { type: "string" },
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
required: ["recommendations"],
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
}),
|
|
184
|
-
signal: AbortSignal.timeout(15000),
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
if (response.ok) {
|
|
188
|
-
const data = (await response.json()) as { content?: Array<{ text?: string }> };
|
|
189
|
-
const text = data?.content?.[0]?.text?.trim();
|
|
190
|
-
if (text) {
|
|
191
|
-
const parsed = JSON.parse(text) as { recommendations: string[] };
|
|
192
|
-
if (parsed.recommendations?.length > 0) return parsed.recommendations.slice(0, 5);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
} catch {
|
|
196
|
-
// Fallback silently
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return frustrations
|
|
200
|
-
.slice(0, 3)
|
|
201
|
-
.map((f) => `Address "${f.pattern}" (${f.count} occurrences)`);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
async function analyzeRatings(
|
|
205
|
-
ratings: Rating[],
|
|
206
|
-
period: string
|
|
207
|
-
): Promise<SynthesisResult> {
|
|
208
|
-
if (ratings.length === 0) {
|
|
209
|
-
return {
|
|
210
|
-
period,
|
|
211
|
-
totalRatings: 0,
|
|
212
|
-
avgRating: 0,
|
|
213
|
-
frustrations: [],
|
|
214
|
-
successes: [],
|
|
215
|
-
topIssues: [],
|
|
216
|
-
recommendations: [],
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const avgRating = ratings.reduce((sum, r) => sum + r.rating, 0) / ratings.length;
|
|
221
|
-
|
|
222
|
-
const frustrationRatings = ratings.filter((r) => r.rating <= 4);
|
|
223
|
-
const successRatings = ratings.filter((r) => r.rating >= 7);
|
|
224
|
-
|
|
225
|
-
const frustrationGroups = detectPatterns(
|
|
226
|
-
frustrationRatings.map((r) => r.context),
|
|
227
|
-
FRUSTRATION_PATTERNS
|
|
228
|
-
);
|
|
229
|
-
const successGroups = detectPatterns(
|
|
230
|
-
successRatings.map((r) => r.context),
|
|
231
|
-
SUCCESS_PATTERNS
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
const frustrations = toPatternGroups(frustrationGroups, frustrationRatings);
|
|
235
|
-
const successes = toPatternGroups(successGroups, successRatings);
|
|
236
|
-
|
|
237
|
-
const topIssues = frustrations
|
|
238
|
-
.slice(0, 3)
|
|
239
|
-
.map(
|
|
240
|
-
(f) => `${f.pattern} (${f.count} occurrences, avg rating ${f.avgRating.toFixed(1)})`
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
const recommendations = await generateRecommendations(
|
|
244
|
-
frustrations,
|
|
245
|
-
successes,
|
|
246
|
-
avgRating
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
return {
|
|
250
|
-
period,
|
|
251
|
-
totalRatings: ratings.length,
|
|
252
|
-
avgRating,
|
|
253
|
-
frustrations,
|
|
254
|
-
successes,
|
|
255
|
-
topIssues,
|
|
256
|
-
recommendations,
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// ── Report ──
|
|
261
|
-
|
|
262
|
-
function formatReport(result: SynthesisResult): string {
|
|
263
|
-
const date = new Date().toISOString().slice(0, 10);
|
|
264
|
-
|
|
265
|
-
const meta: Record<string, unknown> = {
|
|
266
|
-
period: result.period,
|
|
267
|
-
date,
|
|
268
|
-
total_ratings: result.totalRatings,
|
|
269
|
-
average_rating: result.avgRating.toFixed(1),
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
const lines: string[] = ["## Top Issues", ""];
|
|
273
|
-
|
|
274
|
-
if (result.topIssues.length > 0) {
|
|
275
|
-
for (let i = 0; i < result.topIssues.length; i++) {
|
|
276
|
-
lines.push(`${i + 1}. ${result.topIssues[i]}`);
|
|
277
|
-
}
|
|
278
|
-
} else {
|
|
279
|
-
lines.push("No significant issues detected");
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
lines.push("", "## Frustration Patterns", "");
|
|
283
|
-
if (result.frustrations.length === 0) {
|
|
284
|
-
lines.push("*No frustration patterns detected*");
|
|
285
|
-
} else {
|
|
286
|
-
for (const f of result.frustrations) {
|
|
287
|
-
lines.push(
|
|
288
|
-
`### ${f.pattern}`,
|
|
289
|
-
"",
|
|
290
|
-
`- **Occurrences:** ${f.count}`,
|
|
291
|
-
`- **Avg Rating:** ${f.avgRating.toFixed(1)}`,
|
|
292
|
-
`- **Examples:**`,
|
|
293
|
-
...f.examples.map((e) => ` - "${e}"`),
|
|
294
|
-
""
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
lines.push("", "## Success Patterns", "");
|
|
300
|
-
if (result.successes.length === 0) {
|
|
301
|
-
lines.push("*No success patterns detected*");
|
|
302
|
-
} else {
|
|
303
|
-
for (const s of result.successes) {
|
|
304
|
-
lines.push(
|
|
305
|
-
`### ${s.pattern}`,
|
|
306
|
-
"",
|
|
307
|
-
`- **Occurrences:** ${s.count}`,
|
|
308
|
-
`- **Avg Rating:** ${s.avgRating.toFixed(1)}`,
|
|
309
|
-
`- **Examples:**`,
|
|
310
|
-
...s.examples.map((e) => ` - "${e}"`),
|
|
311
|
-
""
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
lines.push(
|
|
317
|
-
"",
|
|
318
|
-
"## Recommendations",
|
|
319
|
-
"",
|
|
320
|
-
...result.recommendations.map((r, i) => `${i + 1}. ${r}`),
|
|
321
|
-
""
|
|
322
|
-
);
|
|
323
|
-
|
|
324
|
-
return stringify(meta, lines.join("\n"));
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function writeReport(result: SynthesisResult, period: string): string {
|
|
328
|
-
const date = new Date().toISOString().slice(0, 10);
|
|
329
|
-
const monthDir = resolve(SYNTHESIS_DIR, date.slice(0, 7));
|
|
330
|
-
if (!existsSync(monthDir)) mkdirSync(monthDir, { recursive: true });
|
|
331
|
-
|
|
332
|
-
const slug = period.toLowerCase().replace(/\s+/g, "-");
|
|
333
|
-
const filename = `${date}_${slug}-patterns.md`;
|
|
334
|
-
const filepath = resolve(monthDir, filename);
|
|
335
|
-
|
|
336
|
-
writeFileSync(filepath, formatReport(result), "utf-8");
|
|
337
|
-
return filepath;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// ── CLI ──
|
|
341
|
-
|
|
342
|
-
const { values } = parseArgs({
|
|
343
|
-
args: Bun.argv.slice(2),
|
|
344
|
-
options: {
|
|
345
|
-
week: { type: "boolean" },
|
|
346
|
-
month: { type: "boolean" },
|
|
347
|
-
all: { type: "boolean" },
|
|
348
|
-
"dry-run": { type: "boolean" },
|
|
349
|
-
help: { type: "boolean", short: "h" },
|
|
350
|
-
},
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
if (values.help) {
|
|
354
|
-
console.log(`
|
|
355
|
-
LearningPatternSynthesis — Aggregate ratings into actionable patterns
|
|
356
|
-
|
|
357
|
-
Usage:
|
|
358
|
-
bun run tool:patterns Analyze last 7 days (default)
|
|
359
|
-
bun run tool:patterns -- --month Analyze last 30 days
|
|
360
|
-
bun run tool:patterns -- --all Analyze all ratings
|
|
361
|
-
bun run tool:patterns -- --dry-run Preview without writing
|
|
362
|
-
|
|
363
|
-
Output: Creates synthesis report in memory/learning/synthesis/YYYY-MM/
|
|
364
|
-
`);
|
|
365
|
-
process.exit(0);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (!existsSync(RATINGS_FILE)) {
|
|
369
|
-
console.log("No ratings file found at:", RATINGS_FILE);
|
|
370
|
-
process.exit(0);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Read ratings
|
|
374
|
-
const allRatings: Rating[] = readFileSync(RATINGS_FILE, "utf-8")
|
|
375
|
-
.split("\n")
|
|
376
|
-
.filter((l) => l.trim())
|
|
377
|
-
.map((l) => {
|
|
378
|
-
try {
|
|
379
|
-
return JSON.parse(l);
|
|
380
|
-
} catch {
|
|
381
|
-
return null;
|
|
382
|
-
}
|
|
383
|
-
})
|
|
384
|
-
.filter((r): r is Rating => r !== null);
|
|
385
|
-
|
|
386
|
-
console.log(`Loaded ${allRatings.length} total ratings`);
|
|
387
|
-
|
|
388
|
-
// Determine period
|
|
389
|
-
let period = "Weekly";
|
|
390
|
-
const cutoff = new Date();
|
|
391
|
-
|
|
392
|
-
if (values.month) {
|
|
393
|
-
period = "Monthly";
|
|
394
|
-
cutoff.setDate(cutoff.getDate() - 30);
|
|
395
|
-
} else if (values.all) {
|
|
396
|
-
period = "All Time";
|
|
397
|
-
cutoff.setTime(0);
|
|
398
|
-
} else {
|
|
399
|
-
cutoff.setDate(cutoff.getDate() - 7);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const filtered = allRatings.filter((r) => new Date(r.ts).getTime() >= cutoff.getTime());
|
|
403
|
-
console.log(`Analyzing ${filtered.length} ratings for ${period.toLowerCase()} period`);
|
|
404
|
-
|
|
405
|
-
if (filtered.length === 0) {
|
|
406
|
-
console.log("No ratings in this period");
|
|
407
|
-
process.exit(0);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
const result = await analyzeRatings(filtered, period);
|
|
411
|
-
|
|
412
|
-
console.log(`\nAverage Rating: ${result.avgRating.toFixed(1)}/10`);
|
|
413
|
-
console.log(`Frustration Patterns: ${result.frustrations.length}`);
|
|
414
|
-
console.log(`Success Patterns: ${result.successes.length}`);
|
|
415
|
-
|
|
416
|
-
if (result.topIssues.length > 0) {
|
|
417
|
-
console.log("\nTop Issues:");
|
|
418
|
-
for (const issue of result.topIssues) {
|
|
419
|
-
console.log(` - ${issue}`);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if (values["dry-run"]) {
|
|
424
|
-
console.log("\n[DRY RUN] Would write synthesis report");
|
|
425
|
-
console.log("\nRecommendations:");
|
|
426
|
-
for (const rec of result.recommendations) {
|
|
427
|
-
console.log(` - ${rec}`);
|
|
428
|
-
}
|
|
429
|
-
} else {
|
|
430
|
-
const filepath = writeReport(result, period);
|
|
431
|
-
console.log(`\nCreated synthesis report: ${filepath}`);
|
|
432
|
-
}
|