@twelvehart/orcats 0.0.0-seed.0 → 0.2.3

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 (275) hide show
  1. package/LICENSE +48 -0
  2. package/NOTICE +5 -0
  3. package/README.md +399 -0
  4. package/bin/orcats +4 -0
  5. package/dist/backends/acp-client.d.ts +74 -0
  6. package/dist/backends/acp-client.d.ts.map +1 -0
  7. package/dist/backends/acp-run.d.ts +31 -0
  8. package/dist/backends/acp-run.d.ts.map +1 -0
  9. package/dist/backends/claude-run.d.ts +35 -0
  10. package/dist/backends/claude-run.d.ts.map +1 -0
  11. package/dist/backends/claude-stream-json.d.ts +15 -0
  12. package/dist/backends/claude-stream-json.d.ts.map +1 -0
  13. package/dist/backends/codex-jsonl.d.ts +34 -0
  14. package/dist/backends/codex-jsonl.d.ts.map +1 -0
  15. package/dist/backends/codex-run.d.ts +40 -0
  16. package/dist/backends/codex-run.d.ts.map +1 -0
  17. package/dist/backends/codex.d.ts +5 -0
  18. package/dist/backends/codex.d.ts.map +1 -0
  19. package/dist/backends/conversation-config.d.ts +12 -0
  20. package/dist/backends/conversation-config.d.ts.map +1 -0
  21. package/dist/backends/index.d.ts +14 -0
  22. package/dist/backends/index.d.ts.map +1 -0
  23. package/dist/backends/opencode-run.d.ts +48 -0
  24. package/dist/backends/opencode-run.d.ts.map +1 -0
  25. package/dist/backends/opencode-sse.d.ts +35 -0
  26. package/dist/backends/opencode-sse.d.ts.map +1 -0
  27. package/dist/backends/pi-rpc.d.ts +21 -0
  28. package/dist/backends/pi-rpc.d.ts.map +1 -0
  29. package/dist/backends/pi-run.d.ts +21 -0
  30. package/dist/backends/pi-run.d.ts.map +1 -0
  31. package/dist/backends/select.d.ts +17 -0
  32. package/dist/backends/select.d.ts.map +1 -0
  33. package/dist/backends/subprocess-run.d.ts +68 -0
  34. package/dist/backends/subprocess-run.d.ts.map +1 -0
  35. package/dist/backends/types.d.ts +17 -0
  36. package/dist/backends/types.d.ts.map +1 -0
  37. package/dist/backends/unsupported.d.ts +9 -0
  38. package/dist/backends/unsupported.d.ts.map +1 -0
  39. package/dist/baseline/index.d.ts +56 -0
  40. package/dist/baseline/index.d.ts.map +1 -0
  41. package/dist/cli/args.d.ts +22 -0
  42. package/dist/cli/args.d.ts.map +1 -0
  43. package/dist/cli/embedded.d.ts +7 -0
  44. package/dist/cli/embedded.d.ts.map +1 -0
  45. package/dist/cli/main.d.ts +6 -0
  46. package/dist/cli/main.d.ts.map +1 -0
  47. package/dist/cli/version.d.ts +2 -0
  48. package/dist/cli/version.d.ts.map +1 -0
  49. package/dist/conversation/ask-user.d.ts +17 -0
  50. package/dist/conversation/ask-user.d.ts.map +1 -0
  51. package/dist/conversation/conversation.d.ts +46 -0
  52. package/dist/conversation/conversation.d.ts.map +1 -0
  53. package/dist/conversation/harness.d.ts +14 -0
  54. package/dist/conversation/harness.d.ts.map +1 -0
  55. package/dist/conversation/index.d.ts +5 -0
  56. package/dist/conversation/index.d.ts.map +1 -0
  57. package/dist/conversation/queue.d.ts +15 -0
  58. package/dist/conversation/queue.d.ts.map +1 -0
  59. package/dist/flow/accessors.d.ts +13 -0
  60. package/dist/flow/accessors.d.ts.map +1 -0
  61. package/dist/flow/approvals.d.ts +6 -0
  62. package/dist/flow/approvals.d.ts.map +1 -0
  63. package/dist/flow/args.d.ts +4 -0
  64. package/dist/flow/args.d.ts.map +1 -0
  65. package/dist/flow/context.d.ts +36 -0
  66. package/dist/flow/context.d.ts.map +1 -0
  67. package/dist/flow/index.d.ts +5 -0
  68. package/dist/flow/index.d.ts.map +1 -0
  69. package/dist/index.d.ts +16 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/loop/builder/index.d.ts +17 -0
  72. package/dist/loop/builder/index.d.ts.map +1 -0
  73. package/dist/loop/builder/types.d.ts +97 -0
  74. package/dist/loop/builder/types.d.ts.map +1 -0
  75. package/dist/loop/context/compaction.d.ts +38 -0
  76. package/dist/loop/context/compaction.d.ts.map +1 -0
  77. package/dist/loop/context/index.d.ts +4 -0
  78. package/dist/loop/context/index.d.ts.map +1 -0
  79. package/dist/loop/context/offload.d.ts +34 -0
  80. package/dist/loop/context/offload.d.ts.map +1 -0
  81. package/dist/loop/context/types.d.ts +13 -0
  82. package/dist/loop/context/types.d.ts.map +1 -0
  83. package/dist/loop/engine/bridge.d.ts +21 -0
  84. package/dist/loop/engine/bridge.d.ts.map +1 -0
  85. package/dist/loop/engine/index.d.ts +73 -0
  86. package/dist/loop/engine/index.d.ts.map +1 -0
  87. package/dist/loop/execution.d.ts +68 -0
  88. package/dist/loop/execution.d.ts.map +1 -0
  89. package/dist/loop/fanout.d.ts +148 -0
  90. package/dist/loop/fanout.d.ts.map +1 -0
  91. package/dist/loop/firing.d.ts +54 -0
  92. package/dist/loop/firing.d.ts.map +1 -0
  93. package/dist/loop/graph/analyze.d.ts +8 -0
  94. package/dist/loop/graph/analyze.d.ts.map +1 -0
  95. package/dist/loop/graph/index.d.ts +3 -0
  96. package/dist/loop/graph/index.d.ts.map +1 -0
  97. package/dist/loop/graph/types.d.ts +35 -0
  98. package/dist/loop/graph/types.d.ts.map +1 -0
  99. package/dist/loop/index.d.ts +7 -0
  100. package/dist/loop/index.d.ts.map +1 -0
  101. package/dist/loop/io/index.d.ts +4 -0
  102. package/dist/loop/io/index.d.ts.map +1 -0
  103. package/dist/loop/io/linear.d.ts +142 -0
  104. package/dist/loop/io/linear.d.ts.map +1 -0
  105. package/dist/loop/io/sink.d.ts +47 -0
  106. package/dist/loop/io/sink.d.ts.map +1 -0
  107. package/dist/loop/io/source.d.ts +64 -0
  108. package/dist/loop/io/source.d.ts.map +1 -0
  109. package/dist/loop/serve.d.ts +97 -0
  110. package/dist/loop/serve.d.ts.map +1 -0
  111. package/dist/loop/state/index.d.ts +5 -0
  112. package/dist/loop/state/index.d.ts.map +1 -0
  113. package/dist/loop/state/manifest.d.ts +36 -0
  114. package/dist/loop/state/manifest.d.ts.map +1 -0
  115. package/dist/loop/state/port.d.ts +33 -0
  116. package/dist/loop/state/port.d.ts.map +1 -0
  117. package/dist/loop/state/snapshot.d.ts +15 -0
  118. package/dist/loop/state/snapshot.d.ts.map +1 -0
  119. package/dist/loop/state/sqlite.d.ts +31 -0
  120. package/dist/loop/state/sqlite.d.ts.map +1 -0
  121. package/dist/loop/termination-contract.d.ts +28 -0
  122. package/dist/loop/termination-contract.d.ts.map +1 -0
  123. package/dist/loop/termination.d.ts +33 -0
  124. package/dist/loop/termination.d.ts.map +1 -0
  125. package/dist/model/backend-config.d.ts +30 -0
  126. package/dist/model/backend-config.d.ts.map +1 -0
  127. package/dist/model/brand.d.ts +9 -0
  128. package/dist/model/brand.d.ts.map +1 -0
  129. package/dist/model/errors.d.ts +11 -0
  130. package/dist/model/errors.d.ts.map +1 -0
  131. package/dist/model/index.d.ts +8 -0
  132. package/dist/model/index.d.ts.map +1 -0
  133. package/dist/model/json-schema.d.ts +4 -0
  134. package/dist/model/json-schema.d.ts.map +1 -0
  135. package/dist/model/result.d.ts +3 -0
  136. package/dist/model/result.d.ts.map +1 -0
  137. package/dist/model/schemas.d.ts +435 -0
  138. package/dist/model/schemas.d.ts.map +1 -0
  139. package/dist/model/structured.d.ts +9 -0
  140. package/dist/model/structured.d.ts.map +1 -0
  141. package/dist/monitor/index.d.ts +139 -0
  142. package/dist/monitor/index.d.ts.map +1 -0
  143. package/dist/plan/index.d.ts +3 -0
  144. package/dist/plan/index.d.ts.map +1 -0
  145. package/dist/plan/persistent.d.ts +23 -0
  146. package/dist/plan/persistent.d.ts.map +1 -0
  147. package/dist/plan/verdict.d.ts +9 -0
  148. package/dist/plan/verdict.d.ts.map +1 -0
  149. package/dist/review/index.d.ts +4 -0
  150. package/dist/review/index.d.ts.map +1 -0
  151. package/dist/review/loop.d.ts +117 -0
  152. package/dist/review/loop.d.ts.map +1 -0
  153. package/dist/review/reviewers.d.ts +13 -0
  154. package/dist/review/reviewers.d.ts.map +1 -0
  155. package/dist/review/tool.d.ts +20 -0
  156. package/dist/review/tool.d.ts.map +1 -0
  157. package/dist/run-output/index.d.ts +112 -0
  158. package/dist/run-output/index.d.ts.map +1 -0
  159. package/dist/runner/index.d.ts +2 -0
  160. package/dist/runner/index.d.ts.map +1 -0
  161. package/dist/runner/typecheck.d.ts +23 -0
  162. package/dist/runner/typecheck.d.ts.map +1 -0
  163. package/dist/test-utils/fakes.d.ts +76 -0
  164. package/dist/test-utils/fakes.d.ts.map +1 -0
  165. package/dist/test-utils/fixtures.d.ts +4 -0
  166. package/dist/test-utils/fixtures.d.ts.map +1 -0
  167. package/dist/test-utils/index.d.ts +4 -0
  168. package/dist/test-utils/index.d.ts.map +1 -0
  169. package/dist/test-utils/tier2.d.ts +8 -0
  170. package/dist/test-utils/tier2.d.ts.map +1 -0
  171. package/dist/tools/fs.d.ts +11 -0
  172. package/dist/tools/fs.d.ts.map +1 -0
  173. package/dist/tools/git.d.ts +10 -0
  174. package/dist/tools/git.d.ts.map +1 -0
  175. package/dist/tools/github.d.ts +13 -0
  176. package/dist/tools/github.d.ts.map +1 -0
  177. package/dist/tools/index.d.ts +7 -0
  178. package/dist/tools/index.d.ts.map +1 -0
  179. package/dist/tools/linear.d.ts +117 -0
  180. package/dist/tools/linear.d.ts.map +1 -0
  181. package/dist/tools/process.d.ts +40 -0
  182. package/dist/tools/process.d.ts.map +1 -0
  183. package/dist/tools/terminal.d.ts +23 -0
  184. package/dist/tools/terminal.d.ts.map +1 -0
  185. package/package.json +89 -9
  186. package/src/backends/acp-client.ts +324 -0
  187. package/src/backends/acp-run.ts +636 -0
  188. package/src/backends/claude-run.ts +226 -0
  189. package/src/backends/claude-stream-json.ts +273 -0
  190. package/src/backends/codex-jsonl.ts +339 -0
  191. package/src/backends/codex-run.ts +177 -0
  192. package/src/backends/codex.ts +84 -0
  193. package/src/backends/conversation-config.ts +26 -0
  194. package/src/backends/index.ts +13 -0
  195. package/src/backends/opencode-run.ts +432 -0
  196. package/src/backends/opencode-sse.ts +360 -0
  197. package/src/backends/pi-rpc.ts +242 -0
  198. package/src/backends/pi-run.ts +154 -0
  199. package/src/backends/select.ts +77 -0
  200. package/src/backends/subprocess-run.ts +305 -0
  201. package/src/backends/types.ts +29 -0
  202. package/src/backends/unsupported.ts +48 -0
  203. package/src/baseline/index.ts +437 -0
  204. package/src/cli/args.ts +109 -0
  205. package/src/cli/embedded.ts +206 -0
  206. package/src/cli/main.ts +332 -0
  207. package/src/cli/version.ts +1 -0
  208. package/src/conversation/ask-user.ts +105 -0
  209. package/src/conversation/conversation.ts +109 -0
  210. package/src/conversation/harness.ts +39 -0
  211. package/src/conversation/index.ts +4 -0
  212. package/src/conversation/queue.ts +86 -0
  213. package/src/flow/accessors.ts +43 -0
  214. package/src/flow/approvals.ts +10 -0
  215. package/src/flow/args.ts +26 -0
  216. package/src/flow/context.ts +106 -0
  217. package/src/flow/index.ts +4 -0
  218. package/src/index.ts +17 -0
  219. package/src/loop/builder/index.ts +346 -0
  220. package/src/loop/builder/types.ts +131 -0
  221. package/src/loop/context/compaction.ts +175 -0
  222. package/src/loop/context/index.ts +3 -0
  223. package/src/loop/context/offload.ts +79 -0
  224. package/src/loop/context/types.ts +17 -0
  225. package/src/loop/engine/bridge.ts +61 -0
  226. package/src/loop/engine/index.ts +166 -0
  227. package/src/loop/execution.ts +453 -0
  228. package/src/loop/fanout.ts +444 -0
  229. package/src/loop/firing.ts +188 -0
  230. package/src/loop/graph/analyze.ts +128 -0
  231. package/src/loop/graph/index.ts +2 -0
  232. package/src/loop/graph/types.ts +45 -0
  233. package/src/loop/index.ts +11 -0
  234. package/src/loop/io/index.ts +3 -0
  235. package/src/loop/io/linear.ts +714 -0
  236. package/src/loop/io/sink.ts +170 -0
  237. package/src/loop/io/source.ts +255 -0
  238. package/src/loop/serve.ts +307 -0
  239. package/src/loop/state/index.ts +4 -0
  240. package/src/loop/state/manifest.ts +56 -0
  241. package/src/loop/state/port.ts +43 -0
  242. package/src/loop/state/snapshot.ts +129 -0
  243. package/src/loop/state/sqlite.ts +265 -0
  244. package/src/loop/termination-contract.ts +89 -0
  245. package/src/loop/termination.ts +156 -0
  246. package/src/model/backend-config.ts +34 -0
  247. package/src/model/brand.ts +11 -0
  248. package/src/model/errors.ts +28 -0
  249. package/src/model/index.ts +7 -0
  250. package/src/model/json-schema.ts +14 -0
  251. package/src/model/result.ts +28 -0
  252. package/src/model/schemas.ts +218 -0
  253. package/src/model/structured.ts +26 -0
  254. package/src/monitor/index.ts +367 -0
  255. package/src/plan/index.ts +2 -0
  256. package/src/plan/persistent.ts +98 -0
  257. package/src/plan/verdict.ts +3 -0
  258. package/src/review/index.ts +3 -0
  259. package/src/review/loop.ts +365 -0
  260. package/src/review/reviewers.ts +92 -0
  261. package/src/review/tool.ts +41 -0
  262. package/src/run-output/index.ts +319 -0
  263. package/src/runner/index.ts +1 -0
  264. package/src/runner/typecheck.ts +72 -0
  265. package/src/test-utils/fakes.ts +260 -0
  266. package/src/test-utils/fixtures.ts +33 -0
  267. package/src/test-utils/index.ts +3 -0
  268. package/src/test-utils/tier2.ts +12 -0
  269. package/src/tools/fs.ts +44 -0
  270. package/src/tools/git.ts +34 -0
  271. package/src/tools/github.ts +25 -0
  272. package/src/tools/index.ts +6 -0
  273. package/src/tools/linear.ts +428 -0
  274. package/src/tools/process.ts +156 -0
  275. package/src/tools/terminal.ts +116 -0
