portable-agent-layer 0.6.1 → 0.6.2

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.
@@ -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
- }