@personaxis/core 0.11.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.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agent.d.ts +105 -0
  3. package/dist/agent.js +429 -0
  4. package/dist/appraisal.d.ts +196 -0
  5. package/dist/appraisal.js +159 -0
  6. package/dist/approval.d.ts +42 -0
  7. package/dist/approval.js +71 -0
  8. package/dist/blackboard.d.ts +86 -0
  9. package/dist/blackboard.js +139 -0
  10. package/dist/compile/assemble.d.ts +44 -0
  11. package/dist/compile/assemble.js +338 -0
  12. package/dist/compile/dist.d.ts +30 -0
  13. package/dist/compile/dist.js +66 -0
  14. package/dist/compile/faithfulness.d.ts +47 -0
  15. package/dist/compile/faithfulness.js +112 -0
  16. package/dist/compile/index.d.ts +10 -0
  17. package/dist/compile/index.js +10 -0
  18. package/dist/compile/targets.d.ts +62 -0
  19. package/dist/compile/targets.js +114 -0
  20. package/dist/config-layers.d.ts +42 -0
  21. package/dist/config-layers.js +59 -0
  22. package/dist/config-scan.d.ts +30 -0
  23. package/dist/config-scan.js +118 -0
  24. package/dist/context.d.ts +67 -0
  25. package/dist/context.js +178 -0
  26. package/dist/envelopes.d.ts +48 -0
  27. package/dist/envelopes.js +131 -0
  28. package/dist/events.d.ts +135 -0
  29. package/dist/events.js +19 -0
  30. package/dist/evolution-view.d.ts +43 -0
  31. package/dist/evolution-view.js +67 -0
  32. package/dist/generated/version.d.ts +1 -0
  33. package/dist/generated/version.js +2 -0
  34. package/dist/governance.d.ts +108 -0
  35. package/dist/governance.js +204 -0
  36. package/dist/heuristic-appraiser.d.ts +13 -0
  37. package/dist/heuristic-appraiser.js +48 -0
  38. package/dist/hooks.d.ts +52 -0
  39. package/dist/hooks.js +122 -0
  40. package/dist/index.d.ts +53 -0
  41. package/dist/index.js +53 -0
  42. package/dist/injection.d.ts +48 -0
  43. package/dist/injection.js +141 -0
  44. package/dist/live-sync.d.ts +60 -0
  45. package/dist/live-sync.js +85 -0
  46. package/dist/llm-appraiser.d.ts +31 -0
  47. package/dist/llm-appraiser.js +143 -0
  48. package/dist/lock.d.ts +26 -0
  49. package/dist/lock.js +99 -0
  50. package/dist/loop.d.ts +52 -0
  51. package/dist/loop.js +253 -0
  52. package/dist/memory-kinds.d.ts +67 -0
  53. package/dist/memory-kinds.js +106 -0
  54. package/dist/memory.d.ts +123 -0
  55. package/dist/memory.js +228 -0
  56. package/dist/model-config.d.ts +60 -0
  57. package/dist/model-config.js +102 -0
  58. package/dist/persona-theme.d.ts +57 -0
  59. package/dist/persona-theme.js +183 -0
  60. package/dist/persona.d.ts +88 -0
  61. package/dist/persona.js +93 -0
  62. package/dist/ports/index.d.ts +81 -0
  63. package/dist/ports/index.js +50 -0
  64. package/dist/provenance.d.ts +63 -0
  65. package/dist/provenance.js +132 -0
  66. package/dist/recompile-marker.d.ts +19 -0
  67. package/dist/recompile-marker.js +36 -0
  68. package/dist/registry.d.ts +99 -0
  69. package/dist/registry.js +168 -0
  70. package/dist/responder.d.ts +47 -0
  71. package/dist/responder.js +78 -0
  72. package/dist/sandbox.d.ts +120 -0
  73. package/dist/sandbox.js +265 -0
  74. package/dist/self-evolution.d.ts +148 -0
  75. package/dist/self-evolution.js +364 -0
  76. package/dist/session-writer.d.ts +52 -0
  77. package/dist/session-writer.js +104 -0
  78. package/dist/sessions.d.ts +96 -0
  79. package/dist/sessions.js +179 -0
  80. package/dist/sigil.d.ts +41 -0
  81. package/dist/sigil.js +92 -0
  82. package/dist/skill-lifecycle.d.ts +74 -0
  83. package/dist/skill-lifecycle.js +126 -0
  84. package/dist/skill-review.d.ts +25 -0
  85. package/dist/skill-review.js +67 -0
  86. package/dist/spec-edit.d.ts +30 -0
  87. package/dist/spec-edit.js +137 -0
  88. package/dist/state-engine.d.ts +40 -0
  89. package/dist/state-engine.js +50 -0
  90. package/dist/state-rebuild.d.ts +48 -0
  91. package/dist/state-rebuild.js +60 -0
  92. package/dist/sync.d.ts +32 -0
  93. package/dist/sync.js +95 -0
  94. package/dist/tool-calling.d.ts +59 -0
  95. package/dist/tool-calling.js +168 -0
  96. package/dist/tool-repair.d.ts +20 -0
  97. package/dist/tool-repair.js +119 -0
  98. package/dist/tools/exec.d.ts +46 -0
  99. package/dist/tools/exec.js +132 -0
  100. package/dist/tools/registry.d.ts +38 -0
  101. package/dist/tools/registry.js +162 -0
  102. package/dist/trace.d.ts +63 -0
  103. package/dist/trace.js +162 -0
  104. package/dist/verification.d.ts +65 -0
  105. package/dist/verification.js +190 -0
  106. package/package.json +38 -0
