portable-agent-layer 0.6.0 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
@@ -49,8 +49,7 @@
49
49
  "ai:pdf-download": "bun run src/tools/pdf-download.ts",
50
50
  "ai:youtube-analyze": "bun run src/tools/youtube-analyze.ts",
51
51
  "tool:eval": "bun run src/tools/eval-principles.ts",
52
- "tool:graduate": "bun run src/tools/graduate.ts",
53
- "tool:patterns": "bun run src/tools/pattern-synthesis.ts",
52
+ "tool:analyze": "bun run src/tools/analyze.ts",
54
53
  "tool:reflect": "bun run src/tools/relationship-reflect.ts",
55
54
  "tool:export": "bun run src/tools/export.ts",
56
55
  "tool:import": "bun run src/tools/import.ts",
@@ -1,25 +1,18 @@
1
1
  /**
2
2
  * Deep Failure Capture — full context dump for ratings 1–3.
3
3
  *
4
- * Writes to memory/learning/failures/YYYY-MM/{timestamp}_{slug}/
5
- * capture.md — frontmatter metadata + failure context body
6
- * sentiment.json — DEPRECATED legacy format (kept for backward compat)
4
+ * Stores raw conversation data for later review, matching the original PAI pattern:
5
+ * capture.md — frontmatter metadata + conversation summary
6
+ *
7
+ * Analysis is left to the human or the graduation pipeline, not auto-generated.
7
8
  */
8
9
 
9
10
  import { writeFileSync } from "node:fs";
10
11
  import { resolve } from "node:path";
11
12
  import { stringify } from "../lib/frontmatter";
12
- import { inference } from "../lib/inference";
13
13
  import { ensureDir, paths } from "../lib/paths";
14
- import { FAILURE_PRINCIPLE_PROMPT } from "../lib/prompts";
15
14
  import { fileTimestamp, monthPath } from "../lib/time";
16
- import { logTokenUsage } from "../lib/token-usage";
17
- import {
18
- extractContent,
19
- extractLastAssistant,
20
- extractLastUser,
21
- parseMessages,
22
- } from "../lib/transcript";
15
+ import { extractContent, parseMessages } from "../lib/transcript";
23
16
 
