reflex-state 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +255 -0
  3. package/README_ja.md +253 -0
  4. package/dist/cli_io.d.ts +8 -0
  5. package/dist/cli_io.js +45 -0
  6. package/dist/composition.d.ts +3 -0
  7. package/dist/composition.js +8 -0
  8. package/dist/core/config.d.ts +37 -0
  9. package/dist/core/config.js +27 -0
  10. package/dist/core/config_validation.d.ts +6 -0
  11. package/dist/core/config_validation.js +109 -0
  12. package/dist/core/engine.d.ts +28 -0
  13. package/dist/core/engine.js +72 -0
  14. package/dist/core/events.d.ts +7 -0
  15. package/dist/core/events.js +56 -0
  16. package/dist/core/extraction.d.ts +10 -0
  17. package/dist/core/extraction.js +119 -0
  18. package/dist/core/metrics.d.ts +52 -0
  19. package/dist/core/metrics.js +100 -0
  20. package/dist/core/reducer.d.ts +18 -0
  21. package/dist/core/reducer.js +206 -0
  22. package/dist/core/serialization.d.ts +11 -0
  23. package/dist/core/serialization.js +160 -0
  24. package/dist/core/state_view.d.ts +27 -0
  25. package/dist/core/state_view.js +28 -0
  26. package/dist/core/types.d.ts +188 -0
  27. package/dist/core/types.js +1 -0
  28. package/dist/core/updater.d.ts +31 -0
  29. package/dist/core/updater.js +27 -0
  30. package/dist/core/verification.d.ts +20 -0
  31. package/dist/core/verification.js +234 -0
  32. package/dist/export_trace_cli.d.ts +2 -0
  33. package/dist/export_trace_cli.js +45 -0
  34. package/dist/index.d.ts +13 -0
  35. package/dist/index.js +8 -0
  36. package/dist/pi/commands.d.ts +3 -0
  37. package/dist/pi/commands.js +123 -0
  38. package/dist/pi/configuration.d.ts +12 -0
  39. package/dist/pi/configuration.js +48 -0
  40. package/dist/pi/extension.d.ts +3 -0
  41. package/dist/pi/extension.js +113 -0
  42. package/dist/pi/index.d.ts +2 -0
  43. package/dist/pi/index.js +5 -0
  44. package/dist/pi/normalization.d.ts +31 -0
  45. package/dist/pi/normalization.js +72 -0
  46. package/dist/pi/persistence.d.ts +15 -0
  47. package/dist/pi/persistence.js +65 -0
  48. package/dist/pi/projection.d.ts +11 -0
  49. package/dist/pi/projection.js +159 -0
  50. package/dist/pi/runtime.d.ts +35 -0
  51. package/dist/pi/runtime.js +139 -0
  52. package/dist/pi/state_block.d.ts +10 -0
  53. package/dist/pi/state_block.js +123 -0
  54. package/dist/pi/trace.d.ts +14 -0
  55. package/dist/pi/trace.js +106 -0
  56. package/dist/replay/runner.d.ts +50 -0
  57. package/dist/replay/runner.js +21 -0
  58. package/dist/replay/trace.d.ts +2 -0
  59. package/dist/replay/trace.js +7 -0
  60. package/dist/replay_cli.d.ts +2 -0
  61. package/dist/replay_cli.js +71 -0
  62. package/dist/typesafe/client.d.ts +21 -0
  63. package/dist/typesafe/client.js +49 -0
  64. package/dist/typesafe/deadline.d.ts +8 -0
  65. package/dist/typesafe/deadline.js +35 -0
  66. package/dist/typesafe/decisions.d.ts +5 -0
  67. package/dist/typesafe/decisions.js +94 -0
  68. package/dist/typesafe/gating.d.ts +7 -0
  69. package/dist/typesafe/gating.js +48 -0
  70. package/dist/typesafe/input.d.ts +3 -0
  71. package/dist/typesafe/input.js +115 -0
  72. package/dist/typesafe/questions.d.ts +5 -0
  73. package/dist/typesafe/questions.js +50 -0
  74. package/dist/typesafe/request_plan.d.ts +19 -0
  75. package/dist/typesafe/request_plan.js +96 -0
  76. package/dist/typesafe/updater.d.ts +17 -0
  77. package/dist/typesafe/updater.js +82 -0
  78. package/package.json +97 -0
