@visual-z/xeno 0.1.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.
Files changed (180) hide show
  1. package/README.md +498 -0
  2. package/docs/install.md +130 -0
  3. package/package.json +43 -0
  4. package/src/apps/approve.ts +49 -0
  5. package/src/apps/cli.ts +37 -0
  6. package/src/apps/commands.ts +226 -0
  7. package/src/apps/completions.ts +28 -0
  8. package/src/apps/input.ts +297 -0
  9. package/src/apps/render.ts +207 -0
  10. package/src/apps/shell.ts +2353 -0
  11. package/src/apps/startup.ts +1302 -0
  12. package/src/core/benchmark.ts +195 -0
  13. package/src/core/boundary.ts +174 -0
  14. package/src/core/churn.ts +59 -0
  15. package/src/core/completion-review.ts +66 -0
  16. package/src/core/context-policy.ts +60 -0
  17. package/src/core/context-window.ts +54 -0
  18. package/src/core/continuation.ts +114 -0
  19. package/src/core/cycle.ts +17 -0
  20. package/src/core/data-query.ts +201 -0
  21. package/src/core/deadline.ts +39 -0
  22. package/src/core/derive.ts +489 -0
  23. package/src/core/edit-plan.ts +200 -0
  24. package/src/core/environment.ts +29 -0
  25. package/src/core/event-validation.ts +99 -0
  26. package/src/core/events.ts +486 -0
  27. package/src/core/evidence-links.ts +71 -0
  28. package/src/core/evidence.ts +89 -0
  29. package/src/core/execution.ts +12 -0
  30. package/src/core/exhaustive.ts +18 -0
  31. package/src/core/experiment.ts +40 -0
  32. package/src/core/export.ts +169 -0
  33. package/src/core/file-references.ts +35 -0
  34. package/src/core/frontmatter.ts +213 -0
  35. package/src/core/handoff.ts +397 -0
  36. package/src/core/harness-comparison.ts +90 -0
  37. package/src/core/history-query.ts +103 -0
  38. package/src/core/history.ts +100 -0
  39. package/src/core/json-event.ts +135 -0
  40. package/src/core/json-pointer.ts +46 -0
  41. package/src/core/keymap.ts +241 -0
  42. package/src/core/observation-view.ts +87 -0
  43. package/src/core/outcome.ts +45 -0
  44. package/src/core/overflow.ts +59 -0
  45. package/src/core/permissions.ts +182 -0
  46. package/src/core/policy-evaluation.ts +55 -0
  47. package/src/core/progress.ts +101 -0
  48. package/src/core/project.ts +372 -0
  49. package/src/core/prompts.ts +83 -0
  50. package/src/core/queue.ts +60 -0
  51. package/src/core/read-bounds.ts +15 -0
  52. package/src/core/recall-analysis.ts +90 -0
  53. package/src/core/recall-trial.ts +153 -0
  54. package/src/core/response-recovery.ts +46 -0
  55. package/src/core/scheduling.ts +8 -0
  56. package/src/core/settings.ts +174 -0
  57. package/src/core/skills.ts +121 -0
  58. package/src/core/stall.ts +48 -0
  59. package/src/core/text-normalize.ts +81 -0
  60. package/src/core/theme.ts +179 -0
  61. package/src/core/tokens.ts +149 -0
  62. package/src/core/tool-contract.ts +102 -0
  63. package/src/core/tool-input.ts +38 -0
  64. package/src/core/transcript.ts +60 -0
  65. package/src/core/window-trial.ts +65 -0
  66. package/src/core/work-state.ts +103 -0
  67. package/src/core/yaml.ts +282 -0
  68. package/src/index.ts +10 -0
  69. package/src/runtime/auth.ts +255 -0
  70. package/src/runtime/benchmark.ts +187 -0
  71. package/src/runtime/catalog.ts +365 -0
  72. package/src/runtime/evidence-notes.ts +33 -0
  73. package/src/runtime/exec-sessions.ts +214 -0
  74. package/src/runtime/execution-control.ts +39 -0
  75. package/src/runtime/harness-comparison.ts +356 -0
  76. package/src/runtime/history-tools.ts +108 -0
  77. package/src/runtime/loop.ts +1983 -0
  78. package/src/runtime/model.ts +614 -0
  79. package/src/runtime/models-fetch.ts +155 -0
  80. package/src/runtime/oauth-device.ts +86 -0
  81. package/src/runtime/oauth.ts +759 -0
  82. package/src/runtime/openai-responses.ts +286 -0
  83. package/src/runtime/openai.ts +675 -0
  84. package/src/runtime/provider.ts +498 -0
  85. package/src/runtime/recall-trial.ts +226 -0
  86. package/src/runtime/resume.ts +66 -0
  87. package/src/runtime/retry.ts +297 -0
  88. package/src/runtime/tools.ts +1193 -0
  89. package/src/runtime/usage.ts +225 -0
  90. package/src/runtime/window-trial.ts +140 -0
  91. package/src/runtime/working-notes.ts +30 -0
  92. package/src/store/atomic.ts +31 -0
  93. package/src/store/auth-store.ts +50 -0
  94. package/src/store/blobs.ts +142 -0
  95. package/src/store/command.ts +75 -0
  96. package/src/store/config.ts +69 -0
  97. package/src/store/context-files.ts +356 -0
  98. package/src/store/file-index.ts +81 -0
  99. package/src/store/git-branch.ts +35 -0
  100. package/src/store/history-search.ts +19 -0
  101. package/src/store/log.ts +642 -0
  102. package/src/store/models.ts +41 -0
  103. package/src/store/observations.ts +181 -0
  104. package/src/store/query-source.ts +44 -0
  105. package/src/store/recovery.ts +39 -0
  106. package/src/store/replay.ts +63 -0
  107. package/src/store/resources.ts +361 -0
  108. package/src/store/restore.ts +302 -0
  109. package/src/store/search.ts +644 -0
  110. package/src/store/serial.ts +13 -0
  111. package/src/store/sessions.ts +463 -0
  112. package/src/store/trust.ts +69 -0
  113. package/src/store/workspace.ts +256 -0
  114. package/src/store/worktree.ts +233 -0
  115. package/src/tui/ansi.ts +67 -0
  116. package/src/tui/autocomplete.ts +29 -0
  117. package/src/tui/complete.ts +87 -0
  118. package/src/tui/diff.ts +160 -0
  119. package/src/tui/editor.ts +893 -0
  120. package/src/tui/exhaustive.ts +14 -0
  121. package/src/tui/fuzzy.ts +75 -0
  122. package/src/tui/host.ts +223 -0
  123. package/src/tui/input.ts +69 -0
  124. package/src/tui/keys.ts +378 -0
  125. package/src/tui/markdown.ts +17 -0
  126. package/src/tui/render.ts +153 -0
  127. package/src/tui/select.ts +240 -0
  128. package/src/tui/settings.ts +40 -0
  129. package/src/tui/statusline.ts +162 -0
  130. package/src/tui/style.ts +205 -0
  131. package/src/tui/terminal.ts +30 -0
  132. package/src/tui/text.ts +52 -0
  133. package/src/tui/toolview.ts +386 -0
  134. package/src/tui/transcript.ts +71 -0
  135. package/src/tui/tree.ts +405 -0
  136. package/src/tui/width.ts +567 -0
  137. package/src/vendor/pi-tui/LICENSE.upstream +21 -0
  138. package/src/vendor/pi-tui/VENDOR.md +23 -0
  139. package/src/vendor/pi-tui/alt-screen-search.ts +327 -0
  140. package/src/vendor/pi-tui/autocomplete.ts +835 -0
  141. package/src/vendor/pi-tui/components/alt-screen-flash.ts +51 -0
  142. package/src/vendor/pi-tui/components/box.ts +167 -0
  143. package/src/vendor/pi-tui/components/cancellable-loader.ts +40 -0
  144. package/src/vendor/pi-tui/components/editor.ts +2470 -0
  145. package/src/vendor/pi-tui/components/h-stack.ts +44 -0
  146. package/src/vendor/pi-tui/components/image.ts +127 -0
  147. package/src/vendor/pi-tui/components/input.ts +494 -0
  148. package/src/vendor/pi-tui/components/loader.ts +101 -0
  149. package/src/vendor/pi-tui/components/markdown.ts +1015 -0
  150. package/src/vendor/pi-tui/components/mouse-region.ts +33 -0
  151. package/src/vendor/pi-tui/components/scroll-view.ts +224 -0
  152. package/src/vendor/pi-tui/components/select-list.ts +273 -0
  153. package/src/vendor/pi-tui/components/settings-list.ts +328 -0
  154. package/src/vendor/pi-tui/components/spacer.ts +28 -0
  155. package/src/vendor/pi-tui/components/stack.ts +154 -0
  156. package/src/vendor/pi-tui/components/text.ts +107 -0
  157. package/src/vendor/pi-tui/components/truncated-text.ts +65 -0
  158. package/src/vendor/pi-tui/components/v-stack.ts +33 -0
  159. package/src/vendor/pi-tui/editor-component.ts +74 -0
  160. package/src/vendor/pi-tui/fuzzy.ts +138 -0
  161. package/src/vendor/pi-tui/index.ts +156 -0
  162. package/src/vendor/pi-tui/keybindings.ts +320 -0
  163. package/src/vendor/pi-tui/keys.ts +1401 -0
  164. package/src/vendor/pi-tui/kill-ring.ts +46 -0
  165. package/src/vendor/pi-tui/latex.ts +1506 -0
  166. package/src/vendor/pi-tui/layout-node.ts +51 -0
  167. package/src/vendor/pi-tui/layout.ts +449 -0
  168. package/src/vendor/pi-tui/native-modifiers.ts +13 -0
  169. package/src/vendor/pi-tui/native-module-path.ts +31 -0
  170. package/src/vendor/pi-tui/native-platform.ts +63 -0
  171. package/src/vendor/pi-tui/stdin-buffer.ts +444 -0
  172. package/src/vendor/pi-tui/terminal-colors.ts +73 -0
  173. package/src/vendor/pi-tui/terminal-image.ts +696 -0
  174. package/src/vendor/pi-tui/terminal.ts +547 -0
  175. package/src/vendor/pi-tui/tui-alt-screen.ts +1742 -0
  176. package/src/vendor/pi-tui/tui-main-screen.ts +655 -0
  177. package/src/vendor/pi-tui/tui.ts +1456 -0
  178. package/src/vendor/pi-tui/undo-stack.ts +28 -0
  179. package/src/vendor/pi-tui/utils.ts +1345 -0
  180. package/src/vendor/pi-tui/word-navigation.ts +117 -0