@@ -0,0 +1,453 @@
1
+ import { err, ok, type Result } from "neverthrow";
2
+
3
+ import type { BranchObservation } from "../monitor/index.ts";
4
+ import type { RuntimeError, Usage } from "../model/index.ts";
5
+ import {
6
+ compact,
7
+ createOffloadStore,
8
+ DEFAULT_COMPACTION_CONFIG,
9
+ type CompactionConfig,
10
+ type CompactionStage,
11
+ type Observation,
12
+ type OffloadStore,
13
+ } from "./context/index.ts";
14
+ import {
15
+ makeFingerprintStuckDetector,
16
+ TokenBudgetCounter,
17
+ type FingerprintAction,
18
+ type FingerprintDetectorOptions,
19
+ type TokenUsageSummary,
20
+ } from "./termination.ts";
21
+
22
+ export type LoopExecutionStop =
23
+ | "converged"
24
+ | "unfixable"
25
+ | "stuck"
26
+ | "timeout"
27
+ | "ceiling"
28
+ | "budget-exhausted"
29
+ | "cancelled";
30
+
31
+ export type LoopExecutionAction = FingerprintAction;
32
+
33
+ export interface LoopContextPressure {
34
+ readonly offloadCount: number;
35
+ readonly compactionStages: readonly CompactionStage[];
36
+ readonly tokensBefore: number;
37
+ readonly tokensAfter: number;
38
+ readonly observationCount: number;
39
+ }
40
+
41
+ export interface LoopExecutionStepResult {
42
+ readonly usage?: Usage;
43
+ readonly observations?: readonly Observation[];
44
+ readonly branches?: readonly BranchObservation[];
45
+ readonly measure?: number;
46
+ readonly stopReason?: LoopExecutionStop;
47
+ readonly contextPressure?: LoopContextPressure;
48
+ }
49
+
50
+ export interface LoopExecutionProgress {
51
+ readonly iteration: number;
52
+ readonly usage?: Usage;
53
+ readonly branches?: readonly BranchObservation[];
54
+ readonly measure?: number;
55
+ readonly stopReason?: LoopExecutionStop;
56
+ readonly contextPressure?: LoopContextPressure;
57
+ }
58
+
59
+ export interface LoopExecutionContextOptions {
60
+ readonly root?: string;
61
+ readonly config?: CompactionConfig;
62
+ readonly offloadStore?: OffloadStore;
63
+ readonly observations?: readonly Observation[];
64
+ }
65
+
66
+ export interface LoopExecutionContextSnapshot {
67
+ readonly observations: readonly Observation[];
68
+ readonly lastPressure?: LoopContextPressure;
69
+ }
70
+
71
+ export interface LoopExecutionSummary<State> {
72
+ readonly iterations: number;
73
+ readonly converged: boolean;
74
+ readonly stop: LoopExecutionStop;
75
+ readonly lastEvaluation: State;
76
+ readonly events: readonly string[];
77
+ readonly tokenUsage?: TokenUsageSummary;
78
+ readonly context: LoopExecutionContextSnapshot;
79
+ }
80
+
81
+ export interface LoopExecutionOptions<State, Action extends LoopExecutionAction = LoopExecutionAction> {
82
+ readonly evaluate: () => Promise<Result<State, RuntimeError>>;
83
+ readonly converged: (state: State) => boolean;
84
+ readonly nextAction: (state: State) => Action | undefined;
85
+ readonly execute: (action: Action, state: State, iteration: number) => Promise<Result<unknown, RuntimeError>>;
86
+ readonly maxIterations?: number;
87
+ readonly wallClockMs?: number;
88
+ readonly tokenBudget?: number;
89
+ readonly stalled?: (state: State, action: Action) => boolean;
90
+ readonly fingerprint?:
91
+ | false
92
+ | Omit<FingerprintDetectorOptions<{ readonly state: State; readonly action: Action }>, "project">;
93
+ readonly now?: () => number;
94
+ readonly context?: LoopExecutionContextOptions;
95
+ readonly onCycle?: (progress: LoopExecutionProgress) => void;
96
+ }
97
+
98
+ export async function executeLoop<State, Action extends LoopExecutionAction = LoopExecutionAction>(
99
+ options: LoopExecutionOptions<State, Action>,
100
+ ): Promise<Result<LoopExecutionSummary<State>, RuntimeError>> {
101
+ const maxIterations = options.maxIterations ?? Number.POSITIVE_INFINITY;
102
+ const now = options.now ?? Date.now;
103
+ const startedAt = now();
104
+ const events: string[] = [];
105
+ const budget = new TokenBudgetCounter(options.tokenBudget);
106
+ const fingerprintStalled = executionFingerprintDetector(options.fingerprint);
107
+ const managedContext = createManagedContext(options.context);
108
+ let iterations = 0;
109
+
110
+ for (;;) {
111
+ const evalResult = await evaluateState(options.evaluate, events);
112
+ if (evalResult.isErr()) return err(evalResult.error);
113
+
114
+ const state = evalResult.value;
115
+ if (options.converged(state)) {
116
+ return ok(executionSummary(state, iterations, "converged", events, budget, managedContext.snapshot()));
117
+ }
118
+
119
+ const action = options.nextAction(state);
120
+ if (action === undefined) {
121
+ return ok(executionSummary(state, iterations, "unfixable", events, budget, managedContext.snapshot()));
122
+ }
123
+
124
+ if (options.stalled?.(state, action) || fingerprintStalled?.({ state, action })) {
125
+ events.push("no-progress");
126
+ return ok(executionSummary(state, iterations, "stuck", events, budget, managedContext.snapshot()));
127
+ }
128
+
129
+ if (iterations >= maxIterations) {
130
+ return ok(executionSummary(state, iterations, "ceiling", events, budget, managedContext.snapshot()));
131
+ }
132
+
133
+ if (options.wallClockMs !== undefined && now() - startedAt >= options.wallClockMs) {
134
+ return ok(executionSummary(state, iterations, "timeout", events, budget, managedContext.snapshot()));
135
+ }
136
+
137
+ events.push("fix:started");
138
+ const stepResult = await options.execute(action, state, iterations);
139
+ if (stepResult.isErr()) return err(stepResult.error);
140
+ events.push("fix:completed");
141
+ iterations++;
142
+
143
+ const step = normalizeStepResult(stepResult.value);
144
+ budget.record(step.usage);
145
+
146
+ const pressure = await managedContext.record(step.observations);
147
+ if (pressure.isErr()) return err(pressure.error);
148
+
149
+ const contextPressure = mergePressure(step.contextPressure, pressure.value);
150
+ options.onCycle?.({
151
+ iteration: iterations,
152
+ ...(step.usage === undefined ? {} : { usage: step.usage }),
153
+ ...(step.branches === undefined ? {} : { branches: step.branches }),
154
+ ...(step.measure === undefined ? {} : { measure: step.measure }),
155
+ ...(step.stopReason === undefined ? {} : { stopReason: step.stopReason }),
156
+ ...(contextPressure === undefined ? {} : { contextPressure }),
157
+ });
158
+
159
+ if (step.stopReason !== undefined) {
160
+ const refreshed = await evaluateState(options.evaluate, events);
161
+ if (refreshed.isErr()) return err(refreshed.error);
162
+ return ok(
163
+ executionSummary(refreshed.value, iterations, step.stopReason, events, budget, managedContext.snapshot()),
164
+ );
165
+ }
166
+
167
+ if (budget.exhausted) {
168
+ const refreshed = await evaluateState(options.evaluate, events);
169
+ if (refreshed.isErr()) return err(refreshed.error);
170
+ events.push("budget:exhausted");
171
+ return ok(
172
+ executionSummary(refreshed.value, iterations, "budget-exhausted", events, budget, managedContext.snapshot()),
173
+ );
174
+ }
175
+ }
176
+ }
177
+
178
+ async function evaluateState<State>(
179
+ evaluate: () => Promise<Result<State, RuntimeError>>,
180
+ events: string[],
181
+ ): Promise<Result<State, RuntimeError>> {
182
+ events.push("evaluate:started");
183
+ const evalResult = await evaluate();
184
+ if (evalResult.isErr()) return err(evalResult.error);
185
+ events.push("evaluate:completed");
186
+ return evalResult;
187
+ }
188
+
189
+ function executionSummary<State>(
190
+ state: State,
191
+ iterations: number,
192
+ stop: LoopExecutionStop,
193
+ events: readonly string[],
194
+ budget: TokenBudgetCounter,
195
+ context: LoopExecutionContextSnapshot,
196
+ ): LoopExecutionSummary<State> {
197
+ return {
198
+ iterations,
199
+ converged: stop === "converged",
200
+ stop,
201
+ lastEvaluation: state,
202
+ events,
203
+ ...(budget.shouldReport ? { tokenUsage: budget.summary() } : {}),
204
+ context,
205
+ };
206
+ }
207
+
208
+ function executionFingerprintDetector<State, Action extends LoopExecutionAction>(
209
+ fingerprint: LoopExecutionOptions<State, Action>["fingerprint"],
210
+ ): ((value: { readonly state: State; readonly action: Action }) => boolean) | undefined {
211
+ if (fingerprint === undefined || fingerprint === false) {
212
+ return undefined;
213
+ }
214
+
215
+ return makeFingerprintStuckDetector({
216
+ project: ({ action }) => action,
217
+ ...(fingerprint.windowSize === undefined ? {} : { windowSize: fingerprint.windowSize }),
218
+ ...(fingerprint.repeatThreshold === undefined ? {} : { repeatThreshold: fingerprint.repeatThreshold }),
219
+ });
220
+ }
221
+
222
+ function normalizeStepResult(value: unknown): LoopExecutionStepResult {
223
+ if (typeof value !== "object" || value === null) {
224
+ return {};
225
+ }
226
+
227
+ const record = value as Record<string, unknown>;
228
+ const usage = usageFromStepResult(record);
229
+ const observations = observationsFrom(record.observations);
230
+ const branches = branchesFrom(record.branches);
231
+ const measure = typeof record.measure === "number" ? record.measure : undefined;
232
+ const stopReason = stopReasonFrom(record.stopReason);
233
+ const contextPressure = contextPressureFrom(record.contextPressure);
234
+
235
+ return {
236
+ ...(usage === undefined ? {} : { usage }),
237
+ ...(observations === undefined ? {} : { observations }),
238
+ ...(branches === undefined ? {} : { branches }),
239
+ ...(measure === undefined ? {} : { measure }),
240
+ ...(stopReason === undefined ? {} : { stopReason }),
241
+ ...(contextPressure === undefined ? {} : { contextPressure }),
242
+ };
243
+ }
244
+
245
+ function usageFromStepResult(record: Record<string, unknown>): Usage | undefined {
246
+ const usage = record.usage;
247
+ if (typeof usage !== "object" || usage === null || !("input" in usage) || !("output" in usage)) {
248
+ return undefined;
249
+ }
250
+
251
+ const input = usage.input;
252
+ const output = usage.output;
253
+ const reasoning = "reasoning" in usage ? usage.reasoning : undefined;
254
+ if (
255
+ typeof input !== "number" ||
256
+ typeof output !== "number" ||
257
+ !Number.isInteger(input) ||
258
+ !Number.isInteger(output) ||
259
+ (reasoning !== undefined && (typeof reasoning !== "number" || !Number.isInteger(reasoning)))
260
+ ) {
261
+ return undefined;
262
+ }
263
+
264
+ return reasoning === undefined ? { input, output } : { input, output, reasoning };
265
+ }
266
+
267
+ function observationsFrom(value: unknown): readonly Observation[] | undefined {
268
+ if (!Array.isArray(value)) {
269
+ return undefined;
270
+ }
271
+ const observations: Observation[] = [];
272
+ for (const item of value) {
273
+ if (typeof item !== "object" || item === null) {
274
+ return undefined;
275
+ }
276
+ const record = item as Record<string, unknown>;
277
+ if (typeof record.id !== "string" || typeof record.content !== "string") {
278
+ return undefined;
279
+ }
280
+ observations.push({
281
+ id: record.id,
282
+ content: record.content,
283
+ ...(record.pinned === true ? { pinned: true } : {}),
284
+ ...(typeof record.tokens === "number" ? { tokens: record.tokens } : {}),
285
+ });
286
+ }
287
+ return observations;
288
+ }
289
+
290
+ function branchesFrom(value: unknown): readonly BranchObservation[] | undefined {
291
+ if (!Array.isArray(value)) {
292
+ return undefined;
293
+ }
294
+ const branches: BranchObservation[] = [];
295
+ for (const item of value) {
296
+ if (typeof item !== "object" || item === null) {
297
+ return undefined;
298
+ }
299
+ const record = item as Record<string, unknown>;
300
+ if (
301
+ typeof record.id !== "string" ||
302
+ (record.status !== "completed" && record.status !== "failed")
303
+ ) {
304
+ return undefined;
305
+ }
306
+ const usage = usageFromStepResult(record);
307
+ branches.push({
308
+ id: record.id,
309
+ status: record.status,
310
+ ...(usage === undefined ? {} : { usage }),
311
+ });
312
+ }
313
+ return branches;
314
+ }
315
+
316
+ function stopReasonFrom(value: unknown): LoopExecutionStop | undefined {
317
+ switch (value) {
318
+ case "converged":
319
+ case "unfixable":
320
+ case "stuck":
321
+ case "timeout":
322
+ case "ceiling":
323
+ case "budget-exhausted":
324
+ case "cancelled":
325
+ return value;
326
+ default:
327
+ return undefined;
328
+ }
329
+ }
330
+
331
+ function contextPressureFrom(value: unknown): LoopContextPressure | undefined {
332
+ if (typeof value !== "object" || value === null) {
333
+ return undefined;
334
+ }
335
+ const record = value as Record<string, unknown>;
336
+ if (
337
+ typeof record.offloadCount !== "number" ||
338
+ !Array.isArray(record.compactionStages) ||
339
+ typeof record.tokensBefore !== "number" ||
340
+ typeof record.tokensAfter !== "number" ||
341
+ typeof record.observationCount !== "number"
342
+ ) {
343
+ return undefined;
344
+ }
345
+ const stages = record.compactionStages.filter(isCompactionStage);
346
+ if (stages.length !== record.compactionStages.length) {
347
+ return undefined;
348
+ }
349
+ return {
350
+ offloadCount: record.offloadCount,
351
+ compactionStages: stages,
352
+ tokensBefore: record.tokensBefore,
353
+ tokensAfter: record.tokensAfter,
354
+ observationCount: record.observationCount,
355
+ };
356
+ }
357
+
358
+ function isCompactionStage(value: unknown): value is CompactionStage {
359
+ return value === "mask" || value === "prune" || value === "summarize";
360
+ }
361
+
362
+ function createManagedContext(options: LoopExecutionContextOptions | undefined): {
363
+ record: (observations: readonly Observation[] | undefined) => Promise<Result<LoopContextPressure | undefined, RuntimeError>>;
364
+ snapshot: () => LoopExecutionContextSnapshot;
365
+ } {
366
+ if (options === undefined) {
367
+ return {
368
+ record: () => Promise.resolve(ok(undefined)),
369
+ snapshot: () => ({ observations: [] }),
370
+ };
371
+ }
372
+
373
+ const config = options.config ?? DEFAULT_COMPACTION_CONFIG;
374
+ const offloadStore =
375
+ options.offloadStore ??
376
+ createOffloadStore({
377
+ root: options.root ?? process.cwd(),
378
+ thresholdChars: config.offloadThresholdChars,
379
+ });
380
+ let observations = [...(options.observations ?? [])];
381
+ let lastPressure: LoopContextPressure | undefined;
382
+
383
+ return {
384
+ async record(nextObservations) {
385
+ let offloadCount = 0;
386
+ if (nextObservations !== undefined) {
387
+ const bounded: Observation[] = [];
388
+ for (const observation of nextObservations) {
389
+ const intercepted = await offloadStore.intercept(observation.content);
390
+ if (intercepted.isErr()) {
391
+ return err(intercepted.error);
392
+ }
393
+ if (intercepted.value.offloaded) {
394
+ offloadCount++;
395
+ bounded.push({
396
+ id: observation.id,
397
+ content: intercepted.value.ref,
398
+ ...(observation.pinned === undefined ? {} : { pinned: observation.pinned }),
399
+ });
400
+ } else {
401
+ bounded.push(observation);
402
+ }
403
+ }
404
+ observations = [...observations, ...bounded];
405
+ }
406
+
407
+ if (observations.length === 0) {
408
+ return ok(undefined);
409
+ }
410
+
411
+ const compacted = compact(observations, config);
412
+ observations = [...compacted.observations];
413
+ if (offloadCount === 0 && compacted.stagesApplied.length === 0) {
414
+ return ok(undefined);
415
+ }
416
+ const pressure = {
417
+ offloadCount,
418
+ compactionStages: compacted.stagesApplied,
419
+ tokensBefore: compacted.tokensBefore,
420
+ tokensAfter: compacted.tokensAfter,
421
+ observationCount: observations.length,
422
+ };
423
+ lastPressure = pressure;
424
+ return ok(pressure);
425
+ },
426
+
427
+ snapshot() {
428
+ return {
429
+ observations: [...observations],
430
+ ...(lastPressure === undefined ? {} : { lastPressure }),
431
+ };
432
+ },
433
+ };
434
+ }
435
+
436
+ function mergePressure(
437
+ explicit: LoopContextPressure | undefined,
438
+ managed: LoopContextPressure | undefined,
439
+ ): LoopContextPressure | undefined {
440
+ if (explicit === undefined) {
441
+ return managed;
442
+ }
443
+ if (managed === undefined) {
444
+ return explicit;
445
+ }
446
+ return {
447
+ offloadCount: explicit.offloadCount + managed.offloadCount,
448
+ compactionStages: [...explicit.compactionStages, ...managed.compactionStages],
449
+ tokensBefore: Math.max(explicit.tokensBefore, managed.tokensBefore),
450
+ tokensAfter: managed.tokensAfter,
451
+ observationCount: managed.observationCount,
452
+ };
453
+ }