@personaxis/sdk 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Personaxis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # @personaxis/sdk
2
+
3
+ Embed a living, governed persona in a Node/TypeScript backend.
4
+
5
+ This is the **embed SDK**: it runs the personaxis engine **in your process** (Modo 2 self-host), so
6
+ your app owns the model, the state, and the data. It is a thin, typed wrapper over
7
+ [`@personaxis/core`](../core) — the engine already does the governance (clamp + audit + injection
8
+ scan + hash-chained memory + the governance gate); this package gives your app a small, obvious API.
9
+
10
+ ## Where this fits (the SDK strategy)
11
+
12
+ Like Anthropic/OpenAI, personaxis has **two kinds of SDK**, one per deployment mode:
13
+
14
+ | SDK kind | What it does | Package | Status |
15
+ |---|---|---|---|
16
+ | **Embed SDK** | Runs the engine **in-process** (your backend, your model) | `@personaxis/sdk` (this) — TS, in the monorepo | **Shipping** |
17
+ | **API-client SDK** | Calls the **managed SaaS** HTTP API (like `anthropic`/`openai` clients) | separate repos, one per language (`personaxis-python`, …) | With the SaaS (future) |
18
+
19
+ The TS embed SDK lives in this monorepo because the whole toolchain is TS and it depends directly on
20
+ `@personaxis/core`. Per-language **API-client** SDKs are separate repos (the professional pattern:
21
+ `anthropic-sdk-typescript`, `anthropic-sdk-python`, … are each their own repo) and wrap the SaaS
22
+ HTTP surface — they arrive with the managed SaaS.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ npm add @personaxis/sdk # (or pnpm/yarn) — depends on @personaxis/core
28
+ ```
29
+
30
+ ## Use
31
+
32
+ ```ts
33
+ import { Persona } from "@personaxis/sdk";
34
+
35
+ const persona = new Persona("./.personaxis/personas/support/personaxis.md");
36
+
37
+ // 1) Load the identity as system-prompt slot #1 for YOUR LLM call.
38
+ const systemPrompt = persona.compiledIdentity();
39
+
40
+ // 2) Learn from an interaction on your configured model (env > project > global config).
41
+ await persona.observe("the customer is frustrated about a double charge", "user");
42
+
43
+ // 3) Read / nudge the runtime dials (clamped + audited).
44
+ const { values } = persona.state();
45
+ persona.adjust("mood.tone", -0.1, "customer frustrated");
46
+
47
+ // 4) Verify integrity (hash-chained memory, anomaly detection).
48
+ const audit = persona.audit();
49
+ ```
50
+
51
+ ## API
52
+
53
+ - `new Persona(personaPath)` — bind to a `personaxis.md` (its `state.json` + memory live alongside).
54
+ - `compiledIdentity(): string` — the compiled `PERSONA.md` (falls back to the spec body).
55
+ - `state(): { values, recentMutations }` — current envelope dials + recent audited mutations.
56
+ - `observe(observation, source?): Promise<{ report, events, recompilePending }>` — one governed
57
+ Living-Loop tick on the resolved model (heuristic fallback offline).
58
+ - `adjust(field, delta, reason)` — a single clamped, audited mutation.
59
+ - `audit(): { mutationCount, memoryEntries, memoryChainIntact, anomalies }`.
60
+ - `reload()` — re-read the spec after an external recompile/decompile.
61
+
62
+ ## Config & secrets
63
+
64
+ The model/key resolve through the same layered config the CLI uses (`resolveModel`: env > project >
65
+ global; the key from the env var named by `apiKeyEnv`). In production the key comes from your deploy's
66
+ secret manager — never a file. See the CLI's [configuration guide](../../docs/configuration.md) and
67
+ [deployment modes](../../docs/architecture/deployment.md).
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @personaxis/sdk — the SINGLE engine façade (F3.5).
3
+ *
4
+ * Embed a living, governed persona in a Node/TS backend (Modo 2 self-host). The
5
+ * engine (@personaxis/core) does the governance — clamp + audit + injection scan
6
+ * + hash-chained memory + the agent loop; this SDK is the ONE ergonomic surface
7
+ * that drives it, with full parity across state, evolution, memory, agent, and
8
+ * safety operations. The MCP server, `serve`, and the REPL consume this façade
9
+ * rather than re-wrapping core (end of the wrapper triplication): host-specific
10
+ * concerns (MCP path-confinement, HTTP shaping, REPL rendering) wrap the SDK,
11
+ * they do not re-implement the engine.
12
+ *
13
+ * Example:
14
+ * import { Persona } from "@personaxis/sdk";
15
+ * const persona = new Persona("./.personaxis/personas/support/personaxis.md");
16
+ * const systemPrompt = persona.compiledIdentity(); // system-prompt slot #1
17
+ * await persona.observe("the customer is frustrated about billing", "user");
18
+ * const { values } = persona.state();
19
+ */
20
+ import { applyMutation, reviewSkill, scanForInjection, scanAgentConfig, evaluateCommand, type LoopEvent, type ProvenanceSource } from "@personaxis/core";
21
+ export interface ObserveResult {
22
+ report: {
23
+ mutationsApplied: number;
24
+ memoriesWritten: number;
25
+ abstained: boolean;
26
+ };
27
+ events: LoopEvent[];
28
+ /** True if a governed self-edit left the compiled PERSONA.md stale (call `personaxis compile`). */
29
+ recompilePending: boolean;
30
+ }
31
+ export interface PersonaStateView {
32
+ values: Record<string, number>;
33
+ recentMutations: unknown[];
34
+ }
35
+ export interface PersonaAuditView {
36
+ mutationCount: number;
37
+ memoryEntries: number;
38
+ memoryChainIntact: boolean;
39
+ memoryChainBrokenAt: number | null;
40
+ anomalies: unknown[];
41
+ }
42
+ export interface AgentRunResult {
43
+ result: unknown;
44
+ events: LoopEvent[];
45
+ trace: unknown[];
46
+ }
47
+ /** A live persona bound to its `personaxis.md` spec (its state.json + memory live alongside it). */
48
+ export declare class Persona {
49
+ readonly personaPath: string;
50
+ private handle;
51
+ constructor(personaPath: string);
52
+ private fm;
53
+ /** The compiled, LLM-facing identity document (system-prompt slot #1). Falls back to the spec
54
+ * body if PERSONA.md hasn't been compiled yet. */
55
+ compiledIdentity(): string;
56
+ /** The raw qualitative spec body (the compiled document as stored on the spec). */
57
+ compiledBody(): string;
58
+ /** Current runtime state: envelope values + the last few audited mutations. */
59
+ state(): PersonaStateView;
60
+ /** The mutable surface: envelope fields + the hard-enforced virtues that are immutable. */
61
+ envelopes(): {
62
+ mutableFields: Record<string, unknown>;
63
+ hardEnforcedVirtues: unknown;
64
+ };
65
+ /**
66
+ * Run ONE governed Living-Loop cycle on an observation, on the persona's resolved model
67
+ * (falls back to the deterministic heuristic appraiser if no model is configured). Every mutation
68
+ * is clamped + audited; a malicious observation is injection-scanned and cannot steer evolution.
69
+ */
70
+ observe(observation: string, source?: ProvenanceSource): Promise<ObserveResult>;
71
+ /** Apply a single clamped, audited mutation to an envelope field (the spec's adjust_persona_state). */
72
+ adjust(field: string, delta: number, reason: string): ReturnType<typeof applyMutation>;
73
+ /**
74
+ * Run the governed Agent Loop on a task. Non-interactive: any tool whose verdict is `ask` is
75
+ * denied unless the persona's permissions allow-list pre-authorizes it. Requires a configured model.
76
+ */
77
+ agentRun(task: string, opts?: {
78
+ maxSteps?: number;
79
+ onApproval?: () => Promise<"deny" | "approve">;
80
+ }): Promise<AgentRunResult | {
81
+ error: string;
82
+ }>;
83
+ /** Integrity view: mutation count, memory size, hash-chain validity, detected anomalies. */
84
+ audit(): PersonaAuditView;
85
+ /** Honor deletion_policy.user_request_supported: tombstone a memory entry (retrieval removal). */
86
+ forget(targetHash: string, reason: string): {
87
+ tombstoned: string;
88
+ by: string;
89
+ liveEntries: number;
90
+ };
91
+ /** Propose a governed self-edit (queued or applied per improvement_policy.mode). */
92
+ proposeEdit(targetPath: string, toValue: unknown, rationale: string, sources?: ProvenanceSource[]): Record<string, unknown>;
93
+ /** Pending self-edit proposals + the active applied overlay. */
94
+ listProposals(): {
95
+ proposals: unknown;
96
+ activeOverlay: unknown;
97
+ };
98
+ /**
99
+ * Decide a pending proposal. `approver` MUST differ from the proposer (proposer≠approver);
100
+ * hosts pass their own identity so the audit trail is meaningful.
101
+ */
102
+ decideEdit(id: string, decision: "approve" | "reject", approver: string): Record<string, unknown>;
103
+ /** Whether the compiled PERSONA.md is stale (a governed self-edit was applied since the last compile). */
104
+ recompileStatus(): {
105
+ recompilePending: boolean;
106
+ reason: string | null;
107
+ since: string | null;
108
+ };
109
+ /** Reload the spec from disk (e.g. after an external recompile/decompile). */
110
+ reload(): void;
111
+ }
112
+ /** Scan untrusted text for prompt-injection before it reaches a persona. */
113
+ export declare function scanText(text: string): ReturnType<typeof scanForInjection>;
114
+ /** Scan an agent config file's content for injection/poisoning (kind inferred from the filename). */
115
+ export declare function scanConfig(content: string, filename?: string): ReturnType<typeof scanAgentConfig>;
116
+ /** Security-review a skill before use (supply-chain defense). */
117
+ export declare function skillReview(skillPath: string): ReturnType<typeof reviewSkill>;
118
+ /**
119
+ * Evaluate a shell command against a two-axis (approval × sandbox) policy. With a persona path, the
120
+ * persona's OWN declared `permissions` posture is used (v0.8); otherwise the explicit args apply.
121
+ */
122
+ export declare function evaluateCmd(command: string, sandbox: "read-only" | "workspace-write" | "danger-full-access", approval: "untrusted" | "on-failure" | "on-request" | "never", personaPath?: string): ReturnType<typeof evaluateCommand>;
123
+ export { resolveModel } from "@personaxis/core";
package/dist/index.js ADDED
@@ -0,0 +1,196 @@
1
+ /**
2
+ * @personaxis/sdk — the SINGLE engine façade (F3.5).
3
+ *
4
+ * Embed a living, governed persona in a Node/TS backend (Modo 2 self-host). The
5
+ * engine (@personaxis/core) does the governance — clamp + audit + injection scan
6
+ * + hash-chained memory + the agent loop; this SDK is the ONE ergonomic surface
7
+ * that drives it, with full parity across state, evolution, memory, agent, and
8
+ * safety operations. The MCP server, `serve`, and the REPL consume this façade
9
+ * rather than re-wrapping core (end of the wrapper triplication): host-specific
10
+ * concerns (MCP path-confinement, HTTP shaping, REPL rendering) wrap the SDK,
11
+ * they do not re-implement the engine.
12
+ *
13
+ * Example:
14
+ * import { Persona } from "@personaxis/sdk";
15
+ * const persona = new Persona("./.personaxis/personas/support/personaxis.md");
16
+ * const systemPrompt = persona.compiledIdentity(); // system-prompt slot #1
17
+ * await persona.observe("the customer is frustrated about billing", "user");
18
+ * const { values } = persona.state();
19
+ */
20
+ import { LivingLoop, HeuristicAppraiser, LlmAppraiser, resolveModel, PersonaAgent, EventBus, Tracer, readObservability, loadPersona, readState, writeState, withStateLock, ensureState, extractEnvelopes, resolveField, applyMutation, readMemory, readLiveMemory, tombstoneMemory, verifyMemoryChain, detectMemoryAnomalies, readMode, proposeSelfEdit, applySelfEdit, rejectSelfEdit, proposals, activeOverlay, readRecompilePending, reviewSkill, scanForInjection, scanAgentConfig, detectKind, evaluateCommand, policyFromFrontmatter, readAgentBudget, readVerification, DEFAULT_POLICY, } from "@personaxis/core";
21
+ import { readFileSync, existsSync } from "node:fs";
22
+ import { dirname, join, resolve } from "node:path";
23
+ /** A live persona bound to its `personaxis.md` spec (its state.json + memory live alongside it). */
24
+ export class Persona {
25
+ personaPath;
26
+ handle;
27
+ constructor(personaPath) {
28
+ this.personaPath = resolve(personaPath);
29
+ this.handle = loadPersona(this.personaPath);
30
+ ensureState(this.handle);
31
+ }
32
+ fm() {
33
+ return this.handle.frontmatter;
34
+ }
35
+ /** The compiled, LLM-facing identity document (system-prompt slot #1). Falls back to the spec
36
+ * body if PERSONA.md hasn't been compiled yet. */
37
+ compiledIdentity() {
38
+ const compiled = join(dirname(this.personaPath), "PERSONA.md");
39
+ return existsSync(compiled) ? readFileSync(compiled, "utf-8") : this.handle.body;
40
+ }
41
+ /** The raw qualitative spec body (the compiled document as stored on the spec). */
42
+ compiledBody() {
43
+ return this.handle.body;
44
+ }
45
+ /** Current runtime state: envelope values + the last few audited mutations. */
46
+ state() {
47
+ const st = readState(this.handle.statePath);
48
+ return { values: st.values, recentMutations: st.mutation_log.slice(-5) };
49
+ }
50
+ /** The mutable surface: envelope fields + the hard-enforced virtues that are immutable. */
51
+ envelopes() {
52
+ const { envelopes, hardEnforcedVirtues } = extractEnvelopes(this.handle.frontmatter);
53
+ return { mutableFields: envelopes, hardEnforcedVirtues };
54
+ }
55
+ /**
56
+ * Run ONE governed Living-Loop cycle on an observation, on the persona's resolved model
57
+ * (falls back to the deterministic heuristic appraiser if no model is configured). Every mutation
58
+ * is clamped + audited; a malicious observation is injection-scanned and cannot steer evolution.
59
+ */
60
+ async observe(observation, source = "user") {
61
+ const m = resolveModel({ personaPath: this.personaPath, frontmatter: this.fm() });
62
+ const events = [];
63
+ const loop = new LivingLoop(this.personaPath, {
64
+ appraiser: m ? new LlmAppraiser({ ...m, timeoutMs: 30_000 }) : new HeuristicAppraiser(),
65
+ });
66
+ loop.bus.on((e) => events.push(e));
67
+ try {
68
+ const report = await loop.tick({ observation, source });
69
+ return { report, events, recompilePending: readRecompilePending(this.personaPath).pending };
70
+ }
71
+ catch (e) {
72
+ return {
73
+ report: { mutationsApplied: 0, memoriesWritten: 0, abstained: true },
74
+ events: [...events, { type: "error", message: e.message }],
75
+ recompilePending: readRecompilePending(this.personaPath).pending,
76
+ };
77
+ }
78
+ }
79
+ /** Apply a single clamped, audited mutation to an envelope field (the spec's adjust_persona_state). */
80
+ adjust(field, delta, reason) {
81
+ const env = extractEnvelopes(this.handle.frontmatter);
82
+ const resolved = resolveField(field, env.envelopes);
83
+ // Locked read→apply→write: an embedding app may run ticks/adjusts concurrently (F1.4).
84
+ return withStateLock(this.handle.statePath, () => {
85
+ const st = readState(this.handle.statePath);
86
+ const result = applyMutation(st, env.envelopes, { field: resolved, delta, reason, actor: "actor-llm" });
87
+ writeState(this.handle.statePath, st);
88
+ return result;
89
+ });
90
+ }
91
+ /**
92
+ * Run the governed Agent Loop on a task. Non-interactive: any tool whose verdict is `ask` is
93
+ * denied unless the persona's permissions allow-list pre-authorizes it. Requires a configured model.
94
+ */
95
+ async agentRun(task, opts = {}) {
96
+ const fm = this.fm();
97
+ const llm = resolveModel({ personaPath: this.personaPath, frontmatter: fm });
98
+ if (!llm) {
99
+ return { error: "agent requires a configured model (config.json local.endpoint/model or PERSONAXIS_ENDPOINT + PERSONAXIS_MODEL)" };
100
+ }
101
+ const events = [];
102
+ const bus = new EventBus();
103
+ bus.on((e) => events.push(e));
104
+ const agent = new PersonaAgent({
105
+ llm,
106
+ policy: policyFromFrontmatter(fm, process.cwd()),
107
+ personaBody: this.handle.body,
108
+ onApproval: opts.onApproval ?? (async () => "deny"),
109
+ maxSteps: opts.maxSteps ?? 12,
110
+ budget: readAgentBudget(fm),
111
+ verification: readVerification(fm),
112
+ judge: llm,
113
+ personaPath: this.personaPath,
114
+ bus,
115
+ });
116
+ const obs = readObservability(fm);
117
+ const tracer = obs.trace !== "off" ? new Tracer(bus, obs) : null;
118
+ const result = await agent.run(task);
119
+ const trace = tracer ? tracer.write(this.personaPath).paths : [];
120
+ tracer?.stop();
121
+ return { result, events, trace };
122
+ }
123
+ /** Integrity view: mutation count, memory size, hash-chain validity, detected anomalies. */
124
+ audit() {
125
+ const st = readState(this.handle.statePath);
126
+ const mem = readMemory(this.personaPath);
127
+ const chain = verifyMemoryChain(this.personaPath);
128
+ return {
129
+ mutationCount: st.mutation_log.length,
130
+ memoryEntries: mem.length,
131
+ memoryChainIntact: chain.ok,
132
+ memoryChainBrokenAt: chain.brokenAt ?? null,
133
+ anomalies: detectMemoryAnomalies(mem),
134
+ };
135
+ }
136
+ /** Honor deletion_policy.user_request_supported: tombstone a memory entry (retrieval removal). */
137
+ forget(targetHash, reason) {
138
+ const entry = tombstoneMemory(this.personaPath, targetHash, reason);
139
+ return { tombstoned: targetHash, by: entry.hash, liveEntries: readLiveMemory(this.personaPath).length };
140
+ }
141
+ /** Propose a governed self-edit (queued or applied per improvement_policy.mode). */
142
+ proposeEdit(targetPath, toValue, rationale, sources = ["user"]) {
143
+ const mode = readMode(this.fm(), this.personaPath);
144
+ const result = proposeSelfEdit(this.personaPath, { targetPath, toValue, rationale, sources }, mode);
145
+ return { ...result, recompilePending: readRecompilePending(this.personaPath).pending };
146
+ }
147
+ /** Pending self-edit proposals + the active applied overlay. */
148
+ listProposals() {
149
+ return { proposals: proposals(this.personaPath), activeOverlay: activeOverlay(this.personaPath) };
150
+ }
151
+ /**
152
+ * Decide a pending proposal. `approver` MUST differ from the proposer (proposer≠approver);
153
+ * hosts pass their own identity so the audit trail is meaningful.
154
+ */
155
+ decideEdit(id, decision, approver) {
156
+ if (decision === "approve") {
157
+ const applied = applySelfEdit(this.personaPath, id, approver);
158
+ return { ...applied, recompilePending: readRecompilePending(this.personaPath).pending };
159
+ }
160
+ rejectSelfEdit(this.personaPath, id, approver);
161
+ return { id, status: "rejected" };
162
+ }
163
+ /** Whether the compiled PERSONA.md is stale (a governed self-edit was applied since the last compile). */
164
+ recompileStatus() {
165
+ const s = readRecompilePending(this.personaPath);
166
+ return { recompilePending: s.pending, reason: s.reason ?? null, since: s.ts ?? null };
167
+ }
168
+ /** Reload the spec from disk (e.g. after an external recompile/decompile). */
169
+ reload() {
170
+ this.handle = loadPersona(this.personaPath);
171
+ }
172
+ }
173
+ // ── Persona-independent safety helpers (the same façade; no persona instance needed) ──────────────
174
+ /** Scan untrusted text for prompt-injection before it reaches a persona. */
175
+ export function scanText(text) {
176
+ return scanForInjection(text);
177
+ }
178
+ /** Scan an agent config file's content for injection/poisoning (kind inferred from the filename). */
179
+ export function scanConfig(content, filename) {
180
+ return scanAgentConfig(content, filename ? detectKind(filename) : undefined);
181
+ }
182
+ /** Security-review a skill before use (supply-chain defense). */
183
+ export function skillReview(skillPath) {
184
+ return reviewSkill(skillPath);
185
+ }
186
+ /**
187
+ * Evaluate a shell command against a two-axis (approval × sandbox) policy. With a persona path, the
188
+ * persona's OWN declared `permissions` posture is used (v0.8); otherwise the explicit args apply.
189
+ */
190
+ export function evaluateCmd(command, sandbox, approval, personaPath) {
191
+ const policy = personaPath
192
+ ? policyFromFrontmatter(loadPersona(resolve(personaPath)).frontmatter, process.cwd())
193
+ : { ...DEFAULT_POLICY, sandbox, approval, workspaceRoot: process.cwd() };
194
+ return evaluateCommand(command, policy);
195
+ }
196
+ export { resolveModel } from "@personaxis/core";
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@personaxis/sdk",
3
+ "version": "0.11.0",
4
+ "description": "Ergonomic SDK for embedding a living, governed persona in a Node/TypeScript backend (Modo 2 self-host). A thin wrapper over @personaxis/core.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "dependencies": {
13
+ "@personaxis/core": "0.11.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "^22.10.0",
17
+ "typescript": "^5.8.3",
18
+ "vitest": "^3.0.0"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/personaxis/cli.git",
26
+ "directory": "packages/sdk"
27
+ },
28
+ "scripts": {
29
+ "build": "tsc",
30
+ "lint": "tsc --noEmit",
31
+ "test": "vitest run"
32
+ }
33
+ }