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,31 @@
1
+ import type { AgentEndEvent as PiAgentEndEvent, ToolCallEvent as PiToolCallEvent, ToolResultEvent as PiToolResultEvent } from "@earendil-works/pi-coding-agent";
2
+ import type { ReflexStateConfig } from "../core/config.js";
3
+ import type { AgentEndEvent, SourceRef, ToolCallEvent, ToolResultEvent, UserPromptEvent } from "../core/types.js";
4
+ interface NormalizerOptions {
5
+ readonly eventCount: number;
6
+ readonly turnIndex: number;
7
+ readonly config: ReflexStateConfig;
8
+ readonly now?: () => number;
9
+ readonly cwd?: string;
10
+ }
11
+ export declare class PiEventNormalizer {
12
+ private readonly options;
13
+ private ordinal;
14
+ private turnIndex;
15
+ constructor(options: NormalizerOptions);
16
+ prompt(text: string, timestamp?: number): UserPromptEvent;
17
+ call(event: PiToolCallEvent): ToolCallEvent;
18
+ result(event: PiToolResultEvent): ToolResultEvent;
19
+ end(messages: PiAgentEndEvent["messages"]): AgentEndEvent;
20
+ resume(reason?: "resume" | "branch_switch"): {
21
+ type: "session_resume";
22
+ reason: "resume" | "branch_switch";
23
+ id: `E${string}`;
24
+ timestamp: string;
25
+ turnIndex: number;
26
+ source: SourceRef;
27
+ };
28
+ private base;
29
+ private now;
30
+ }
31
+ export {};
@@ -0,0 +1,72 @@
1
+ import { boundedInput, boundedText, createExcerpt, eventId, textContent } from "../core/events.js";
2
+ export class PiEventNormalizer {
3
+ options;
4
+ ordinal;
5
+ turnIndex;
6
+ constructor(options) {
7
+ this.options = options;
8
+ this.ordinal = options.eventCount;
9
+ this.turnIndex = options.turnIndex;
10
+ }
11
+ prompt(text, timestamp = this.now()) {
12
+ this.turnIndex++;
13
+ return {
14
+ ...this.base({ kind: "user_prompt", timestamp }),
15
+ type: "user_prompt",
16
+ text: boundedText(text, this.options.config.limits.maxPromptChars),
17
+ };
18
+ }
19
+ call(event) {
20
+ const maxChars = this.options.config.limits.maxExcerptHeadChars +
21
+ this.options.config.limits.maxExcerptTailChars;
22
+ const input = event.input;
23
+ const command = typeof input.command === "string" ? input.command : undefined;
24
+ return {
25
+ ...this.base({ kind: "tool_call", toolCallId: event.toolCallId, timestamp: this.now() }),
26
+ type: "tool_call",
27
+ toolCallId: event.toolCallId,
28
+ toolName: event.toolName,
29
+ input: boundedInput(event.input, maxChars),
30
+ ...(command !== undefined && command.length > maxChars ? { commandTruncated: true } : {}),
31
+ ...(this.options.cwd ? { cwd: this.options.cwd } : {}),
32
+ };
33
+ }
34
+ result(event) {
35
+ return {
36
+ ...this.base({ kind: "tool_call", toolCallId: event.toolCallId, timestamp: this.now() }),
37
+ type: "tool_result",
38
+ toolCallId: event.toolCallId,
39
+ toolName: event.toolName,
40
+ isError: event.isError,
41
+ excerpt: createExcerpt(textContent(event.content), this.options.config.limits),
42
+ };
43
+ }
44
+ end(messages) {
45
+ const last = messages.findLast((message) => message.role === "assistant");
46
+ const stopReason = last?.role === "assistant" ? last.stopReason : "aborted";
47
+ return {
48
+ ...this.base({ kind: "assistant_message", timestamp: last?.timestamp ?? this.now() }),
49
+ type: "agent_end",
50
+ finalText: createExcerpt(last?.role === "assistant" ? textContent(last.content) : "", this.options.config.limits),
51
+ stopReason: stopReason === "toolUse" || stopReason === "pending" ? "aborted" : stopReason,
52
+ };
53
+ }
54
+ resume(reason = "resume") {
55
+ return {
56
+ ...this.base({ kind: "assistant_message", timestamp: this.now() }),
57
+ type: "session_resume",
58
+ reason,
59
+ };
60
+ }
61
+ base(source) {
62
+ return {
63
+ id: eventId(++this.ordinal),
64
+ timestamp: new Date(source.timestamp).toISOString(),
65
+ turnIndex: this.turnIndex,
66
+ source,
67
+ };
68
+ }
69
+ now() {
70
+ return (this.options.now ?? Date.now)();
71
+ }
72
+ }
@@ -0,0 +1,15 @@
1
+ import type { AgentEvent, StateTransitionRecord } from "../core/types.js";
2
+ interface StoredEntry {
3
+ readonly type: string;
4
+ readonly customType?: string;
5
+ readonly data?: unknown;
6
+ }
7
+ export declare function reconstruct(branch: readonly StoredEntry[]): {
8
+ state: import("../core/types.js").HotState;
9
+ events: Map<`E${string}`, AgentEvent>;
10
+ transitions: StateTransitionRecord[];
11
+ hasMeta: boolean;
12
+ legacy: boolean;
13
+ };
14
+ export declare function highestEventOrdinal(entries: readonly StoredEntry[]): number;
15
+ export {};
@@ -0,0 +1,65 @@
1
+ import { initialState } from "../core/reducer.js";
2
+ import { isRecord, LegacyTraceError, parseTransition } from "../core/serialization.js";
3
+ export function reconstruct(branch) {
4
+ let state = initialState();
5
+ const events = new Map();
6
+ const transitions = [];
7
+ let hasMeta = false;
8
+ let legacy = false;
9
+ for (const entry of branch) {
10
+ if (entry.type !== "custom")
11
+ continue;
12
+ if (entry.customType === "reflex-state.meta" &&
13
+ isRecord(entry.data) &&
14
+ entry.data.stateVersion === 2)
15
+ hasMeta = true;
16
+ if (entry.customType === "reflex-state.reset") {
17
+ state = initialState();
18
+ events.clear();
19
+ transitions.length = 0;
20
+ legacy = false;
21
+ continue;
22
+ }
23
+ if (entry.customType !== "reflex-state.transition")
24
+ continue;
25
+ let record;
26
+ try {
27
+ record = parseTransition(entry.data);
28
+ }
29
+ catch (error) {
30
+ if (error instanceof LegacyTraceError) {
31
+ legacy = true;
32
+ continue;
33
+ }
34
+ throw error;
35
+ }
36
+ if (legacy)
37
+ continue;
38
+ if (events.has(record.event.id))
39
+ throw new Error("Duplicate ReflexState event on branch");
40
+ state = record.after;
41
+ events.set(record.event.id, record.event);
42
+ transitions.push(record);
43
+ }
44
+ return {
45
+ state: legacy
46
+ ? { ...initialState(), stateHealth: "legacy_state_requires_reset" }
47
+ : state,
48
+ events,
49
+ transitions,
50
+ hasMeta,
51
+ legacy,
52
+ };
53
+ }
54
+ export function highestEventOrdinal(entries) {
55
+ let highest = 0;
56
+ for (const entry of entries) {
57
+ if (entry.type !== "custom" || entry.customType !== "reflex-state.transition")
58
+ continue;
59
+ const data = entry.data;
60
+ const id = isRecord(data) && isRecord(data.event) ? data.event.id : undefined;
61
+ if (typeof id === "string" && /^E\d+$/.test(id))
62
+ highest = Math.max(highest, Number(id.slice(1)));
63
+ }
64
+ return highest;
65
+ }
@@ -0,0 +1,11 @@
1
+ import type { ContextEvent } from "@earendil-works/pi-coding-agent";
2
+ import type { ProjectionMeasurement } from "../core/types.js";
3
+ import type { StateBlockContext } from "./state_block.js";
4
+ type Message = ContextEvent["messages"][number];
5
+ export declare function projectContext(messages: Message[], context: StateBlockContext & {
6
+ compacting?: boolean;
7
+ }): {
8
+ messages: Message[];
9
+ measurement: ProjectionMeasurement;
10
+ };
11
+ export {};
@@ -0,0 +1,159 @@
1
+ import { boundedText, textContent } from "../core/events.js";
2
+ import { stateBlock } from "./state_block.js";
3
+ export function projectContext(messages, context) {
4
+ if (!context.config.enabled || !context.config.projection.enabled)
5
+ return projectionResult(messages, messages, "disabled", "disabled");
6
+ if (context.state.stateHealth && context.state.stateHealth !== "valid")
7
+ return projectionResult(messages, messages, context.state.stateHealth, context.config.projection.mode);
8
+ if (context.compacting)
9
+ return projectionResult(messages, messages, "compacting", "disabled");
10
+ const runs = splitRuns(messages);
11
+ const current = runs.at(-1);
12
+ if (!current || !containsGoal(current.messages, context))
13
+ return projectionResult(messages, messages, "missing_user_prompt", context.config.projection.mode);
14
+ if (!completeExchanges(current.messages))
15
+ return projectionResult(messages, messages, "incomplete_exchange", context.config.projection.mode);
16
+ const retained = context.config.projection.mode === "append"
17
+ ? messages
18
+ : retainRecentRuns(messages, runs, context);
19
+ if (!retained)
20
+ return projectionResult(messages, messages, "unsafe_boundary", "current-run");
21
+ const target = placementTarget(retained, current, context);
22
+ if (!target)
23
+ return projectionResult(messages, messages, "unsupported_placement", context.config.projection.mode);
24
+ const block = stateBlock({
25
+ ...context,
26
+ projectionMode: context.config.projection.mode,
27
+ messagesOmitted: messages.length - retained.length,
28
+ });
29
+ if (!block)
30
+ return projectionResult(messages, messages, "state_block_budget", context.config.projection.mode);
31
+ const projected = addBlock(retained, target, block);
32
+ return projectionResult(messages, projected, undefined, context.config.projection.mode, block.length);
33
+ }
34
+ function splitRuns(messages) {
35
+ const runs = [];
36
+ let start = -1;
37
+ for (let index = 0; index < messages.length; index++) {
38
+ const message = messages[index];
39
+ if (!message)
40
+ continue;
41
+ if (message.role === "user" && start < 0)
42
+ start = index;
43
+ if (start >= 0 && isTerminalAssistant(message)) {
44
+ runs.push({ start, end: index + 1, messages: messages.slice(start, index + 1) });
45
+ start = -1;
46
+ }
47
+ }
48
+ if (start >= 0)
49
+ runs.push({ start, end: messages.length, messages: messages.slice(start) });
50
+ return runs;
51
+ }
52
+ function retainRecentRuns(messages, runs, context) {
53
+ if (runs.length <= 2)
54
+ return runs.every((run) => completeExchanges(run.messages)) ? messages : undefined;
55
+ const first = runs.at(-2);
56
+ const last = runs.at(-1);
57
+ if (!first || !last)
58
+ return undefined;
59
+ if (runs[0]?.start !== 0)
60
+ return undefined;
61
+ const omitted = messages.slice(0, first.start);
62
+ if (omitted.some((message) => !isSafeToOmit(message)))
63
+ return undefined;
64
+ if (runs.slice(0, -2).some((run) => !completeExchanges(run.messages)))
65
+ return undefined;
66
+ if (messages.slice(last.end).length > 0)
67
+ return undefined;
68
+ if (!completeExchanges(first.messages) || !completeExchanges(last.messages))
69
+ return undefined;
70
+ if (!containsGoal(last.messages, context))
71
+ return undefined;
72
+ return messages.slice(first.start, last.end);
73
+ }
74
+ function isSafeToOmit(message) {
75
+ return ["user", "assistant", "toolResult", "bashExecution"].includes(message.role);
76
+ }
77
+ function placementTarget(retained, current, context) {
78
+ const first = current.messages.find((message) => message.role === "user");
79
+ const last = current.messages.at(-1);
80
+ const index = context.config.projection.placement === "run-start"
81
+ ? first
82
+ ? retained.indexOf(first)
83
+ : -1
84
+ : last
85
+ ? retained.indexOf(last)
86
+ : -1;
87
+ const message = retained[index];
88
+ return message && (message.role === "user" || message.role === "toolResult")
89
+ ? { index, message }
90
+ : undefined;
91
+ }
92
+ function isTerminalAssistant(message) {
93
+ return (message.role === "assistant" &&
94
+ "stopReason" in message &&
95
+ message.stopReason !== "toolUse" &&
96
+ message.stopReason !== "pending");
97
+ }
98
+ function addBlock(messages, target, block) {
99
+ if (target.message.role !== "user" && target.message.role !== "toolResult")
100
+ return messages;
101
+ const content = typeof target.message.content === "string"
102
+ ? [{ type: "text", text: target.message.content }]
103
+ : [...target.message.content];
104
+ const result = messages.slice();
105
+ result[target.index] = {
106
+ ...target.message,
107
+ content: [...content, { type: "text", text: block }],
108
+ };
109
+ return result;
110
+ }
111
+ function containsGoal(messages, context) {
112
+ const goal = context.state.goal ? context.evidence.get(context.state.goal) : undefined;
113
+ if (context.state.goal && goal?.type !== "user_prompt")
114
+ return false;
115
+ return messages.some((message) => message.role === "user" &&
116
+ (!goal ||
117
+ (goal.type === "user_prompt" &&
118
+ boundedText(textContent(message.content), context.config.limits.maxPromptChars) ===
119
+ goal.text)));
120
+ }
121
+ function completeExchanges(messages) {
122
+ const pending = new Map();
123
+ const seen = new Set();
124
+ for (const message of messages) {
125
+ if (message.role === "assistant") {
126
+ if (pending.size)
127
+ return false;
128
+ for (const block of message.content) {
129
+ if (block.type !== "toolCall")
130
+ continue;
131
+ if (seen.has(block.id))
132
+ return false;
133
+ seen.add(block.id);
134
+ pending.set(block.id, block.name);
135
+ }
136
+ }
137
+ if (message.role === "toolResult") {
138
+ if (pending.get(message.toolCallId) !== message.toolName)
139
+ return false;
140
+ pending.delete(message.toolCallId);
141
+ }
142
+ }
143
+ return pending.size === 0;
144
+ }
145
+ function projectionResult(before, messages, fallback, mode = "current-run", stateBlockChars) {
146
+ return {
147
+ messages,
148
+ measurement: {
149
+ mode,
150
+ messagesBefore: before.length,
151
+ messagesAfter: messages.length,
152
+ messagesOmitted: before.length - messages.length,
153
+ charsBefore: JSON.stringify(before).length,
154
+ charsAfter: JSON.stringify(messages).length,
155
+ ...(stateBlockChars === undefined ? {} : { stateBlockChars }),
156
+ ...(fallback ? { fallback } : {}),
157
+ },
158
+ };
159
+ }
@@ -0,0 +1,35 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { ReflexStateConfig } from "../core/config.js";
3
+ import { StateEngine } from "../core/engine.js";
4
+ import { Metrics } from "../core/metrics.js";
5
+ import type { AgentEvent, HotState } from "../core/types.js";
6
+ import type { StateUpdater } from "../core/updater.js";
7
+ import { PiEventNormalizer } from "./normalization.js";
8
+ export type UpdaterFactory = (config: ReflexStateConfig, notify: (message: string) => void) => StateUpdater;
9
+ export declare class SessionRuntime {
10
+ private readonly options;
11
+ engine: StateEngine;
12
+ normalizer: PiEventNormalizer;
13
+ metrics: Metrics;
14
+ compacting: boolean;
15
+ projectionSafe: boolean;
16
+ expectedPrompt: string | undefined;
17
+ private currentConfig;
18
+ private updater;
19
+ private historyRecords;
20
+ constructor(options: {
21
+ pi: ExtensionAPI;
22
+ ctx: ExtensionContext;
23
+ config: ReflexStateConfig;
24
+ createUpdater: UpdaterFactory;
25
+ });
26
+ get config(): ReflexStateConfig;
27
+ get state(): HotState;
28
+ get history(): import("../core/types.js").StateTransitionRecord[];
29
+ get health(): import("../core/updater.js").UpdaterHealth;
30
+ record(event: AgentEvent, ctx: ExtensionContext): Promise<void>;
31
+ toggle(target: "projection" | "jev", enabled: boolean): Promise<void>;
32
+ reset(): Promise<void>;
33
+ widget(ctx: ExtensionContext): void;
34
+ private restore;
35
+ }
@@ -0,0 +1,139 @@
1
+ import { StateEngine } from "../core/engine.js";
2
+ import { Metrics } from "../core/metrics.js";
3
+ import { blockerView } from "../core/state_view.js";
4
+ import { PiEventNormalizer } from "./normalization.js";
5
+ import { highestEventOrdinal, reconstruct } from "./persistence.js";
6
+ export class SessionRuntime {
7
+ options;
8
+ engine;
9
+ normalizer;
10
+ metrics = new Metrics();
11
+ compacting = false;
12
+ projectionSafe = true;
13
+ expectedPrompt;
14
+ currentConfig;
15
+ updater;
16
+ historyRecords;
17
+ constructor(options) {
18
+ this.options = options;
19
+ this.currentConfig = options.config;
20
+ this.updater = options.createUpdater(options.config, (message) => options.ctx.ui.notify(message, "warning"));
21
+ const restored = this.restore();
22
+ this.engine = restored.engine;
23
+ this.normalizer = restored.normalizer;
24
+ this.historyRecords = restored.transitions;
25
+ this.projectionSafe = !restored.legacy && restored.engine.state.stateHealth === "valid";
26
+ if (restored.legacy)
27
+ options.ctx.ui.notify("legacy_state_requires_reset", "warning");
28
+ else if (restored.engine.state.stateHealth !== "valid")
29
+ options.ctx.ui.notify("invalid_state_requires_reset", "warning");
30
+ }
31
+ get config() {
32
+ return this.currentConfig;
33
+ }
34
+ get state() {
35
+ return this.engine.state;
36
+ }
37
+ get history() {
38
+ return this.historyRecords.slice();
39
+ }
40
+ get health() {
41
+ return (this.updater.health ?? { status: "disabled", circuit: "closed", reason: "configuration" });
42
+ }
43
+ async record(event, ctx) {
44
+ if (!this.config.enabled || !this.projectionSafe)
45
+ return;
46
+ try {
47
+ await this.engine.process(event, ctx.signal);
48
+ }
49
+ catch (error) {
50
+ this.projectionSafe = false;
51
+ throw error;
52
+ }
53
+ this.widget(ctx);
54
+ }
55
+ async toggle(target, enabled) {
56
+ const config = {
57
+ ...this.config,
58
+ [target]: { ...this.config[target], enabled },
59
+ };
60
+ const updater = target === "jev"
61
+ ? this.options.createUpdater(config, (message) => this.options.ctx.ui.notify(message, "warning"))
62
+ : this.updater;
63
+ await this.engine.configure(config, updater);
64
+ this.currentConfig = config;
65
+ this.updater = updater;
66
+ }
67
+ async reset() {
68
+ await this.engine.idle();
69
+ this.options.pi.appendEntry("reflex-state.reset", { reason: "user" });
70
+ this.metrics = new Metrics();
71
+ const restored = this.restore();
72
+ this.engine = restored.engine;
73
+ this.normalizer = restored.normalizer;
74
+ this.historyRecords = restored.transitions;
75
+ this.projectionSafe = !restored.legacy && restored.engine.state.stateHealth === "valid";
76
+ }
77
+ widget(ctx) {
78
+ if (!ctx.hasUI)
79
+ return;
80
+ const last = this.historyRecords.findLast((record) => record.decisions.telemetry.questionsAsked > 0)?.decisions.telemetry;
81
+ const projection = this.metrics.lastProjection;
82
+ const latency = last?.latencyMs === undefined ? "" : " " + Math.round(last.latencyMs) + "ms";
83
+ const counts = projection
84
+ ? " | ctx " + projection.messagesBefore + "→" + projection.messagesAfter + " msgs"
85
+ : "";
86
+ const test = this.state.verification.test;
87
+ const testStatus = test.status + (test.freshness && test.freshness !== "current" ? "/" + test.freshness : "");
88
+ const blockers = blockerView(this.state, this.config);
89
+ ctx.ui.setWidget("reflex-state", [
90
+ "ReflexState " +
91
+ (this.config.enabled ? this.state.phase : "disabled") +
92
+ " | tests " +
93
+ testStatus +
94
+ " | blockers " +
95
+ blockers.shownCount +
96
+ "/" +
97
+ blockers.unresolvedTotal +
98
+ " | Jev " +
99
+ this.health.status +
100
+ latency +
101
+ counts,
102
+ ]);
103
+ }
104
+ restore() {
105
+ const { pi, ctx } = this.options;
106
+ const restored = reconstruct(ctx.sessionManager.getBranch());
107
+ for (const record of restored.transitions)
108
+ this.metrics.transition(record);
109
+ let hasMeta = restored.hasMeta;
110
+ const engine = new StateEngine({
111
+ cwd: ctx.cwd,
112
+ config: this.config,
113
+ updater: this.updater,
114
+ state: restored.state,
115
+ events: restored.events,
116
+ onTransition: (record) => {
117
+ if (!hasMeta) {
118
+ pi.appendEntry("reflex-state.meta", {
119
+ specVersion: "0.1",
120
+ stateVersion: 2,
121
+ config: this.config,
122
+ piVersion: "0.83.0",
123
+ });
124
+ hasMeta = true;
125
+ }
126
+ pi.appendEntry("reflex-state.transition", record);
127
+ restored.transitions.push(record);
128
+ this.metrics.transition(record);
129
+ },
130
+ });
131
+ const normalizer = new PiEventNormalizer({
132
+ config: this.config,
133
+ cwd: ctx.cwd,
134
+ eventCount: highestEventOrdinal(ctx.sessionManager.getEntries()),
135
+ turnIndex: restored.state.cursor.turnIndex,
136
+ });
137
+ return { engine, normalizer, transitions: restored.transitions, legacy: restored.legacy };
138
+ }
139
+ }
@@ -0,0 +1,10 @@
1
+ import type { ReflexStateConfig } from "../core/config.js";
2
+ import type { AgentEvent, EventId, HotState } from "../core/types.js";
3
+ export interface StateBlockContext {
4
+ readonly state: HotState;
5
+ readonly evidence: ReadonlyMap<EventId, AgentEvent>;
6
+ readonly config: ReflexStateConfig;
7
+ readonly projectionMode?: "append" | "current-run";
8
+ readonly messagesOmitted?: number;
9
+ }
10
+ export declare function stateBlock(context: StateBlockContext): string | undefined;
@@ -0,0 +1,123 @@
1
+ import { boundedText } from "../core/events.js";
2
+ import { blockerView, workingSetView } from "../core/state_view.js";
3
+ export function stateBlock(context) {
4
+ const { state, evidence, config } = context;
5
+ const blockers = blockerView(state, config);
6
+ const workingSet = workingSetView(state, config.limits.maxWorkingSetEvents);
7
+ const requests = [...evidence.values()]
8
+ .filter((event) => event.type === "user_prompt")
9
+ .slice(-config.limits.maxRecentUserPrompts)
10
+ .map((event) => ({ event: event.id, text: event.text }));
11
+ const evidenceIds = uniqueEvidenceIds(state, blockers.blockers, workingSet.events);
12
+ let excerptLimit = 1200;
13
+ let blockerLimit = blockers.blockers.length;
14
+ let workingLimit = workingSet.events.length;
15
+ const render = () => {
16
+ const shownBlockers = blockers.blockers.slice(-blockerLimit);
17
+ const shownWorking = workingSet.events.slice(-workingLimit);
18
+ const displayed = new Set([
19
+ ...shownBlockers.map((item) => item.eventId),
20
+ ...shownWorking,
21
+ ...failedEvidence(state),
22
+ ]);
23
+ const evidenceItems = [...evidenceIds]
24
+ .filter((id) => displayed.has(id) || shownBlockers.some((item) => item.eventId === id))
25
+ .map((id) => [id, evidenceItem(evidence.get(id), evidence, excerptLimit)]);
26
+ return ("<reflex-state>\n" +
27
+ JSON.stringify({
28
+ note: context.projectionMode === "append"
29
+ ? "This block augments the complete conversation history."
30
+ : "This block describes the current execution state; older history may be omitted in current-run mode.",
31
+ projection_mode: context.projectionMode ?? config.projection.mode,
32
+ history_omitted: (context.messagesOmitted ?? 0) > 0,
33
+ messages_omitted: context.messagesOmitted ?? 0,
34
+ goal: state.goal,
35
+ phase: state.phase,
36
+ task_status: state.taskStatus,
37
+ state_health: state.stateHealth ?? "valid",
38
+ observation_generation: state.observationGeneration ?? 0,
39
+ pending_changes: state.pendingChanges ?? [],
40
+ modified_files: state.modifiedFiles,
41
+ verification: state.verification,
42
+ blockers: {
43
+ unresolved_total: blockers.unresolvedTotal,
44
+ shown_count: shownBlockers.length,
45
+ omitted_count: blockers.unresolvedTotal - shownBlockers.length,
46
+ items: shownBlockers,
47
+ },
48
+ working_set: {
49
+ total: workingSet.total,
50
+ shown_count: shownWorking.length,
51
+ omitted_count: workingSet.omittedCount + (workingSet.events.length - shownWorking.length),
52
+ items: shownWorking.map((id) => evidenceItem(evidence.get(id), evidence, excerptLimit)),
53
+ },
54
+ recent_user_requests: requests,
55
+ evidence: Object.fromEntries(evidenceItems),
56
+ }, null, 2) +
57
+ "\n</reflex-state>");
58
+ };
59
+ let block = render();
60
+ while (block.length > config.limits.maxStateBlockChars && excerptLimit > 80) {
61
+ excerptLimit = Math.floor(excerptLimit / 2);
62
+ block = render();
63
+ }
64
+ while (block.length > config.limits.maxStateBlockChars && workingLimit > 0) {
65
+ workingLimit--;
66
+ block = render();
67
+ }
68
+ while (block.length > config.limits.maxStateBlockChars && blockerLimit > 0) {
69
+ blockerLimit--;
70
+ block = render();
71
+ }
72
+ return block.length <= config.limits.maxStateBlockChars ? block : undefined;
73
+ }
74
+ function uniqueEvidenceIds(state, blockers, workingSet) {
75
+ return [
76
+ ...new Set([...blockers.map((item) => item.eventId), ...workingSet, ...failedEvidence(state)]),
77
+ ];
78
+ }
79
+ function failedEvidence(state) {
80
+ return Object.values(state.verification).flatMap((item) => item.status === "failed" && item.evidence ? [item.evidence] : []);
81
+ }
82
+ function evidenceItem(event, evidence, excerptLimit) {
83
+ if (!event)
84
+ return { available: false, reason: "evidence_unavailable" };
85
+ const related = relatedCall(event, evidence);
86
+ const item = { id: event.id, type: event.type };
87
+ if (event.type === "tool_call") {
88
+ item.tool = event.toolName;
89
+ item.command =
90
+ typeof event.input.command === "string" ? boundedText(event.input.command, 400) : undefined;
91
+ item.path = typeof event.input.path === "string" ? event.input.path : undefined;
92
+ item.truncated = event.commandTruncated === true;
93
+ }
94
+ if (event.type === "tool_result") {
95
+ item.tool = event.toolName;
96
+ item.error = event.isError;
97
+ item.excerpt = excerptText(event, excerptLimit);
98
+ item.truncated = event.excerpt.truncated;
99
+ if (!related)
100
+ item.related_call = { available: false, reason: "related_call_unavailable" };
101
+ }
102
+ if (event.type === "file_change") {
103
+ item.paths = event.paths;
104
+ item.truncated = false;
105
+ }
106
+ if (related && related.id !== event.id) {
107
+ item.related_call = evidenceItem(related, evidence, Math.min(excerptLimit, 400));
108
+ }
109
+ return item;
110
+ }
111
+ function relatedCall(event, evidence) {
112
+ if (event.type === "tool_call")
113
+ return event;
114
+ if (event.type !== "tool_result")
115
+ return undefined;
116
+ return [...evidence.values()].find((candidate) => candidate.type === "tool_call" &&
117
+ candidate.toolCallId === event.toolCallId &&
118
+ candidate.toolName === event.toolName);
119
+ }
120
+ function excerptText(event, limit) {
121
+ const text = event.excerpt.head + (event.excerpt.tail ? "\n[excerpt gap]\n" + event.excerpt.tail : "");
122
+ return boundedText(text, limit);
123
+ }
@@ -0,0 +1,14 @@
1
+ import type { ReflexStateConfig } from "../core/config.js";
2
+ import type { AgentEvent } from "../core/types.js";
3
+ export declare function exportSession(entries: readonly Record<string, unknown>[], options: {
4
+ leaf?: string;
5
+ config: ReflexStateConfig;
6
+ }): {
7
+ events: AgentEvent[];
8
+ transitions: import("../core/types.js").StateTransitionRecord[];
9
+ cwd: string;
10
+ config: ReflexStateConfig;
11
+ leaf: {} | null;
12
+ formatVersion: number;
13
+ legacy: boolean;
14
+ };