@@ -0,0 +1,27 @@
1
+ export function defaultConfig() {
2
+ return {
3
+ enabled: true,
4
+ jev: {
5
+ enabled: true,
6
+ model: "jev-latest",
7
+ timeoutMs: 3000,
8
+ maxRetries: 0,
9
+ deadlineMs: 4000,
10
+ cooldownMs: 60_000,
11
+ },
12
+ thresholds: { noulAccept: 0.8, noulReject: 0.2, minChoiceConfidence: 0.65, minChoiceMargin: 0 },
13
+ limits: {
14
+ maxWorkingSetEvents: 16,
15
+ maxActiveBlockers: 8,
16
+ maxProjectedBlockers: 8,
17
+ maxExcerptHeadChars: 1200,
18
+ maxExcerptTailChars: 600,
19
+ maxPromptChars: 2000,
20
+ maxRecentUserPrompts: 3,
21
+ maxStateBlockChars: 6000,
22
+ },
23
+ projection: { enabled: false, mode: "append", placement: "last-message" },
24
+ shadowQuestions: ["phase"],
25
+ verificationCommands: { test: [], build: [], lint: [] },
26
+ };
27
+ }
@@ -0,0 +1,6 @@
1
+ import type { ReflexStateConfig } from "./config.js";
2
+ export declare function mergeConfig(base: ReflexStateConfig, input: unknown): {
3
+ config: ReflexStateConfig;
4
+ warnings: string[];
5
+ valid: boolean;
6
+ };
@@ -0,0 +1,109 @@
1
+ import { defaultConfig } from "./config.js";
2
+ import { isRecord } from "./serialization.js";
3
+ class InvalidConfigError extends Error {
4
+ }
5
+ export function mergeConfig(base, input) {
6
+ const warnings = [];
7
+ try {
8
+ if (containsCredential(input))
9
+ throw new InvalidConfigError("Credentials are not allowed in configuration files");
10
+ const normalized = normalizeAliases(input, warnings);
11
+ const merged = mergeValue(base, normalized, { path: "config", warnings });
12
+ const config = {
13
+ ...merged,
14
+ limits: {
15
+ ...merged.limits,
16
+ maxActiveBlockers: merged.limits.maxProjectedBlockers,
17
+ },
18
+ };
19
+ validateConfig(config);
20
+ return { config, warnings, valid: true };
21
+ }
22
+ catch (error) {
23
+ warnings.push(error instanceof InvalidConfigError ? error.message : "Invalid configuration");
24
+ return { config: defaultConfig(), warnings, valid: false };
25
+ }
26
+ }
27
+ function mergeValue(base, input, context) {
28
+ if (input === undefined)
29
+ return base;
30
+ if (Array.isArray(base)) {
31
+ if (!Array.isArray(input) || !input.every((value) => typeof value === "string"))
32
+ throw new InvalidConfigError("Expected string array at " + context.path);
33
+ return [...input];
34
+ }
35
+ if (!isRecord(base)) {
36
+ if (typeof input !== typeof base)
37
+ throw new InvalidConfigError("Invalid value type at " + context.path);
38
+ return input;
39
+ }
40
+ if (!isRecord(input))
41
+ throw new InvalidConfigError("Expected object at " + context.path);
42
+ for (const key of Object.keys(input)) {
43
+ if (!Object.hasOwn(base, key))
44
+ context.warnings.push("Unknown configuration key: " + context.path + "." + key.slice(0, 80));
45
+ }
46
+ return Object.fromEntries(Object.entries(base).map(([key, value]) => [
47
+ key,
48
+ mergeValue(value, input[key], { path: context.path + "." + key, warnings: context.warnings }),
49
+ ]));
50
+ }
51
+ function validateConfig(config) {
52
+ for (const value of Object.values(config.thresholds)) {
53
+ if (!Number.isFinite(value) || value < 0 || value > 1)
54
+ throw new InvalidConfigError("Thresholds must be between zero and one");
55
+ }
56
+ if (config.thresholds.noulReject >= config.thresholds.noulAccept)
57
+ throw new InvalidConfigError("noulReject must be below noulAccept");
58
+ for (const [key, value] of Object.entries(config.limits)) {
59
+ if (!Number.isSafeInteger(value) || value < (key === "maxExcerptTailChars" ? 0 : 1))
60
+ throw new InvalidConfigError("Invalid limit: " + key);
61
+ }
62
+ for (const key of ["timeoutMs", "deadlineMs", "cooldownMs"]) {
63
+ if (!Number.isSafeInteger(config.jev[key]) ||
64
+ config.jev[key] < 1 ||
65
+ config.jev[key] > 2_147_483_647)
66
+ throw new InvalidConfigError("Invalid Jev duration: " + key);
67
+ }
68
+ if (!Number.isSafeInteger(config.jev.maxRetries) || config.jev.maxRetries < 0)
69
+ throw new InvalidConfigError("Invalid retry count");
70
+ if (!config.jev.model.trim())
71
+ throw new InvalidConfigError("Jev model must not be empty");
72
+ if (!["append", "current-run"].includes(config.projection.mode) ||
73
+ !["last-message", "run-start"].includes(config.projection.placement))
74
+ throw new InvalidConfigError("Unsupported projection configuration");
75
+ if (config.shadowQuestions.some((question) => question !== "phase"))
76
+ throw new InvalidConfigError("Unsupported shadow question");
77
+ for (const expressions of Object.values(config.verificationCommands)) {
78
+ for (const expression of expressions) {
79
+ try {
80
+ new RegExp(expression);
81
+ }
82
+ catch {
83
+ throw new InvalidConfigError("Invalid verification command expression");
84
+ }
85
+ }
86
+ }
87
+ }
88
+ function normalizeAliases(input, warnings) {
89
+ if (!isRecord(input) || !isRecord(input.limits))
90
+ return input;
91
+ const limits = input.limits;
92
+ if (!Object.hasOwn(limits, "maxActiveBlockers"))
93
+ return input;
94
+ warnings.push("limits.maxActiveBlockers is deprecated; use limits.maxProjectedBlockers");
95
+ const nextLimits = { ...limits };
96
+ if (!Object.hasOwn(limits, "maxProjectedBlockers"))
97
+ nextLimits.maxProjectedBlockers = limits.maxActiveBlockers;
98
+ delete nextLimits.maxActiveBlockers;
99
+ return { ...input, limits: nextLimits };
100
+ }
101
+ function containsCredential(value) {
102
+ if (typeof value === "string")
103
+ return /\bsk-[a-zA-Z0-9_-]{8,}|\bAKIA[A-Z0-9]{16}\b|-----BEGIN [^-]*PRIVATE KEY|\bBearer\s+\S+/i.test(value);
104
+ if (Array.isArray(value))
105
+ return value.some(containsCredential);
106
+ if (!isRecord(value))
107
+ return false;
108
+ return Object.entries(value).some(([key, item]) => /api[_-]?key/i.test(key) || containsCredential(item));
109
+ }
@@ -0,0 +1,28 @@
1
+ import type { ReflexStateConfig } from "./config.js";
2
+ import type { AgentEvent, EventId, HotState, StateTransitionRecord } from "./types.js";
3
+ import type { StateUpdater } from "./updater.js";
4
+ interface EngineOptions {
5
+ readonly cwd: string;
6
+ readonly config: ReflexStateConfig;
7
+ readonly updater: StateUpdater;
8
+ readonly state?: HotState;
9
+ readonly events?: ReadonlyMap<EventId, AgentEvent>;
10
+ readonly onTransition?: (record: StateTransitionRecord) => void | Promise<void>;
11
+ }
12
+ export declare class StateEngine {
13
+ private readonly options;
14
+ private currentState;
15
+ private readonly eventStore;
16
+ private pending;
17
+ private config;
18
+ private updater;
19
+ private cwd;
20
+ constructor(options: EngineOptions);
21
+ get state(): HotState;
22
+ get events(): ReadonlyMap<EventId, AgentEvent>;
23
+ process(event: AgentEvent, signal?: AbortSignal): Promise<StateTransitionRecord>;
24
+ idle(): Promise<void>;
25
+ configure(config: ReflexStateConfig, updater: StateUpdater, cwd?: string): Promise<void>;
26
+ private transition;
27
+ }
28
+ export {};
@@ -0,0 +1,72 @@
1
+ import { extractFacts } from "./extraction.js";
2
+ import { initialState, reduce } from "./reducer.js";
3
+ export class StateEngine {
4
+ options;
5
+ currentState;
6
+ eventStore;
7
+ pending = Promise.resolve();
8
+ config;
9
+ updater;
10
+ cwd;
11
+ constructor(options) {
12
+ this.options = options;
13
+ this.currentState = options.state ?? initialState();
14
+ this.eventStore = new Map(options.events);
15
+ this.config = options.config;
16
+ this.updater = options.updater;
17
+ this.cwd = options.cwd;
18
+ }
19
+ get state() {
20
+ return this.currentState;
21
+ }
22
+ get events() {
23
+ return new Map(this.eventStore);
24
+ }
25
+ process(event, signal) {
26
+ const result = this.pending.then(() => this.transition(event, signal));
27
+ this.pending = result.catch(() => undefined);
28
+ return result;
29
+ }
30
+ async idle() {
31
+ await this.pending;
32
+ }
33
+ configure(config, updater, cwd = this.cwd) {
34
+ const result = this.pending.then(() => {
35
+ this.config = config;
36
+ this.updater = updater;
37
+ this.cwd = cwd;
38
+ });
39
+ this.pending = result;
40
+ return result;
41
+ }
42
+ async transition(event, signal) {
43
+ if (this.eventStore.has(event.id))
44
+ throw new Error("Duplicate event ID: " + event.id);
45
+ const context = {
46
+ state: this.currentState,
47
+ event,
48
+ evidence: this.eventStore,
49
+ config: this.config,
50
+ cwd: this.cwd,
51
+ };
52
+ const facts = extractFacts(context);
53
+ const decisions = await this.updater.evaluate({ ...context, facts }, signal);
54
+ const result = reduce({ ...context, facts, decisions, now: event.timestamp });
55
+ const record = {
56
+ id: "T" + event.id.slice(1),
57
+ timestamp: event.timestamp,
58
+ event,
59
+ after: result.state,
60
+ deterministicPhase: facts.phaseProposal ?? this.currentState.phase,
61
+ changes: result.changes,
62
+ decisions,
63
+ updater: this.updater.name,
64
+ config: this.config,
65
+ cwd: this.cwd,
66
+ };
67
+ await this.options.onTransition?.(record);
68
+ this.eventStore.set(event.id, event);
69
+ this.currentState = result.state;
70
+ return record;
71
+ }
72
+ }
@@ -0,0 +1,7 @@
1
+ import type { ReflexStateConfig } from "./config.js";
2
+ import type { EventId, Excerpt } from "./types.js";
3
+ export declare function eventId(ordinal: number): EventId;
4
+ export declare function createExcerpt(text: string, limits: ReflexStateConfig["limits"]): Excerpt;
5
+ export declare function boundedText(text: string, limit: number): string;
6
+ export declare function textContent(content: unknown): string;
7
+ export declare function boundedInput(input: Readonly<Record<string, unknown>>, maxChars: number): Record<string, unknown>;
@@ -0,0 +1,56 @@
1
+ import { createHash } from "node:crypto";
2
+ export function eventId(ordinal) {
3
+ return ("E" + String(ordinal).padStart(4, "0"));
4
+ }
5
+ export function createExcerpt(text, limits) {
6
+ const truncated = text.length > limits.maxExcerptHeadChars + limits.maxExcerptTailChars;
7
+ return {
8
+ head: truncated ? text.slice(0, limits.maxExcerptHeadChars) : text,
9
+ ...(truncated && limits.maxExcerptTailChars
10
+ ? { tail: text.slice(-limits.maxExcerptTailChars) }
11
+ : {}),
12
+ totalChars: text.length,
13
+ sha256: createHash("sha256").update(text).digest("hex"),
14
+ truncated,
15
+ };
16
+ }
17
+ export function boundedText(text, limit) {
18
+ if (text.length <= limit)
19
+ return text;
20
+ const marker = "\n[truncated]\n";
21
+ if (limit <= marker.length)
22
+ return text.slice(0, limit);
23
+ const available = limit - marker.length;
24
+ const head = Math.ceil((available * 2) / 3);
25
+ const tail = available - head;
26
+ return text.slice(0, head) + marker + (tail ? text.slice(-tail) : "");
27
+ }
28
+ export function textContent(content) {
29
+ if (typeof content === "string")
30
+ return content;
31
+ if (!Array.isArray(content))
32
+ return "";
33
+ return content
34
+ .flatMap((block) => {
35
+ if (!block || typeof block !== "object" || !("type" in block) || block.type !== "text")
36
+ return [];
37
+ return "text" in block && typeof block.text === "string" ? [block.text] : [];
38
+ })
39
+ .join("\n");
40
+ }
41
+ export function boundedInput(input, maxChars) {
42
+ return boundedValue(input, maxChars, 0);
43
+ }
44
+ function boundedValue(value, maxChars, depth) {
45
+ if (typeof value === "string")
46
+ return boundedText(value, maxChars);
47
+ if (depth > 5)
48
+ return "[truncated]";
49
+ if (Array.isArray(value))
50
+ return value.slice(0, 64).map((item) => boundedValue(item, maxChars, depth + 1));
51
+ if (value && typeof value === "object")
52
+ return Object.fromEntries(Object.entries(value)
53
+ .slice(0, 64)
54
+ .map(([key, item]) => [key, boundedValue(item, maxChars, depth + 1)]));
55
+ return value;
56
+ }
@@ -0,0 +1,10 @@
1
+ import type { ReflexStateConfig } from "./config.js";
2
+ import type { AgentEvent, DeterministicFacts, EventId, HotState } from "./types.js";
3
+ export interface ExtractionContext {
4
+ readonly state: HotState;
5
+ readonly event: AgentEvent;
6
+ readonly evidence: ReadonlyMap<EventId, AgentEvent>;
7
+ readonly cwd: string;
8
+ readonly config: ReflexStateConfig;
9
+ }
10
+ export declare function extractFacts(context: ExtractionContext): DeterministicFacts;
@@ -0,0 +1,119 @@
1
+ import { relative, resolve } from "node:path";
2
+ import { verificationFact } from "./verification.js";
3
+ export function extractFacts(context) {
4
+ const { event } = context;
5
+ const exitCode = resultExitCode(event);
6
+ const call = relatedCall(context);
7
+ const observedCall = call && call.cwd ? call : call ? { ...call, cwd: context.cwd } : undefined;
8
+ const paths = callPaths(context);
9
+ const fileChanges = changedPaths(context);
10
+ const generation = context.state.observationGeneration ?? 0;
11
+ const started = observedCall ? startedVerification(context, observedCall.id) : undefined;
12
+ const verification = verificationFact(event, observedCall, context.config, generation, context.state.pendingChanges ?? [], started);
13
+ const mutation = mutationFact(context, call, fileChanges);
14
+ return {
15
+ fileChanges,
16
+ filesRead: event.type === "tool_call" && call?.toolName === "read" ? paths : [],
17
+ phaseProposal: phaseProposal(event, verification),
18
+ deterministicallyResolved: verification?.status === "passed" &&
19
+ verification.freshness === "current" &&
20
+ verification.checkKey
21
+ ? context.state.activeBlockers
22
+ .filter((blocker) => blocker.origin === "verification" && blocker.checkKey === verification.checkKey)
23
+ .map((blocker) => blocker.eventId)
24
+ : [],
25
+ supersededInWorkingSet: context.state.workingSet.filter((id) => {
26
+ const previous = context.evidence.get(id);
27
+ if (!previous)
28
+ return false;
29
+ const previousContext = { ...context, event: previous };
30
+ if (verification &&
31
+ event.type === "tool_result" &&
32
+ previous.type === "tool_result" &&
33
+ verificationFact(previous, withCwd(relatedCall(previousContext), context.cwd), context.config, context.state.observationGeneration ?? 0, [])?.checkKey === verification.checkKey)
34
+ return true;
35
+ const oldPaths = changedPaths(previousContext);
36
+ return oldPaths.length > 0 && oldPaths.every((path) => fileChanges.includes(path));
37
+ }),
38
+ ...(exitCode === undefined ? {} : { exitCode }),
39
+ ...(verification ? { verification } : {}),
40
+ ...(mutation ? { mutation } : {}),
41
+ };
42
+ }
43
+ function withCwd(call, cwd) {
44
+ return call && call.cwd ? call : call ? { ...call, cwd } : undefined;
45
+ }
46
+ function phaseProposal(event, verification) {
47
+ if (event.type === "user_prompt")
48
+ return "planning";
49
+ if (verification?.status === "failed")
50
+ return "debugging";
51
+ if (verification?.status === "running")
52
+ return "testing";
53
+ if (event.type === "file_change")
54
+ return "editing";
55
+ if (event.type !== "tool_call" && event.type !== "tool_result")
56
+ return null;
57
+ if (["read", "grep", "find", "ls"].includes(event.toolName))
58
+ return "exploring";
59
+ if (["edit", "write"].includes(event.toolName))
60
+ return "editing";
61
+ return null;
62
+ }
63
+ function relatedCall(context) {
64
+ const { event, evidence } = context;
65
+ if (event.type === "tool_call")
66
+ return event;
67
+ if (event.type !== "tool_result")
68
+ return undefined;
69
+ return [...evidence.values()]
70
+ .reverse()
71
+ .find((candidate) => candidate.type === "tool_call" &&
72
+ candidate.toolCallId === event.toolCallId &&
73
+ candidate.toolName === event.toolName);
74
+ }
75
+ function resultExitCode(event) {
76
+ if (event.type !== "tool_result" || !event.isError || event.toolName !== "bash")
77
+ return undefined;
78
+ const text = event.excerpt.head + (event.excerpt.tail ? "\n" + event.excerpt.tail : "");
79
+ const match = /(?:^|\n)Command exited with code (\d+)\s*$/.exec(text);
80
+ return match ? Number(match[1]) : undefined;
81
+ }
82
+ function callPaths(context) {
83
+ const call = relatedCall(context);
84
+ return typeof call?.input.path === "string" ? normalizePaths([call.input.path], context.cwd) : [];
85
+ }
86
+ function changedPaths(context) {
87
+ const { event } = context;
88
+ if (event.type === "file_change")
89
+ return normalizePaths(event.paths, context.cwd);
90
+ if (event.type !== "tool_result" || event.isError || !["edit", "write"].includes(event.toolName))
91
+ return [];
92
+ return callPaths(context);
93
+ }
94
+ function startedVerification(context, callId) {
95
+ const running = Object.values(context.state.verification).find((verification) => verification.startedEvent === callId);
96
+ return running?.startedEvent
97
+ ? {
98
+ eventId: running.startedEvent,
99
+ generation: running.observedGeneration ?? context.state.observationGeneration ?? 0,
100
+ ...(running.checkKey ? { checkKey: running.checkKey } : {}),
101
+ }
102
+ : undefined;
103
+ }
104
+ function mutationFact(context, call, paths) {
105
+ const { event } = context;
106
+ if (event.type === "file_change")
107
+ return { operationId: event.id, possible: true, completed: true, paths };
108
+ if (!call || !["bash", "edit", "write"].includes(call.toolName))
109
+ return undefined;
110
+ if (event.type === "tool_call")
111
+ return { operationId: event.id, possible: true, completed: false, paths: [] };
112
+ if (event.type === "tool_result")
113
+ return { operationId: call.id, possible: true, completed: true, paths };
114
+ return undefined;
115
+ }
116
+ function normalizePaths(paths, cwd) {
117
+ const root = resolve("/", cwd);
118
+ return paths.map((path) => relative(root, resolve(root, path)));
119
+ }
@@ -0,0 +1,52 @@
1
+ import type { GatedDecision, ProjectionMeasurement, SemanticDecisions, StateTransitionRecord } from "./types.js";
2
+ export declare function decisionEntries(decisions: SemanticDecisions): [string, GatedDecision<unknown>][];
3
+ export declare class Metrics {
4
+ private readonly records;
5
+ private readonly projections;
6
+ private readonly providerUsage;
7
+ transition(record: StateTransitionRecord): void;
8
+ projection(measurement: ProjectionMeasurement): void;
9
+ get lastProjection(): ProjectionMeasurement | undefined;
10
+ provider(usage: {
11
+ input: number;
12
+ cacheRead: number;
13
+ cacheWrite: number;
14
+ output: number;
15
+ }): void;
16
+ snapshot(): {
17
+ events: number;
18
+ transitions: number;
19
+ jevCalls: number;
20
+ jevFailures: Record<string, number>;
21
+ questions: Record<string, number>;
22
+ applied: number;
23
+ uncertain: number;
24
+ shadowAgreement: number | undefined;
25
+ jevLatencyMs: {
26
+ mean: number | undefined;
27
+ p50: number | undefined;
28
+ p95: number | undefined;
29
+ };
30
+ jevInputTokens: number | undefined;
31
+ jevOutputTokens: number | undefined;
32
+ projection: {
33
+ calls: number;
34
+ last: ProjectionMeasurement | undefined;
35
+ modes: Record<string, number>;
36
+ meanMessagesBefore: number | undefined;
37
+ meanMessagesAfter: number | undefined;
38
+ meanMessagesOmitted: number | undefined;
39
+ meanCharsBefore: number | undefined;
40
+ meanCharsAfter: number | undefined;
41
+ meanStateBlockChars: number | undefined;
42
+ fallbacks: Record<string, number>;
43
+ };
44
+ provider: {
45
+ calls: number;
46
+ input: number | undefined;
47
+ cacheRead: number | undefined;
48
+ cacheWrite: number | undefined;
49
+ output: number | undefined;
50
+ } | undefined;
51
+ };
52
+ }
@@ -0,0 +1,100 @@
1
+ export function decisionEntries(decisions) {
2
+ const entries = [
3
+ ["blocker_introduced", decisions.blockerIntroduced],
4
+ ["failure_category", decisions.failureCategory],
5
+ ["task_complete", decisions.taskComplete],
6
+ ["phase_shadow", decisions.phaseShadow],
7
+ ...decisions.resolvedBlockers.map((item) => [
8
+ "resolves_" + item.eventId,
9
+ item.decision,
10
+ ]),
11
+ ...decisions.relevance.map((item) => [
12
+ "relevant_" + item.eventId,
13
+ item.decision,
14
+ ]),
15
+ ];
16
+ return entries.filter((entry) => entry[1] !== undefined);
17
+ }
18
+ export class Metrics {
19
+ records = [];
20
+ projections = [];
21
+ providerUsage = [];
22
+ transition(record) {
23
+ this.records.push(record);
24
+ }
25
+ projection(measurement) {
26
+ this.projections.push(measurement);
27
+ }
28
+ get lastProjection() {
29
+ return this.projections.at(-1);
30
+ }
31
+ provider(usage) {
32
+ this.providerUsage.push(usage);
33
+ }
34
+ snapshot() {
35
+ const telemetry = this.records.map((record) => record.decisions.telemetry);
36
+ const decisions = this.records.flatMap((record) => decisionEntries(record.decisions));
37
+ const latencies = numbers(telemetry.map((entry) => entry.latencyMs)).sort((a, b) => a - b);
38
+ const shadows = this.records.filter((record) => record.decisions.phaseShadow?.value != null && record.deterministicPhase !== undefined);
39
+ return {
40
+ events: this.records.length,
41
+ transitions: this.records.length,
42
+ jevCalls: telemetry.filter((entry) => entry.questionsAsked > 0).length,
43
+ jevFailures: counts(telemetry.flatMap((entry) => entry.error && entry.questionsAsked > 0 ? [entry.error] : [])),
44
+ questions: counts(telemetry.flatMap((entry) => entry.questionIds)),
45
+ applied: decisions.filter(([, decision]) => decision.gate === "applied" && !decision.shadow)
46
+ .length,
47
+ uncertain: decisions.filter(([, decision]) => decision.gate === "uncertain").length,
48
+ shadowAgreement: shadows.length
49
+ ? shadows.filter((record) => record.decisions.phaseShadow?.value === record.deterministicPhase).length / shadows.length
50
+ : undefined,
51
+ jevLatencyMs: {
52
+ mean: mean(latencies),
53
+ p50: percentile(latencies, 0.5),
54
+ p95: percentile(latencies, 0.95),
55
+ },
56
+ jevInputTokens: sumAvailable(telemetry.map((entry) => entry.inputTokens)),
57
+ jevOutputTokens: sumAvailable(telemetry.map((entry) => entry.outputTokens)),
58
+ projection: {
59
+ calls: this.projections.length,
60
+ last: this.lastProjection,
61
+ modes: counts(this.projections.flatMap((entry) => (entry.mode ? [entry.mode] : []))),
62
+ meanMessagesBefore: mean(this.projections.map((entry) => entry.messagesBefore)),
63
+ meanMessagesAfter: mean(this.projections.map((entry) => entry.messagesAfter)),
64
+ meanMessagesOmitted: mean(this.projections.flatMap((entry) => entry.messagesOmitted === undefined ? [] : [entry.messagesOmitted])),
65
+ meanCharsBefore: mean(this.projections.map((entry) => entry.charsBefore)),
66
+ meanCharsAfter: mean(this.projections.map((entry) => entry.charsAfter)),
67
+ meanStateBlockChars: mean(this.projections.flatMap((entry) => entry.stateBlockChars === undefined ? [] : [entry.stateBlockChars])),
68
+ fallbacks: counts(this.projections.flatMap((entry) => (entry.fallback ? [entry.fallback] : []))),
69
+ },
70
+ provider: this.providerUsage.length
71
+ ? {
72
+ calls: this.providerUsage.length,
73
+ input: sumAvailable(this.providerUsage.map((usage) => usage.input)),
74
+ cacheRead: sumAvailable(this.providerUsage.map((usage) => usage.cacheRead)),
75
+ cacheWrite: sumAvailable(this.providerUsage.map((usage) => usage.cacheWrite)),
76
+ output: sumAvailable(this.providerUsage.map((usage) => usage.output)),
77
+ }
78
+ : undefined,
79
+ };
80
+ }
81
+ }
82
+ function numbers(values) {
83
+ return values.filter((value) => value !== undefined && Number.isFinite(value));
84
+ }
85
+ function sumAvailable(values) {
86
+ const available = numbers(values);
87
+ return available.length ? available.reduce((sum, value) => sum + value, 0) : undefined;
88
+ }
89
+ function mean(values) {
90
+ return values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : undefined;
91
+ }
92
+ function percentile(sorted, fraction) {
93
+ return sorted[Math.max(0, Math.ceil(sorted.length * fraction) - 1)];
94
+ }
95
+ function counts(values) {
96
+ const result = {};
97
+ for (const value of values)
98
+ result[value] = (result[value] ?? 0) + 1;
99
+ return result;
100
+ }
@@ -0,0 +1,18 @@
1
+ import type { ReflexStateConfig } from "./config.js";
2
+ import type { AgentEvent, DeterministicFacts, EventId, HotState, SemanticDecisions } from "./types.js";
3
+ interface ReductionContext {
4
+ readonly state: HotState;
5
+ readonly event: AgentEvent;
6
+ readonly facts: DeterministicFacts;
7
+ readonly decisions: SemanticDecisions;
8
+ readonly evidence: ReadonlyMap<EventId, AgentEvent>;
9
+ readonly config: ReflexStateConfig;
10
+ readonly now: string;
11
+ }
12
+ export declare function initialState(): HotState;
13
+ export declare function reduce(context: ReductionContext): {
14
+ state: HotState;
15
+ changes: string[];
16
+ };
17
+ export declare function admitsEvidence(event: AgentEvent, facts: DeterministicFacts): boolean;
18
+ export {};