24
17
  function slugify(text: string): string {
25
18
  return (
@@ -37,68 +30,24 @@ export async function captureFailure(
37
30
  rating: number,
38
31
  context: string,
39
32
  transcript: string,
40
- detailedContext?: string,
41
- savedResponse?: string,
42
- savedUserMessage?: string
33
+ detailedContext?: string
43
34
  ): Promise<void> {
44
35
  const messages = parseMessages(transcript);
45
- // Prefer messages saved at rating time (before the AI replied to the rating)
46
- const lastUser =
47
- savedUserMessage?.slice(0, 400) ||
48
- extractContent(extractLastUser(messages)).slice(0, 400);
49
- const lastAssistant =
50
- savedResponse?.slice(0, 600) ||
51
- extractContent(extractLastAssistant(messages)).slice(0, 600);
36
+
37
+ // Conversation summary — last 10 exchanges, like the original PAI
38
+ const recentMessages = messages.slice(-10);
39
+ const conversationSummary = recentMessages
40
+ .map((m) => {
41
+ const text = extractContent(m).slice(0, 500);
42
+ return `**${m.role.toUpperCase()}:** ${text}`;
43
+ })
44
+ .join("\n\n");
52
45
 
53
46
  const slug = slugify(context);
54
47
  const dir = ensureDir(
55
48
  resolve(paths.failures(), monthPath(), `${fileTimestamp()}_${slug}`)
56
49
  );
57
50
 
58
- // Attempt inference to fill root cause analysis + candidate principle
59
- let whatWentWrong = "";
60
- let whatToDoDifferently = "";
61
- let principle = "";
62
- try {
63
- const analysisResult = await inference({
64
- system: `You are analyzing a failed AI assistant interaction. Based on the context, identify what went wrong and what should be done differently. Be specific and actionable. Also write a principle — ${FAILURE_PRINCIPLE_PROMPT}`,
65
- user: [
66
- `Rating: ${rating}/10`,
67
- `Context: ${context}`,
68
- detailedContext ? `Analysis: ${detailedContext}` : "",
69
- `Assistant response (what the user reacted to): ${lastAssistant}`,
70
- `User reaction (the frustrated message): ${lastUser}`,
71
- ]
72
- .filter(Boolean)
73
- .join("\n"),
74
- maxTokens: 400,
75
- timeout: 15000,
76
- jsonSchema: {
77
- type: "object" as const,
78
- additionalProperties: false,
79
- properties: {
80
- what_went_wrong: { type: "string" as const },
81
- what_to_do_differently: { type: "string" as const },
82
- principle: { type: "string" as const },
83
- },
84
- required: ["what_went_wrong", "what_to_do_differently", "principle"],
85
- },
86
- });
87
- if (analysisResult.usage) logTokenUsage("failure", analysisResult.usage);
88
- if (analysisResult.success && analysisResult.output) {
89
- const parsed = JSON.parse(analysisResult.output) as {
90
- what_went_wrong?: string;
91
- what_to_do_differently?: string;
92
- principle?: string;
93
- };
94
- whatWentWrong = parsed.what_went_wrong ?? "";
95
- whatToDoDifferently = parsed.what_to_do_differently ?? "";
96
- principle = parsed.principle ?? "";
97
- }
98
- } catch {
99
- // Graceful fallback — empty sections are still useful with the other context
100
- }
101
-
102
51
  const meta: Record<string, unknown> = {
103
52
  rating,
104
53
  context,
@@ -106,29 +55,17 @@ export async function captureFailure(
106
55
  ts: new Date().toISOString(),
107
56
  slug,
108
57
  };
109
- if (principle) meta.principle = principle;
110
58
 
111
59
  const body = [
112
- "## Last User Message",
113
- lastUser || "*(unavailable)*",
60
+ "## What Happened",
114
61
  "",
115
- "## Last Assistant Response",
116
- lastAssistant || "*(unavailable)*",
62
+ detailedContext ||
63
+ "No detailed analysis available. Review the conversation for context.",
117
64
  "",
118
- ...(detailedContext ? ["## AI Response Context", detailedContext, ""] : []),
119
- "## What Went Wrong?",
120
- whatWentWrong || "",
65
+ "## Conversation Summary",
121
66
  "",
122
- "## What Should Be Done Differently?",
123
- whatToDoDifferently || "",
67
+ conversationSummary || "*(unavailable)*",
124
68
  ].join("\n");
125
69
 
126
70
  writeFileSync(resolve(dir, "capture.md"), stringify(meta, body), "utf-8");
127
-
128
- // DEPRECATED: legacy sentiment.json — remove once all readers use capture.md frontmatter
129
- writeFileSync(
130
- resolve(dir, "sentiment.json"),
131
- JSON.stringify({ rating, context, ts: new Date().toISOString(), slug }, null, 2),
132
- "utf-8"
133
- );
134
71
  }
@@ -365,7 +365,7 @@ async function handleImplicitSentiment(
365
365
  if (typeof rating === "number" && rating >= 1 && rating <= 10 && rating !== 5) {
366
366
  handleRating(
367
367
  rating,
368
- `${parsed.summary}: ${trimmed.slice(0, 150)}`,
368
+ `${parsed.summary}: ${trimmed.slice(0, 200)}`,
369
369
  "implicit",
370
370
  parsed.detailed_context,
371
371
  sessionId,
@@ -11,7 +11,6 @@ import { stringify } from "../lib/frontmatter";
11
11
  import { inference } from "../lib/inference";
12
12
  import { categorizeLearning } from "../lib/learning-category";
13
13
  import { ensureDir, paths } from "../lib/paths";
14
- import { LEARNING_PRINCIPLE_PROMPT } from "../lib/prompts";
15
14
  import { fileTimestamp, monthPath } from "../lib/time";
16
15
  import { logTokenUsage } from "../lib/token-usage";
17
16
  import {
@@ -108,7 +107,6 @@ export async function captureWorkLearning(
108
107
  let title = rawTitle;
109
108
  let summary = rawSummary;
110
109
  let insights = "";
111
- let principle = "";
112
110
  try {
113
111
  const userMessages = messages
114
112
  .filter((m) => m.role === "user")
@@ -116,9 +114,10 @@ export async function captureWorkLearning(
116
114
  .slice(-8)
117
115
  .join("\n");
118
116
  const result = await inference({
119
- system: `You summarize AI coding sessions between a human user and an AI assistant. The 'Human messages' are what the user said. The 'AI response' is what the assistant said. Produce: 1) a short title (5-10 words) describing what was accomplished, 2) a summary of what the AI assistant did for the user (2-4 sentences, write from the AI's perspective using 'we'), 3) insights — what worked well, what was surprising, or what should be done differently next time (2-3 bullet points, no markdown), 4) principle — ${LEARNING_PRINCIPLE_PROMPT}`,
117
+ system:
118
+ "You summarize AI coding sessions between a human user and an AI assistant. The 'Human messages' are what the user said. The 'AI response' is what the assistant said. Produce: 1) a short title (5-10 words) describing what was accomplished, 2) a summary of what the AI assistant did for the user (2-4 sentences, write from the AI's perspective using 'we'), 3) insights — what worked well, what was surprising, or what should be done differently next time (2-3 bullet points, no markdown).",
120
119
  user: `Human messages:\n${userMessages}\n\nAI response:\n${rawSummary.slice(0, 400)}`,
121
- maxTokens: 350,
120
+ maxTokens: 300,
122
121
  timeout: 15000,
123
122
  jsonSchema: {
124
123
  type: "object" as const,
@@ -127,9 +126,8 @@ export async function captureWorkLearning(
127
126
  title: { type: "string" as const },
128
127
  summary: { type: "string" as const },
129
128
  insights: { type: "string" as const },
130
- principle: { type: "string" as const },
131
129
  },
132
- required: ["title", "summary", "insights", "principle"],
130
+ required: ["title", "summary", "insights"],
133
131
  },
134
132
  });
135
133
  if (result.usage) logTokenUsage("work-learning", result.usage);
@@ -138,12 +136,10 @@ export async function captureWorkLearning(
138
136
  title?: string;
139
137
  summary?: string;
140
138
  insights?: string;
141
- principle?: string;
142
139
  };
143
140
  if (parsed.title) title = parsed.title.slice(0, 100);
144
141
  if (parsed.summary) summary = parsed.summary;
145
142
  if (parsed.insights) insights = parsed.insights;
146
- if (parsed.principle) principle = parsed.principle;
147
143
  }
148
144
  } catch {
149
145
  // Fallback to raw values
@@ -159,7 +155,6 @@ export async function captureWorkLearning(
159
155
  category,
160
156
  date: new Date().toISOString().slice(0, 10),
161
157
  };
162
- if (principle) meta.principle = principle;
163
158
  if (sessionId) meta.session = sessionId;
164
159
 
165
160
  const body = [
@@ -5,7 +5,8 @@
5
5
 
6
6
  import { existsSync, readdirSync, readFileSync } from "node:fs";
7
7
  import { resolve } from "node:path";
8
- import { hasFrontmatter, parse } from "./frontmatter";
8
+ import { parse } from "./frontmatter";
9
+ import { readFailures, readLearnings } from "./learning-store";
9
10
  import { paths } from "./paths";
10
11
  import { loadRecentNotes } from "./relationship";
11
12
  import { readSessionNames } from "./session-names";
@@ -217,84 +218,22 @@ export function loadWisdomContext(): string {
217
218
  /** Load recent session learning files as digest, split by category */
218
219
  export function loadLearningDigest(): string {
219
220
  try {
220
- const sessionDir = paths.sessionLearning();
221
- if (!existsSync(sessionDir)) return "";
221
+ const entries = readLearnings(paths.sessionLearning(), 6);
222
+ if (entries.length === 0) return "";
222
223
 
223
- const files: { path: string; category: string }[] = [];
224
- // Structure: session/{year}/{month}/*.md
225
- for (const year of readdirSync(sessionDir).sort().reverse()) {
226
- const yearDir = resolve(sessionDir, year);
227
- try {
228
- for (const month of readdirSync(yearDir).sort().reverse()) {
229
- const monthDir = resolve(yearDir, month);
230
- try {
231
- const monthFiles = readdirSync(monthDir)
232
- .filter((f) => f.endsWith(".md"))
233
- .sort()
234
- .reverse()
235
- .map((f) => {
236
- const category = f.includes("_system") ? "system" : "algorithm";
237
- return { path: resolve(monthDir, f), category };
238
- });
239
- files.push(...monthFiles);
240
- } catch {
241
- /* skip */
242
- }
243
- if (files.length >= 6) break;
244
- }
245
- } catch {
246
- /* skip */
247
- }
248
- if (files.length >= 6) break;
249
- }
250
-
251
- function extractMeta(filePath: string): {
252
- title: string;
253
- category: string;
254
- } {
255
- const content = readFileSync(filePath, "utf-8").trim();
256
-
257
- // Frontmatter format (new)
258
- if (hasFrontmatter(content)) {
259
- const { meta } = parse<{ title?: string; category?: string }>(content);
260
- return {
261
- title: meta.title ? `**Title:** ${meta.title}` : content.slice(0, 80),
262
- category: meta.category || "algorithm",
263
- };
264
- }
265
-
266
- // DEPRECATED: legacy **Title:** inline format — remove once old learning files are migrated
267
- const titleLine = content.split("\n").find((l) => l.startsWith("**Title:**"));
268
- const fallback = content.split("\n").find((l) => l.trim() && !l.startsWith("#"));
269
- return {
270
- title: titleLine ?? fallback?.slice(0, 100) ?? content.slice(0, 80),
271
- category: "algorithm", // legacy files use filename for category
272
- };
273
- }
274
-
275
- // Extract metadata, preferring frontmatter over filename for category
276
- const enriched = files.map((f) => {
277
- const meta = extractMeta(f.path);
278
- return {
279
- ...f,
280
- title: meta.title,
281
- category: meta.category !== "algorithm" ? meta.category : f.category,
282
- };
283
- });
224
+ const approach = entries.filter((e) => e.category !== "system").slice(0, 2);
225
+ const system = entries.filter((e) => e.category === "system").slice(0, 2);
284
226
 
285
- const algorithm = enriched.filter((f) => f.category === "algorithm").slice(0, 2);
286
- const system = enriched.filter((f) => f.category === "system").slice(0, 2);
287
-
288
- if (algorithm.length === 0 && system.length === 0) return "";
227
+ if (approach.length === 0 && system.length === 0) return "";
289
228
 
290
229
  const lines: string[] = ["## Recent Session Learnings"];
291
- if (algorithm.length > 0) {
230
+ if (approach.length > 0) {
292
231
  lines.push("### Approach");
293
- for (const f of algorithm) lines.push(`- ${f.title}`);
232
+ for (const e of approach) lines.push(`- **Title:** ${e.title}`);
294
233
  }
295
234
  if (system.length > 0) {
296
235
  lines.push("### System");
297
- for (const f of system) lines.push(`- ${f.title}`);
236
+ for (const e of system) lines.push(`- **Title:** ${e.title}`);
298
237
  }
299
238
  return lines.join("\n");
300
239
  } catch {
@@ -305,68 +244,15 @@ export function loadLearningDigest(): string {
305
244
  /** Load 5 most recent failure contexts as an "avoid" list */
306
245
  export function loadFailurePatterns(): string {
307
246
  try {
308
- const failuresDir = paths.failures();
309
- if (!existsSync(failuresDir)) return "";
310
-
311
- // Structure: failures/{year}/{month}/{timestamp}_{slug}/
312
- const failures: string[] = [];
313
- for (const year of readdirSync(failuresDir).sort().reverse()) {
314
- const yearPath = resolve(failuresDir, year);
315
- for (const month of readdirSync(yearPath).sort().reverse()) {
316
- const monthPath = resolve(yearPath, month);
317
- try {
318
- const dirs = readdirSync(monthPath).sort().reverse();
319
- for (const dir of dirs) {
320
- if (!/^\d{8}-\d{6}_/.test(dir)) continue;
321
- // Try capture.md (new format), fall back to sentiment.json (legacy)
322
- const capturePath = resolve(monthPath, dir, "capture.md");
323
- const sentimentPath = resolve(monthPath, dir, "sentiment.json");
324
-
325
- let rating: number | undefined;
326
- let ctx: string | undefined;
327
-
328
- if (existsSync(capturePath)) {
329
- try {
330
- const content = readFileSync(capturePath, "utf-8");
331
- const { meta } = parse<{ rating?: number; context?: string }>(content);
332
- rating = meta.rating;
333
- ctx = meta.context;
334
- } catch {
335
- /* fallback below */
336
- }
337
- }
338
-
339
- // DEPRECATED: legacy sentiment.json fallback — remove once old failures have capture.md
340
- if (!ctx && existsSync(sentimentPath)) {
341
- try {
342
- const data = JSON.parse(readFileSync(sentimentPath, "utf-8"));
343
- rating = data.rating;
344
- ctx = data.context;
345
- } catch {
346
- /* skip */
347
- }
348
- }
349
-
350
- if (ctx) {
351
- const label = rating ? `[${rating}/10]` : "";
352
- failures.push(`${label} ${ctx}`.trim());
353
- } else {
354
- failures.push(dir.replace(/^\d{8}-\d{6}_/, ""));
355
- }
356
- if (failures.length >= 5) break;
357
- }
358
- } catch {
359
- /* skip */
360
- }
361
- if (failures.length >= 5) break;
362
- }
363
- if (failures.length >= 5) break;
364
- }
247
+ const entries = readFailures(paths.failures(), 5);
248
+ if (entries.length === 0) return "";
365
249
 
366
- if (failures.length === 0) return "";
367
- return ["## Recent Failure Patterns (Avoid)", ...failures.map((f) => `- ${f}`)].join(
368
- "\n"
369
- );
250
+ const lines = entries.map((e) => {
251
+ const label = e.rating ? `[${e.rating}/10]` : "";
252
+ return `- ${label} ${e.context}`.trim();
253
+ });
254
+
255
+ return ["## Recent Failure Patterns (Avoid)", ...lines].join("\n");
370
256
  } catch {
371
257
  return "";
372
258
  }
@@ -403,24 +289,12 @@ export function loadSynthesisRecommendations(): string {
403
289
 
404
290
  if (recs.length === 0) continue;
405
291
 
406
- // Extract metadata frontmatter or legacy
407
- let period = "";
408
- let avgRating = "";
409
-
410
- if (hasFrontmatter(content)) {
411
- const { meta } = parse<{
412
- period?: string;
413
- average_rating?: string;
414
- }>(content);
415
- period = meta.period || "";
416
- avgRating = meta.average_rating ? `${meta.average_rating}/10` : "";
417
- } else {
418
- // DEPRECATED: legacy **Key:** format
419
- const periodMatch = content.match(/\*\*Period:\*\* (.+)/);
420
- const avgMatch = content.match(/\*\*Average Rating:\*\* (.+)/);
421
- period = periodMatch?.[1] || "";
422
- avgRating = avgMatch?.[1] || "";
423
- }
292
+ const { meta } = parse<{
293
+ period?: string;
294
+ average_rating?: string;
295
+ }>(content);
296
+ const period = meta.period || "";
297
+ const avgRating = meta.average_rating ? `${meta.average_rating}/10` : "";
424
298
 
425
299
  const header = [
426
300
  "## Pattern Synthesis",