@@ -0,0 +1,195 @@
1
+ /** Evaluation contracts, separate from production policies (research M.3). */
2
+ import { shuffled } from "./experiment.ts";
3
+
4
+ export const LONGBENCH_SOURCE = {
5
+ revision: "2b48e494f2c7a2f0af81aae178e05c7e1dde0fe9",
6
+ sha256: "15d61c22d92c96900b3c4948b6aeea218d3214b676a65df48e7b8555604c7fe2",
7
+ scorerRevision: "2e00731f8d0bff23dc4325161044d0ed8af94c1e",
8
+ } as const;
9
+
10
+ export const BENCHMARK_ARMS = ["node", "replay", "mask"] as const;
11
+ export type BenchmarkArm = typeof BENCHMARK_ARMS[number];
12
+ export type Choice = "A" | "B" | "C" | "D";
13
+ export interface BenchmarkItem {
14
+ _id: string;
15
+ domain: string;
16
+ sub_domain: string;
17
+ difficulty: "easy" | "hard";
18
+ length: "short" | "medium" | "long";
19
+ question: string;
20
+ choice_A: string;
21
+ choice_B: string;
22
+ choice_C: string;
23
+ choice_D: string;
24
+ answer: Choice;
25
+ context: string;
26
+ }
27
+
28
+ function isBenchmarkItem(value: unknown): value is BenchmarkItem {
29
+ if (typeof value !== "object" || value === null) return false;
30
+ const row = value as Record<string, unknown>;
31
+ for (const key of ["_id", "domain", "sub_domain", "question", "choice_A", "choice_B", "choice_C", "choice_D", "context"]) {
32
+ if (typeof row[key] !== "string" || row[key].length === 0) return false;
33
+ }
34
+ return (row["answer"] === "A" || row["answer"] === "B" || row["answer"] === "C" || row["answer"] === "D") &&
35
+ (row["difficulty"] === "easy" || row["difficulty"] === "hard") &&
36
+ (row["length"] === "short" || row["length"] === "medium" || row["length"] === "long");
37
+ }
38
+
39
+ export function parseBenchmark(data: unknown): BenchmarkItem[] {
40
+ if (!Array.isArray(data) || data.length === 0) throw new Error("empty or invalid benchmark");
41
+ const ids = new Set<string>();
42
+ return data.map((item: unknown) => {
43
+ if (!isBenchmarkItem(item)) throw new Error("invalid benchmark item");
44
+ if (ids.has(item._id)) throw new Error(`duplicate benchmark id: ${item._id}`);
45
+ ids.add(item._id);
46
+ return item;
47
+ });
48
+ }
49
+
50
+ /** Exact LongBench v2 pred.py extraction, pinned in docs/benchmarks.md (MIT). */
51
+ export function extractBenchmarkAnswer(response: string): Choice | null {
52
+ const plain = response.replaceAll("*", "");
53
+ return (plain.match(/The correct answer is \(([A-D])\)/)?.[1] ??
54
+ plain.match(/The correct answer is ([A-D])/)?.[1] ?? null) as Choice | null;
55
+ }
56
+
57
+ /** Domain-balanced sampling; neither correctness nor answer labels enter selection. */
58
+ export function sampleBenchmark(items: readonly BenchmarkItem[], count: number, seed: number): BenchmarkItem[] {
59
+ if (!Number.isSafeInteger(count) || count < 1 || count > items.length) throw new RangeError("invalid sample count");
60
+ const domains = [...new Set(items.map((item) => item.domain))].sort();
61
+ const buckets = shuffled(domains, seed).map((domain, index) => shuffled(
62
+ items.filter((item) => item.domain === domain).toSorted((a, b) => a._id.localeCompare(b._id)), seed + index + 1,
63
+ ));
64
+ const out: BenchmarkItem[] = [];
65
+ for (let round = 0; out.length < count; round++) {
66
+ for (const bucket of buckets) {
67
+ const item = bucket[round];
68
+ if (item !== undefined && out.length < count) out.push(item);
69
+ }
70
+ }
71
+ return out;
72
+ }
73
+
74
+ export interface BenchmarkJob { id: string; arm: BenchmarkArm }
75
+ export type HarnessProfile = "control" | "enhanced";
76
+ export interface HarnessJob { id: string; arm: "node" | "replay"; profile: HarnessProfile }
77
+
78
+ /** A Latin rotation balances each treatment's position before running any model. */
79
+ export function harnessSchedule(items: readonly BenchmarkItem[], seed: number): HarnessJob[] {
80
+ const treatments = shuffled([
81
+ { arm: "node", profile: "control" }, { arm: "replay", profile: "control" },
82
+ { arm: "node", profile: "enhanced" }, { arm: "replay", profile: "enhanced" },
83
+ ] as const, seed);
84
+ const offsets = shuffled(items.map((_, index) => index % treatments.length), seed + 1);
85
+ return items.flatMap((item, index) => treatments.map((_, position) => ({
86
+ id: item._id, ...treatments[(position + offsets[index]!) % treatments.length]!,
87
+ })));
88
+ }
89
+
90
+ export function summarizeHarness(plan: readonly HarnessJob[], results: readonly (BenchmarkOutcome & { profile: HarnessProfile })[]) {
91
+ return (["control", "enhanced"] as const).flatMap((profile) => summarizeBenchmark(
92
+ plan.filter((job) => job.profile === profile), results.filter((result) => result.profile === profile),
93
+ ).filter((row) => row.planned > 0).map((row) => ({ profile, ...row })));
94
+ }
95
+ /** Pair nearby in time, randomize treatment order inside each task, once before running. */
96
+ export function benchmarkSchedule(items: readonly BenchmarkItem[], seed: number): BenchmarkJob[] {
97
+ // Balance all six orders before shuffling. Adjacent LCG seeds yield correlated
98
+ // first draws and can accidentally put the same arm last in every block.
99
+ const orders: readonly (readonly BenchmarkArm[])[] = [
100
+ ["node", "replay", "mask"], ["node", "mask", "replay"], ["replay", "node", "mask"],
101
+ ["replay", "mask", "node"], ["mask", "node", "replay"], ["mask", "replay", "node"],
102
+ ];
103
+ const balanced = shuffled(items.map((_, index) => orders[index % orders.length]!), seed);
104
+ return items.flatMap((item, index) => balanced[index]!.map((arm) => ({ id: item._id, arm })));
105
+ }
106
+
107
+ /** Split losslessly, including surrogate pairs; chunks are files, not a context truncation. */
108
+ export function benchmarkChunks(context: string, size = 16_000): string[] {
109
+ if (!Number.isSafeInteger(size) || size < 2) throw new RangeError("chunk size must be at least two");
110
+ const chunks: string[] = [];
111
+ for (let start = 0; start < context.length;) {
112
+ let end = Math.min(context.length, start + size);
113
+ const last = context.charCodeAt(end - 1);
114
+ if (end < context.length && last >= 0xd800 && last <= 0xdbff) end--;
115
+ chunks.push(context.slice(start, end));
116
+ start = end;
117
+ }
118
+ return chunks;
119
+ }
120
+
121
+ /** Explicit field selection prevents evaluator-only metadata from leaking to the agent. */
122
+ export function benchmarkPrompt(item: BenchmarkItem, chunks: number): string {
123
+ return `Answer the question using the complete source document in corpus/part-00000.txt through corpus/part-${String(chunks - 1).padStart(5, "0")}.txt. ` +
124
+ "The numbered files concatenate in order to the original document; none of it was removed. Use read and grep to examine the evidence.\n\n" +
125
+ `Question: ${item.question}\nChoices:\n(A) ${item.choice_A}\n(B) ${item.choice_B}\n(C) ${item.choice_C}\n(D) ${item.choice_D}\n\n` +
126
+ 'Format your final response as follows: "The correct answer is (insert answer here)".';
127
+ }
128
+
129
+ export interface BenchmarkPrice { input: number; output: number; cacheRead: number }
130
+ export interface BenchmarkUsage { input_tokens: number; output_tokens: number; cache_read_input_tokens: number; cache_creation_input_tokens: number }
131
+
132
+ /** A conservative allowance, not a tokenizer: wire bytes plus framing, at uncached peak rates. */
133
+ export function requestReservation(wireBytes: number, maxOutput: number, price: BenchmarkPrice): number {
134
+ if (!Number.isSafeInteger(wireBytes) || wireBytes < 0 || !Number.isSafeInteger(maxOutput) || maxOutput < 1) throw new RangeError("invalid request size");
135
+ return ((wireBytes + 4096) * price.input + maxOutput * price.output) / 1_000_000;
136
+ }
137
+
138
+ export function measuredCharge(usage: BenchmarkUsage, price: BenchmarkPrice): number | null {
139
+ const counts = [usage.input_tokens, usage.output_tokens, usage.cache_read_input_tokens, usage.cache_creation_input_tokens];
140
+ if (counts.some((value) => !Number.isSafeInteger(value) || value < 0) ||
141
+ usage.input_tokens + usage.cache_read_input_tokens + usage.cache_creation_input_tokens === 0 || usage.output_tokens === 0) return null;
142
+ return ((usage.input_tokens + usage.cache_creation_input_tokens) * price.input +
143
+ usage.cache_read_input_tokens * price.cacheRead + usage.output_tokens * price.output) / 1_000_000;
144
+ }
145
+
146
+ export function canReserve(charged: number, reserve: number, budget: number): boolean {
147
+ if (![charged, reserve, budget].every(Number.isFinite) || charged < 0 || reserve <= 0 || budget <= 0) return false;
148
+ return charged + reserve <= budget;
149
+ }
150
+
151
+ /** Unknown outcomes keep their reservation; an unexpectedly high bill is never clipped. */
152
+ export function settledCharge(reserve: number, measured: number | null): number {
153
+ return measured === null ? reserve : measured;
154
+ }
155
+
156
+ export function wilsonInterval(correct: number, total: number): [number, number] | null {
157
+ if (!Number.isSafeInteger(total) || !Number.isSafeInteger(correct) || correct < 0 || total < correct) throw new RangeError("invalid score");
158
+ if (total === 0) return null;
159
+ const z = 1.959963984540054;
160
+ const p = correct / total;
161
+ const d = 1 + z * z / total;
162
+ const center = (p + z * z / (2 * total)) / d;
163
+ const radius = z * Math.sqrt(p * (1 - p) / total + z * z / (4 * total * total)) / d;
164
+ return [Math.max(0, center - radius), Math.min(1, center + radius)];
165
+ }
166
+
167
+ export interface BenchmarkOutcome {
168
+ id: string;
169
+ arm: BenchmarkArm;
170
+ status: "answered" | "error" | "budget" | "stopped";
171
+ correct: boolean;
172
+ }
173
+
174
+ export function summarizeBenchmark(plan: readonly BenchmarkJob[], outcomes: readonly BenchmarkOutcome[]) {
175
+ const planned = new Set(plan.map((job) => `${job.id}/${job.arm}`));
176
+ const seen = new Set<string>();
177
+ for (const outcome of outcomes) {
178
+ const key = `${outcome.id}/${outcome.arm}`;
179
+ if (!planned.has(key) || seen.has(key)) throw new Error("unknown or duplicate benchmark outcome");
180
+ if (outcome.correct && outcome.status !== "answered") throw new Error("unfinished result cannot be correct");
181
+ seen.add(key);
182
+ }
183
+ return BENCHMARK_ARMS.map((arm) => {
184
+ const rows = outcomes.filter((row) => row.arm === arm);
185
+ const total = plan.filter((job) => job.arm === arm).length;
186
+ const correct = rows.filter((row) => row.correct).length;
187
+ const attempted = rows.length;
188
+ const answered = rows.filter((row) => row.status === "answered").length;
189
+ return { arm, planned: total, attempted, answered, correct, missing: total - attempted,
190
+ accuracy: attempted > 0 ? correct / attempted : null,
191
+ interval95: wilsonInterval(correct, attempted),
192
+ plannedSolvedFraction: total > 0 ? correct / total : null,
193
+ complete: attempted === total };
194
+ });
195
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Explicit fixed-segment policy (decisions D1, D4, D5; new default in N.8).
3
+ *
4
+ * Pure. Given a token count and where the loop currently stands, decide whether
5
+ * to close the segment.
6
+ *
7
+ * Three rules, each with a reason:
8
+ *
9
+ * ONLY AT A TURN BOUNDARY (D5). A cut between a tool_use and its tool_result
10
+ * would produce an unpairable request. The gap between trigger and hard limit is
11
+ * the headroom that makes waiting for the boundary safe.
12
+ *
13
+ * THE MODEL MAY ADVANCE A CUT BUT NEVER DEFER ONE (D4). A checkpoint request at
14
+ * a clean semantic boundary is usually better than an arbitrary threshold, so it
15
+ * is honoured -- but the hard limit is checked first and cannot be overridden.
16
+ *
17
+ * ABSOLUTE TOKENS, NOT A PERCENTAGE OF THE WINDOW. Cost and quality both argue
18
+ * for tens of thousands of tokens, and neither argument scales with whatever
19
+ * window the model happens to advertise.
20
+ *
21
+ * On the defaults below: the optimum from the cost model is L - H = 5*sqrt(H*d),
22
+ * where H is handoff size and d is per-turn growth. H was originally *assumed* to
23
+ * be ~10k, which put the optimum near 37k and the thresholds at 60k/80k.
24
+ * The original single-segment measurement (prev=null) yielded 366 to 3201
25
+ * tokens, 1.6-7.6% of L. It did not measure accumulation across boundaries.
26
+ * D20 adds bounded carried state and continuous-history measurements; full
27
+ * derivation state still grows with distinct facts. The old local cost optimum
28
+ * is a workload diagnostic, not proof of a globally optimal threshold.
29
+ *
30
+ * The defaults are 30k/40k rather than 15k/25k because the cost model prices only
31
+ * bytes. Two things it does not price argue for longer segments: the re-read a new
32
+ * segment usually needs before it can act (the digest check on file_anchors), and
33
+ * the fact that too-short segments break stateful work -- the documented failure
34
+ * mode of fresh-context loops is that extended debugging loses context that was
35
+ * load-bearing. 30k/40k is within 15% of the cost optimum for every re-read cost
36
+ * from 0 to 24k tokens per boundary, which 60k/80k is not.
37
+ */
38
+
39
+ import type { CutReason } from "./events.ts";
40
+
41
+ export interface BoundaryConfig {
42
+ /** Start looking for a turn boundary at or above this count. */
43
+ triggerTokens: number;
44
+ /** Cut at the next turn boundary regardless. Must exceed triggerTokens. */
45
+ hardTokens: number;
46
+ /**
47
+ * How much of the context a node must have produced itself before cutting is
48
+ * worth it.
49
+ *
50
+ * Every node starts with tokens it did not create: the frozen prefix plus,
51
+ * after the first cut, the handoff. Without this floor, a trigger set below
52
+ * that inherited weight makes the first turn of every node cut -- a real run
53
+ * produced 36 nodes for a five-file task, spending its budget on handoffs.
54
+ *
55
+ * The value follows the crossover: a handoff only pays for itself once the
56
+ * node's own history exceeds it. Measured handoffs ran 366-3201 tokens, so
57
+ * the default sits above the top of that range.
58
+ *
59
+ * This is also the floor for a model-requested checkpoint. A separate knob
60
+ * for that case would be the same idea written twice: a checkpoint on a node
61
+ * that has produced nothing costs a handoff write and discards the node's
62
+ * cache for no gain, which is exactly what this floor exists to prevent.
63
+ */
64
+ minNodeWorkTokens: number;
65
+ }
66
+
67
+ export const DEFAULT_BOUNDARY: BoundaryConfig = {
68
+ triggerTokens: 30_000,
69
+ hardTokens: 40_000,
70
+ minNodeWorkTokens: 5_000,
71
+ };
72
+
73
+ export interface BoundaryInput {
74
+ tokens: number;
75
+ /**
76
+ * Tokens the node did not produce: the frozen prefix, plus the rendered
77
+ * handoff for any node after the first. Cutting cannot reduce these, so they
78
+ * do not count toward the node having earned a boundary.
79
+ */
80
+ inheritedTokens?: number;
81
+ /** True only between a completed assistant/tool exchange and the next request. */
82
+ atTurnBoundary: boolean;
83
+ /** Set when the model called the checkpoint tool during this turn. */
84
+ checkpointRequested?: boolean;
85
+ config?: Partial<BoundaryConfig>;
86
+ }
87
+
88
+ export type BoundaryDecision =
89
+ | { cut: true; reason: CutReason }
90
+ | {
91
+ cut: false;
92
+ reason:
93
+ | "below_trigger"
94
+ | "awaiting_turn_boundary"
95
+ | "below_min_node_work"
96
+ /**
97
+ * The inherited floor alone is at or past the hard limit, so a cut
98
+ * would hand the next node a context it cannot work in either. The
99
+ * thresholds need widening or the frozen prefix shrinking; cutting
100
+ * would only thrash.
101
+ */
102
+ | "inherited_floor_exceeds_limit";
103
+ /**
104
+ * True when the count is already past the hard limit and a cut is not yet
105
+ * possible -- i.e. a single turn consumed more than the headroom. The caller
106
+ * should surface this: the thresholds need retuning, or one tool returned an
107
+ * output large enough to need its own handling.
108
+ */
109
+ overrun: boolean;
110
+ };
111
+
112
+ export function shouldCut(input: BoundaryInput): BoundaryDecision {
113
+ const cfg = { ...DEFAULT_BOUNDARY, ...input.config };
114
+ if (cfg.hardTokens <= cfg.triggerTokens) {
115
+ throw new RangeError(
116
+ `boundary: hardTokens (${cfg.hardTokens}) must exceed triggerTokens (${cfg.triggerTokens}); ` +
117
+ "the difference is the headroom needed to reach a turn boundary",
118
+ );
119
+ }
120
+ // The mirror of thrashing, and just as silent: a work floor at or above the
121
+ // trigger means the trigger can never be acted on, so the session grows
122
+ // without bound and nothing says why.
123
+ if (cfg.minNodeWorkTokens >= cfg.triggerTokens) {
124
+ throw new RangeError(
125
+ `boundary: minNodeWorkTokens (${cfg.minNodeWorkTokens}) must be below triggerTokens ` +
126
+ `(${cfg.triggerTokens}); otherwise no node can ever reach a boundary`,
127
+ );
128
+ }
129
+
130
+ const { tokens, atTurnBoundary, checkpointRequested = false, inheritedTokens = 0 } = input;
131
+ const ownWork = Math.max(0, tokens - inheritedTokens);
132
+
133
+ if (!atTurnBoundary) {
134
+ return {
135
+ cut: false,
136
+ reason: "awaiting_turn_boundary",
137
+ overrun: tokens >= cfg.hardTokens,
138
+ };
139
+ }
140
+
141
+ // Is there any reason to consider a boundary at all?
142
+ const wouldCut =
143
+ tokens >= cfg.hardTokens || checkpointRequested || tokens >= cfg.triggerTokens;
144
+ if (!wouldCut) return { cut: false, reason: "below_trigger", overrun: false };
145
+
146
+ // There is -- but a node that has produced almost nothing cannot be helped
147
+ // by one: the next node inherits the same floor and lands in the same place.
148
+ if (ownWork < cfg.minNodeWorkTokens) {
149
+ if (inheritedTokens >= cfg.hardTokens) {
150
+ return { cut: false, reason: "inherited_floor_exceeds_limit", overrun: true };
151
+ }
152
+ return { cut: false, reason: "below_min_node_work", overrun: tokens >= cfg.hardTokens };
153
+ }
154
+
155
+ // Worth cutting. The hard limit is checked first so the model cannot defer
156
+ // it by asking for a checkpoint.
157
+ if (tokens >= cfg.hardTokens) return { cut: true, reason: "hard_limit" };
158
+ if (checkpointRequested) return { cut: true, reason: "model_checkpoint" };
159
+ return { cut: true, reason: "trigger" };
160
+ }
161
+
162
+ /**
163
+ * Stop-and-ask rule (D9 / M.4). New read observations or fresh successful checks
164
+ * reset the counter. Exact repetitions, annotations alone and gate regressions
165
+ * do not. This detects inactivity; it cannot judge relevance to the user's goal.
166
+ */
167
+ export const DEFAULT_MAX_SEGMENTS_WITHOUT_PROGRESS = 3;
168
+
169
+ export function shouldStopAndAsk(
170
+ segmentsWithoutProgress: number,
171
+ max = DEFAULT_MAX_SEGMENTS_WITHOUT_PROGRESS,
172
+ ): boolean {
173
+ return segmentsWithoutProgress >= max;
174
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Two loops the repetition check cannot see.
3
+ *
4
+ * `continuationGuidance` flags an action/observation cycle that repeats
5
+ * exactly. A real rut rarely does: a rerun test prints a new timestamp, a
6
+ * build a new temp path, and every observation digest differs -- so neither
7
+ * that check nor D9 stall counting fires while the model retries one idea.
8
+ * These two keys come from facts the harness recorded itself, not from
9
+ * comparing output: the exact command with its exit code, and the path of
10
+ * each successful edit (LangChain's harness write-up uses the same edit
11
+ * count; opencode's doom_loop misses exactly these near-repeats).
12
+ *
13
+ * Advice only, once per request: nothing is blocked and the stop rules are
14
+ * unchanged.
15
+ */
16
+ import type { GuidanceEvent, LogEvent } from "./events.ts";
17
+
18
+ /**
19
+ * Assumed, not measured. Three failures leaves room for "change, retry,
20
+ * still failing" -- normal debugging -- before calling it a loop; eight edits
21
+ * of one file sits above a typical refactor. Too low costs one extra notice;
22
+ * too high falls back to today's behaviour.
23
+ */
24
+ export const CHURN_FAILURES = 3;
25
+ export const CHURN_EDITS = 8;
26
+
27
+ export function churnReview(events: readonly LogEvent[]): Pick<GuidanceEvent, "reason" | "text" | "sources"> | null {
28
+ const start = events.findLastIndex((event) => event.t === "user");
29
+ const current = events.slice(Math.max(0, start));
30
+ if (current.some((event) => event.t === "guidance" && event.reason === "churn")) return null;
31
+ const failures = new Map<string, { count: number; last: string }>();
32
+ const edits = new Map<string, { count: number; last: string }>();
33
+ for (const event of current) {
34
+ if (event.t !== "tool_result" || event.effect === undefined) continue;
35
+ const effect = event.effect;
36
+ if (effect.kind === "command") {
37
+ const key = effect.cmd.trim();
38
+ if (effect.exit === 0) { failures.delete(key); continue; }
39
+ const seen = { count: (failures.get(key)?.count ?? 0) + 1, last: event.id };
40
+ failures.set(key, seen);
41
+ if (seen.count >= CHURN_FAILURES) return {
42
+ reason: "churn", sources: [event.id],
43
+ text: `Harness: \`${key}\` failed ${seen.count} times in this request without succeeding. ` +
44
+ "Retrying the same approach is unlikely to change the result. Read the end of the last failure, " +
45
+ "state the cause you believe it has, and test that belief with a different step before running it again.",
46
+ };
47
+ } else if (effect.kind === "edit") {
48
+ const seen = { count: (edits.get(effect.path)?.count ?? 0) + 1, last: event.id };
49
+ edits.set(effect.path, seen);
50
+ if (seen.count >= CHURN_EDITS) return {
51
+ reason: "churn", sources: [event.id],
52
+ text: `Harness: ${effect.path} was edited ${seen.count} times in this request. ` +
53
+ "Repeated small edits usually mean the underlying cause is not yet understood. Re-read the file and the " +
54
+ "failing evidence, then make one change that addresses the cause.",
55
+ };
56
+ }
57
+ }
58
+ return null;
59
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * A one-time reconciliation when the model ends a request.
3
+ *
4
+ * The model deciding it is done is a claim like any other (D11), and the
5
+ * failure modes that end a Terminal-Bench run early all look like a normal
6
+ * final reply: an edit nobody ran, a question handed back to a user who is
7
+ * not there, or nothing at all. ForgeCode found a programmatic check at this
8
+ * point was its largest single gain and that a prompt instruction alone did
9
+ * not reproduce it. The check here only appends a notice; the model still
10
+ * decides, and a second finish is respected -- an honest blocker must not
11
+ * turn into a loop.
12
+ */
13
+ import type { GuidanceEvent, LogEvent } from "./events.ts";
14
+
15
+ /**
16
+ * Wording that hands the task back instead of finishing it. Ported from
17
+ * waner `crates/waner_core/src/turn/premature_stop.rs`, including its window:
18
+ * the first 400 and last 300 characters, where an opening refusal or a
19
+ * closing offer sits. The same words in the middle of a long report are
20
+ * usually a quotation or a description, not a stop.
21
+ */
22
+ const HAND_BACK = [
23
+ "i can't proceed", "i cannot proceed", "i can't continue", "i cannot continue",
24
+ "giving up", "stopping here", "unable to complete", "i'm unable to", "i am unable to",
25
+ "let me know if you", "would you like me to", "do you want me to", "shall i ",
26
+ "我无法继续", "无法完成", "需要你确认", "请确认是否", "是否需要我", "要不要我",
27
+ ];
28
+
29
+ export function looksLikeHandBack(text: string): boolean {
30
+ const chars = [...text.trim()];
31
+ const window = `${chars.slice(0, 400).join("")}\n${chars.slice(Math.max(0, chars.length - 300)).join("")}`.toLowerCase();
32
+ return HAND_BACK.some((pattern) => window.includes(pattern));
33
+ }
34
+
35
+ export function completionReview(
36
+ events: readonly LogEvent[],
37
+ remainingRequests: number,
38
+ ): Pick<GuidanceEvent, "reason" | "text" | "sources"> | null {
39
+ if (remainingRequests < 1) return null;
40
+ const start = events.findLastIndex((event) => event.t === "user");
41
+ if (start < 0) return null;
42
+ const current = events.slice(start);
43
+ if (current.some((event) => event.t === "guidance" && event.reason === "completion_review")) return null;
44
+ const final = current.findLast((event) => event.t === "assistant");
45
+ if (final?.t !== "assistant" || final.blocks.some((block) => block.type === "tool_use")) return null;
46
+
47
+ const text = final.blocks.flatMap((block) => block.type === "text" ? [block.text] : []).join("\n").trim();
48
+ const usedTools = current.some((event) => event.t === "tool_result");
49
+ const lastEdit = current.findLastIndex((event) => event.t === "tool_result" && event.effect?.kind === "edit");
50
+ const verifiedAfter = lastEdit >= 0 && current.slice(lastEdit + 1)
51
+ .some((event) => event.t === "tool_result" && event.effect?.kind === "command" && event.effect.exit === 0);
52
+
53
+ const gaps: string[] = [];
54
+ if (usedTools && text.length === 0) gaps.push("the final reply is empty");
55
+ if (lastEdit >= 0 && !verifiedAfter) gaps.push("files were changed but no command has succeeded since the last edit");
56
+ if (text.length > 0 && looksLikeHandBack(text)) gaps.push("the reply handed the task back instead of finishing it; no one will answer");
57
+ if (gaps.length === 0) return null;
58
+ return {
59
+ reason: "completion_review",
60
+ sources: [current[0]!.id, final.id],
61
+ text: `Harness completion check: ${gaps.join("; ")}. ` +
62
+ `Re-read the original request (event ${current[0]!.id}) and check each requirement against evidence in this session. ` +
63
+ "Run the command that verifies the result, or finish with the specific blocker that prevents verification. " +
64
+ "This check runs once per request.",
65
+ };
66
+ }
@@ -0,0 +1,60 @@
1
+ /** N.6: log ownership is independent of the policy that replaces its context view. */
2
+ import { shouldCut, type BoundaryDecision, type BoundaryInput } from "./boundary.ts";
3
+ import { windowBudget } from "./context-window.ts";
4
+
5
+ export const CONTEXT_POLICIES = ["segmented", "continuous", "adaptive", "windowed"] as const;
6
+ export type ContextPolicy = typeof CONTEXT_POLICIES[number];
7
+ export const DEFAULT_CONTEXT_POLICY: ContextPolicy = "windowed";
8
+ export function isContextPolicy(value: unknown): value is ContextPolicy {
9
+ return typeof value === "string" && (CONTEXT_POLICIES as readonly string[]).includes(value);
10
+ }
11
+
12
+ export interface BoundaryCost { keepUsd: number; rebuildUsd: number }
13
+ /**
14
+ * A one-request input-cost estimate, NOT a forecast of task cost or quality.
15
+ * Reuse only the last observed cached token count; price all new input and the
16
+ * entire replacement cold. This deliberately does not assume a cache hit on the
17
+ * unchanged system prefix, future cache persistence, or free evidence recovery.
18
+ */
19
+ export function estimateBoundaryCost(input: {
20
+ retainedTokens: number; rebuiltTokens: number; observedCachedTokens: number;
21
+ inputPrice: number; cacheReadPrice: number; cacheWritePrice: number;
22
+ }): BoundaryCost | null {
23
+ if (Object.values(input).some((value) => !Number.isFinite(value) || value < 0)) return null;
24
+ const cold = Math.max(input.inputPrice, input.cacheWritePrice);
25
+ const cached = Math.min(input.retainedTokens, input.observedCachedTokens);
26
+ const cost = { keepUsd: (cached * input.cacheReadPrice + (input.retainedTokens - cached) * cold) / 1_000_000,
27
+ rebuildUsd: input.rebuiltTokens * cold / 1_000_000 };
28
+ return Object.values(cost).every(Number.isFinite) ? cost : null;
29
+ }
30
+
31
+ export type ContextDecision = BoundaryDecision | { cut: false; overrun: boolean;
32
+ reason: "continuous" | "awaiting_continuation" | "unknown_cost" | "preserving_cache" };
33
+
34
+ export function decideContext(input: BoundaryInput & {
35
+ policy: ContextPolicy; continuationReady: boolean; cost: BoundaryCost | null;
36
+ contextWindow?: number; outputReserve?: number;
37
+ }): ContextDecision {
38
+ if (!isContextPolicy(input.policy)) throw new Error("unknown context policy");
39
+ if (input.policy === "continuous") return { cut: false, reason: "continuous", overrun: false };
40
+ if (input.policy === "windowed") {
41
+ const budget = windowBudget(input.contextWindow, input.outputReserve ?? 0);
42
+ if (budget !== null) {
43
+ if (!input.atTurnBoundary) return { cut: false, reason: "awaiting_turn_boundary", overrun: input.tokens > budget.usable };
44
+ if (input.tokens < budget.trigger) return { cut: false, reason: "below_trigger", overrun: false };
45
+ if ((input.inheritedTokens ?? 0) >= budget.trigger) return { cut: false, reason: "inherited_floor_exceeds_limit", overrun: true };
46
+ return { cut: true, reason: input.tokens > budget.usable ? "hard_limit" : "trigger" };
47
+ }
48
+ // Unknown capacity is not an infinite window. Keep the explicit legacy bounds.
49
+ return shouldCut(input);
50
+ }
51
+ const baseline = shouldCut(input);
52
+ if (input.policy === "segmented" || !baseline.cut || baseline.reason !== "trigger") return baseline;
53
+ // Readiness and price may defer a soft trigger; neither can veto the hard limit.
54
+ if (!input.continuationReady) return { cut: false, reason: "awaiting_continuation", overrun: false };
55
+ if (input.cost === null || Object.values(input.cost).some((value) => !Number.isFinite(value) || value < 0)) {
56
+ return { cut: false, reason: "unknown_cost", overrun: false };
57
+ }
58
+ if (input.cost.rebuildUsd >= input.cost.keepUsd) return { cut: false, reason: "preserving_cache", overrun: false };
59
+ return baseline;
60
+ }
@@ -0,0 +1,54 @@
1
+ /** N.8: window pressure changes the projection, never the durable transcript. */
2
+ import type { LogEvent } from "./events.ts";
3
+ import type { Handoff } from "./handoff.ts";
4
+ import { projectContext, type FrozenPrefix } from "./project.ts";
5
+ import type { TokenCounter } from "./tokens.ts";
6
+
7
+ export interface WindowBudget { usable: number; prepare: number; trigger: number; target: number }
8
+ export function windowBudget(window: number | undefined, outputReserve: number): WindowBudget | null {
9
+ if (window === undefined) return null;
10
+ if (!Number.isSafeInteger(window) || window <= 0 || !Number.isSafeInteger(outputReserve) || outputReserve < 0) {
11
+ throw new RangeError("context window and output reserve must be finite token counts");
12
+ }
13
+ const usable = Math.max(0, window - outputReserve);
14
+ // Assumptions, not measured optima: 20% headroom absorbs estimator/tool growth;
15
+ // half a window after a cut prevents repeated cold rebuilds. See N.8.
16
+ return { usable, prepare: Math.floor(usable * 0.7), trigger: Math.floor(usable * 0.8), target: Math.floor(usable * 0.5) };
17
+ }
18
+
19
+ /** Resolve a suffix on the effective ancestry; a bad reference never means empty history. */
20
+ export function retainedContext(events: readonly LogEvent[], from: string): LogEvent[] {
21
+ const index = events.findIndex((event) => event.id === from);
22
+ if (index < 0) throw new Error(`MISSING_RETAINED_EVENT: ${from}`);
23
+ const first = events[index]!;
24
+ if (first.t !== "assistant" && first.t !== "user") throw new Error(`INVALID_RETAINED_BOUNDARY: ${from}`);
25
+ const retained = events.slice(index).filter((event) => event.t !== "cut");
26
+ const projection = projectContext({ frozen: { system: "", tools: [] }, handoff: null, events: retained });
27
+ if (projection.violations.length > 0) throw new Error(`UNPAIRED_RETAINED_CONTEXT: ${from}`);
28
+ return retained;
29
+ }
30
+
31
+ /** Pi keeps recent exchanges; here they stay references into the original log (D8). */
32
+ export function selectRecentContext(input: {
33
+ events: readonly LogEvent[]; frozen: FrozenPrefix; handoff: Handoff; counter: TokenCounter;
34
+ targetTokens: number; requiredUserId?: string;
35
+ }): { from: string; tokens: number } | null {
36
+ const { events, frozen, handoff, counter, targetTokens, requiredUserId } = input;
37
+ const required = requiredUserId === undefined ? events.length : events.findIndex((event) => event.id === requiredUserId);
38
+ if (required < 0) throw new Error("missing required user request");
39
+ let result: { from: string; tokens: number } | null = null;
40
+ // The 20k recent-history cap is a conservative starting point borrowed from
41
+ // Pi, not a semantic guarantee. The full replacement must also fit target.
42
+ const floor = projectContext({ frozen, handoff, events: [] });
43
+ const limit = Math.min(targetTokens, counter.count(floor.messages, floor.system, frozen.tools) + 20_000);
44
+ for (let i = events.length - 1; i >= 0; i--) {
45
+ const event = events[i]!;
46
+ if (event.t !== "assistant" && event.t !== "user") continue;
47
+ const projection = projectContext({ frozen, handoff, events: events.slice(i) });
48
+ if (projection.violations.length > 0) continue;
49
+ const tokens = counter.count(projection.messages, projection.system, frozen.tools);
50
+ if (tokens > limit) break;
51
+ if (i <= required) result = { from: event.id, tokens };
52
+ }
53
+ return result;
54
+ }