@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,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool registry (G1) — the governed agent's action vocabulary.
|
|
3
|
+
*
|
|
4
|
+
* Each tool declares: a JSON-Schema for its args (used both for native
|
|
5
|
+
* function-calling and the constrained-JSON fallback), a `gate` that returns a
|
|
6
|
+
* sandbox verdict (allow | ask | deny) WITHOUT side effects, and an `execute`
|
|
7
|
+
* that performs the action and returns a text observation to feed back to the
|
|
8
|
+
* model. The agent loop owns the policy and only calls `execute` after the gate
|
|
9
|
+
* (and, for `ask`, the human) approves.
|
|
10
|
+
*/
|
|
11
|
+
import { evaluateCommand, evaluateFileWrite, pathEscapesWorkspace, } from "../sandbox.js";
|
|
12
|
+
import { executeCommand, executeFileEdit, executeFileWrite, listDirSafe, readFileSafe, } from "./exec.js";
|
|
13
|
+
/**
|
|
14
|
+
* FR.7: validate args against the tool's declared JSON Schema (required keys +
|
|
15
|
+
* primitive types — the registry's schemas are flat by design). Returns the
|
|
16
|
+
* problems found; empty = valid. Runs BEFORE the gate, so a malformed call is
|
|
17
|
+
* an input error, never a policy question.
|
|
18
|
+
*/
|
|
19
|
+
export function validateToolArgs(spec, args) {
|
|
20
|
+
const problems = [];
|
|
21
|
+
const schema = spec.parameters;
|
|
22
|
+
for (const k of schema.required ?? []) {
|
|
23
|
+
if (!(k in args))
|
|
24
|
+
problems.push(`missing required arg '${k}'`);
|
|
25
|
+
}
|
|
26
|
+
for (const [k, v] of Object.entries(args)) {
|
|
27
|
+
const prop = schema.properties?.[k];
|
|
28
|
+
if (!prop) {
|
|
29
|
+
if (schema.additionalProperties === false)
|
|
30
|
+
problems.push(`unknown arg '${k}'`);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (prop.type && typeof v !== prop.type) {
|
|
34
|
+
problems.push(`arg '${k}' must be ${prop.type}, got ${typeof v}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return problems;
|
|
38
|
+
}
|
|
39
|
+
const READ_CLASS = { writesFiles: false, network: false, destructive: false, escapesWorkspace: false };
|
|
40
|
+
/** Reads are allowed within the workspace; escaping it needs approval. */
|
|
41
|
+
function readGate(path, policy) {
|
|
42
|
+
if (policy.sandbox === "danger-full-access") {
|
|
43
|
+
return { decision: "allow", reason: "full access", class: READ_CLASS };
|
|
44
|
+
}
|
|
45
|
+
const escapes = pathEscapesWorkspace(path, policy.workspaceRoot);
|
|
46
|
+
return escapes
|
|
47
|
+
? { decision: "ask", reason: "read escapes the workspace", class: { ...READ_CLASS, escapesWorkspace: true } }
|
|
48
|
+
: { decision: "allow", reason: "in-workspace read", class: READ_CLASS };
|
|
49
|
+
}
|
|
50
|
+
const str = (a, k) => (typeof a[k] === "string" ? a[k] : "");
|
|
51
|
+
export const TOOLS = [
|
|
52
|
+
{
|
|
53
|
+
name: "run_command",
|
|
54
|
+
isReadOnly: false,
|
|
55
|
+
isConcurrencySafe: false,
|
|
56
|
+
description: "Run a shell command in the workspace and return its stdout/stderr. Use the command appropriate to the host OS (provided in context).",
|
|
57
|
+
parameters: {
|
|
58
|
+
type: "object",
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
required: ["command"],
|
|
61
|
+
properties: { command: { type: "string", description: "The exact shell command to run." } },
|
|
62
|
+
},
|
|
63
|
+
gate: (args, policy) => evaluateCommand(str(args, "command"), policy),
|
|
64
|
+
execute: async (args, policy) => {
|
|
65
|
+
const r = await executeCommand(str(args, "command"), policy);
|
|
66
|
+
const parts = [`exit_code: ${r.code}${r.timedOut ? " (timed out)" : ""}`];
|
|
67
|
+
if (r.stdout.trim())
|
|
68
|
+
parts.push(`stdout:\n${r.stdout.trim()}`);
|
|
69
|
+
if (r.stderr.trim())
|
|
70
|
+
parts.push(`stderr:\n${r.stderr.trim()}`);
|
|
71
|
+
if (r.truncated)
|
|
72
|
+
parts.push("[output truncated]");
|
|
73
|
+
return parts.join("\n");
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "read_file",
|
|
78
|
+
isReadOnly: true,
|
|
79
|
+
isConcurrencySafe: true,
|
|
80
|
+
description: "Read a UTF-8 text file relative to the workspace root.",
|
|
81
|
+
parameters: {
|
|
82
|
+
type: "object",
|
|
83
|
+
additionalProperties: false,
|
|
84
|
+
required: ["path"],
|
|
85
|
+
properties: { path: { type: "string" } },
|
|
86
|
+
},
|
|
87
|
+
gate: (args, policy) => readGate(str(args, "path"), policy),
|
|
88
|
+
execute: async (args, policy) => {
|
|
89
|
+
const r = readFileSafe(str(args, "path"), policy);
|
|
90
|
+
return r.ok ? `${r.path}:\n${r.content ?? ""}` : `error: ${r.error}`;
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "list_dir",
|
|
95
|
+
isReadOnly: true,
|
|
96
|
+
isConcurrencySafe: true,
|
|
97
|
+
description: "List the entries of a directory relative to the workspace root.",
|
|
98
|
+
parameters: {
|
|
99
|
+
type: "object",
|
|
100
|
+
additionalProperties: false,
|
|
101
|
+
required: ["path"],
|
|
102
|
+
properties: { path: { type: "string" } },
|
|
103
|
+
},
|
|
104
|
+
gate: (args, policy) => readGate(str(args, "path"), policy),
|
|
105
|
+
execute: async (args, policy) => {
|
|
106
|
+
const r = listDirSafe(str(args, "path"), policy);
|
|
107
|
+
return r.ok ? `${r.path}:\n${r.content ?? "(empty)"}` : `error: ${r.error}`;
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "write_file",
|
|
112
|
+
isReadOnly: false,
|
|
113
|
+
isConcurrencySafe: false,
|
|
114
|
+
description: "Create or overwrite a text file (relative to the workspace root) with the given content.",
|
|
115
|
+
parameters: {
|
|
116
|
+
type: "object",
|
|
117
|
+
additionalProperties: false,
|
|
118
|
+
required: ["path", "content"],
|
|
119
|
+
properties: { path: { type: "string" }, content: { type: "string" } },
|
|
120
|
+
},
|
|
121
|
+
gate: (args, policy) => evaluateFileWrite(str(args, "path"), policy),
|
|
122
|
+
execute: async (args, policy) => {
|
|
123
|
+
const r = executeFileWrite(str(args, "path"), str(args, "content"), policy);
|
|
124
|
+
return r.ok ? `wrote ${r.bytes} bytes to ${r.path}` : `error: ${r.error}`;
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "edit_file",
|
|
129
|
+
isReadOnly: false,
|
|
130
|
+
isConcurrencySafe: false,
|
|
131
|
+
description: "Replace the first occurrence of `find` with `replace` in an existing file.",
|
|
132
|
+
parameters: {
|
|
133
|
+
type: "object",
|
|
134
|
+
additionalProperties: false,
|
|
135
|
+
required: ["path", "find", "replace"],
|
|
136
|
+
properties: { path: { type: "string" }, find: { type: "string" }, replace: { type: "string" } },
|
|
137
|
+
},
|
|
138
|
+
gate: (args, policy) => evaluateFileWrite(str(args, "path"), policy),
|
|
139
|
+
execute: async (args, policy) => {
|
|
140
|
+
const r = executeFileEdit(str(args, "path"), str(args, "find"), str(args, "replace"), policy);
|
|
141
|
+
return r.ok ? `edited ${r.path}` : `error: ${r.error}`;
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "finish",
|
|
146
|
+
isReadOnly: true,
|
|
147
|
+
isConcurrencySafe: true,
|
|
148
|
+
description: "Call this when the task is complete. Provide a short summary of what was done.",
|
|
149
|
+
parameters: {
|
|
150
|
+
type: "object",
|
|
151
|
+
additionalProperties: false,
|
|
152
|
+
required: ["summary"],
|
|
153
|
+
properties: { summary: { type: "string" } },
|
|
154
|
+
},
|
|
155
|
+
gate: () => ({ decision: "allow", reason: "finish", class: READ_CLASS }),
|
|
156
|
+
execute: async (args) => str(args, "summary"),
|
|
157
|
+
},
|
|
158
|
+
];
|
|
159
|
+
export const FINISH_TOOL = "finish";
|
|
160
|
+
export function toolByName(name) {
|
|
161
|
+
return TOOLS.find((t) => t.name === name);
|
|
162
|
+
}
|
package/dist/trace.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability / causal trace (v0.9 — spec `observability`).
|
|
3
|
+
*
|
|
4
|
+
* The engine already produces the raw material of a great trace: every step of
|
|
5
|
+
* the governed loops is an auditable event on the EventBus, and every state change
|
|
6
|
+
* is in the hash-chained mutation_log. 2026 production-agent practice (Braintrust /
|
|
7
|
+
* Arize / Microsoft Foundry) is unanimous that observability must be *causal
|
|
8
|
+
* tracing*, not response logging, and exportable to standard tooling. So a Tracer
|
|
9
|
+
* subscribes to a bus and serializes spans two ways:
|
|
10
|
+
* - native JSONL (line-delimited, diff-able, dependency-free)
|
|
11
|
+
* - OTLP-JSON (OpenTelemetry-compatible; plugs into Braintrust/Arize/Foundry)
|
|
12
|
+
*
|
|
13
|
+
* Secrets are redacted per `observability.redact` before anything is written.
|
|
14
|
+
*/
|
|
15
|
+
import { EventBus } from "./events.js";
|
|
16
|
+
export type TraceFormat = "jsonl" | "otlp" | "both" | "off";
|
|
17
|
+
export interface ObservabilityConfig {
|
|
18
|
+
trace: TraceFormat;
|
|
19
|
+
traceDir: string;
|
|
20
|
+
redact: string[];
|
|
21
|
+
sampleRate: number;
|
|
22
|
+
}
|
|
23
|
+
export declare const DEFAULT_OBSERVABILITY: ObservabilityConfig;
|
|
24
|
+
export declare function readObservability(frontmatter: Record<string, unknown>): ObservabilityConfig;
|
|
25
|
+
export interface TraceSpan {
|
|
26
|
+
/** ISO timestamp. */
|
|
27
|
+
ts: string;
|
|
28
|
+
/** Milliseconds since trace start. */
|
|
29
|
+
t_ms: number;
|
|
30
|
+
/** Monotonic index. */
|
|
31
|
+
seq: number;
|
|
32
|
+
/** Event type (span name). */
|
|
33
|
+
type: string;
|
|
34
|
+
/** Redacted event payload (everything except `type`). */
|
|
35
|
+
data: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
/** Subscribes to a bus and records redacted spans; exports JSONL + OTLP. */
|
|
38
|
+
export declare class Tracer {
|
|
39
|
+
private readonly bus;
|
|
40
|
+
private readonly cfg;
|
|
41
|
+
private readonly spans;
|
|
42
|
+
private readonly start;
|
|
43
|
+
private seq;
|
|
44
|
+
private readonly redactors;
|
|
45
|
+
private readonly off;
|
|
46
|
+
constructor(bus: EventBus, cfg?: ObservabilityConfig);
|
|
47
|
+
private record;
|
|
48
|
+
/** Detach from the bus. */
|
|
49
|
+
stop(): void;
|
|
50
|
+
getSpans(): TraceSpan[];
|
|
51
|
+
toJSONL(): string;
|
|
52
|
+
/** OpenTelemetry OTLP/JSON (trace) shape — point spans (start == end). */
|
|
53
|
+
toOTLP(): unknown;
|
|
54
|
+
/**
|
|
55
|
+
* Write the configured trace formats. `target` is a persona path or a directory;
|
|
56
|
+
* files land under `<target dir>/<traceDir>/trace-<ts>.{jsonl,otlp.json}`.
|
|
57
|
+
*/
|
|
58
|
+
write(target: string, formatOverride?: TraceFormat): {
|
|
59
|
+
paths: string[];
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Parse a native JSONL trace file's text into spans (for the viewer). */
|
|
63
|
+
export declare function parseTraceJSONL(text: string): TraceSpan[];
|
package/dist/trace.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability / causal trace (v0.9 — spec `observability`).
|
|
3
|
+
*
|
|
4
|
+
* The engine already produces the raw material of a great trace: every step of
|
|
5
|
+
* the governed loops is an auditable event on the EventBus, and every state change
|
|
6
|
+
* is in the hash-chained mutation_log. 2026 production-agent practice (Braintrust /
|
|
7
|
+
* Arize / Microsoft Foundry) is unanimous that observability must be *causal
|
|
8
|
+
* tracing*, not response logging, and exportable to standard tooling. So a Tracer
|
|
9
|
+
* subscribes to a bus and serializes spans two ways:
|
|
10
|
+
* - native JSONL (line-delimited, diff-able, dependency-free)
|
|
11
|
+
* - OTLP-JSON (OpenTelemetry-compatible; plugs into Braintrust/Arize/Foundry)
|
|
12
|
+
*
|
|
13
|
+
* Secrets are redacted per `observability.redact` before anything is written.
|
|
14
|
+
*/
|
|
15
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { dirname, join, resolve } from "node:path";
|
|
17
|
+
export const DEFAULT_OBSERVABILITY = {
|
|
18
|
+
trace: "off",
|
|
19
|
+
traceDir: "./traces",
|
|
20
|
+
redact: [],
|
|
21
|
+
sampleRate: 1,
|
|
22
|
+
};
|
|
23
|
+
export function readObservability(frontmatter) {
|
|
24
|
+
const o = frontmatter.observability;
|
|
25
|
+
if (!o)
|
|
26
|
+
return { ...DEFAULT_OBSERVABILITY };
|
|
27
|
+
const trace = o.trace === "jsonl" || o.trace === "otlp" || o.trace === "both" ? o.trace : "off";
|
|
28
|
+
return {
|
|
29
|
+
trace,
|
|
30
|
+
traceDir: typeof o.trace_dir === "string" ? o.trace_dir : DEFAULT_OBSERVABILITY.traceDir,
|
|
31
|
+
redact: Array.isArray(o.redact) ? o.redact.filter((r) => typeof r === "string") : [],
|
|
32
|
+
sampleRate: typeof o.sample_rate === "number" && o.sample_rate >= 0 && o.sample_rate <= 1 ? o.sample_rate : 1,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function compileRedactors(patterns) {
|
|
36
|
+
return patterns
|
|
37
|
+
.map((raw) => {
|
|
38
|
+
// JS has no inline-flag groups like (?i); strip a leading one (we already
|
|
39
|
+
// compile case-insensitive) so specs written with (?i)/(?im) still work.
|
|
40
|
+
const p = raw.replace(/^\(\?[a-z]+\)/i, "");
|
|
41
|
+
try {
|
|
42
|
+
return new RegExp(p, "gi");
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
.filter((r) => r !== null);
|
|
49
|
+
}
|
|
50
|
+
function redactValue(value, redactors) {
|
|
51
|
+
if (typeof value === "string") {
|
|
52
|
+
let s = value;
|
|
53
|
+
for (const r of redactors)
|
|
54
|
+
s = s.replace(r, "[redacted]");
|
|
55
|
+
return s;
|
|
56
|
+
}
|
|
57
|
+
if (Array.isArray(value))
|
|
58
|
+
return value.map((v) => redactValue(v, redactors));
|
|
59
|
+
if (value && typeof value === "object") {
|
|
60
|
+
const out = {};
|
|
61
|
+
for (const [k, v] of Object.entries(value))
|
|
62
|
+
out[k] = redactValue(v, redactors);
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
/** Subscribes to a bus and records redacted spans; exports JSONL + OTLP. */
|
|
68
|
+
export class Tracer {
|
|
69
|
+
bus;
|
|
70
|
+
cfg;
|
|
71
|
+
spans = [];
|
|
72
|
+
start = Date.now();
|
|
73
|
+
seq = 0;
|
|
74
|
+
redactors;
|
|
75
|
+
off;
|
|
76
|
+
constructor(bus, cfg = DEFAULT_OBSERVABILITY) {
|
|
77
|
+
this.bus = bus;
|
|
78
|
+
this.cfg = cfg;
|
|
79
|
+
this.redactors = compileRedactors(cfg.redact);
|
|
80
|
+
this.off = bus.on((e) => this.record(e));
|
|
81
|
+
}
|
|
82
|
+
record(e) {
|
|
83
|
+
if (this.cfg.sampleRate < 1 && Math.random() > this.cfg.sampleRate)
|
|
84
|
+
return;
|
|
85
|
+
const { type, ...rest } = e;
|
|
86
|
+
this.spans.push({
|
|
87
|
+
ts: new Date().toISOString(),
|
|
88
|
+
t_ms: Date.now() - this.start,
|
|
89
|
+
seq: this.seq++,
|
|
90
|
+
type,
|
|
91
|
+
data: redactValue(rest, this.redactors),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/** Detach from the bus. */
|
|
95
|
+
stop() {
|
|
96
|
+
this.off();
|
|
97
|
+
}
|
|
98
|
+
getSpans() {
|
|
99
|
+
return this.spans;
|
|
100
|
+
}
|
|
101
|
+
toJSONL() {
|
|
102
|
+
return this.spans.map((s) => JSON.stringify(s)).join("\n") + (this.spans.length ? "\n" : "");
|
|
103
|
+
}
|
|
104
|
+
/** OpenTelemetry OTLP/JSON (trace) shape — point spans (start == end). */
|
|
105
|
+
toOTLP() {
|
|
106
|
+
const toNano = (ms) => String((this.start + ms) * 1_000_000);
|
|
107
|
+
return {
|
|
108
|
+
resourceSpans: [
|
|
109
|
+
{
|
|
110
|
+
resource: { attributes: [{ key: "service.name", value: { stringValue: "personaxis" } }] },
|
|
111
|
+
scopeSpans: [
|
|
112
|
+
{
|
|
113
|
+
scope: { name: "personaxis.loop" },
|
|
114
|
+
spans: this.spans.map((s) => ({
|
|
115
|
+
name: s.type,
|
|
116
|
+
startTimeUnixNano: toNano(s.t_ms),
|
|
117
|
+
endTimeUnixNano: toNano(s.t_ms),
|
|
118
|
+
attributes: Object.entries(s.data).map(([k, v]) => ({
|
|
119
|
+
key: k,
|
|
120
|
+
value: { stringValue: typeof v === "string" ? v : JSON.stringify(v) },
|
|
121
|
+
})),
|
|
122
|
+
})),
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Write the configured trace formats. `target` is a persona path or a directory;
|
|
131
|
+
* files land under `<target dir>/<traceDir>/trace-<ts>.{jsonl,otlp.json}`.
|
|
132
|
+
*/
|
|
133
|
+
write(target, formatOverride) {
|
|
134
|
+
const fmt = formatOverride ?? this.cfg.trace;
|
|
135
|
+
if (fmt === "off")
|
|
136
|
+
return { paths: [] };
|
|
137
|
+
const baseDir = resolve(dirname(target), this.cfg.traceDir);
|
|
138
|
+
mkdirSync(baseDir, { recursive: true });
|
|
139
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
140
|
+
const paths = [];
|
|
141
|
+
if (fmt === "jsonl" || fmt === "both") {
|
|
142
|
+
const p = join(baseDir, `trace-${stamp}.jsonl`);
|
|
143
|
+
writeFileSync(p, this.toJSONL(), "utf-8");
|
|
144
|
+
paths.push(p);
|
|
145
|
+
}
|
|
146
|
+
if (fmt === "otlp" || fmt === "both") {
|
|
147
|
+
const p = join(baseDir, `trace-${stamp}.otlp.json`);
|
|
148
|
+
writeFileSync(p, JSON.stringify(this.toOTLP(), null, 2), "utf-8");
|
|
149
|
+
paths.push(p);
|
|
150
|
+
}
|
|
151
|
+
for (const p of paths)
|
|
152
|
+
this.bus.emit({ type: "trace-exported", format: p.endsWith(".jsonl") ? "jsonl" : "otlp", path: p, spanCount: this.spans.length });
|
|
153
|
+
return { paths };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** Parse a native JSONL trace file's text into spans (for the viewer). */
|
|
157
|
+
export function parseTraceJSONL(text) {
|
|
158
|
+
return text
|
|
159
|
+
.split("\n")
|
|
160
|
+
.filter((l) => l.trim().length > 0)
|
|
161
|
+
.map((l) => JSON.parse(l));
|
|
162
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Objective verification (v0.9 — spec `verification`): the maker≠checker split.
|
|
3
|
+
*
|
|
4
|
+
* The loop-engineering research is blunt: the #1 reason loops fail is "a second
|
|
5
|
+
* agent asked to review without an objective gate — two optimists agreeing", and
|
|
6
|
+
* "the model that wrote the code is too nice grading its own homework". So the
|
|
7
|
+
* agent that did the work NEVER decides done; an INDEPENDENT verifier does, and
|
|
8
|
+
* where possible it is deterministic (a command's exit code, a predicate) rather
|
|
9
|
+
* than an opinion. Four gate types cover coding AND non-coding domains:
|
|
10
|
+
* - command : run a shell check (test/build/lint); pass = exit 0
|
|
11
|
+
* - predicate: assert over the agent's output (regex | contains | jsonpath)
|
|
12
|
+
* - llm_judge: a separate model judges done/criteria → {pass, reason}
|
|
13
|
+
* - rubric : a separate model scores weighted dimensions → score ≥ threshold
|
|
14
|
+
*
|
|
15
|
+
* Reuses the consensus/quorum vocabulary from self-evolution (VerifierResult).
|
|
16
|
+
*/
|
|
17
|
+
import { type Policy } from "./sandbox.js";
|
|
18
|
+
import type { ConsensusResult } from "./self-evolution.js";
|
|
19
|
+
export type VerificationMode = "off" | "advisory" | "blocking";
|
|
20
|
+
export type OnFail = "retry" | "skip" | "stop";
|
|
21
|
+
export interface VerificationGate {
|
|
22
|
+
type: "command" | "predicate" | "llm_judge" | "rubric";
|
|
23
|
+
name?: string;
|
|
24
|
+
run?: string;
|
|
25
|
+
timeout_ms?: number;
|
|
26
|
+
kind?: "regex" | "jsonpath" | "contains";
|
|
27
|
+
expr?: string;
|
|
28
|
+
criteria?: string;
|
|
29
|
+
model?: string;
|
|
30
|
+
dimensions?: Array<{
|
|
31
|
+
name: string;
|
|
32
|
+
weight: number;
|
|
33
|
+
criteria?: string;
|
|
34
|
+
}>;
|
|
35
|
+
threshold?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface VerificationConfig {
|
|
38
|
+
mode: VerificationMode;
|
|
39
|
+
quorum: "all" | "majority" | number;
|
|
40
|
+
onFail: OnFail;
|
|
41
|
+
maxRetries: number;
|
|
42
|
+
gates: VerificationGate[];
|
|
43
|
+
}
|
|
44
|
+
export declare const DEFAULT_VERIFICATION: VerificationConfig;
|
|
45
|
+
export interface VerificationContext {
|
|
46
|
+
task: string;
|
|
47
|
+
/** The agent's final output / summary. */
|
|
48
|
+
output: string;
|
|
49
|
+
/** Full transcript text (for judges that need the trail). */
|
|
50
|
+
transcript?: string;
|
|
51
|
+
}
|
|
52
|
+
/** Optional LLM access for llm_judge / rubric gates (constrained JSON). */
|
|
53
|
+
export interface JudgeConfig {
|
|
54
|
+
endpoint: string;
|
|
55
|
+
model: string;
|
|
56
|
+
apiKey?: string;
|
|
57
|
+
fetchImpl?: typeof fetch;
|
|
58
|
+
}
|
|
59
|
+
export interface VerifyOptions {
|
|
60
|
+
policy?: Policy;
|
|
61
|
+
judge?: JudgeConfig;
|
|
62
|
+
}
|
|
63
|
+
export declare function readVerification(frontmatter: Record<string, unknown>): VerificationConfig;
|
|
64
|
+
/** Run all gates and apply the quorum. Returns a ConsensusResult (reused shape). */
|
|
65
|
+
export declare function runVerification(config: VerificationConfig, ctx: VerificationContext, opts?: VerifyOptions): Promise<ConsensusResult>;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Objective verification (v0.9 — spec `verification`): the maker≠checker split.
|
|
3
|
+
*
|
|
4
|
+
* The loop-engineering research is blunt: the #1 reason loops fail is "a second
|
|
5
|
+
* agent asked to review without an objective gate — two optimists agreeing", and
|
|
6
|
+
* "the model that wrote the code is too nice grading its own homework". So the
|
|
7
|
+
* agent that did the work NEVER decides done; an INDEPENDENT verifier does, and
|
|
8
|
+
* where possible it is deterministic (a command's exit code, a predicate) rather
|
|
9
|
+
* than an opinion. Four gate types cover coding AND non-coding domains:
|
|
10
|
+
* - command : run a shell check (test/build/lint); pass = exit 0
|
|
11
|
+
* - predicate: assert over the agent's output (regex | contains | jsonpath)
|
|
12
|
+
* - llm_judge: a separate model judges done/criteria → {pass, reason}
|
|
13
|
+
* - rubric : a separate model scores weighted dimensions → score ≥ threshold
|
|
14
|
+
*
|
|
15
|
+
* Reuses the consensus/quorum vocabulary from self-evolution (VerifierResult).
|
|
16
|
+
*/
|
|
17
|
+
import { executeCommand } from "./tools/exec.js";
|
|
18
|
+
import { DEFAULT_POLICY } from "./sandbox.js";
|
|
19
|
+
export const DEFAULT_VERIFICATION = {
|
|
20
|
+
mode: "off",
|
|
21
|
+
quorum: "all",
|
|
22
|
+
onFail: "retry",
|
|
23
|
+
maxRetries: 1,
|
|
24
|
+
gates: [],
|
|
25
|
+
};
|
|
26
|
+
export function readVerification(frontmatter) {
|
|
27
|
+
const v = frontmatter.verification;
|
|
28
|
+
if (!v)
|
|
29
|
+
return { ...DEFAULT_VERIFICATION };
|
|
30
|
+
const mode = v.mode === "advisory" || v.mode === "blocking" ? v.mode : "off";
|
|
31
|
+
const quorum = v.quorum === "majority" || (typeof v.quorum === "number" && v.quorum >= 1) ? v.quorum : "all";
|
|
32
|
+
const onFail = v.on_fail === "skip" || v.on_fail === "stop" ? v.on_fail : "retry";
|
|
33
|
+
const maxRetries = typeof v.max_retries === "number" && v.max_retries >= 0 ? v.max_retries : 1;
|
|
34
|
+
const gates = Array.isArray(v.gates) ? v.gates : [];
|
|
35
|
+
return { mode, quorum, onFail, maxRetries, gates };
|
|
36
|
+
}
|
|
37
|
+
function resolveQuorum(n, quorum) {
|
|
38
|
+
if (quorum === "all")
|
|
39
|
+
return n;
|
|
40
|
+
if (quorum === "majority")
|
|
41
|
+
return Math.floor(n / 2) + 1;
|
|
42
|
+
return Math.min(Math.max(1, quorum), n);
|
|
43
|
+
}
|
|
44
|
+
// ── Individual gate verifiers ────────────────────────────────────────────────
|
|
45
|
+
async function verifyCommand(gate, policy) {
|
|
46
|
+
const name = gate.name ?? `command:${(gate.run ?? "").slice(0, 24)}`;
|
|
47
|
+
if (!gate.run)
|
|
48
|
+
return { verifier: name, pass: false, reason: "no command specified" };
|
|
49
|
+
const r = await executeCommand(gate.run, policy, { timeoutMs: gate.timeout_ms });
|
|
50
|
+
return {
|
|
51
|
+
verifier: name,
|
|
52
|
+
pass: r.ok,
|
|
53
|
+
reason: r.ok ? `exit 0` : `exit ${r.code}${r.timedOut ? " (timeout)" : ""}: ${(r.stderr || r.stdout).split("\n")[0].slice(0, 120)}`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function verifyPredicate(gate, output) {
|
|
57
|
+
const name = gate.name ?? `predicate:${gate.kind}`;
|
|
58
|
+
const expr = gate.expr ?? "";
|
|
59
|
+
let pass = false;
|
|
60
|
+
let reason = "";
|
|
61
|
+
try {
|
|
62
|
+
if (gate.kind === "regex") {
|
|
63
|
+
pass = new RegExp(expr).test(output);
|
|
64
|
+
reason = pass ? "regex matched" : "regex did not match";
|
|
65
|
+
}
|
|
66
|
+
else if (gate.kind === "contains") {
|
|
67
|
+
pass = output.includes(expr);
|
|
68
|
+
reason = pass ? "substring present" : "substring absent";
|
|
69
|
+
}
|
|
70
|
+
else if (gate.kind === "jsonpath") {
|
|
71
|
+
const val = jsonPath(output, expr);
|
|
72
|
+
pass = val !== undefined && val !== null && val !== false;
|
|
73
|
+
reason = pass ? `path ${expr} present/truthy` : `path ${expr} missing/falsy`;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
reason = `unknown predicate kind '${gate.kind}'`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
reason = `predicate error: ${e.message}`;
|
|
81
|
+
}
|
|
82
|
+
return { verifier: name, pass, reason };
|
|
83
|
+
}
|
|
84
|
+
/** Minimal `$.a.b[0]` resolver over JSON.parse(output). Best-effort, dependency-free. */
|
|
85
|
+
function jsonPath(output, path) {
|
|
86
|
+
let obj;
|
|
87
|
+
try {
|
|
88
|
+
obj = JSON.parse(output);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
const m = output.match(/\{[\s\S]*\}/);
|
|
92
|
+
if (!m)
|
|
93
|
+
return undefined;
|
|
94
|
+
try {
|
|
95
|
+
obj = JSON.parse(m[0]);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const parts = path.replace(/^\$\.?/, "").split(/[.\[\]]+/).filter(Boolean);
|
|
102
|
+
let cur = obj;
|
|
103
|
+
for (const p of parts) {
|
|
104
|
+
if (cur == null || typeof cur !== "object")
|
|
105
|
+
return undefined;
|
|
106
|
+
cur = cur[p];
|
|
107
|
+
}
|
|
108
|
+
return cur;
|
|
109
|
+
}
|
|
110
|
+
async function judgeJson(judge, system, user) {
|
|
111
|
+
const fetchImpl = judge.fetchImpl ?? fetch;
|
|
112
|
+
const res = await fetchImpl(`${judge.endpoint.replace(/\/$/, "")}/chat/completions`, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: { "content-type": "application/json", ...(judge.apiKey ? { authorization: `Bearer ${judge.apiKey}` } : {}) },
|
|
115
|
+
body: JSON.stringify({
|
|
116
|
+
model: judge.model,
|
|
117
|
+
messages: [{ role: "system", content: system }, { role: "user", content: user }],
|
|
118
|
+
temperature: 0,
|
|
119
|
+
response_format: { type: "json_object" },
|
|
120
|
+
}),
|
|
121
|
+
});
|
|
122
|
+
if (!res.ok)
|
|
123
|
+
throw new Error(`judge HTTP ${res.status}`);
|
|
124
|
+
const json = (await res.json());
|
|
125
|
+
const content = json.choices?.[0]?.message?.content ?? "{}";
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(content);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
const m = content.match(/\{[\s\S]*\}/);
|
|
131
|
+
return m ? JSON.parse(m[0]) : null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function verifyLlmJudge(gate, ctx, judge) {
|
|
135
|
+
const name = gate.name ?? "llm_judge";
|
|
136
|
+
if (!judge)
|
|
137
|
+
return { verifier: name, pass: true, reason: "skipped (no judge model configured)" };
|
|
138
|
+
try {
|
|
139
|
+
const out = await judgeJson({ ...judge, model: gate.model ?? judge.model }, "You are an INDEPENDENT verifier. You did not do the work. Decide strictly whether the result meets the criteria. Output JSON {\"pass\": boolean, \"reason\": string}.", `# Criteria\n${gate.criteria ?? "Task fully and correctly completed."}\n\n# Task\n${ctx.task}\n\n# Result\n${ctx.output.slice(0, 4000)}`);
|
|
140
|
+
const pass = out?.pass === true;
|
|
141
|
+
return { verifier: name, pass, reason: typeof out?.reason === "string" ? out.reason.slice(0, 160) : pass ? "met" : "not met" };
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
return { verifier: name, pass: false, reason: `judge error: ${e.message}` };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function verifyRubric(gate, ctx, judge) {
|
|
148
|
+
const name = gate.name ?? "rubric";
|
|
149
|
+
const dims = gate.dimensions ?? [];
|
|
150
|
+
const threshold = typeof gate.threshold === "number" ? gate.threshold : 0.7;
|
|
151
|
+
if (!judge)
|
|
152
|
+
return { verifier: name, pass: true, reason: "skipped (no judge model configured)" };
|
|
153
|
+
try {
|
|
154
|
+
const out = await judgeJson({ ...judge, model: gate.model ?? judge.model }, "You are an INDEPENDENT grader. Score each dimension 0..1. Output JSON {\"scores\": {<dim>: number}, \"notes\": string}.", `# Dimensions\n${dims.map((d) => `- ${d.name} (weight ${d.weight})${d.criteria ? ": " + d.criteria : ""}`).join("\n")}\n\n# Criteria\n${gate.criteria ?? ""}\n\n# Task\n${ctx.task}\n\n# Result\n${ctx.output.slice(0, 4000)}`);
|
|
155
|
+
const scores = (out?.scores ?? {});
|
|
156
|
+
const totalW = dims.reduce((s, d) => s + d.weight, 0) || 1;
|
|
157
|
+
const agg = dims.reduce((s, d) => s + (Number(scores[d.name]) || 0) * d.weight, 0) / totalW;
|
|
158
|
+
const pass = agg >= threshold;
|
|
159
|
+
return { verifier: name, pass, reason: `score ${agg.toFixed(2)} ${pass ? "≥" : "<"} threshold ${threshold}` };
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
return { verifier: name, pass: false, reason: `rubric error: ${e.message}` };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Run all gates and apply the quorum. Returns a ConsensusResult (reused shape). */
|
|
166
|
+
export async function runVerification(config, ctx, opts = {}) {
|
|
167
|
+
const policy = opts.policy ?? DEFAULT_POLICY;
|
|
168
|
+
const results = [];
|
|
169
|
+
for (const gate of config.gates) {
|
|
170
|
+
switch (gate.type) {
|
|
171
|
+
case "command":
|
|
172
|
+
results.push(await verifyCommand(gate, policy));
|
|
173
|
+
break;
|
|
174
|
+
case "predicate":
|
|
175
|
+
results.push(verifyPredicate(gate, ctx.output));
|
|
176
|
+
break;
|
|
177
|
+
case "llm_judge":
|
|
178
|
+
results.push(await verifyLlmJudge(gate, ctx, opts.judge));
|
|
179
|
+
break;
|
|
180
|
+
case "rubric":
|
|
181
|
+
results.push(await verifyRubric(gate, ctx, opts.judge));
|
|
182
|
+
break;
|
|
183
|
+
default:
|
|
184
|
+
results.push({ verifier: `unknown:${gate.type}`, pass: false, reason: "unknown gate type" });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const quorum = resolveQuorum(results.length || 1, config.quorum);
|
|
188
|
+
const passes = results.filter((r) => r.pass).length;
|
|
189
|
+
return { passed: results.length === 0 ? true : passes >= quorum, results, quorum, passes };
|
|
190
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@personaxis/core",
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"description": "Personaxis engine: governed Living Loop, bounded memory, state/envelope mutation, persona sigil. Spec-faithful, framework-agnostic.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"gray-matter": "^4.0.3",
|
|
17
|
+
"js-yaml": "^4.1.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/js-yaml": "^4.0.9",
|
|
21
|
+
"@types/node": "^22.10.0",
|
|
22
|
+
"typescript": "^5.8.3",
|
|
23
|
+
"vitest": "^3.0.0"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/personaxis/cli.git",
|
|
31
|
+
"directory": "packages/core"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "node ./scripts/gen-version.mjs && tsc",
|
|
35
|
+
"lint": "node ./scripts/gen-version.mjs && tsc --noEmit",
|
|
36
|
+
"test": "node ./scripts/gen-version.mjs && vitest run"
|
|
37
|
+
}
|
|
38
|
+
}
|