@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.
- package/LICENSE +21 -0
- package/dist/agent.d.ts +105 -0
- package/dist/agent.js +429 -0
- package/dist/appraisal.d.ts +196 -0
- package/dist/appraisal.js +159 -0
- package/dist/approval.d.ts +42 -0
- package/dist/approval.js +71 -0
- package/dist/blackboard.d.ts +86 -0
- package/dist/blackboard.js +139 -0
- package/dist/compile/assemble.d.ts +44 -0
- package/dist/compile/assemble.js +338 -0
- package/dist/compile/dist.d.ts +30 -0
- package/dist/compile/dist.js +66 -0
- package/dist/compile/faithfulness.d.ts +47 -0
- package/dist/compile/faithfulness.js +112 -0
- package/dist/compile/index.d.ts +10 -0
- package/dist/compile/index.js +10 -0
- package/dist/compile/targets.d.ts +62 -0
- package/dist/compile/targets.js +114 -0
- package/dist/config-layers.d.ts +42 -0
- package/dist/config-layers.js +59 -0
- package/dist/config-scan.d.ts +30 -0
- package/dist/config-scan.js +118 -0
- package/dist/context.d.ts +67 -0
- package/dist/context.js +178 -0
- package/dist/envelopes.d.ts +48 -0
- package/dist/envelopes.js +131 -0
- package/dist/events.d.ts +135 -0
- package/dist/events.js +19 -0
- package/dist/evolution-view.d.ts +43 -0
- package/dist/evolution-view.js +67 -0
- package/dist/generated/version.d.ts +1 -0
- package/dist/generated/version.js +2 -0
- package/dist/governance.d.ts +108 -0
- package/dist/governance.js +204 -0
- package/dist/heuristic-appraiser.d.ts +13 -0
- package/dist/heuristic-appraiser.js +48 -0
- package/dist/hooks.d.ts +52 -0
- package/dist/hooks.js +122 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +53 -0
- package/dist/injection.d.ts +48 -0
- package/dist/injection.js +141 -0
- package/dist/live-sync.d.ts +60 -0
- package/dist/live-sync.js +85 -0
- package/dist/llm-appraiser.d.ts +31 -0
- package/dist/llm-appraiser.js +143 -0
- package/dist/lock.d.ts +26 -0
- package/dist/lock.js +99 -0
- package/dist/loop.d.ts +52 -0
- package/dist/loop.js +253 -0
- package/dist/memory-kinds.d.ts +67 -0
- package/dist/memory-kinds.js +106 -0
- package/dist/memory.d.ts +123 -0
- package/dist/memory.js +228 -0
- package/dist/model-config.d.ts +60 -0
- package/dist/model-config.js +102 -0
- package/dist/persona-theme.d.ts +57 -0
- package/dist/persona-theme.js +183 -0
- package/dist/persona.d.ts +88 -0
- package/dist/persona.js +93 -0
- package/dist/ports/index.d.ts +81 -0
- package/dist/ports/index.js +50 -0
- package/dist/provenance.d.ts +63 -0
- package/dist/provenance.js +132 -0
- package/dist/recompile-marker.d.ts +19 -0
- package/dist/recompile-marker.js +36 -0
- package/dist/registry.d.ts +99 -0
- package/dist/registry.js +168 -0
- package/dist/responder.d.ts +47 -0
- package/dist/responder.js +78 -0
- package/dist/sandbox.d.ts +120 -0
- package/dist/sandbox.js +265 -0
- package/dist/self-evolution.d.ts +148 -0
- package/dist/self-evolution.js +364 -0
- package/dist/session-writer.d.ts +52 -0
- package/dist/session-writer.js +104 -0
- package/dist/sessions.d.ts +96 -0
- package/dist/sessions.js +179 -0
- package/dist/sigil.d.ts +41 -0
- package/dist/sigil.js +92 -0
- package/dist/skill-lifecycle.d.ts +74 -0
- package/dist/skill-lifecycle.js +126 -0
- package/dist/skill-review.d.ts +25 -0
- package/dist/skill-review.js +67 -0
- package/dist/spec-edit.d.ts +30 -0
- package/dist/spec-edit.js +137 -0
- package/dist/state-engine.d.ts +40 -0
- package/dist/state-engine.js +50 -0
- package/dist/state-rebuild.d.ts +48 -0
- package/dist/state-rebuild.js +60 -0
- package/dist/sync.d.ts +32 -0
- package/dist/sync.js +95 -0
- package/dist/tool-calling.d.ts +59 -0
- package/dist/tool-calling.js +168 -0
- package/dist/tool-repair.d.ts +20 -0
- package/dist/tool-repair.js +119 -0
- package/dist/tools/exec.d.ts +46 -0
- package/dist/tools/exec.js +132 -0
- package/dist/tools/registry.d.ts +38 -0
- package/dist/tools/registry.js +162 -0
- package/dist/trace.d.ts +63 -0
- package/dist/trace.js +162 -0
- package/dist/verification.d.ts +65 -0
- package/dist/verification.js +190 -0
- 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
|
+
}
|
package/dist/memory.d.ts
ADDED
|
@@ -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
|
+
};
|