@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
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Governance gate — the spec's safety boundary over autonomous evolution.
|
|
3
|
+
*
|
|
4
|
+
* The model's appraisal signal is a *proposal*. Before anything is applied, the
|
|
5
|
+
* gate decides which proposed mutations are admissible. Clamping handles range;
|
|
6
|
+
* the gate handles policy:
|
|
7
|
+
* - unknown / non-mutable fields are rejected outright;
|
|
8
|
+
* - fields tied to hard-enforced virtues are never mutable at runtime;
|
|
9
|
+
* - per-step drift is bounded (anti-runaway, anti-self-reinforcement);
|
|
10
|
+
* - in `locked` mode, only human-actor mutations pass.
|
|
11
|
+
*
|
|
12
|
+
* Nothing here is a black box: every decision yields an auditable verdict.
|
|
13
|
+
*/
|
|
14
|
+
import { type EnvelopeLookup } from "./envelopes.js";
|
|
15
|
+
import type { ProposedMutation } from "./appraisal.js";
|
|
16
|
+
export type ImprovementMode = "locked" | "suggesting" | "autonomous";
|
|
17
|
+
export interface GovernanceConfig {
|
|
18
|
+
mode: ImprovementMode;
|
|
19
|
+
/** Max absolute delta admitted per step (drift guard). Default 0.15. */
|
|
20
|
+
maxStepDelta: number;
|
|
21
|
+
/**
|
|
22
|
+
* True when the mutation is a deliberate human action (`state mutate
|
|
23
|
+
* --actor human-operator`), not autonomous evolution. Human-directed mutations
|
|
24
|
+
* bypass the mode lock and the drift bound — that is this gate's documented
|
|
25
|
+
* intent ("in locked mode, only human-directed mutations pass"). Envelope
|
|
26
|
+
* membership and hard-virtue immutability still apply to EVERY actor.
|
|
27
|
+
*/
|
|
28
|
+
humanDirected?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare const DEFAULT_GOVERNANCE: GovernanceConfig;
|
|
31
|
+
export interface Verdict {
|
|
32
|
+
field: string;
|
|
33
|
+
admitted: boolean;
|
|
34
|
+
/** Delta after drift-bounding (still subject to envelope clamp downstream). */
|
|
35
|
+
delta: number;
|
|
36
|
+
reason: string;
|
|
37
|
+
}
|
|
38
|
+
export interface GovernanceDecision {
|
|
39
|
+
verdicts: Verdict[];
|
|
40
|
+
admitted: ProposedMutation[];
|
|
41
|
+
rejected: Verdict[];
|
|
42
|
+
}
|
|
43
|
+
export declare function governMutations(proposals: ProposedMutation[], env: EnvelopeLookup, cfg?: GovernanceConfig): GovernanceDecision;
|
|
44
|
+
/**
|
|
45
|
+
* Govern a QUALITATIVE self-edit (prose) by improvement_policy.mode. This is a SEPARATE
|
|
46
|
+
* layer from `judge` (which governs numeric envelope mutations): qualitative edits to the
|
|
47
|
+
* spec's character material are higher-stakes, so the mode means something different here —
|
|
48
|
+
* locked → block (no proposal at all);
|
|
49
|
+
* suggesting → queue the proposal for human review (/review), never auto-apply;
|
|
50
|
+
* autonomous → auto-apply, still gated by consensus verifiers + the protected-path list.
|
|
51
|
+
* (Envelope mutations remain reversible/clamped, so suggesting==autonomous for them — see
|
|
52
|
+
* `judge`. Decoupling avoids weakening the numeric drift guard while making prose evolution
|
|
53
|
+
* actually respect the posture.)
|
|
54
|
+
*/
|
|
55
|
+
export declare function governQualitative(mode: ImprovementMode): "block" | "queue" | "auto";
|
|
56
|
+
/**
|
|
57
|
+
* Read the improvement mode. v1.0 precedence (SPEC.md §7.2): the INLINE
|
|
58
|
+
* `improvement_policy.mode` in personaxis.md is authoritative; when a
|
|
59
|
+
* `personaPath` is given, the sibling policy.yaml may only RESTRICT it — the
|
|
60
|
+
* more conservative of the two wins (min-wins). Inline absent → policy.yaml
|
|
61
|
+
* governs; both absent → locked.
|
|
62
|
+
*/
|
|
63
|
+
export declare function readMode(frontmatter: Record<string, unknown>, personaPath?: string): ImprovementMode;
|
|
64
|
+
/** v0.8: read governance.max_step_delta from frontmatter; falls back to the default. */
|
|
65
|
+
export declare function readMaxStepDelta(frontmatter: Record<string, unknown>): number;
|
|
66
|
+
export type StopCondition = "goal_met" | "tool_denied" | "execution_error" | "low_confidence" | "no_progress";
|
|
67
|
+
export interface AgentBudgetConfig {
|
|
68
|
+
maxSteps: number;
|
|
69
|
+
maxTokens?: number;
|
|
70
|
+
maxCostUsd?: number;
|
|
71
|
+
maxWallSeconds?: number;
|
|
72
|
+
stopConditions: StopCondition[];
|
|
73
|
+
onExhaust: "stop" | "summarize_and_stop";
|
|
74
|
+
}
|
|
75
|
+
export declare const DEFAULT_AGENT_BUDGET: AgentBudgetConfig;
|
|
76
|
+
/** Per-1M-token USD prices (input+output averaged) for cost estimation. Best-effort. */
|
|
77
|
+
export declare const MODEL_PRICES_PER_MTOK: Record<string, number>;
|
|
78
|
+
export declare function estimateCostUsd(model: string, tokens: number): number;
|
|
79
|
+
export interface AgentBudgetSpent {
|
|
80
|
+
steps: number;
|
|
81
|
+
tokens: number;
|
|
82
|
+
costUsd: number;
|
|
83
|
+
wallSeconds: number;
|
|
84
|
+
deniedCount: number;
|
|
85
|
+
errorCount: number;
|
|
86
|
+
/** Net progress signal since last step (0 = stalled). */
|
|
87
|
+
progress: number;
|
|
88
|
+
/** The model's last reported confidence in [0,1], if any. */
|
|
89
|
+
confidence?: number;
|
|
90
|
+
/** True if the model signalled completion (finish). */
|
|
91
|
+
goalMet?: boolean;
|
|
92
|
+
}
|
|
93
|
+
export interface BudgetVerdict {
|
|
94
|
+
field: string;
|
|
95
|
+
exceeded: boolean;
|
|
96
|
+
value: number;
|
|
97
|
+
limit: number | null;
|
|
98
|
+
reason: string;
|
|
99
|
+
}
|
|
100
|
+
export interface BudgetCheck {
|
|
101
|
+
verdicts: BudgetVerdict[];
|
|
102
|
+
shouldStop: boolean;
|
|
103
|
+
stopReason: string | null;
|
|
104
|
+
}
|
|
105
|
+
/** Decide whether the agent loop must halt, given what it has spent so far. */
|
|
106
|
+
export declare function checkAgentBudget(spent: AgentBudgetSpent, budget?: AgentBudgetConfig): BudgetCheck;
|
|
107
|
+
/** Read the spec `agent_budget` block from frontmatter; falls back to the default. */
|
|
108
|
+
export declare function readAgentBudget(frontmatter: Record<string, unknown>): AgentBudgetConfig;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Governance gate — the spec's safety boundary over autonomous evolution.
|
|
3
|
+
*
|
|
4
|
+
* The model's appraisal signal is a *proposal*. Before anything is applied, the
|
|
5
|
+
* gate decides which proposed mutations are admissible. Clamping handles range;
|
|
6
|
+
* the gate handles policy:
|
|
7
|
+
* - unknown / non-mutable fields are rejected outright;
|
|
8
|
+
* - fields tied to hard-enforced virtues are never mutable at runtime;
|
|
9
|
+
* - per-step drift is bounded (anti-runaway, anti-self-reinforcement);
|
|
10
|
+
* - in `locked` mode, only human-actor mutations pass.
|
|
11
|
+
*
|
|
12
|
+
* Nothing here is a black box: every decision yields an auditable verdict.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { load as loadYaml } from "js-yaml";
|
|
17
|
+
import { resolveField } from "./envelopes.js";
|
|
18
|
+
export const DEFAULT_GOVERNANCE = {
|
|
19
|
+
mode: "locked",
|
|
20
|
+
maxStepDelta: 0.15,
|
|
21
|
+
};
|
|
22
|
+
export function governMutations(proposals, env, cfg = DEFAULT_GOVERNANCE) {
|
|
23
|
+
const verdicts = [];
|
|
24
|
+
for (const p of proposals) {
|
|
25
|
+
const v = judge(p, env, cfg);
|
|
26
|
+
verdicts.push(v);
|
|
27
|
+
}
|
|
28
|
+
const admitted = verdicts
|
|
29
|
+
.filter((v) => v.admitted)
|
|
30
|
+
.map((v) => ({ field: v.field, delta: v.delta, reason: v.reason }));
|
|
31
|
+
const rejected = verdicts.filter((v) => !v.admitted);
|
|
32
|
+
return { verdicts, admitted, rejected };
|
|
33
|
+
}
|
|
34
|
+
function judge(p, env, cfg) {
|
|
35
|
+
// Accept either key form (short ≤0.10 / full-dot-path 1.0) — resolve onto the
|
|
36
|
+
// persona's canonical envelope key before judging.
|
|
37
|
+
const field = resolveField(p.field, env.envelopes);
|
|
38
|
+
// Autonomous evolution is only ever allowed for state-envelope fields.
|
|
39
|
+
if (!(field in env.envelopes)) {
|
|
40
|
+
return { field, admitted: false, delta: 0, reason: `not a mutable envelope field` };
|
|
41
|
+
}
|
|
42
|
+
// A trait backing a hard-enforced virtue is immutable at runtime — for every
|
|
43
|
+
// actor, human included (change the spec, not the state, to move it).
|
|
44
|
+
// v1.0: protectedFields carries the exact keys (incl. refs-derived ones);
|
|
45
|
+
// legacy lookups without it fall back to the name-match rule.
|
|
46
|
+
if (env.protectedFields) {
|
|
47
|
+
if (env.protectedFields.includes(field)) {
|
|
48
|
+
return { field, admitted: false, delta: 0, reason: `field backs a hard-enforced virtue` };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const traitName = field.startsWith("traits.")
|
|
53
|
+
? field.slice("traits.".length)
|
|
54
|
+
: field.startsWith("personality.traits.")
|
|
55
|
+
? field.slice("personality.traits.".length)
|
|
56
|
+
: null;
|
|
57
|
+
if (traitName && env.hardEnforcedVirtues.includes(traitName)) {
|
|
58
|
+
return { field, admitted: false, delta: 0, reason: `field backs a hard-enforced virtue` };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Deliberate human mutations are not autonomous evolution: no mode lock, no
|
|
62
|
+
// drift bound (the envelope clamp downstream still applies).
|
|
63
|
+
if (cfg.humanDirected) {
|
|
64
|
+
return { field, admitted: true, delta: p.delta, reason: p.reason };
|
|
65
|
+
}
|
|
66
|
+
// In locked mode, the actor LLM cannot self-evolve; only human-directed
|
|
67
|
+
// mutations (applied via the CLI, not the loop) change state.
|
|
68
|
+
if (cfg.mode === "locked") {
|
|
69
|
+
return { field, admitted: false, delta: 0, reason: `improvement_policy=locked` };
|
|
70
|
+
}
|
|
71
|
+
// Drift guard: bound the per-step magnitude.
|
|
72
|
+
const bounded = Math.max(-cfg.maxStepDelta, Math.min(cfg.maxStepDelta, p.delta));
|
|
73
|
+
const note = bounded !== p.delta ? ` (drift-bounded from ${p.delta})` : "";
|
|
74
|
+
return { field, admitted: true, delta: bounded, reason: p.reason + note };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Govern a QUALITATIVE self-edit (prose) by improvement_policy.mode. This is a SEPARATE
|
|
78
|
+
* layer from `judge` (which governs numeric envelope mutations): qualitative edits to the
|
|
79
|
+
* spec's character material are higher-stakes, so the mode means something different here —
|
|
80
|
+
* locked → block (no proposal at all);
|
|
81
|
+
* suggesting → queue the proposal for human review (/review), never auto-apply;
|
|
82
|
+
* autonomous → auto-apply, still gated by consensus verifiers + the protected-path list.
|
|
83
|
+
* (Envelope mutations remain reversible/clamped, so suggesting==autonomous for them — see
|
|
84
|
+
* `judge`. Decoupling avoids weakening the numeric drift guard while making prose evolution
|
|
85
|
+
* actually respect the posture.)
|
|
86
|
+
*/
|
|
87
|
+
export function governQualitative(mode) {
|
|
88
|
+
if (mode === "locked")
|
|
89
|
+
return "block";
|
|
90
|
+
if (mode === "autonomous")
|
|
91
|
+
return "auto";
|
|
92
|
+
return "queue"; // suggesting
|
|
93
|
+
}
|
|
94
|
+
const MODE_RANK = { locked: 0, suggesting: 1, autonomous: 2 };
|
|
95
|
+
function normalizeMode(m) {
|
|
96
|
+
if (m === "locked" || m === "suggesting" || m === "autonomous")
|
|
97
|
+
return m;
|
|
98
|
+
if (m === "auto")
|
|
99
|
+
return "autonomous"; // legacy policy.yaml enum value
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
/** Mode declared by the sibling policy.yaml, if any (undefined when absent/unreadable). */
|
|
103
|
+
function readPolicyMode(personaPath) {
|
|
104
|
+
try {
|
|
105
|
+
const p = join(dirname(personaPath), "policy.yaml");
|
|
106
|
+
if (!existsSync(p))
|
|
107
|
+
return undefined;
|
|
108
|
+
const doc = loadYaml(readFileSync(p, "utf-8"));
|
|
109
|
+
return normalizeMode(doc?.improvement_policy?.mode);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return undefined; // a malformed policy.yaml must not crash mode resolution
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Read the improvement mode. v1.0 precedence (SPEC.md §7.2): the INLINE
|
|
117
|
+
* `improvement_policy.mode` in personaxis.md is authoritative; when a
|
|
118
|
+
* `personaPath` is given, the sibling policy.yaml may only RESTRICT it — the
|
|
119
|
+
* more conservative of the two wins (min-wins). Inline absent → policy.yaml
|
|
120
|
+
* governs; both absent → locked.
|
|
121
|
+
*/
|
|
122
|
+
export function readMode(frontmatter, personaPath) {
|
|
123
|
+
const ip = frontmatter.improvement_policy;
|
|
124
|
+
const inline = normalizeMode(ip?.mode);
|
|
125
|
+
if (!personaPath)
|
|
126
|
+
return inline ?? "locked";
|
|
127
|
+
const policy = readPolicyMode(personaPath);
|
|
128
|
+
if (inline === undefined)
|
|
129
|
+
return policy ?? "locked";
|
|
130
|
+
if (policy === undefined)
|
|
131
|
+
return inline;
|
|
132
|
+
return MODE_RANK[policy] < MODE_RANK[inline] ? policy : inline;
|
|
133
|
+
}
|
|
134
|
+
/** v0.8: read governance.max_step_delta from frontmatter; falls back to the default. */
|
|
135
|
+
export function readMaxStepDelta(frontmatter) {
|
|
136
|
+
const g = frontmatter.governance;
|
|
137
|
+
const v = g?.max_step_delta;
|
|
138
|
+
return typeof v === "number" && v > 0 && v <= 1 ? v : DEFAULT_GOVERNANCE.maxStepDelta;
|
|
139
|
+
}
|
|
140
|
+
export const DEFAULT_AGENT_BUDGET = {
|
|
141
|
+
maxSteps: 20,
|
|
142
|
+
stopConditions: [],
|
|
143
|
+
onExhaust: "stop",
|
|
144
|
+
};
|
|
145
|
+
/** Per-1M-token USD prices (input+output averaged) for cost estimation. Best-effort. */
|
|
146
|
+
export const MODEL_PRICES_PER_MTOK = {
|
|
147
|
+
"command-a": 5, "command-r-plus": 5, "command-r": 0.6,
|
|
148
|
+
"gpt-5": 7, "gpt-4o": 5, "claude-opus": 18, "claude-sonnet": 9, "claude-haiku": 2,
|
|
149
|
+
"gemini-2.5-pro": 5, "deepseek": 0.4, "qwen": 0.2, "llama": 0.2, "default": 3,
|
|
150
|
+
};
|
|
151
|
+
export function estimateCostUsd(model, tokens) {
|
|
152
|
+
const key = Object.keys(MODEL_PRICES_PER_MTOK).find((k) => k !== "default" && model.toLowerCase().includes(k));
|
|
153
|
+
const price = MODEL_PRICES_PER_MTOK[key ?? "default"];
|
|
154
|
+
return (tokens / 1_000_000) * price;
|
|
155
|
+
}
|
|
156
|
+
/** Decide whether the agent loop must halt, given what it has spent so far. */
|
|
157
|
+
export function checkAgentBudget(spent, budget = DEFAULT_AGENT_BUDGET) {
|
|
158
|
+
const verdicts = [];
|
|
159
|
+
const cap = (field, value, limit) => {
|
|
160
|
+
const exceeded = typeof limit === "number" && value >= limit;
|
|
161
|
+
verdicts.push({ field, exceeded, value, limit: limit ?? null, reason: exceeded ? `${field} cap reached (${value} ≥ ${limit})` : `${field} ok` });
|
|
162
|
+
return exceeded;
|
|
163
|
+
};
|
|
164
|
+
let stopReason = null;
|
|
165
|
+
if (cap("steps", spent.steps, budget.maxSteps))
|
|
166
|
+
stopReason ??= "max_steps";
|
|
167
|
+
if (cap("tokens", spent.tokens, budget.maxTokens))
|
|
168
|
+
stopReason ??= "max_tokens";
|
|
169
|
+
if (cap("cost_usd", Number(spent.costUsd.toFixed(4)), budget.maxCostUsd))
|
|
170
|
+
stopReason ??= "max_cost_usd";
|
|
171
|
+
if (cap("wall_seconds", Math.floor(spent.wallSeconds), budget.maxWallSeconds))
|
|
172
|
+
stopReason ??= "max_wall_seconds";
|
|
173
|
+
// Declarative stop-conditions (checked only if listed).
|
|
174
|
+
const sc = budget.stopConditions;
|
|
175
|
+
if (sc.includes("goal_met") && spent.goalMet)
|
|
176
|
+
stopReason ??= "goal_met";
|
|
177
|
+
if (sc.includes("tool_denied") && spent.deniedCount > 0)
|
|
178
|
+
stopReason ??= "tool_denied";
|
|
179
|
+
if (sc.includes("execution_error") && spent.errorCount > 0)
|
|
180
|
+
stopReason ??= "execution_error";
|
|
181
|
+
if (sc.includes("low_confidence") && typeof spent.confidence === "number" && spent.confidence < 0.2)
|
|
182
|
+
stopReason ??= "low_confidence";
|
|
183
|
+
if (sc.includes("no_progress") && spent.steps > 1 && spent.progress === 0)
|
|
184
|
+
stopReason ??= "no_progress";
|
|
185
|
+
return { verdicts, shouldStop: stopReason !== null, stopReason };
|
|
186
|
+
}
|
|
187
|
+
/** Read the spec `agent_budget` block from frontmatter; falls back to the default. */
|
|
188
|
+
export function readAgentBudget(frontmatter) {
|
|
189
|
+
const b = frontmatter.agent_budget;
|
|
190
|
+
if (!b)
|
|
191
|
+
return { ...DEFAULT_AGENT_BUDGET };
|
|
192
|
+
const num = (v) => (typeof v === "number" && v > 0 ? v : undefined);
|
|
193
|
+
const conds = Array.isArray(b.stop_conditions)
|
|
194
|
+
? (b.stop_conditions.filter((c) => ["goal_met", "tool_denied", "execution_error", "low_confidence", "no_progress"].includes(c)))
|
|
195
|
+
: [];
|
|
196
|
+
return {
|
|
197
|
+
maxSteps: num(b.max_steps) ?? DEFAULT_AGENT_BUDGET.maxSteps,
|
|
198
|
+
maxTokens: num(b.max_tokens),
|
|
199
|
+
maxCostUsd: typeof b.max_cost_usd === "number" && b.max_cost_usd >= 0 ? b.max_cost_usd : undefined,
|
|
200
|
+
maxWallSeconds: num(b.max_wall_seconds),
|
|
201
|
+
stopConditions: conds,
|
|
202
|
+
onExhaust: b.on_exhaust === "summarize_and_stop" ? "summarize_and_stop" : "stop",
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic appraiser — deterministic, offline Appraiser.
|
|
3
|
+
*
|
|
4
|
+
* Lets the Living Loop run with zero model dependency (tests, demos, air-gapped
|
|
5
|
+
* use, MCP hosts that don't pass a model). It proposes only small bounded nudges;
|
|
6
|
+
* the governance gate + envelope clamp decide what actually applies. The
|
|
7
|
+
* LLM-backed appraiser (constrained decoding, plan/04-small-models) implements
|
|
8
|
+
* the same `Appraiser` interface, so the loop is unchanged.
|
|
9
|
+
*/
|
|
10
|
+
import type { AppraiseInput, AppraisalSignal, Appraiser } from "./appraisal.js";
|
|
11
|
+
export declare class HeuristicAppraiser implements Appraiser {
|
|
12
|
+
appraise(input: AppraiseInput): Promise<AppraisalSignal>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic appraiser — deterministic, offline Appraiser.
|
|
3
|
+
*
|
|
4
|
+
* Lets the Living Loop run with zero model dependency (tests, demos, air-gapped
|
|
5
|
+
* use, MCP hosts that don't pass a model). It proposes only small bounded nudges;
|
|
6
|
+
* the governance gate + envelope clamp decide what actually applies. The
|
|
7
|
+
* LLM-backed appraiser (constrained decoding, plan/04-small-models) implements
|
|
8
|
+
* the same `Appraiser` interface, so the loop is unchanged.
|
|
9
|
+
*/
|
|
10
|
+
const POSITIVE = /\b(good|great|love|nice|thanks|excellent|win|happy|success|works?|fixed|clean|elegant)\b/gi;
|
|
11
|
+
const NEGATIVE = /\b(bad|hate|angry|fail|failed|broken|bug|error|slow|ugly|wrong|stuck|frustrat\w*)\b/gi;
|
|
12
|
+
function count(re, s) {
|
|
13
|
+
return (s.match(re) ?? []).length;
|
|
14
|
+
}
|
|
15
|
+
export class HeuristicAppraiser {
|
|
16
|
+
async appraise(input) {
|
|
17
|
+
const pos = count(POSITIVE, input.observation);
|
|
18
|
+
const neg = count(NEGATIVE, input.observation);
|
|
19
|
+
const net = pos - neg;
|
|
20
|
+
const magnitude = Math.min(0.12, Math.abs(net) * 0.04);
|
|
21
|
+
const dir = Math.sign(net);
|
|
22
|
+
const mutations = [];
|
|
23
|
+
if (dir !== 0) {
|
|
24
|
+
if (input.mutableFields.includes("mood.tone")) {
|
|
25
|
+
mutations.push({ field: "mood.tone", delta: dir * magnitude, reason: "tone of observation" });
|
|
26
|
+
}
|
|
27
|
+
if (input.mutableFields.includes("affect.valence")) {
|
|
28
|
+
mutations.push({
|
|
29
|
+
field: "affect.valence",
|
|
30
|
+
delta: dir * magnitude,
|
|
31
|
+
reason: "affective valence of observation",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const memories = [
|
|
36
|
+
{ content: input.observation.slice(0, 480), source: input.source, tags: ["episode", input.source] },
|
|
37
|
+
];
|
|
38
|
+
const confidence = Math.max(0.2, Math.min(0.9, 0.4 + Math.abs(net) * 0.15));
|
|
39
|
+
return {
|
|
40
|
+
appraisal: net === 0
|
|
41
|
+
? "Neutral observation; holding baseline."
|
|
42
|
+
: `Observation reads ${net > 0 ? "positive" : "negative"} (net ${net}); nudging affect within envelope.`,
|
|
43
|
+
mutations,
|
|
44
|
+
memories,
|
|
45
|
+
confidence,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FR.4 — inbound hooks v2: the shell-out contract.
|
|
3
|
+
*
|
|
4
|
+
* Users extend the engine WITHOUT forking it: a hook is any executable that
|
|
5
|
+
* receives the event payload as JSON on stdin and answers with its exit code —
|
|
6
|
+
* exit 0 → ok (stdout MAY carry a JSON decision, e.g. {"decision":"block"})
|
|
7
|
+
* exit 2 → BLOCK the action (blocking events only)
|
|
8
|
+
* other → warning; never blocks
|
|
9
|
+
* (Claude Code's contract; OpenClaw/Hermes use the same folder+manifest idea.)
|
|
10
|
+
*
|
|
11
|
+
* Six initial events. Only PreToolUse and UserPromptSubmit are BLOCKING-capable
|
|
12
|
+
* (awaited, bounded by a timeout that fails OPEN to "continue with warning" —
|
|
13
|
+
* a hook must never hang the pipeline). The rest are fire-and-forget.
|
|
14
|
+
*
|
|
15
|
+
* Config: `.personaxis/hooks.json` beside the persona (project scope), same
|
|
16
|
+
* structural shape as Claude Code's settings hooks block.
|
|
17
|
+
*/
|
|
18
|
+
export type HookEvent = "PreToolUse" | "PostToolUse" | "UserPromptSubmit" | "Stop" | "SessionStart" | "SessionEnd";
|
|
19
|
+
export declare const BLOCKING_EVENTS: readonly HookEvent[];
|
|
20
|
+
export interface HookSpec {
|
|
21
|
+
type: "command";
|
|
22
|
+
command: string;
|
|
23
|
+
/** Per-hook timeout (ms) for blocking events. Default 5000. */
|
|
24
|
+
timeout?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface HookGroup {
|
|
27
|
+
/** Regex matched against the payload's `matcher_target` (e.g. tool name). */
|
|
28
|
+
matcher?: string;
|
|
29
|
+
hooks: HookSpec[];
|
|
30
|
+
}
|
|
31
|
+
export interface HooksConfig {
|
|
32
|
+
hooks?: Partial<Record<HookEvent, HookGroup[]>>;
|
|
33
|
+
}
|
|
34
|
+
export interface HookOutcome {
|
|
35
|
+
command: string;
|
|
36
|
+
/** "ok" | "block" | "warn" — block only possible on blocking events. */
|
|
37
|
+
result: "ok" | "block" | "warn";
|
|
38
|
+
exitCode: number | null;
|
|
39
|
+
/** Parsed JSON stdout decision, when the hook emitted one. */
|
|
40
|
+
decision?: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
/** Load `.personaxis/hooks.json` for a persona ({} when absent/corrupt). */
|
|
43
|
+
export declare function readHooksConfig(personaPath: string): HooksConfig;
|
|
44
|
+
/**
|
|
45
|
+
* Run every hook configured for `event`. Blocking events await all matched
|
|
46
|
+
* hooks and report `blocked` when ANY says block; fire-and-forget events
|
|
47
|
+
* return immediately (`blocked: false` always).
|
|
48
|
+
*/
|
|
49
|
+
export declare function runHooks(event: HookEvent, payload: Record<string, unknown>, config: HooksConfig, matcherTarget?: string): Promise<{
|
|
50
|
+
blocked: boolean;
|
|
51
|
+
outcomes: HookOutcome[];
|
|
52
|
+
}>;
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FR.4 — inbound hooks v2: the shell-out contract.
|
|
3
|
+
*
|
|
4
|
+
* Users extend the engine WITHOUT forking it: a hook is any executable that
|
|
5
|
+
* receives the event payload as JSON on stdin and answers with its exit code —
|
|
6
|
+
* exit 0 → ok (stdout MAY carry a JSON decision, e.g. {"decision":"block"})
|
|
7
|
+
* exit 2 → BLOCK the action (blocking events only)
|
|
8
|
+
* other → warning; never blocks
|
|
9
|
+
* (Claude Code's contract; OpenClaw/Hermes use the same folder+manifest idea.)
|
|
10
|
+
*
|
|
11
|
+
* Six initial events. Only PreToolUse and UserPromptSubmit are BLOCKING-capable
|
|
12
|
+
* (awaited, bounded by a timeout that fails OPEN to "continue with warning" —
|
|
13
|
+
* a hook must never hang the pipeline). The rest are fire-and-forget.
|
|
14
|
+
*
|
|
15
|
+
* Config: `.personaxis/hooks.json` beside the persona (project scope), same
|
|
16
|
+
* structural shape as Claude Code's settings hooks block.
|
|
17
|
+
*/
|
|
18
|
+
import { spawn } from "node:child_process";
|
|
19
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
21
|
+
export const BLOCKING_EVENTS = ["PreToolUse", "UserPromptSubmit"];
|
|
22
|
+
/** Load `.personaxis/hooks.json` for a persona ({} when absent/corrupt). */
|
|
23
|
+
export function readHooksConfig(personaPath) {
|
|
24
|
+
const p = join(dirname(personaPath), "hooks.json");
|
|
25
|
+
if (!existsSync(p))
|
|
26
|
+
return {};
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(readFileSync(p, "utf-8"));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return {}; // a corrupt hooks file must never break the engine
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function matching(config, event, matcherTarget) {
|
|
35
|
+
const groups = config.hooks?.[event] ?? [];
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const g of groups) {
|
|
38
|
+
if (g.matcher) {
|
|
39
|
+
try {
|
|
40
|
+
if (!new RegExp(g.matcher).test(matcherTarget))
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
continue; // invalid matcher regex: skip the group, never crash
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
out.push(...(g.hooks ?? []));
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
function runOne(spec, payload, awaitResult) {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
const child = spawn(spec.command, {
|
|
54
|
+
shell: true,
|
|
55
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
56
|
+
windowsHide: true,
|
|
57
|
+
});
|
|
58
|
+
const outcome = (result, exitCode, stdout = "") => {
|
|
59
|
+
let decision;
|
|
60
|
+
if (stdout.trim()) {
|
|
61
|
+
try {
|
|
62
|
+
decision = JSON.parse(stdout);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* non-JSON stdout is fine */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// A JSON {"decision":"block"} on exit 0 also blocks (blocking events).
|
|
69
|
+
if (awaitResult && decision?.decision === "block" && result === "ok")
|
|
70
|
+
result = "block";
|
|
71
|
+
resolve({ command: spec.command, result, exitCode, decision });
|
|
72
|
+
};
|
|
73
|
+
if (!awaitResult) {
|
|
74
|
+
// Fire-and-forget: detach outcome from the pipeline entirely.
|
|
75
|
+
child.on("error", () => { });
|
|
76
|
+
try {
|
|
77
|
+
child.stdin.write(JSON.stringify(payload));
|
|
78
|
+
child.stdin.end();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
/* a dead stdin must not throw into the engine */
|
|
82
|
+
}
|
|
83
|
+
resolve({ command: spec.command, result: "ok", exitCode: null });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
let stdout = "";
|
|
87
|
+
child.stdout.on("data", (c) => (stdout += String(c)));
|
|
88
|
+
const timer = setTimeout(() => {
|
|
89
|
+
child.kill();
|
|
90
|
+
outcome("warn", null, stdout); // timeout fails OPEN: warn, never hang
|
|
91
|
+
}, spec.timeout ?? 5_000);
|
|
92
|
+
child.on("error", () => {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
outcome("warn", null);
|
|
95
|
+
});
|
|
96
|
+
child.on("close", (code) => {
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
outcome(code === 0 ? "ok" : code === 2 ? "block" : "warn", code, stdout);
|
|
99
|
+
});
|
|
100
|
+
try {
|
|
101
|
+
child.stdin.write(JSON.stringify(payload));
|
|
102
|
+
child.stdin.end();
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
/* the close handler still fires */
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Run every hook configured for `event`. Blocking events await all matched
|
|
111
|
+
* hooks and report `blocked` when ANY says block; fire-and-forget events
|
|
112
|
+
* return immediately (`blocked: false` always).
|
|
113
|
+
*/
|
|
114
|
+
export async function runHooks(event, payload, config, matcherTarget = "") {
|
|
115
|
+
const specs = matching(config, event, matcherTarget);
|
|
116
|
+
if (specs.length === 0)
|
|
117
|
+
return { blocked: false, outcomes: [] };
|
|
118
|
+
const awaitResult = BLOCKING_EVENTS.includes(event);
|
|
119
|
+
const body = { hook_event: event, ...payload };
|
|
120
|
+
const outcomes = await Promise.all(specs.map((s) => runOne(s, body, awaitResult)));
|
|
121
|
+
return { blocked: awaitResult && outcomes.some((o) => o.result === "block"), outcomes };
|
|
122
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @personaxis/core — the governed Living-Persona engine.
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic, spec-faithful primitives shared by every entry point
|
|
5
|
+
* (CLI REPL, MCP server, TUI, HTTP). The engine never prints; it emits events.
|
|
6
|
+
*/
|
|
7
|
+
export * from "./persona.js";
|
|
8
|
+
export * from "./lock.js";
|
|
9
|
+
export * from "./envelopes.js";
|
|
10
|
+
export * from "./state-engine.js";
|
|
11
|
+
export * from "./state-rebuild.js";
|
|
12
|
+
export * from "./spec-edit.js";
|
|
13
|
+
export * from "./appraisal.js";
|
|
14
|
+
export * from "./evolution-view.js";
|
|
15
|
+
export * from "./heuristic-appraiser.js";
|
|
16
|
+
export * from "./llm-appraiser.js";
|
|
17
|
+
export * from "./responder.js";
|
|
18
|
+
export * from "./governance.js";
|
|
19
|
+
export * from "./verification.js";
|
|
20
|
+
export * from "./self-evolution.js";
|
|
21
|
+
export * from "./recompile-marker.js";
|
|
22
|
+
export * from "./registry.js";
|
|
23
|
+
export * from "./model-config.js";
|
|
24
|
+
export * from "./blackboard.js";
|
|
25
|
+
export * from "./live-sync.js";
|
|
26
|
+
export * from "./sync.js";
|
|
27
|
+
export * from "./skill-review.js";
|
|
28
|
+
export * from "./skill-lifecycle.js";
|
|
29
|
+
export * from "./memory.js";
|
|
30
|
+
export * from "./memory-kinds.js";
|
|
31
|
+
export * from "./sessions.js";
|
|
32
|
+
export * from "./session-writer.js";
|
|
33
|
+
export * from "./provenance.js";
|
|
34
|
+
export * from "./injection.js";
|
|
35
|
+
export * from "./config-scan.js";
|
|
36
|
+
export * from "./config-layers.js";
|
|
37
|
+
export * from "./compile/index.js";
|
|
38
|
+
export * from "./ports/index.js";
|
|
39
|
+
export * from "./sandbox.js";
|
|
40
|
+
export * from "./hooks.js";
|
|
41
|
+
export * from "./approval.js";
|
|
42
|
+
export * from "./tool-repair.js";
|
|
43
|
+
export * from "./sigil.js";
|
|
44
|
+
export * from "./persona-theme.js";
|
|
45
|
+
export * from "./events.js";
|
|
46
|
+
export * from "./context.js";
|
|
47
|
+
export * from "./trace.js";
|
|
48
|
+
export * from "./loop.js";
|
|
49
|
+
export * from "./tools/exec.js";
|
|
50
|
+
export * from "./tools/registry.js";
|
|
51
|
+
export * from "./tool-calling.js";
|
|
52
|
+
export * from "./agent.js";
|
|
53
|
+
export { CORE_VERSION } from "./generated/version.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @personaxis/core — the governed Living-Persona engine.
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic, spec-faithful primitives shared by every entry point
|
|
5
|
+
* (CLI REPL, MCP server, TUI, HTTP). The engine never prints; it emits events.
|
|
6
|
+
*/
|
|
7
|
+
export * from "./persona.js";
|
|
8
|
+
export * from "./lock.js";
|
|
9
|
+
export * from "./envelopes.js";
|
|
10
|
+
export * from "./state-engine.js";
|
|
11
|
+
export * from "./state-rebuild.js";
|
|
12
|
+
export * from "./spec-edit.js";
|
|
13
|
+
export * from "./appraisal.js";
|
|
14
|
+
export * from "./evolution-view.js";
|
|
15
|
+
export * from "./heuristic-appraiser.js";
|
|
16
|
+
export * from "./llm-appraiser.js";
|
|
17
|
+
export * from "./responder.js";
|
|
18
|
+
export * from "./governance.js";
|
|
19
|
+
export * from "./verification.js";
|
|
20
|
+
export * from "./self-evolution.js";
|
|
21
|
+
export * from "./recompile-marker.js";
|
|
22
|
+
export * from "./registry.js";
|
|
23
|
+
export * from "./model-config.js";
|
|
24
|
+
export * from "./blackboard.js";
|
|
25
|
+
export * from "./live-sync.js";
|
|
26
|
+
export * from "./sync.js";
|
|
27
|
+
export * from "./skill-review.js";
|
|
28
|
+
export * from "./skill-lifecycle.js";
|
|
29
|
+
export * from "./memory.js";
|
|
30
|
+
export * from "./memory-kinds.js";
|
|
31
|
+
export * from "./sessions.js";
|
|
32
|
+
export * from "./session-writer.js";
|
|
33
|
+
export * from "./provenance.js";
|
|
34
|
+
export * from "./injection.js";
|
|
35
|
+
export * from "./config-scan.js";
|
|
36
|
+
export * from "./config-layers.js";
|
|
37
|
+
export * from "./compile/index.js";
|
|
38
|
+
export * from "./ports/index.js";
|
|
39
|
+
export * from "./sandbox.js";
|
|
40
|
+
export * from "./hooks.js";
|
|
41
|
+
export * from "./approval.js";
|
|
42
|
+
export * from "./tool-repair.js";
|
|
43
|
+
export * from "./sigil.js";
|
|
44
|
+
export * from "./persona-theme.js";
|
|
45
|
+
export * from "./events.js";
|
|
46
|
+
export * from "./context.js";
|
|
47
|
+
export * from "./trace.js";
|
|
48
|
+
export * from "./loop.js";
|
|
49
|
+
export * from "./tools/exec.js";
|
|
50
|
+
export * from "./tools/registry.js";
|
|
51
|
+
export * from "./tool-calling.js";
|
|
52
|
+
export * from "./agent.js";
|
|
53
|
+
export { CORE_VERSION } from "./generated/version.js";
|