package/dist/loop.d.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The Living Loop — what makes a persona "alive", governed end to end.
3
+ *
4
+ * observe -> appraise -> evolve (clamped to envelopes) -> recompile -> memory
5
+ *
6
+ * Every mutation is clamped + audited; every memory write is dry-run verified +
7
+ * lineage-chained; the governance gate decides admissibility; the universal
8
+ * invariants are never touched. The model proposes signals; the spec engine
9
+ * imposes safety. Nothing is a black box — each step emits an auditable event.
10
+ */
11
+ import { type GovernanceConfig } from "./governance.js";
12
+ import { type PersonaHandle } from "./persona.js";
13
+ import { type Storage } from "./ports/index.js";
14
+ import { EventBus } from "./events.js";
15
+ import type { Appraiser, ProvenanceSource } from "./appraisal.js";
16
+ export interface LivingLoopOptions {
17
+ appraiser: Appraiser;
18
+ /** Override governance; otherwise read improvement_policy.mode from frontmatter. */
19
+ governance?: Partial<GovernanceConfig>;
20
+ /** Optional recompile hook (LLM-backed, lives in the CLI). Called on drift. */
21
+ recompile?: (handle: PersonaHandle) => Promise<void>;
22
+ /** F3.3 — storage adapters (state/lock/ledger/memory). Defaults to the fs bundle;
23
+ * the SaaS injects Postgres/S3 adapters over the SAME engine. */
24
+ storage?: Storage;
25
+ }
26
+ export interface TickInput {
27
+ observation: string;
28
+ source: ProvenanceSource;
29
+ /** Actor recorded in the mutation log. Defaults to actor-llm. */
30
+ actor?: "actor-llm" | "runtime-context";
31
+ }
32
+ export interface TickReport {
33
+ mutationsApplied: number;
34
+ memoriesWritten: number;
35
+ abstained: boolean;
36
+ }
37
+ export declare class LivingLoop {
38
+ private readonly opts;
39
+ readonly bus: EventBus;
40
+ private handle;
41
+ /** v0.8: stamped on every mutation for cross-OS reconciliation + traceability. */
42
+ readonly sessionId: `${string}-${string}-${string}-${string}-${string}`;
43
+ /** F3.3 — persistence seam; fs adapters by default. */
44
+ private readonly storage;
45
+ constructor(personaPath: string, opts: LivingLoopOptions);
46
+ /** Reload the persona document (e.g. after a recompile). */
47
+ reload(): void;
48
+ get persona(): PersonaHandle;
49
+ private resolveGovernance;
50
+ /** Run one full governed cycle. */
51
+ tick(input: TickInput): Promise<TickReport>;
52
+ }
package/dist/loop.js ADDED
@@ -0,0 +1,253 @@
1
+ /**
2
+ * The Living Loop — what makes a persona "alive", governed end to end.
3
+ *
4
+ * observe -> appraise -> evolve (clamped to envelopes) -> recompile -> memory
5
+ *
6
+ * Every mutation is clamped + audited; every memory write is dry-run verified +
7
+ * lineage-chained; the governance gate decides admissibility; the universal
8
+ * invariants are never touched. The model proposes signals; the spec engine
9
+ * imposes safety. Nothing is a black box — each step emits an auditable event.
10
+ */
11
+ import { extractEnvelopes } from "./envelopes.js";
12
+ import { governMutations, readMode, readMaxStepDelta, DEFAULT_GOVERNANCE, } from "./governance.js";
13
+ import { applyMutation } from "./state-engine.js";
14
+ import { prepareMemoryEntry, readMemoryTypes, } from "./memory.js";
15
+ import { recordEvaluation, scoreMemoryEntry, setPreference } from "./memory-kinds.js";
16
+ import { detectMemoryAnomalies } from "./provenance.js";
17
+ import { scanForInjection } from "./injection.js";
18
+ import { activeOverlay, applyOverlay, proposeSelfEdit, editGate, editableLayers, SelfEditError } from "./self-evolution.js";
19
+ import { buildEvolutionView } from "./evolution-view.js";
20
+ import { machineId } from "./registry.js";
21
+ import { randomUUID } from "node:crypto";
22
+ import { loadPersona } from "./persona.js";
23
+ import { defaultFsStorage } from "./ports/index.js";
24
+ import { EventBus } from "./events.js";
25
+ export class LivingLoop {
26
+ opts;
27
+ bus = new EventBus();
28
+ handle;
29
+ /** v0.8: stamped on every mutation for cross-OS reconciliation + traceability. */
30
+ sessionId = randomUUID();
31
+ /** F3.3 — persistence seam; fs adapters by default. */
32
+ storage;
33
+ constructor(personaPath, opts) {
34
+ this.opts = opts;
35
+ this.handle = loadPersona(personaPath);
36
+ this.storage = opts.storage ?? defaultFsStorage();
37
+ }
38
+ /** Reload the persona document (e.g. after a recompile). */
39
+ reload() {
40
+ this.handle = loadPersona(this.handle.personaPath);
41
+ }
42
+ get persona() {
43
+ return this.handle;
44
+ }
45
+ resolveGovernance() {
46
+ const fm = this.handle.frontmatter;
47
+ const mode = readMode(fm, this.handle.personaPath);
48
+ const maxStepDelta = readMaxStepDelta(fm);
49
+ return { ...DEFAULT_GOVERNANCE, mode, maxStepDelta, ...this.opts.governance };
50
+ }
51
+ /** Run one full governed cycle. */
52
+ async tick(input) {
53
+ const bus = this.bus;
54
+ try {
55
+ // 1. observe
56
+ bus.emit({ type: "observe", observation: input.observation, source: input.source });
57
+ // 1a. injection scan — untrusted text must not steer evolution.
58
+ const scan = scanForInjection(input.observation);
59
+ const injectionBlocked = scan.verdict === "malicious";
60
+ if (scan.verdict !== "clean") {
61
+ bus.emit({
62
+ type: "anomaly",
63
+ kind: `injection:${scan.verdict}`,
64
+ detail: scan.findings.map((f) => f.rule).join(", "),
65
+ });
66
+ }
67
+ // Applied self-edits (governed, versioned) take effect here: the overlay is
68
+ // merged onto the frontmatter before envelopes are read, so an approved edit
69
+ // to e.g. a trait's range actually changes clamping. The spec file is untouched.
70
+ const fm = applyOverlay(this.handle.frontmatter, activeOverlay(this.handle.personaPath));
71
+ const env = extractEnvelopes(fm);
72
+ // 2. appraise (model proposes structured signals only). A failing/unreachable
73
+ // appraiser (network, auth, unsupported response_format) must NOT end the
74
+ // session: degrade to "no evolution this turn" — the persona still replied
75
+ // and the spec invariants are untouched.
76
+ // F3.8: build the grounded evolution view (current values + envelopes + mode)
77
+ // so the appraiser proposes against reality, not blind against field names.
78
+ const mode = readMode(fm, this.handle.personaPath);
79
+ const editableSections = editableLayers(fm, mode);
80
+ const currentValues = this.storage.state.exists(this.handle.statePath)
81
+ ? this.storage.state.read(this.handle.statePath).values
82
+ : {};
83
+ const evolutionView = buildEvolutionView({ values: currentValues, envelopes: env.envelopes, editableSections, mode });
84
+ let signal;
85
+ try {
86
+ signal = await this.opts.appraiser.appraise({
87
+ observation: input.observation,
88
+ source: input.source,
89
+ personaBody: this.handle.body,
90
+ mutableFields: Object.keys(env.envelopes),
91
+ editableSections,
92
+ evolutionView,
93
+ });
94
+ }
95
+ catch (err) {
96
+ bus.emit({ type: "error", message: `appraiser unavailable: ${err.message}` });
97
+ bus.emit({ type: "abstain", reason: "appraiser error" });
98
+ return { mutationsApplied: 0, memoriesWritten: 0, abstained: true };
99
+ }
100
+ bus.emit({ type: "appraise", signal });
101
+ // Uncertainty policy: abstain from evolving when confidence is too low.
102
+ if (signal.confidence < 0.2) {
103
+ bus.emit({ type: "abstain", reason: `low confidence (${signal.confidence.toFixed(2)})` });
104
+ return { mutationsApplied: 0, memoriesWritten: 0, abstained: true };
105
+ }
106
+ // 3. evolve — govern, then clamp + audit
107
+ const gov = this.resolveGovernance();
108
+ const decision = governMutations(signal.mutations, env, gov);
109
+ bus.emit({ type: "govern", verdicts: decision.verdicts });
110
+ // A malicious injection blocks evolution this turn (content can still be
111
+ // remembered, tagged, for audit) — defense in depth over the governance gate.
112
+ const admitted = injectionBlocked ? [] : decision.admitted;
113
+ let mutationsApplied = 0;
114
+ if (admitted.length > 0) {
115
+ // Serialize the read→apply→write against concurrent writers (serve, MCP,
116
+ // another tick): re-read fresh state under the lock — the proposed deltas
117
+ // are relative, so applying them to fresh values is correct — and never
118
+ // hold the lock across a model call (the appraisal already happened).
119
+ this.storage.lock.withLock(this.handle.statePath, () => {
120
+ const fresh = this.storage.state.read(this.handle.statePath);
121
+ for (const m of admitted) {
122
+ const result = applyMutation(fresh, env.envelopes, {
123
+ field: m.field,
124
+ delta: m.delta,
125
+ reason: m.reason,
126
+ actor: input.actor ?? "actor-llm",
127
+ originNode: machineId(),
128
+ sessionId: this.sessionId,
129
+ });
130
+ bus.emit({ type: "mutate", result });
131
+ if (result.to !== result.from)
132
+ mutationsApplied++;
133
+ }
134
+ this.storage.state.write(this.handle.statePath, fresh);
135
+ });
136
+ }
137
+ // 3b. evolve QUALITATIVE — the appraiser may propose prose self-edits to
138
+ // persona_prompting. Governed by improvement_policy.mode via governQualitative:
139
+ // locked → none; suggesting → queued for /review; autonomous → auto-applied (still
140
+ // gated by consensus verifiers + protected paths + the self_edit provenance gate,
141
+ // which requires a `user`-trust justification). A malicious injection blocks ALL
142
+ // self-edits this turn. Self-edits do NOT count as envelope mutations — keeping the
143
+ // `mutationsApplied` metric (and the injection eval) about numbers only.
144
+ const selfEdits = signal.selfEdits ?? [];
145
+ if (!injectionBlocked && selfEdits.length > 0 && signal.confidence >= 0.6) {
146
+ for (const se of selfEdits) {
147
+ // ANY spec section may be proposed; editGate composes the safety floor + the
148
+ // author's declared per-layer policy + the global mode into block | queue | auto.
149
+ const action = editGate(se.targetPath, fm, gov.mode);
150
+ if (action === "block") {
151
+ bus.emit({ type: "self-edit", op: "rejected", targetPath: se.targetPath, reason: "section is protected or locked by policy" });
152
+ continue;
153
+ }
154
+ // Map the gate to the effective mode proposeSelfEdit understands: a layer the author
155
+ // marked review-required QUEUES even when the global mode is autonomous.
156
+ const effectiveMode = action === "auto" ? "autonomous" : "suggesting";
157
+ try {
158
+ const r = proposeSelfEdit(this.handle.personaPath, { targetPath: se.targetPath, toValue: se.toValue, rationale: se.rationale, sources: [input.source] }, effectiveMode, input.actor ?? "actor-llm");
159
+ bus.emit({ type: "self-edit", op: r.status === "applied" ? "applied" : "queued", targetPath: se.targetPath, id: r.id });
160
+ }
161
+ catch (e) {
162
+ const reason = e instanceof SelfEditError ? e.message : e.message;
163
+ bus.emit({ type: "self-edit", op: "rejected", targetPath: se.targetPath, reason });
164
+ }
165
+ }
166
+ }
167
+ // 3c. user preferences — written only when declared (memory.types.user_preferences)
168
+ // and never under a malicious injection.
169
+ const memTypesForPrefs = readMemoryTypes(fm);
170
+ const prefs = signal.preferences ?? [];
171
+ if (!injectionBlocked && memTypesForPrefs.user_preferences && prefs.length > 0) {
172
+ for (const pref of prefs)
173
+ setPreference(this.handle.personaPath, pref.key, pref.value, pref.rationale);
174
+ bus.emit({ type: "memory-kind", kind: "user_preferences", detail: `+${prefs.length} pref(s)` });
175
+ }
176
+ // 5. memory — HONOR memory.types (spec fidelity). Episodic writes only when
177
+ // the persona declares `memory.types.episodic`; otherwise nothing is logged.
178
+ const memTypes = readMemoryTypes(fm);
179
+ let memoriesWritten = 0;
180
+ const written = [];
181
+ if (memTypes.episodic) {
182
+ for (const mem of signal.memories) {
183
+ const entry = prepareMemoryEntry(this.handle.personaPath, {
184
+ content: mem.content,
185
+ source: mem.source,
186
+ tags: injectionBlocked ? [...(mem.tags ?? []), "injection-flagged"] : mem.tags,
187
+ });
188
+ const chain = this.storage.ledger.verify(this.handle.personaPath);
189
+ if (!chain.ok) {
190
+ bus.emit({ type: "error", message: `memory chain broken at #${chain.brokenAt}; refusing write` });
191
+ continue;
192
+ }
193
+ this.storage.ledger.append(this.handle.personaPath, entry);
194
+ bus.emit({ type: "memory", entry });
195
+ written.push(entry);
196
+ memoriesWritten++;
197
+ }
198
+ }
199
+ else if (signal.memories.length > 0) {
200
+ bus.emit({ type: "abstain", reason: `memory.types.episodic=false — ${signal.memories.length} note(s) not stored` });
201
+ }
202
+ // Consensus / anomaly pass — surface poisoning signals (A-MemGuard-style).
203
+ if (memoriesWritten > 0) {
204
+ for (const a of detectMemoryAnomalies(this.storage.ledger.read(this.handle.personaPath))) {
205
+ bus.emit({ type: "anomaly", kind: a.kind, detail: a.detail });
206
+ }
207
+ // Episodic → semantic consolidation when the persona declares it.
208
+ if (memTypes.semantic) {
209
+ const c = this.storage.memory.consolidate(this.handle.personaPath);
210
+ bus.emit({ type: "recompile", reason: `semantic consolidation (${c.count} entries → memory.md)` });
211
+ }
212
+ }
213
+ // evaluations — deterministic quality/utility scoring of what was written this turn.
214
+ // Each score is surfaced individually (target + dimension + score) so the UI can show WHAT
215
+ // was judged, not an opaque "+N eval(s)"; a compact rollup is kept for the one-line summary.
216
+ if (memTypes.evaluations) {
217
+ let evals = 0;
218
+ const emitScore = (s) => {
219
+ recordEvaluation(this.handle.personaPath, s);
220
+ bus.emit({ type: "evaluation", target: s.target, dimension: s.dimension, score: s.score, rationale: s.rationale });
221
+ evals++;
222
+ };
223
+ if (written.length > 0) {
224
+ for (const entry of written)
225
+ for (const s of scoreMemoryEntry(entry, { injectionBlocked }))
226
+ emitScore(s);
227
+ }
228
+ else {
229
+ emitScore({
230
+ target: "turn",
231
+ dimension: "safety",
232
+ score: injectionBlocked ? 0 : 1,
233
+ rationale: injectionBlocked ? "injection blocked this turn" : "no injection signal",
234
+ });
235
+ }
236
+ if (evals > 0)
237
+ bus.emit({ type: "memory-kind", kind: "evaluations", detail: `+${evals} eval(s)` });
238
+ }
239
+ // 4. recompile on drift (after state changes so the doc reflects them)
240
+ if (mutationsApplied > 0 && this.opts.recompile) {
241
+ bus.emit({ type: "recompile", reason: `${mutationsApplied} envelope mutation(s) applied` });
242
+ await this.opts.recompile(this.handle);
243
+ this.reload();
244
+ }
245
+ bus.emit({ type: "tick-complete", mutationsApplied, memoriesWritten });
246
+ return { mutationsApplied, memoriesWritten, abstained: false };
247
+ }
248
+ catch (err) {
249
+ bus.emit({ type: "error", message: err.message });
250
+ throw err;
251
+ }
252
+ }
253
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The non-episodic memory kinds (F4) — procedural, autobiographical, user_preferences,
3
+ * evaluations. The spec's `memory.types` declares six flags; episodic + semantic live in
4
+ * memory.ts. These four were previously declared-but-unenforced; here they become real,
5
+ * each honoring its flag at the producer call site (the established pattern — see
6
+ * loop.ts / agent.ts gating episodic on `readMemoryTypes(...).episodic`).
7
+ *
8
+ * Storage mirrors episodic memory: append-only JSONL under `<personaDir>/memory/`, except
9
+ * user_preferences which is a small last-wins JSON map. Dependency-free (node:fs only).
10
+ */
11
+ import type { MemoryEntry } from "./memory.js";
12
+ export interface ProceduralEntry {
13
+ ts: string;
14
+ task: string;
15
+ procedure: string;
16
+ tags: string[];
17
+ }
18
+ export declare function appendProcedural(personaPath: string, req: {
19
+ task: string;
20
+ procedure: string;
21
+ tags?: string[];
22
+ }): ProceduralEntry;
23
+ export declare function readProcedural(personaPath: string, limit?: number): ProceduralEntry[];
24
+ export interface AutobiographicalEntry {
25
+ ts: string;
26
+ event: string;
27
+ detail?: string;
28
+ tags: string[];
29
+ }
30
+ export declare function appendAutobiographical(personaPath: string, req: {
31
+ event: string;
32
+ detail?: string;
33
+ tags?: string[];
34
+ }): AutobiographicalEntry;
35
+ export declare function readAutobiographical(personaPath: string, limit?: number): AutobiographicalEntry[];
36
+ export interface PreferenceValue {
37
+ value: string;
38
+ ts: string;
39
+ rationale?: string;
40
+ }
41
+ export declare function setPreference(personaPath: string, key: string, value: string, rationale?: string): void;
42
+ export declare function readPreferences(personaPath: string): Record<string, PreferenceValue>;
43
+ export declare function getPreference(personaPath: string, key: string): string | undefined;
44
+ export type EvalDimension = "usefulness" | "accuracy" | "safety";
45
+ export interface EvaluationEntry {
46
+ ts: string;
47
+ /** What was scored: a memory hash (`#abc12345`) or a turn marker (`turn`). */
48
+ target: string;
49
+ dimension: EvalDimension;
50
+ score: number;
51
+ rationale: string;
52
+ }
53
+ export declare function recordEvaluation(personaPath: string, req: {
54
+ target: string;
55
+ dimension: EvalDimension;
56
+ score: number;
57
+ rationale: string;
58
+ }): EvaluationEntry;
59
+ export declare function readEvaluations(personaPath: string, limit?: number): EvaluationEntry[];
60
+ /**
61
+ * Deterministic, offline quality scorer for an episodic memory entry. Returns one
62
+ * evaluation per dimension. Heuristic (no LLM): safety reflects whether the content was
63
+ * injection-flagged; usefulness rewards substantive, user/synthesis-sourced content.
64
+ */
65
+ export declare function scoreMemoryEntry(entry: MemoryEntry, opts?: {
66
+ injectionBlocked?: boolean;
67
+ }): Array<Omit<EvaluationEntry, "ts">>;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The non-episodic memory kinds (F4) — procedural, autobiographical, user_preferences,
3
+ * evaluations. The spec's `memory.types` declares six flags; episodic + semantic live in
4
+ * memory.ts. These four were previously declared-but-unenforced; here they become real,
5
+ * each honoring its flag at the producer call site (the established pattern — see
6
+ * loop.ts / agent.ts gating episodic on `readMemoryTypes(...).episodic`).
7
+ *
8
+ * Storage mirrors episodic memory: append-only JSONL under `<personaDir>/memory/`, except
9
+ * user_preferences which is a small last-wins JSON map. Dependency-free (node:fs only).
10
+ */
11
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
+ import { dirname, join } from "node:path";
13
+ function memDir(personaPath) {
14
+ return join(dirname(personaPath), "memory");
15
+ }
16
+ function kindPath(personaPath, file) {
17
+ return join(memDir(personaPath), file);
18
+ }
19
+ function appendJsonl(personaPath, file, row) {
20
+ const p = kindPath(personaPath, file);
21
+ mkdirSync(dirname(p), { recursive: true });
22
+ appendFileSync(p, JSON.stringify(row) + "\n", "utf-8");
23
+ }
24
+ function readJsonl(personaPath, file) {
25
+ const p = kindPath(personaPath, file);
26
+ if (!existsSync(p))
27
+ return [];
28
+ const out = [];
29
+ for (const line of readFileSync(p, "utf-8").split("\n")) {
30
+ if (!line.trim())
31
+ continue;
32
+ try {
33
+ out.push(JSON.parse(line));
34
+ }
35
+ catch {
36
+ /* skip a corrupt line */
37
+ }
38
+ }
39
+ return out;
40
+ }
41
+ export function appendProcedural(personaPath, req) {
42
+ const entry = { ts: new Date().toISOString(), task: req.task, procedure: req.procedure, tags: req.tags ?? [] };
43
+ appendJsonl(personaPath, "procedural.jsonl", entry);
44
+ return entry;
45
+ }
46
+ export function readProcedural(personaPath, limit = 200) {
47
+ return readJsonl(personaPath, "procedural.jsonl").slice(-limit);
48
+ }
49
+ export function appendAutobiographical(personaPath, req) {
50
+ const entry = { ts: new Date().toISOString(), event: req.event, tags: req.tags ?? [], ...(req.detail ? { detail: req.detail } : {}) };
51
+ appendJsonl(personaPath, "autobiographical.jsonl", entry);
52
+ return entry;
53
+ }
54
+ export function readAutobiographical(personaPath, limit = 200) {
55
+ return readJsonl(personaPath, "autobiographical.jsonl").slice(-limit);
56
+ }
57
+ export function setPreference(personaPath, key, value, rationale) {
58
+ const prefs = readPreferences(personaPath);
59
+ prefs[key] = { value, ts: new Date().toISOString(), ...(rationale ? { rationale } : {}) };
60
+ const p = kindPath(personaPath, "preferences.json");
61
+ mkdirSync(dirname(p), { recursive: true });
62
+ writeFileSync(p, JSON.stringify(prefs, null, 2) + "\n", "utf-8");
63
+ }
64
+ export function readPreferences(personaPath) {
65
+ const p = kindPath(personaPath, "preferences.json");
66
+ if (!existsSync(p))
67
+ return {};
68
+ try {
69
+ return JSON.parse(readFileSync(p, "utf-8"));
70
+ }
71
+ catch {
72
+ return {};
73
+ }
74
+ }
75
+ export function getPreference(personaPath, key) {
76
+ return readPreferences(personaPath)[key]?.value;
77
+ }
78
+ export function recordEvaluation(personaPath, req) {
79
+ const entry = { ts: new Date().toISOString(), target: req.target, dimension: req.dimension, score: clamp01(req.score), rationale: req.rationale };
80
+ appendJsonl(personaPath, "evaluations.jsonl", entry);
81
+ return entry;
82
+ }
83
+ export function readEvaluations(personaPath, limit = 200) {
84
+ return readJsonl(personaPath, "evaluations.jsonl").slice(-limit);
85
+ }
86
+ function clamp01(n) {
87
+ return Math.max(0, Math.min(1, n));
88
+ }
89
+ /**
90
+ * Deterministic, offline quality scorer for an episodic memory entry. Returns one
91
+ * evaluation per dimension. Heuristic (no LLM): safety reflects whether the content was
92
+ * injection-flagged; usefulness rewards substantive, user/synthesis-sourced content.
93
+ */
94
+ export function scoreMemoryEntry(entry, opts = {}) {
95
+ const target = `#${entry.hash.slice(0, 8)}`;
96
+ const flagged = opts.injectionBlocked || entry.tags.includes("injection-flagged");
97
+ const safety = flagged ? 0 : 1;
98
+ const len = entry.content.trim().length;
99
+ const sourceWeight = entry.source === "user" || entry.source === "synthesis" ? 0.6 : 0.35;
100
+ const lengthWeight = Math.min(0.4, len / 600);
101
+ const usefulness = flagged ? 0.1 : clamp01(sourceWeight + lengthWeight);
102
+ return [
103
+ { target, dimension: "safety", score: safety, rationale: flagged ? "injection-flagged content" : "no injection signal" },
104
+ { target, dimension: "usefulness", score: usefulness, rationale: `source=${entry.source}, ${len} chars` },
105
+ ];
106
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Governed episodic memory — bounded, provenance-tagged, append-only, auditable.
3
+ *
4
+ * The research is unambiguous (see plan/02-governed-memory, plan/11-security):
5
+ * once untrusted content reaches long-term memory it persists and is later
6
+ * retrieved as "trusted" context — the Zombie-Agent / memory-poisoning failure
7
+ * mode. Defenses adopted here:
8
+ * - every entry carries a provenance source (user/tool/internal/synthesis);
9
+ * - entries form a hash chain (prev_hash -> hash): tamper-evident, time-travel
10
+ * auditable, MemLineage-style append-only log;
11
+ * - the writer is dry-run friendly (verify before commit).
12
+ *
13
+ * Stored as JSON Lines at <personaDir>/memory/episodic.jsonl so it diffs cleanly
14
+ * in git and never mutates prior lines.
15
+ */
16
+ import type { ProvenanceSource } from "./appraisal.js";
17
+ /**
18
+ * The spec's `memory.types` map (MUST). The runtime now HONORS these instead of
19
+ * always writing episodic. `episodic` defaults to true only when the persona
20
+ * declares no `memory` block (backward compatibility); once declared, the flags
21
+ * are authoritative — a persona with `episodic: false` writes nothing to the log.
22
+ */
23
+ export interface MemoryTypes {
24
+ episodic: boolean;
25
+ semantic: boolean;
26
+ procedural: boolean;
27
+ autobiographical: boolean;
28
+ user_preferences: boolean;
29
+ evaluations: boolean;
30
+ }
31
+ export declare function readMemoryTypes(frontmatter: Record<string, unknown>): MemoryTypes;
32
+ export interface MemoryEntry {
33
+ ts: string;
34
+ content: string;
35
+ source: ProvenanceSource;
36
+ tags: string[];
37
+ /**
38
+ * v1.0: sha256 of `content`. The chain hash commits to THIS, not to the
39
+ * content bytes — so `content` can be REDACTED (real erasure, D6/GDPR) while
40
+ * the chain stays verifiable. Absent on legacy (≤0.10) entries, whose chain
41
+ * hash commits to the content directly (redaction there breaks the chain;
42
+ * re-anchor with migrateMemoryChain first).
43
+ */
44
+ content_hash?: string;
45
+ /** v1.0: true when `content` was erased by redactMemory (content_hash retained). */
46
+ redacted?: boolean;
47
+ /** Hash of the previous entry (lineage chain); "" for the first. */
48
+ prev_hash: string;
49
+ /** v1.0: sha256 over {ts, content_hash, source, tags, prev_hash}. Legacy: over {ts, content, …}. */
50
+ hash: string;
51
+ }
52
+ export interface MemoryWriteRequest {
53
+ content: string;
54
+ source: ProvenanceSource;
55
+ tags?: string[];
56
+ }
57
+ export declare function readMemory(personaPath: string): MemoryEntry[];
58
+ /** Build (but do not write) the next entry — the dry-run half of write-path audit.
59
+ * New entries are always v1.0 format (content_hash-anchored: erasure-capable). */
60
+ export declare function prepareMemoryEntry(personaPath: string, req: MemoryWriteRequest): MemoryEntry;
61
+ /** Commit a prepared entry to the append-only log. */
62
+ export declare function commitMemoryEntry(personaPath: string, entry: MemoryEntry): void;
63
+ /**
64
+ * Honor deletion_policy.user_request_supported WITHOUT breaking the append-only
65
+ * chain: append a tombstone that supersedes a prior entry. The original line is
66
+ * never rewritten (chain stays verifiable); live readers filter it out. The
67
+ * deletion itself is thus auditable — you can prove what was removed and when.
68
+ */
69
+ export declare function tombstoneMemory(personaPath: string, targetHash: string, reason: string): MemoryEntry;
70
+ /** Read memory with tombstoned entries (and the tombstones themselves) removed. */
71
+ export declare function readLiveMemory(personaPath: string): MemoryEntry[];
72
+ /**
73
+ * Consolidate episodic memory into a semantic snapshot at <personaDir>/memory.md
74
+ * (the spec's `memory.types.semantic`). Deterministic + dependency-free: groups
75
+ * live (non-tombstoned) entries by provenance source into a digest. Idempotent —
76
+ * rewritten each call, so it always reflects the current live memory.
77
+ */
78
+ export declare function consolidateSemantic(personaPath: string, limit?: number): {
79
+ ok: boolean;
80
+ path: string;
81
+ count: number;
82
+ };
83
+ /**
84
+ * Outcome label for an agent run. The run summary is recorded as an EPISODIC
85
+ * memory entry (source: "synthesis", tag: "agent-run") — no separate STATE.md /
86
+ * agent-state log: the spec's memory.md (consolidated) + episodic + state.json's
87
+ * agent_session already cover task resumption.
88
+ */
89
+ export type AgentOutcome = "success" | "denied" | "error" | "verification_failed" | "stopped";
90
+ /** Read the consolidated semantic memory (memory.md), if present. */
91
+ export declare function readSemanticMemory(personaPath: string): string;
92
+ /**
93
+ * Verify the hash chain is intact (tamper / poisoning detection). Each entry is
94
+ * verified per its own format: v1.0 entries (content_hash present) recompute the
95
+ * chain hash over content_hash AND — unless redacted — check the content still
96
+ * matches its content_hash; legacy entries recompute over the content directly.
97
+ */
98
+ export declare function verifyMemoryChain(personaPath: string): {
99
+ ok: boolean;
100
+ brokenAt?: number;
101
+ };
102
+ /**
103
+ * REAL erasure (v1.0, D6): remove the content bytes of a prior entry while the
104
+ * chain stays verifiable — the chain hash commits to content_hash, which is
105
+ * retained. This is the ONLY sanctioned rewrite of a prior line, it is
106
+ * irreversible, and it is itself audited (a `redaction` record is appended).
107
+ * Complements tombstoneMemory (which hides but retains bytes): tombstone for
108
+ * retrieval removal, redact for right-to-erasure.
109
+ */
110
+ export declare function redactMemory(personaPath: string, targetHash: string, reason: string): {
111
+ redacted: MemoryEntry;
112
+ audit: MemoryEntry;
113
+ };
114
+ /**
115
+ * Re-anchor a legacy (≤0.10) episodic log to the v1.0 content_hash format so its
116
+ * entries become redactable. Rewrites every line: adds content_hash, recomputes
117
+ * each chain hash, re-links prev_hash, and remaps tombstone `target:` tags to
118
+ * the new hashes. One-time, deliberate migration — the old hashes are replaced.
119
+ */
120
+ export declare function migrateMemoryChain(personaPath: string): {
121
+ migrated: number;
122
+ remapped: Record<string, string>;
123
+ };