portable-agent-layer 0.71.0 → 0.72.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 +1 -1
- package/src/cli/migrate.ts +1 -1
- package/src/cli/skill.ts +1 -1
- package/src/hooks/CompactRecover.ts +28 -86
- package/src/hooks/LedgerUnapplied.ts +3 -28
- package/src/hooks/LoadContext.ts +33 -60
- package/src/hooks/SecurityValidator.ts +16 -109
- package/src/hooks/handlers/failure-principle.ts +19 -44
- package/src/hooks/handlers/session-intelligence.ts +13 -70
- package/src/hooks/lib/capture-store.ts +103 -0
- package/src/hooks/lib/compact-recall.ts +89 -0
- package/src/hooks/lib/failure-principle.ts +98 -0
- package/src/hooks/lib/ledger-hook.ts +35 -0
- package/src/hooks/lib/ledger.ts +48 -1
- package/src/hooks/lib/security-gate.ts +159 -0
- package/src/hooks/lib/session-context.ts +74 -0
- package/src/tools/agent/algorithm-reflect.ts +28 -97
- package/src/tools/agent/analyze.ts +19 -120
- package/src/tools/agent/handoff-note.ts +29 -77
- package/src/tools/agent/project.ts +13 -134
- package/src/tools/agent/relationship-note.ts +27 -46
- package/src/tools/agent/synthesize.ts +1 -1
- package/src/tools/agent/thread.ts +43 -123
- package/src/tools/control-room/data.ts +2 -2
- package/src/tools/control-room/matrix.ts +1 -1
- package/src/tools/control-room/ui/ledger.tsx +2 -1
- package/src/tools/ledger/view.ts +3 -0
- package/src/tools/lib/algorithm-reflect.ts +84 -0
- package/src/tools/lib/analyze-report.ts +120 -0
- package/src/tools/lib/handoff-note.ts +88 -0
- package/src/tools/lib/note-flags.ts +59 -0
- package/src/tools/lib/project-isc.ts +151 -0
- package/src/tools/lib/relationship-reflect.ts +402 -0
- package/src/tools/lib/self-model.ts +499 -0
- package/src/tools/lib/session-usage.ts +216 -0
- package/src/tools/lib/skill-doctor.ts +457 -0
- package/src/tools/lib/thread.ts +119 -0
- package/src/tools/lib/token-report.ts +173 -0
- package/src/tools/lib/transcript-usage.ts +42 -0
- package/src/tools/lib/usage-buckets.ts +329 -0
- package/src/tools/relationship-reflect.ts +48 -412
- package/src/tools/self-model.ts +76 -558
- package/src/tools/session-summary.ts +8 -215
- package/src/tools/skill-doctor.ts +9 -444
- package/src/tools/token-cost.ts +18 -428
package/src/tools/self-model.ts
CHANGED
|
@@ -6,16 +6,16 @@
|
|
|
6
6
|
* first-person reflection. Reads opinions, ratings, wisdom frames,
|
|
7
7
|
* graduated failure patterns, algorithm reflections, relationship notes,
|
|
8
8
|
* and session history. Produces a self-aware narrative at
|
|
9
|
-
* ~/.pal/memory/self-model.md that is injected at session start.
|
|
9
|
+
* ~/.pal/memory/self-model/current.md that is injected at session start.
|
|
10
10
|
*
|
|
11
11
|
* Usage:
|
|
12
|
-
* bun ~/.pal/tools/self-model.ts [--days 30] [--force]
|
|
12
|
+
* bun ~/.pal/tools/self-model.ts [--days 30] [--force] [--dry-run]
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Every decision this makes lives in ./lib/self-model.ts, where the suite
|
|
15
|
+
* reaches it directly; what stays here is paths, inference and the writes.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
19
|
import { resolve } from "node:path";
|
|
20
20
|
import { parseArgs } from "node:util";
|
|
21
21
|
import { inference } from "../hooks/lib/inference";
|
|
@@ -23,512 +23,67 @@ import { SONNET_MODEL } from "../hooks/lib/models";
|
|
|
23
23
|
import { ensureDir, paths } from "../hooks/lib/paths";
|
|
24
24
|
import { identity as loadSettingsIdentity } from "../hooks/lib/settings";
|
|
25
25
|
import { logTokenUsage } from "../hooks/lib/token-usage";
|
|
26
|
+
import {
|
|
27
|
+
archiveDateOf,
|
|
28
|
+
buildPrompt,
|
|
29
|
+
failedSynthesisModel,
|
|
30
|
+
formatDataForInference,
|
|
31
|
+
gatherData,
|
|
32
|
+
inferenceUserContent,
|
|
33
|
+
metaFooter,
|
|
34
|
+
type SelfModelSources,
|
|
35
|
+
synthesisIsDue,
|
|
36
|
+
} from "./lib/self-model";
|
|
37
|
+
|
|
38
|
+
const HELP = `
|
|
39
|
+
SelfModel — Synthesize a first-person self-model from accumulated data
|
|
26
40
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const SELF_MODEL_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
30
|
-
|
|
31
|
-
// ── Types ──
|
|
32
|
-
|
|
33
|
-
interface Opinion {
|
|
34
|
-
id: string;
|
|
35
|
-
statement: string;
|
|
36
|
-
confidence: number;
|
|
37
|
-
category: string;
|
|
38
|
-
evidence: { date: string; type: string; source: string }[];
|
|
39
|
-
created: string;
|
|
40
|
-
updated: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
interface Rating {
|
|
44
|
-
ts: string;
|
|
45
|
-
type: string;
|
|
46
|
-
rating: number;
|
|
47
|
-
context: string;
|
|
48
|
-
source: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
interface GraduatedPattern {
|
|
52
|
-
pattern: string;
|
|
53
|
-
domain: string;
|
|
54
|
-
confidence: number;
|
|
55
|
-
occurrences: number;
|
|
56
|
-
sources: string[];
|
|
57
|
-
graduatedAt: string;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
interface AlgorithmReflection {
|
|
61
|
-
timestamp: string;
|
|
62
|
-
cwd?: string;
|
|
63
|
-
task: string;
|
|
64
|
-
criteria_count: number;
|
|
65
|
-
criteria_passed: number;
|
|
66
|
-
criteria_failed: number;
|
|
67
|
-
sentiment: number;
|
|
68
|
-
q1: string;
|
|
69
|
-
q2: string;
|
|
70
|
-
q3: string;
|
|
71
|
-
}
|
|
41
|
+
Usage:
|
|
42
|
+
bun self-model.ts [--days 30] [--force] [--dry-run]
|
|
72
43
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
date: string;
|
|
78
|
-
}
|
|
44
|
+
Options:
|
|
45
|
+
--days Lookback window (default: 30)
|
|
46
|
+
--force Skip 24h guard
|
|
47
|
+
--dry-run Print to stdout without writing
|
|
79
48
|
|
|
80
|
-
|
|
49
|
+
Output: ~/.pal/memory/self-model/current.md (synthesized by Sonnet)
|
|
50
|
+
`;
|
|
81
51
|
|
|
82
52
|
function selfModelDir(): string {
|
|
83
53
|
return ensureDir(resolve(paths.memory(), "self-model"));
|
|
84
54
|
}
|
|
85
55
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function selfModelMetaPath(): string {
|
|
91
|
-
return resolve(selfModelDir(), "meta.json");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function archiveDir(): string {
|
|
95
|
-
return ensureDir(resolve(selfModelDir(), "archive"));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function shouldRun(force: boolean): boolean {
|
|
99
|
-
if (force) return true;
|
|
100
|
-
const p = selfModelMetaPath();
|
|
101
|
-
if (!existsSync(p)) return true;
|
|
102
|
-
try {
|
|
103
|
-
const meta = JSON.parse(readFileSync(p, "utf-8")) as { timestamp: string };
|
|
104
|
-
return Date.now() - new Date(meta.timestamp).getTime() > SELF_MODEL_TTL_MS;
|
|
105
|
-
} catch {
|
|
106
|
-
return true;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function readJsonl<T>(path: string): T[] {
|
|
111
|
-
if (!existsSync(path)) return [];
|
|
112
|
-
try {
|
|
113
|
-
return readFileSync(path, "utf-8")
|
|
114
|
-
.split("\n")
|
|
115
|
-
.filter((l) => l.trim())
|
|
116
|
-
.map((l) => JSON.parse(l) as T);
|
|
117
|
-
} catch {
|
|
118
|
-
return [];
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function safeReadJson<T>(path: string, fallback: T): T {
|
|
123
|
-
if (!existsSync(path)) return fallback;
|
|
124
|
-
try {
|
|
125
|
-
return JSON.parse(readFileSync(path, "utf-8")) as T;
|
|
126
|
-
} catch {
|
|
127
|
-
return fallback;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function safeReaddir(dir: string): string[] {
|
|
132
|
-
try {
|
|
133
|
-
return readdirSync(dir);
|
|
134
|
-
} catch {
|
|
135
|
-
return [];
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function daysAgo(days: number): Date {
|
|
140
|
-
return new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function formatDate(iso: string): string {
|
|
144
|
-
return iso.slice(0, 10);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function round1(n: number): number {
|
|
148
|
-
return Math.round(n * 10) / 10;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// ── Data Readers ──
|
|
152
|
-
|
|
153
|
-
function readOpinions(): Opinion[] {
|
|
154
|
-
const data = safeReadJson<{ opinions?: Opinion[] }>(
|
|
155
|
-
resolve(paths.relationship(), "opinions.json"),
|
|
156
|
-
{ opinions: [] }
|
|
157
|
-
);
|
|
158
|
-
return (data.opinions ?? []).sort((a, b) => b.confidence - a.confidence);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function readRatings(since: Date): {
|
|
162
|
-
count: number;
|
|
163
|
-
avg: number;
|
|
164
|
-
recentAvg: number;
|
|
165
|
-
lowCount: number;
|
|
166
|
-
highCount: number;
|
|
167
|
-
trend: "improving" | "declining" | "stable";
|
|
168
|
-
recentContexts: string[];
|
|
169
|
-
} {
|
|
170
|
-
const all = readJsonl<Rating>(resolve(paths.signals(), "ratings.jsonl"));
|
|
171
|
-
const ratings = all.filter((r) => new Date(r.ts) >= since);
|
|
172
|
-
|
|
173
|
-
if (ratings.length === 0) {
|
|
174
|
-
return {
|
|
175
|
-
count: 0,
|
|
176
|
-
avg: 0,
|
|
177
|
-
recentAvg: 0,
|
|
178
|
-
lowCount: 0,
|
|
179
|
-
highCount: 0,
|
|
180
|
-
trend: "stable",
|
|
181
|
-
recentContexts: [],
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const avg = ratings.reduce((s, r) => s + r.rating, 0) / ratings.length;
|
|
186
|
-
const recent = ratings.slice(-10);
|
|
187
|
-
const recentAvg = recent.reduce((s, r) => s + r.rating, 0) / recent.length;
|
|
188
|
-
const lowCount = ratings.filter((r) => r.rating <= 3).length;
|
|
189
|
-
const highCount = ratings.filter((r) => r.rating >= 8).length;
|
|
190
|
-
|
|
191
|
-
// Trend
|
|
192
|
-
const mid = Math.floor(ratings.length / 2);
|
|
193
|
-
let trend: "improving" | "declining" | "stable" = "stable";
|
|
194
|
-
if (mid >= 3) {
|
|
195
|
-
const firstAvg = ratings.slice(0, mid).reduce((s, r) => s + r.rating, 0) / mid;
|
|
196
|
-
const secondAvg =
|
|
197
|
-
ratings.slice(mid).reduce((s, r) => s + r.rating, 0) / (ratings.length - mid);
|
|
198
|
-
if (secondAvg - firstAvg > 0.5) trend = "improving";
|
|
199
|
-
else if (secondAvg - firstAvg < -0.5) trend = "declining";
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Recent low-rating contexts for weakness detection
|
|
203
|
-
const recentContexts = ratings
|
|
204
|
-
.filter((r) => r.rating <= 3 && r.context)
|
|
205
|
-
.slice(-5)
|
|
206
|
-
.map((r) => r.context);
|
|
56
|
+
const selfModelPath = () => resolve(selfModelDir(), "current.md");
|
|
57
|
+
const selfModelMetaPath = () => resolve(selfModelDir(), "meta.json");
|
|
207
58
|
|
|
59
|
+
function sources(): SelfModelSources {
|
|
208
60
|
return {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
61
|
+
opinionsFile: resolve(paths.relationship(), "opinions.json"),
|
|
62
|
+
ratingsFile: resolve(paths.signals(), "ratings.jsonl"),
|
|
63
|
+
wisdomDir: paths.wisdom(),
|
|
64
|
+
graduatedFile: resolve(paths.wisdomState(), "graduated.json"),
|
|
65
|
+
reflectionsFile: resolve(paths.reflections(), "algorithm-reflections.jsonl"),
|
|
66
|
+
relationshipDir: paths.relationship(),
|
|
67
|
+
sessionDir: resolve(paths.learning(), "session"),
|
|
216
68
|
};
|
|
217
69
|
}
|
|
218
70
|
|
|
219
|
-
function
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const domain = file.replace(/\.md$/, "");
|
|
225
|
-
const content = readFileSync(resolve(framesDir, file), "utf-8");
|
|
226
|
-
|
|
227
|
-
// Extract CRYSTAL principles
|
|
228
|
-
const principles = content
|
|
229
|
-
.split("\n")
|
|
230
|
-
.filter((line) => line.includes("[CRYSTAL:"))
|
|
231
|
-
.map((line) =>
|
|
232
|
-
line
|
|
233
|
-
.replace(/^-\s*/, "")
|
|
234
|
-
.replace(/\s*\[CRYSTAL:.*$/, "")
|
|
235
|
-
.trim()
|
|
236
|
-
);
|
|
237
|
-
|
|
238
|
-
if (principles.length > 0) {
|
|
239
|
-
frames.push({ domain, principles });
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return frames;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function readGraduatedPatterns(): GraduatedPattern[] {
|
|
247
|
-
return (
|
|
248
|
-
safeReadJson<{ graduated?: GraduatedPattern[] }>(
|
|
249
|
-
resolve(paths.wisdomState(), "graduated.json"),
|
|
250
|
-
{ graduated: [] }
|
|
251
|
-
).graduated ?? []
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function readAlgorithmReflections(since: Date): AlgorithmReflection[] {
|
|
256
|
-
const p = resolve(
|
|
257
|
-
ensureDir(resolve(paths.learning(), "reflections")),
|
|
258
|
-
"algorithm-reflections.jsonl"
|
|
259
|
-
);
|
|
260
|
-
return readJsonl<AlgorithmReflection>(p).filter((r) => new Date(r.timestamp) >= since);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function readRelationshipNotes(since: Date): RelationshipNote[] {
|
|
264
|
-
const baseDir = paths.relationship();
|
|
265
|
-
const notes: RelationshipNote[] = [];
|
|
266
|
-
const sinceStr = formatDate(since.toISOString());
|
|
267
|
-
|
|
268
|
-
for (const monthDir of safeReaddir(baseDir).filter((d) =>
|
|
269
|
-
new RegExp(/^\d{4}-\d{2}$/).exec(d)
|
|
270
|
-
)) {
|
|
271
|
-
const fullMonthDir = resolve(baseDir, monthDir);
|
|
272
|
-
for (const file of safeReaddir(fullMonthDir).filter((f) => f.endsWith(".md"))) {
|
|
273
|
-
const dateStr = file.replace(/\.md$/, "");
|
|
274
|
-
if (dateStr < sinceStr) continue;
|
|
275
|
-
|
|
276
|
-
const content = readFileSync(resolve(fullMonthDir, file), "utf-8");
|
|
277
|
-
for (const line of content.split("\n")) {
|
|
278
|
-
const trimmed = line.trim();
|
|
279
|
-
if (!trimmed.startsWith("- ")) continue;
|
|
280
|
-
|
|
281
|
-
const noteContent = trimmed.substring(2);
|
|
282
|
-
|
|
283
|
-
// Parse O(c=X.XX): ..., W: ..., B: ...
|
|
284
|
-
const opinionMatch = new RegExp(/^O\(c=([\d.]+)\):\s*(.+)$/).exec(noteContent);
|
|
285
|
-
if (opinionMatch) {
|
|
286
|
-
notes.push({
|
|
287
|
-
type: "O",
|
|
288
|
-
confidence: parseFloat(opinionMatch[1]),
|
|
289
|
-
content: opinionMatch[2],
|
|
290
|
-
date: dateStr,
|
|
291
|
-
});
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const wisdomMatch = new RegExp(/^W:\s*(.+)$/).exec(noteContent);
|
|
296
|
-
if (wisdomMatch) {
|
|
297
|
-
notes.push({ type: "W", content: wisdomMatch[1], date: dateStr });
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const behaviorMatch = new RegExp(/^Session:\s*(.+)$/).exec(noteContent);
|
|
302
|
-
if (behaviorMatch) {
|
|
303
|
-
notes.push({ type: "Session", content: behaviorMatch[1], date: dateStr });
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return notes;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function readSessionCount(since: Date): number {
|
|
313
|
-
const baseDir = resolve(paths.learning(), "session");
|
|
314
|
-
if (!existsSync(baseDir)) return 0;
|
|
315
|
-
|
|
316
|
-
const sinceStr = formatDate(since.toISOString());
|
|
317
|
-
let count = 0;
|
|
318
|
-
|
|
319
|
-
for (const year of safeReaddir(baseDir)) {
|
|
320
|
-
for (const month of safeReaddir(resolve(baseDir, year))) {
|
|
321
|
-
for (const file of safeReaddir(resolve(baseDir, year, month)).filter((f) =>
|
|
322
|
-
f.endsWith(".md")
|
|
323
|
-
)) {
|
|
324
|
-
const dateStr = file.slice(0, 8);
|
|
325
|
-
const isoDate = `${dateStr.slice(0, 4)}-${dateStr.slice(4, 6)}-${dateStr.slice(6, 8)}`;
|
|
326
|
-
if (isoDate >= sinceStr) count++;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return count;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// ── Data Gathering (deterministic) ──
|
|
335
|
-
|
|
336
|
-
interface SelfModelData {
|
|
337
|
-
days: number;
|
|
338
|
-
now: string;
|
|
339
|
-
sessionCount: number;
|
|
340
|
-
opinions: Opinion[];
|
|
341
|
-
ratings: ReturnType<typeof readRatings>;
|
|
342
|
-
wisdomFrames: ReturnType<typeof readWisdomFrames>;
|
|
343
|
-
graduated: GraduatedPattern[];
|
|
344
|
-
reflections: AlgorithmReflection[];
|
|
345
|
-
behaviorNotes: string[];
|
|
346
|
-
wisdomNotes: string[];
|
|
347
|
-
selfObservations: string[];
|
|
348
|
-
algorithmObservations: string[];
|
|
349
|
-
passRate: number;
|
|
350
|
-
avgSentiment: number;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function gatherData(days: number): SelfModelData {
|
|
354
|
-
const since = daysAgo(days);
|
|
355
|
-
const now = new Date().toISOString().slice(0, 10);
|
|
356
|
-
|
|
357
|
-
const opinions = readOpinions();
|
|
358
|
-
const ratings = readRatings(since);
|
|
359
|
-
const wisdomFrames = readWisdomFrames();
|
|
360
|
-
const graduated = readGraduatedPatterns();
|
|
361
|
-
const reflections = readAlgorithmReflections(since);
|
|
362
|
-
const relNotes = readRelationshipNotes(since);
|
|
363
|
-
const sessionCount = readSessionCount(since);
|
|
364
|
-
|
|
365
|
-
let passRate = 0;
|
|
366
|
-
let avgSentiment = 0;
|
|
367
|
-
if (reflections.length > 0) {
|
|
368
|
-
const totalCriteria = reflections.reduce((s, r) => s + r.criteria_count, 0);
|
|
369
|
-
const totalPassed = reflections.reduce((s, r) => s + r.criteria_passed, 0);
|
|
370
|
-
passRate = totalCriteria > 0 ? Math.round((totalPassed / totalCriteria) * 100) : 0;
|
|
371
|
-
avgSentiment = round1(
|
|
372
|
-
reflections.reduce((s, r) => s + r.sentiment, 0) / reflections.length
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
return {
|
|
377
|
-
days,
|
|
378
|
-
now,
|
|
379
|
-
sessionCount,
|
|
380
|
-
opinions,
|
|
381
|
-
ratings,
|
|
382
|
-
wisdomFrames,
|
|
383
|
-
graduated,
|
|
384
|
-
reflections,
|
|
385
|
-
behaviorNotes: relNotes.filter((n) => n.type === "Session").map((n) => n.content),
|
|
386
|
-
wisdomNotes: relNotes.filter((n) => n.type === "W").map((n) => n.content),
|
|
387
|
-
selfObservations: reflections.map((r) => r.q1).filter(Boolean),
|
|
388
|
-
algorithmObservations: reflections.map((r) => r.q2).filter(Boolean),
|
|
389
|
-
passRate,
|
|
390
|
-
avgSentiment,
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function formatDataForInference(data: SelfModelData): string {
|
|
395
|
-
const sections: string[] = [];
|
|
396
|
-
|
|
397
|
-
sections.push(
|
|
398
|
-
`## Raw Data — ${data.days}-day window, ${data.now}`,
|
|
399
|
-
`Sessions: ${data.sessionCount}`,
|
|
400
|
-
`Ratings: ${data.ratings.count} total, ${data.ratings.avg}/10 avg, recent ${data.ratings.recentAvg}/10, trend ${data.ratings.trend}`,
|
|
401
|
-
`${data.ratings.highCount} high (8+), ${data.ratings.lowCount} low (<=3)`
|
|
402
|
-
);
|
|
403
|
-
|
|
404
|
-
if (data.opinions.length > 0) {
|
|
405
|
-
const principalName = loadSettingsIdentity().principal.name;
|
|
406
|
-
sections.push(`\n### Opinions about ${principalName} (confidence-scored)`);
|
|
407
|
-
for (const o of data.opinions.filter((o) => o.confidence >= 0.6)) {
|
|
408
|
-
sections.push(
|
|
409
|
-
`- [${o.category}] ${o.statement} (${Math.round(o.confidence * 100)}%)`
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
if (data.wisdomFrames.length > 0) {
|
|
415
|
-
sections.push(`\n### Crystallized Principles`);
|
|
416
|
-
for (const f of data.wisdomFrames) {
|
|
417
|
-
for (const p of f.principles) {
|
|
418
|
-
sections.push(`- [${f.domain}] ${p}`);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if (data.graduated.length > 0) {
|
|
424
|
-
sections.push(`\n### Graduated Failure Patterns`);
|
|
425
|
-
for (const g of data.graduated) {
|
|
426
|
-
sections.push(`- [${g.domain}] ${g.pattern} (${g.occurrences}x)`);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
if (data.ratings.recentContexts.length > 0) {
|
|
431
|
-
sections.push(`\n### Recent Frustration Signals (rated <=3)`);
|
|
432
|
-
for (const ctx of data.ratings.recentContexts) {
|
|
433
|
-
sections.push(`- "${ctx}"`);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
if (data.selfObservations.length > 0) {
|
|
438
|
-
sections.push(`\n### Self-Observations (Q1 from algorithm reflections)`);
|
|
439
|
-
for (const obs of data.selfObservations.slice(-8)) {
|
|
440
|
-
sections.push(`- ${obs}`);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (data.algorithmObservations.length > 0) {
|
|
445
|
-
sections.push(`\n### Algorithm Observations (Q2 from reflections)`);
|
|
446
|
-
for (const obs of data.algorithmObservations.slice(-5)) {
|
|
447
|
-
sections.push(`- ${obs}`);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
if (data.behaviorNotes.length > 0) {
|
|
452
|
-
sections.push(`\n### Behavioral Notes (from relationship tracking)`);
|
|
453
|
-
for (const note of data.behaviorNotes.slice(-8)) {
|
|
454
|
-
sections.push(`- ${note}`);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
if (data.wisdomNotes.length > 0) {
|
|
459
|
-
sections.push(`\n### World/Context Notes`);
|
|
460
|
-
for (const note of data.wisdomNotes.slice(-5)) {
|
|
461
|
-
sections.push(`- ${note}`);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
if (data.reflections.length > 0) {
|
|
466
|
-
sections.push(
|
|
467
|
-
`\n### Algorithm Performance: ${data.passRate}% pass rate, ${data.avgSentiment}/10 sentiment, ${data.reflections.length} reflections`
|
|
468
|
-
);
|
|
71
|
+
function readFileOrEmpty(path: string): string {
|
|
72
|
+
try {
|
|
73
|
+
return existsSync(path) ? readFileSync(path, "utf-8") : "";
|
|
74
|
+
} catch {
|
|
75
|
+
return "";
|
|
469
76
|
}
|
|
470
|
-
|
|
471
|
-
return sections.join("\n");
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function buildPrompt(aiName: string, principalName: string): string {
|
|
475
|
-
return `You are writing a self-model for an AI assistant named ${aiName}. You ARE ${aiName}. Write in first person.
|
|
476
|
-
|
|
477
|
-
You will receive structured data about your performance, your user's preferences, and behavioral patterns over a time window.
|
|
478
|
-
|
|
479
|
-
Produce a short, actionable self-model — not a data dump. Every sentence must change behavior, not just describe it.
|
|
480
|
-
|
|
481
|
-
## Required Sections
|
|
482
|
-
|
|
483
|
-
**# Self-Model — ${aiName}**
|
|
484
|
-
Include synthesis date and window.
|
|
485
|
-
|
|
486
|
-
**## Who ${principalName} Is**
|
|
487
|
-
One paragraph. Synthesize the opinions and behavioral notes into a working portrait — how ${principalName} thinks, communicates, and what frustrates him. Do not list raw opinion statements. Write it as understanding, not inventory.
|
|
488
|
-
|
|
489
|
-
**## My Priority Right Now**
|
|
490
|
-
One sentence. The single most impactful behavioral change to make immediately, derived from the failure patterns and trajectory. Specific and actionable — not "be more careful" but "before generating output that names a command or path, verify it exists."
|
|
491
|
-
|
|
492
|
-
## Rules
|
|
493
|
-
- First person, present tense
|
|
494
|
-
- No raw numbers anywhere — a footer carries them
|
|
495
|
-
- Under 150 words total
|
|
496
|
-
- Do not add extra sections
|
|
497
|
-
- Do not write a footer or meta line — one is appended automatically after your output`;
|
|
498
77
|
}
|
|
499
78
|
|
|
500
|
-
// ── Narrative Composer ──
|
|
501
|
-
|
|
502
79
|
async function composeSelfModel(days: number): Promise<string> {
|
|
503
|
-
const data = gatherData(days);
|
|
504
|
-
const
|
|
505
|
-
const
|
|
506
|
-
const aiName = id.ai.name;
|
|
507
|
-
const principalName = id.principal.name;
|
|
508
|
-
|
|
509
|
-
// Include previous self-model for trajectory comparison
|
|
510
|
-
let previousModel = "";
|
|
511
|
-
const currentPath = selfModelPath();
|
|
512
|
-
if (existsSync(currentPath)) {
|
|
513
|
-
try {
|
|
514
|
-
const prev = readFileSync(currentPath, "utf-8");
|
|
515
|
-
// Never feed a failed-synthesis fallback back in — it is a raw data dump,
|
|
516
|
-
// not a model. Doing so bloats the prompt and drives the next run into the
|
|
517
|
-
// same timeout, a self-reinforcing failure loop. Skip it and synthesize fresh.
|
|
518
|
-
if (!prev.includes("Synthesis failed — raw data below")) previousModel = prev;
|
|
519
|
-
} catch {
|
|
520
|
-
/* best effort */
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const strippedPrev = previousModel.replace(/\n\n\*\d+ ratings[^\n]*\n?$/, "").trimEnd();
|
|
525
|
-
const userContent = strippedPrev
|
|
526
|
-
? `${rawData}\n\n---\n\n## Previous Self-Model (compare against this — what changed?)\n\n${strippedPrev}`
|
|
527
|
-
: rawData;
|
|
80
|
+
const data = gatherData(sources(), days);
|
|
81
|
+
const { ai, principal } = loadSettingsIdentity();
|
|
82
|
+
const rawData = formatDataForInference(data, principal.name);
|
|
528
83
|
|
|
529
84
|
const result = await inference({
|
|
530
|
-
system: buildPrompt(
|
|
531
|
-
user:
|
|
85
|
+
system: buildPrompt(ai.name, principal.name),
|
|
86
|
+
user: inferenceUserContent(rawData, readFileOrEmpty(selfModelPath())),
|
|
532
87
|
model: SONNET_MODEL,
|
|
533
88
|
maxTokens: 1500,
|
|
534
89
|
timeout: 90000,
|
|
@@ -536,51 +91,37 @@ async function composeSelfModel(days: number): Promise<string> {
|
|
|
536
91
|
});
|
|
537
92
|
|
|
538
93
|
if (result.usage) logTokenUsage("self-model", result.usage, SONNET_MODEL);
|
|
539
|
-
|
|
540
94
|
if (result.success && result.output) {
|
|
541
|
-
|
|
542
|
-
`\n\n*${data.ratings.count} ratings · ${data.sessionCount} sessions · ` +
|
|
543
|
-
`${data.reflections.length} reflections · window: ${daysAgo(data.days).toISOString().slice(0, 10)} → ${data.now}*`;
|
|
544
|
-
return result.output.trimEnd() + meta;
|
|
95
|
+
return result.output.trimEnd() + metaFooter(data);
|
|
545
96
|
}
|
|
546
|
-
|
|
547
|
-
// Fallback: return raw data summary if inference fails
|
|
548
|
-
return `# Self-Model — ${aiName}\n*Synthesis failed — raw data below*\n\n${rawData}`;
|
|
97
|
+
return failedSynthesisModel(ai.name, rawData);
|
|
549
98
|
}
|
|
550
99
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
100
|
+
function archivePrevious(modelPath: string, metaPath: string): void {
|
|
101
|
+
if (!existsSync(modelPath)) return;
|
|
102
|
+
try {
|
|
103
|
+
const meta = existsSync(metaPath)
|
|
104
|
+
? (JSON.parse(readFileSync(metaPath, "utf-8")) as { timestamp?: string })
|
|
105
|
+
: {};
|
|
106
|
+
const archivePath = resolve(
|
|
107
|
+
ensureDir(resolve(selfModelDir(), "archive")),
|
|
108
|
+
`${archiveDateOf(meta, new Date())}.md`
|
|
109
|
+
);
|
|
110
|
+
if (!existsSync(archivePath)) copyFileSync(modelPath, archivePath);
|
|
111
|
+
} catch {
|
|
112
|
+
/* archive is best-effort */
|
|
559
113
|
}
|
|
114
|
+
}
|
|
560
115
|
|
|
561
|
-
|
|
562
|
-
const modelPath = selfModelPath();
|
|
116
|
+
async function writeSelfModel(days: number, force: boolean): Promise<string | null> {
|
|
563
117
|
const metaPath = selfModelMetaPath();
|
|
118
|
+
const meta = existsSync(metaPath) ? readFileSync(metaPath, "utf-8") : null;
|
|
119
|
+
if (!force && !synthesisIsDue(meta, new Date())) return null;
|
|
564
120
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
try {
|
|
568
|
-
const meta = existsSync(metaPath)
|
|
569
|
-
? (JSON.parse(readFileSync(metaPath, "utf-8")) as { timestamp?: string })
|
|
570
|
-
: {};
|
|
571
|
-
const date = meta.timestamp
|
|
572
|
-
? meta.timestamp.slice(0, 10)
|
|
573
|
-
: new Date().toISOString().slice(0, 10);
|
|
574
|
-
const archivePath = resolve(archiveDir(), `${date}.md`);
|
|
575
|
-
if (!existsSync(archivePath)) {
|
|
576
|
-
const { copyFileSync } = await import("node:fs");
|
|
577
|
-
copyFileSync(modelPath, archivePath);
|
|
578
|
-
}
|
|
579
|
-
} catch {
|
|
580
|
-
/* archive is best-effort */
|
|
581
|
-
}
|
|
582
|
-
}
|
|
121
|
+
const content = await composeSelfModel(days);
|
|
122
|
+
const modelPath = selfModelPath();
|
|
583
123
|
|
|
124
|
+
archivePrevious(modelPath, metaPath);
|
|
584
125
|
writeFileSync(modelPath, content, "utf-8");
|
|
585
126
|
writeFileSync(
|
|
586
127
|
metaPath,
|
|
@@ -588,11 +129,9 @@ async function writeSelfModel(
|
|
|
588
129
|
"utf-8"
|
|
589
130
|
);
|
|
590
131
|
|
|
591
|
-
return
|
|
132
|
+
return modelPath;
|
|
592
133
|
}
|
|
593
134
|
|
|
594
|
-
// ── CLI ──
|
|
595
|
-
|
|
596
135
|
async function run() {
|
|
597
136
|
const { values } = parseArgs({
|
|
598
137
|
args: Bun.argv.slice(2),
|
|
@@ -605,34 +144,20 @@ async function run() {
|
|
|
605
144
|
});
|
|
606
145
|
|
|
607
146
|
if (values.help) {
|
|
608
|
-
console.log(
|
|
609
|
-
SelfModel — Synthesize a first-person self-model from accumulated data
|
|
610
|
-
|
|
611
|
-
Usage:
|
|
612
|
-
bun self-model.ts [--days 30] [--force] [--dry-run]
|
|
613
|
-
|
|
614
|
-
Options:
|
|
615
|
-
--days Lookback window (default: 30)
|
|
616
|
-
--force Skip 24h guard
|
|
617
|
-
--dry-run Print to stdout without writing
|
|
618
|
-
|
|
619
|
-
Output: ~/.pal/memory/self-model.md (synthesized by Sonnet)
|
|
620
|
-
`);
|
|
147
|
+
console.log(HELP);
|
|
621
148
|
process.exit(0);
|
|
622
149
|
}
|
|
623
150
|
|
|
624
|
-
const
|
|
625
|
-
const dryRun = values["dry-run"] ?? false;
|
|
626
|
-
const days = parseInt(values.days ?? "30", 10);
|
|
151
|
+
const days = Number.parseInt(values.days ?? "30", 10);
|
|
627
152
|
|
|
628
|
-
if (
|
|
153
|
+
if (values["dry-run"]) {
|
|
629
154
|
console.log(await composeSelfModel(days));
|
|
630
155
|
return;
|
|
631
156
|
}
|
|
632
157
|
|
|
633
|
-
const
|
|
158
|
+
const path = await writeSelfModel(days, values.force ?? false);
|
|
634
159
|
|
|
635
|
-
if (
|
|
160
|
+
if (path === null) {
|
|
636
161
|
console.log(
|
|
637
162
|
JSON.stringify({
|
|
638
163
|
skipped: true,
|
|
@@ -642,16 +167,9 @@ Output: ~/.pal/memory/self-model.md (synthesized by Sonnet)
|
|
|
642
167
|
return;
|
|
643
168
|
}
|
|
644
169
|
|
|
645
|
-
const { path } = result;
|
|
646
|
-
|
|
647
170
|
console.log(
|
|
648
171
|
JSON.stringify(
|
|
649
|
-
{
|
|
650
|
-
success: true,
|
|
651
|
-
path,
|
|
652
|
-
days,
|
|
653
|
-
message: `Self-model written (${days}-day window)`,
|
|
654
|
-
},
|
|
172
|
+
{ success: true, path, days, message: `Self-model written (${days}-day window)` },
|
|
655
173
|
null,
|
|
656
174
|
2
|
|
657
175
|
)
|