@miller-tech/uap 2.1.7 → 2.11.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 (143) hide show
  1. package/README.md +7 -6
  2. package/config/capacity-policy.json +32 -0
  3. package/config/classify-thresholds.json +9 -0
  4. package/config/supervise-policy.json +13 -0
  5. package/dist/.tsbuildinfo +1 -1
  6. package/dist/bin/cli.js +87 -2
  7. package/dist/bin/cli.js.map +1 -1
  8. package/dist/capacity/policy.d.ts +67 -0
  9. package/dist/capacity/policy.d.ts.map +1 -0
  10. package/dist/capacity/policy.js +133 -0
  11. package/dist/capacity/policy.js.map +1 -0
  12. package/dist/capacity/probe.d.ts +23 -0
  13. package/dist/capacity/probe.d.ts.map +1 -0
  14. package/dist/capacity/probe.js +93 -0
  15. package/dist/capacity/probe.js.map +1 -0
  16. package/dist/classify/baseline.d.ts +24 -0
  17. package/dist/classify/baseline.d.ts.map +1 -0
  18. package/dist/classify/baseline.js +140 -0
  19. package/dist/classify/baseline.js.map +1 -0
  20. package/dist/classify/index.d.ts +30 -0
  21. package/dist/classify/index.d.ts.map +1 -0
  22. package/dist/classify/index.js +84 -0
  23. package/dist/classify/index.js.map +1 -0
  24. package/dist/classify/shadow.d.ts +34 -0
  25. package/dist/classify/shadow.d.ts.map +1 -0
  26. package/dist/classify/shadow.js +33 -0
  27. package/dist/classify/shadow.js.map +1 -0
  28. package/dist/classify/types.d.ts +64 -0
  29. package/dist/classify/types.d.ts.map +1 -0
  30. package/dist/classify/types.js +37 -0
  31. package/dist/classify/types.js.map +1 -0
  32. package/dist/cli/classify.d.ts +24 -0
  33. package/dist/cli/classify.d.ts.map +1 -0
  34. package/dist/cli/classify.js +94 -0
  35. package/dist/cli/classify.js.map +1 -0
  36. package/dist/cli/deliver.d.ts.map +1 -1
  37. package/dist/cli/deliver.js +26 -0
  38. package/dist/cli/deliver.js.map +1 -1
  39. package/dist/cli/doctor.d.ts +9 -0
  40. package/dist/cli/doctor.d.ts.map +1 -0
  41. package/dist/cli/doctor.js +49 -0
  42. package/dist/cli/doctor.js.map +1 -0
  43. package/dist/cli/review.d.ts +12 -0
  44. package/dist/cli/review.d.ts.map +1 -0
  45. package/dist/cli/review.js +118 -0
  46. package/dist/cli/review.js.map +1 -0
  47. package/dist/cli/supervise.d.ts +8 -0
  48. package/dist/cli/supervise.d.ts.map +1 -0
  49. package/dist/cli/supervise.js +76 -0
  50. package/dist/cli/supervise.js.map +1 -0
  51. package/dist/delivery/convergence-loop.d.ts +8 -0
  52. package/dist/delivery/convergence-loop.d.ts.map +1 -1
  53. package/dist/delivery/convergence-loop.js +1 -0
  54. package/dist/delivery/convergence-loop.js.map +1 -1
  55. package/dist/delivery/delivery-result.d.ts.map +1 -1
  56. package/dist/delivery/delivery-result.js +5 -0
  57. package/dist/delivery/delivery-result.js.map +1 -1
  58. package/dist/delivery/gate-evidence.d.ts +143 -0
  59. package/dist/delivery/gate-evidence.d.ts.map +1 -0
  60. package/dist/delivery/gate-evidence.js +239 -0
  61. package/dist/delivery/gate-evidence.js.map +1 -0
  62. package/dist/delivery/index.d.ts +1 -0
  63. package/dist/delivery/index.d.ts.map +1 -1
  64. package/dist/delivery/index.js +1 -0
  65. package/dist/delivery/index.js.map +1 -1
  66. package/dist/evals/routing-corpus.d.ts +35 -0
  67. package/dist/evals/routing-corpus.d.ts.map +1 -0
  68. package/dist/evals/routing-corpus.js +142 -0
  69. package/dist/evals/routing-corpus.js.map +1 -0
  70. package/dist/evals/routing-eval.d.ts +104 -0
  71. package/dist/evals/routing-eval.d.ts.map +1 -0
  72. package/dist/evals/routing-eval.js +231 -0
  73. package/dist/evals/routing-eval.js.map +1 -0
  74. package/dist/evals/tfidf.d.ts +35 -0
  75. package/dist/evals/tfidf.d.ts.map +1 -0
  76. package/dist/evals/tfidf.js +132 -0
  77. package/dist/evals/tfidf.js.map +1 -0
  78. package/dist/review/prepass.d.ts +88 -0
  79. package/dist/review/prepass.d.ts.map +1 -0
  80. package/dist/review/prepass.js +428 -0
  81. package/dist/review/prepass.js.map +1 -0
  82. package/dist/review/visual-captures.d.ts +60 -0
  83. package/dist/review/visual-captures.d.ts.map +1 -0
  84. package/dist/review/visual-captures.js +222 -0
  85. package/dist/review/visual-captures.js.map +1 -0
  86. package/dist/supervise/assess.d.ts +73 -0
  87. package/dist/supervise/assess.d.ts.map +1 -0
  88. package/dist/supervise/assess.js +215 -0
  89. package/dist/supervise/assess.js.map +1 -0
  90. package/dist/supervise/config.d.ts +13 -0
  91. package/dist/supervise/config.d.ts.map +1 -0
  92. package/dist/supervise/config.js +81 -0
  93. package/dist/supervise/config.js.map +1 -0
  94. package/dist/supervise/loop.d.ts +78 -0
  95. package/dist/supervise/loop.d.ts.map +1 -0
  96. package/dist/supervise/loop.js +311 -0
  97. package/dist/supervise/loop.js.map +1 -0
  98. package/dist/supervise/observe.d.ts +31 -0
  99. package/dist/supervise/observe.d.ts.map +1 -0
  100. package/dist/supervise/observe.js +172 -0
  101. package/dist/supervise/observe.js.map +1 -0
  102. package/dist/supervise/policy.d.ts +16 -0
  103. package/dist/supervise/policy.d.ts.map +1 -0
  104. package/dist/supervise/policy.js +147 -0
  105. package/dist/supervise/policy.js.map +1 -0
  106. package/dist/supervise/types.d.ts +109 -0
  107. package/dist/supervise/types.d.ts.map +1 -0
  108. package/dist/supervise/types.js +11 -0
  109. package/dist/supervise/types.js.map +1 -0
  110. package/docs/INDEX.md +15 -4
  111. package/docs/architecture/adr/0001-automode-risk-floor.md +75 -0
  112. package/docs/architecture/adr/0002-evidence-bound-ship.md +81 -0
  113. package/docs/guides/CAPACITY_POLICY.md +99 -0
  114. package/docs/performance/automode-risk-calibration.md +72 -0
  115. package/docs/performance/reliability-ladder.md +78 -0
  116. package/docs/plans/system1-uplift-2026-09-18.md +190 -0
  117. package/docs/reference/automode-risk.md +134 -0
  118. package/docs/reference/evidence-bound-ship.md +146 -0
  119. package/docs/reference/review-prepass.md +77 -0
  120. package/docs/reference/routing-evals.md +71 -0
  121. package/docs/reference/semantic-supervisor.md +140 -0
  122. package/docs/reference/system1-classifier.md +103 -0
  123. package/docs/reference/visual-captures.md +113 -0
  124. package/package.json +5 -3
  125. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  126. package/src/policies/enforcers/__pycache__/expert_review_required.cpython-312.pyc +0 -0
  127. package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-312.pyc +0 -0
  128. package/src/policies/enforcers/enforcement_self_protect.py +25 -2
  129. package/src/policies/enforcers/evidence_bound_ship.py +257 -0
  130. package/src/policies/enforcers/expert_review_required.py +120 -0
  131. package/src/policies/schemas/policies/evidence-bound-ship.md +106 -0
  132. package/src/policies/schemas/policies/visual-verification.md +8 -0
  133. package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
  134. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
  135. package/tools/agents/scripts/anthropic_proxy.py +782 -19
  136. package/tools/agents/scripts/project_telemetry.py +52 -0
  137. package/tools/agents/scripts/tool_risk.py +789 -0
  138. package/tools/agents/scripts/tool_risk_calibrate.py +255 -0
  139. package/tools/agents/tests/fixtures/tool_risk_cases.json +124 -0
  140. package/tools/agents/tests/test_enforcer_escape_hatches.py +13 -0
  141. package/tools/agents/tests/test_gate_evidence.py +56 -0
  142. package/tools/agents/tests/test_tool_risk.py +521 -0
  143. package/tools/agents/tests/test_verbatim_compact.py +629 -0
@@ -0,0 +1,629 @@
1
+ #!/usr/bin/env python3
2
+ """Verbatim decision compaction (uplift 1.2).
3
+
4
+ The proxy's context-pressure path used to drop a contiguous oldest-middle
5
+ block and replace it with a one-line breadcrumb summary — LOSSY: dropped
6
+ findings evaporate. `verbatim_compact_conversation` sits AHEAD of that path
7
+ and makes per-tool-call keep/truncate/drop decisions:
8
+
9
+ stage 1: drop pure-noise tool pairs (acks, superseded reads, repeats)
10
+ stage 2: truncate large kept tool results in place (verbatim head + note)
11
+ stage 3: return None -> fall back to the contiguous breadcrumb pruner
12
+
13
+ Hard invariants pinned here:
14
+ - kept content is VERBATIM (no summarization of retained messages);
15
+ - PAIRING INTEGRITY: a tool_use and its tool_result are an atomic pair —
16
+ no orphan tool_result, no dangling tool_use; any integrity failure or
17
+ insufficient reduction fails CLOSED to the old path (returns None);
18
+ - PROXY_VERBATIM_COMPACT=off restores the old path entirely;
19
+ - on a noise-bearing oversized fixture the verbatim path reduces at least
20
+ as much as the lossy pruner.
21
+ """
22
+
23
+ import copy
24
+ import importlib.util
25
+ import unittest
26
+ from pathlib import Path
27
+
28
+ proxy_path = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
29
+ spec = importlib.util.spec_from_file_location("anthropic_proxy", proxy_path)
30
+ ap = importlib.util.module_from_spec(spec)
31
+ spec.loader.exec_module(ap)
32
+
33
+
34
+ BIG = 1400 # chars per large tool result (~400 tokens at 3.5 chars/token)
35
+
36
+
37
+ def _pad(prefix, char):
38
+ return prefix + char * max(0, BIG - len(prefix))
39
+
40
+
41
+ def _assistant_call(idx, name, tool_input, text=None):
42
+ blocks = []
43
+ if text:
44
+ blocks.append({"type": "text", "text": text})
45
+ blocks.append(
46
+ {"type": "tool_use", "id": f"toolu_{idx}", "name": name, "input": tool_input}
47
+ )
48
+ return {"role": "assistant", "content": blocks}
49
+
50
+
51
+ def _user_result(idx, text):
52
+ return {
53
+ "role": "user",
54
+ "content": [
55
+ {"type": "tool_result", "tool_use_id": f"toolu_{idx}", "content": text}
56
+ ],
57
+ }
58
+
59
+
60
+ def _pair(idx, name, tool_input, result_text, assistant_text=None):
61
+ return [
62
+ _assistant_call(idx, name, tool_input, text=assistant_text),
63
+ _user_result(idx, result_text),
64
+ ]
65
+
66
+
67
+ def _noise_fixture():
68
+ """Oversized conversation whose middle mixes pure-noise pairs with
69
+ unique signal pairs. Window 8000 x 0.50 -> 4000-token message budget;
70
+ the middle alone is ~4800 tokens, ~45% of it pure noise, so stage 1
71
+ alone brings the conversation under budget without stage-2 truncation."""
72
+ messages = [{"role": "user", "content": "recon: map the auth flow"}]
73
+ idx = 0
74
+
75
+ def add(name, tool_input, result_text):
76
+ nonlocal idx
77
+ messages.extend(_pair(idx, name, tool_input, result_text))
78
+ idx += 1
79
+
80
+ add("Read", {"file_path": "/src/dupC.py"}, _pad("DUPC-OLD ", "e")) # superseded below
81
+ add("Read", {"file_path": "/src/dupA.py"}, _pad("DUPA-OLD ", "a")) # superseded below
82
+ add("Write", {"file_path": "/src/out1.py"},
83
+ "File created successfully at: /src/out1.py") # ack (toolu_2)
84
+ add("Read", {"file_path": "/src/sig1.py"}, _pad("SIG1 ", "1")) # signal
85
+ add("Bash", {"command": "git status"},
86
+ "On branch master\nnothing to commit, working tree clean") # repeat below
87
+ add("Read", {"file_path": "/src/dupB.py"}, _pad("DUPB-OLD ", "b")) # superseded below
88
+ add("Read", {"file_path": "/src/sig2.py"}, _pad("SIG2 ", "2")) # signal
89
+ add("Read", {"file_path": "/src/dupA.py"}, _pad("DUPA-NEW ", "c")) # freshest A
90
+ add("Write", {"file_path": "/src/out2.py"},
91
+ "File created successfully at: /src/out2.py") # ack
92
+ add("Read", {"file_path": "/src/sig3.py"}, _pad("SIG3 ", "3")) # signal
93
+ add("Bash", {"command": "git status"},
94
+ "On branch master\nnothing to commit, working tree clean") # freshest repeat
95
+ add("Read", {"file_path": "/src/dupB.py"}, _pad("DUPB-NEW ", "d")) # freshest B
96
+ add("Read", {"file_path": "/src/dupD.py"}, _pad("DUPD-OLD ", "g")) # superseded below
97
+ add("Read", {"file_path": "/src/dupD.py"}, _pad("DUPD-NEW ", "h")) # freshest D
98
+ add("Read", {"file_path": "/src/dupC.py"}, _pad("DUPC-NEW ", "f")) # freshest C
99
+ add("Read", {"file_path": "/src/sig4.py"}, _pad("SIG4 ", "4")) # signal
100
+ for tail in ("t0", "t1", "t2", "t3"):
101
+ messages.append({"role": "user", "content": tail})
102
+ return {"messages": messages}
103
+
104
+
105
+ def _big_signal_fixture(n_pairs=6, result_chars=3000):
106
+ """Oversized conversation with NO noise: every pair is a unique read of
107
+ a unique path with a large unique result."""
108
+ messages = [{"role": "user", "content": "analyze everything"}]
109
+ for i in range(n_pairs):
110
+ messages.extend(
111
+ _pair(
112
+ i,
113
+ "Read",
114
+ {"file_path": f"/src/uniq{i}.py"},
115
+ f"HEAD{i} " + (f"u{i}" * (result_chars // 2)) + f" TAIL{i}",
116
+ )
117
+ )
118
+ for tail in ("t0", "t1", "t2", "t3"):
119
+ messages.append({"role": "user", "content": tail})
120
+ return {"messages": messages}
121
+
122
+
123
+ def _all_text(body):
124
+ out = []
125
+ for msg in body["messages"]:
126
+ content = msg.get("content", "")
127
+ if isinstance(content, str):
128
+ out.append(content)
129
+ elif isinstance(content, list):
130
+ for block in content:
131
+ if isinstance(block, dict):
132
+ if block.get("type") == "text":
133
+ out.append(block.get("text", ""))
134
+ elif block.get("type") == "tool_result":
135
+ out.append(ap._extract_text(block.get("content", "")))
136
+ return "\n".join(out)
137
+
138
+
139
+ class TestStage1KeepDropDecisions(unittest.TestCase):
140
+ def test_drops_pure_noise_keeps_signal_verbatim(self):
141
+ body = _noise_fixture()
142
+ original_total = ap.estimate_total_tokens(body)
143
+ out = ap.verbatim_compact_conversation(
144
+ copy.deepcopy(body), 8000, target_fraction=0.50, keep_last=4
145
+ )
146
+ self.assertIsNotNone(out)
147
+ blob = _all_text(out)
148
+ # noise gone: superseded first reads, ack writes, the repeated call
149
+ self.assertNotIn("DUPA-OLD", blob)
150
+ self.assertNotIn("DUPB-OLD", blob)
151
+ self.assertNotIn("DUPC-OLD", blob)
152
+ self.assertNotIn("DUPD-OLD", blob)
153
+ self.assertNotIn("File created successfully", blob)
154
+ self.assertEqual(blob.count("working tree clean"), 1) # freshest kept
155
+ # signal kept VERBATIM — the full untruncated result text
156
+ self.assertIn(_pad("SIG1 ", "1"), blob)
157
+ self.assertIn(_pad("SIG4 ", "4"), blob)
158
+ self.assertIn(_pad("DUPA-NEW ", "c"), blob)
159
+ self.assertIn(_pad("DUPD-NEW ", "h"), blob)
160
+ self.assertIn(_pad("DUPC-NEW ", "f"), blob)
161
+ # no breadcrumb summary anywhere — verbatim path never summarizes
162
+ self.assertNotIn("CONTEXT PRUNED", blob)
163
+ # stage 1 sufficed — nothing was truncated
164
+ self.assertNotIn("[… truncated ~", blob)
165
+ # head and tail pinned
166
+ self.assertEqual(out["messages"][0]["content"], "recon: map the auth flow")
167
+ self.assertEqual([m["content"] for m in out["messages"][-4:]], ["t0", "t1", "t2", "t3"])
168
+ # actually reduced
169
+ self.assertLess(ap.estimate_total_tokens(out), original_total)
170
+
171
+ def test_ack_pair_with_meaningful_prose_is_kept(self):
172
+ """When in doubt, keep: an ack result riding on an assistant message
173
+ with real prose is not pure noise."""
174
+ body = _noise_fixture()
175
+ # give the first Write pair's (toolu_2) assistant message meaningful text
176
+ for msg in body["messages"]:
177
+ content = msg.get("content", [])
178
+ if isinstance(content, list) and any(
179
+ isinstance(b, dict) and b.get("id") == "toolu_2" for b in content
180
+ ):
181
+ content.insert(
182
+ 0, {"type": "text", "text": "Now writing the entrypoint module."}
183
+ )
184
+ break
185
+ out = ap.verbatim_compact_conversation(
186
+ copy.deepcopy(body), 8000, target_fraction=0.50, keep_last=4
187
+ )
188
+ self.assertIsNotNone(out)
189
+ blob = _all_text(out)
190
+ self.assertIn("Now writing the entrypoint module.", blob)
191
+ self.assertIn("File created successfully at: /src/out1.py", blob)
192
+
193
+ def test_survivors_keep_original_relative_order(self):
194
+ """KV doctrine: never reshuffle — surviving middle messages appear in
195
+ their original order."""
196
+ body = _noise_fixture()
197
+ original = list(body["messages"])
198
+ out = ap.verbatim_compact_conversation(
199
+ copy.deepcopy(body), 8000, target_fraction=0.50, keep_last=4
200
+ )
201
+ self.assertIsNotNone(out)
202
+ idxs = [
203
+ i
204
+ for msg in out["messages"]
205
+ for i, orig in enumerate(original)
206
+ if msg is not orig and msg == orig
207
+ ]
208
+ self.assertEqual(idxs, sorted(idxs))
209
+
210
+
211
+ class TestStage2Truncation(unittest.TestCase):
212
+ def test_truncates_oldest_first_keeps_head_verbatim(self):
213
+ body = _big_signal_fixture()
214
+ original_texts = [
215
+ ap._extract_text(m["content"][0].get("content", ""))
216
+ for m in body["messages"]
217
+ if m.get("role") == "user"
218
+ and isinstance(m.get("content"), list)
219
+ ]
220
+ out = ap.verbatim_compact_conversation(
221
+ copy.deepcopy(body), 8000, target_fraction=0.50, keep_last=4
222
+ )
223
+ self.assertIsNotNone(out)
224
+ budget = int(8000 * 0.50)
225
+ total = sum(ap.estimate_message_tokens(m) for m in out["messages"])
226
+ self.assertLessEqual(total, budget)
227
+
228
+ result_texts = [
229
+ ap._extract_text(m["content"][0].get("content", ""))
230
+ for m in out["messages"]
231
+ if m.get("role") == "user" and isinstance(m.get("content"), list)
232
+ ]
233
+ truncated = [t for t in result_texts if "[… truncated ~" in t]
234
+ untouched = [t for t in result_texts if "[… truncated ~" not in t]
235
+ self.assertTrue(truncated, "expected at least one truncated result")
236
+ for orig, now in zip(original_texts, result_texts):
237
+ if "[… truncated ~" in now:
238
+ # head kept VERBATIM, then the note
239
+ self.assertTrue(now.startswith(orig[: ap._VERBATIM_TRUNCATE_HEAD_CHARS]))
240
+ self.assertLess(len(now), len(orig))
241
+ else:
242
+ self.assertEqual(now, orig) # untouched stays byte-identical
243
+ # oldest-first: the newest middle results survive untruncated
244
+ self.assertTrue(any(t == original_texts[-1] for t in untouched))
245
+
246
+ def test_truncation_preserves_pairing(self):
247
+ body = _big_signal_fixture()
248
+ out = ap.verbatim_compact_conversation(
249
+ copy.deepcopy(body), 8000, target_fraction=0.50, keep_last=4
250
+ )
251
+ self.assertIsNotNone(out)
252
+ self.assertTrue(ap._tool_pairing_intact(out["messages"]))
253
+ # every surviving tool_use id still has its tool_result and vice versa
254
+ use_ids = {
255
+ b["id"]
256
+ for m in out["messages"]
257
+ for b in ap._iter_tool_use_blocks(m)
258
+ }
259
+ result_ids = {
260
+ b["tool_use_id"]
261
+ for m in out["messages"]
262
+ for b in ap._iter_tool_result_blocks(m)
263
+ }
264
+ self.assertEqual(use_ids, result_ids)
265
+
266
+
267
+ class TestPairingIntegrityValidation(unittest.TestCase):
268
+ def test_valid_paired_conversation(self):
269
+ body = _noise_fixture()
270
+ self.assertTrue(ap._tool_pairing_intact(body["messages"]))
271
+
272
+ def test_orphan_tool_result_rejected(self):
273
+ messages = [
274
+ {"role": "user", "content": [
275
+ {"type": "tool_result", "tool_use_id": "toolu_x", "content": "data"}]},
276
+ {"role": "user", "content": "next"},
277
+ ]
278
+ self.assertFalse(ap._tool_pairing_intact(messages))
279
+
280
+ def test_dangling_tool_use_mid_conversation_rejected(self):
281
+ messages = [
282
+ {"role": "user", "content": "go"},
283
+ _assistant_call(0, "Read", {"file_path": "/a"}),
284
+ {"role": "user", "content": "no result for that call"},
285
+ ]
286
+ self.assertFalse(ap._tool_pairing_intact(messages))
287
+
288
+ def test_final_assistant_prefill_is_allowed(self):
289
+ messages = [
290
+ {"role": "user", "content": "go"},
291
+ _assistant_call(0, "Read", {"file_path": "/a"}),
292
+ ]
293
+ self.assertTrue(ap._tool_pairing_intact(messages))
294
+
295
+ def test_partial_result_coverage_rejected(self):
296
+ pair = _assistant_call(0, "Read", {"file_path": "/a"})
297
+ pair["content"].append(
298
+ {"type": "tool_use", "id": "toolu_99", "name": "Read",
299
+ "input": {"file_path": "/b"}}
300
+ )
301
+ messages = [
302
+ {"role": "user", "content": "go"},
303
+ pair,
304
+ _user_result(0, "only one of two answered"),
305
+ ]
306
+ self.assertFalse(ap._tool_pairing_intact(messages))
307
+
308
+ def test_unfixable_input_pairing_fails_closed(self):
309
+ """A conversation whose pairing is ALREADY broken (orphan result) can
310
+ be stage-1 reduced but never validated — the verbatim path must fail
311
+ closed (None) rather than ship the broken pairing."""
312
+ body = _noise_fixture()
313
+ # inject an orphan tool_result into the middle
314
+ body["messages"].insert(
315
+ 5,
316
+ {"role": "user", "content": [
317
+ {"type": "tool_result", "tool_use_id": "toolu_orphan",
318
+ "content": "orphaned"}]},
319
+ )
320
+ out = ap.verbatim_compact_conversation(
321
+ copy.deepcopy(body), 8000, target_fraction=0.50, keep_last=4
322
+ )
323
+ self.assertIsNone(out)
324
+
325
+
326
+ class TestFallbacks(unittest.TestCase):
327
+ def test_none_when_stages_cannot_reach_budget(self):
328
+ """All-signal conversation far over budget: even truncating every
329
+ result to its head cannot fit — stage 3 fails closed."""
330
+ body = _big_signal_fixture(n_pairs=8, result_chars=3000)
331
+ out = ap.verbatim_compact_conversation(
332
+ copy.deepcopy(body), 4000, target_fraction=0.25, keep_last=4
333
+ )
334
+ self.assertIsNone(out)
335
+
336
+ def test_none_when_nothing_to_do(self):
337
+ body = _big_signal_fixture(n_pairs=2, result_chars=200)
338
+ out = ap.verbatim_compact_conversation(
339
+ copy.deepcopy(body), 128000, target_fraction=0.65, keep_last=4
340
+ )
341
+ self.assertIsNone(out)
342
+
343
+ def test_none_on_few_message_body(self):
344
+ body = {"messages": [{"role": "user", "content": "x" * 900000}]}
345
+ self.assertIsNone(
346
+ ap.verbatim_compact_conversation(body, 100000, target_fraction=0.5)
347
+ )
348
+
349
+ def test_callers_body_is_pristine_after_fallback(self):
350
+ """The verbatim path works on a copy: a fallback must leave the
351
+ original body untouched for the lossy pruner."""
352
+ body = _big_signal_fixture(n_pairs=8, result_chars=3000)
353
+ snapshot = copy.deepcopy(body)
354
+ out = ap.verbatim_compact_conversation(
355
+ body, 4000, target_fraction=0.25, keep_last=4
356
+ )
357
+ self.assertIsNone(out)
358
+ self.assertEqual(body, snapshot)
359
+
360
+
361
+ class TestEscapeHatchAndWiring(unittest.TestCase):
362
+ def test_env_off_restores_old_path_entirely(self):
363
+ body = _noise_fixture()
364
+ expected = ap.prune_conversation(
365
+ copy.deepcopy(body), 8000,
366
+ monitor=ap.SessionMonitor(context_window=8000),
367
+ target_fraction=0.50, keep_last=4,
368
+ )
369
+ old = ap.PROXY_VERBATIM_COMPACT
370
+ try:
371
+ ap.PROXY_VERBATIM_COMPACT = "off"
372
+ got = ap._compact_conversation(
373
+ copy.deepcopy(body), 8000,
374
+ monitor=ap.SessionMonitor(context_window=8000),
375
+ target_fraction=0.50, keep_last=4,
376
+ )
377
+ finally:
378
+ ap.PROXY_VERBATIM_COMPACT = old
379
+ self.assertEqual(got["messages"], expected["messages"])
380
+ self.assertIn("CONTEXT PRUNED", _all_text(got))
381
+
382
+ def test_wiring_prefers_verbatim_and_skips_drop_boundary(self):
383
+ """With compaction on, the wrapper returns the verbatim result and
384
+ does NOT advance the fallback's monotonic prune_drop_count."""
385
+ monitor = ap.SessionMonitor(context_window=8000)
386
+ out = ap._compact_conversation(
387
+ _noise_fixture(), 8000,
388
+ monitor=monitor, target_fraction=0.50, keep_last=4,
389
+ )
390
+ blob = _all_text(out)
391
+ self.assertNotIn("CONTEXT PRUNED", blob)
392
+ self.assertIn(_pad("SIG1 ", "1"), blob)
393
+ self.assertEqual(monitor.prune_drop_count, 0)
394
+
395
+ def test_wiring_fails_closed_on_exception(self):
396
+ """ANY exception in the verbatim path falls back to the known-good
397
+ pruner with the caller's body intact."""
398
+ old_fn = ap.verbatim_compact_conversation
399
+
400
+ def _boom(*args, **kwargs):
401
+ raise RuntimeError("classifier exploded")
402
+
403
+ ap.verbatim_compact_conversation = _boom
404
+ try:
405
+ out = ap._compact_conversation(
406
+ _noise_fixture(), 8000,
407
+ monitor=ap.SessionMonitor(context_window=8000),
408
+ target_fraction=0.50, keep_last=4,
409
+ )
410
+ finally:
411
+ ap.verbatim_compact_conversation = old_fn
412
+ self.assertIn("CONTEXT PRUNED", _all_text(out))
413
+
414
+
415
+ class TestReductionComparison(unittest.TestCase):
416
+ def test_verbatim_reduces_at_least_as_much_as_lossy_prune(self):
417
+ """On the same oversized noise-bearing fixture, the verbatim path's
418
+ context reduction is >= the old pruner's — it removes only noise and
419
+ adds no breadcrumb/carryover marker back."""
420
+ body = _noise_fixture()
421
+ before = ap.estimate_total_tokens(body)
422
+ verbatim = ap.verbatim_compact_conversation(
423
+ copy.deepcopy(body), 8000, target_fraction=0.50, keep_last=4
424
+ )
425
+ lossy = ap.prune_conversation(
426
+ copy.deepcopy(body), 8000,
427
+ monitor=ap.SessionMonitor(context_window=8000),
428
+ target_fraction=0.50, keep_last=4,
429
+ )
430
+ self.assertIsNotNone(verbatim)
431
+ v_reduction = before - ap.estimate_total_tokens(verbatim)
432
+ l_reduction = before - ap.estimate_total_tokens(lossy)
433
+ self.assertGreaterEqual(v_reduction, l_reduction)
434
+ # and the verbatim result fits the same budget the pruner targets
435
+ budget = int(8000 * 0.50)
436
+ self.assertLessEqual(
437
+ sum(ap.estimate_message_tokens(m) for m in verbatim["messages"]),
438
+ budget,
439
+ )
440
+
441
+
442
+ def _custom_fixture(middle_messages):
443
+ """Head + given middle + 4-message tail, matching the keep_last=4 shape."""
444
+ return {
445
+ "messages": [{"role": "user", "content": "task"}]
446
+ + middle_messages
447
+ + [{"role": "user", "content": t} for t in ("t0", "t1", "t2", "t3")]
448
+ }
449
+
450
+
451
+ def _padding_pairs(start_idx, n, result_chars=3000):
452
+ """n unique signal Read pairs (never noise) to push a fixture over budget."""
453
+ msgs = []
454
+ for i in range(start_idx, start_idx + n):
455
+ msgs.extend(
456
+ _pair(i, "Read", {"file_path": f"/src/pad{i}.py"},
457
+ f"PAD{i} " + (f"p{i}" * (result_chars // 2)))
458
+ )
459
+ return msgs
460
+
461
+
462
+ class TestTruncateBandRegression(unittest.TestCase):
463
+ def test_1300_char_band_block_untouched(self):
464
+ """A text in the (MIN=1200, HEAD=1500 + note] band would GROW by the
465
+ note with nothing cut — it must be skipped entirely."""
466
+ pair = _pair(0, "Read", {"file_path": "/a"}, "B" * 1300)
467
+ removed = ap._truncate_pair(pair)
468
+ self.assertEqual(removed, 0)
469
+ self.assertEqual(pair[1]["content"][0]["content"], "B" * 1300)
470
+
471
+ def test_truncate_text_block_band_boundaries(self):
472
+ self.assertEqual(ap._truncate_text_block("x" * 1300), ("x" * 1300, 0))
473
+ big = "y" * 5000
474
+ new_text, removed = ap._truncate_text_block(big)
475
+ self.assertGreater(removed, 0)
476
+ self.assertTrue(new_text.startswith("y" * ap._VERBATIM_TRUNCATE_HEAD_CHARS))
477
+ self.assertIn("[… truncated ~", new_text)
478
+ self.assertLess(len(new_text), len(big))
479
+
480
+
481
+ class TestRepeatedCallResultEquality(unittest.TestCase):
482
+ def test_earlier_error_result_is_not_dropped(self):
483
+ """A repeated call whose EARLIER result was an error must not vanish
484
+ because the later identical call succeeded blandly."""
485
+ middle = _pair(0, "Bash", {"command": "probe"}, "Error: connection refused")
486
+ middle[1]["content"][0]["is_error"] = True
487
+ middle += _pair(1, "Bash", {"command": "probe"}, "probe ok")
488
+ middle += _padding_pairs(10, 6)
489
+ body = _custom_fixture(middle)
490
+ out = ap.verbatim_compact_conversation(
491
+ copy.deepcopy(body), 8000, target_fraction=0.50, keep_last=4
492
+ )
493
+ self.assertIsNotNone(out)
494
+ self.assertIn("Error: connection refused", _all_text(out))
495
+
496
+ def test_identical_bland_repeat_is_dropped(self):
497
+ middle = _pair(0, "Bash", {"command": "probe"}, "probe ok")
498
+ middle += _pair(1, "Bash", {"command": "probe"}, "probe ok")
499
+ middle += _padding_pairs(10, 6)
500
+ body = _custom_fixture(middle)
501
+ out = ap.verbatim_compact_conversation(
502
+ copy.deepcopy(body), 8000, target_fraction=0.50, keep_last=4
503
+ )
504
+ self.assertIsNotNone(out)
505
+ self.assertEqual(_all_text(out).count("probe ok"), 1) # freshest kept
506
+
507
+ def test_ack_phrase_with_real_payload_is_not_ack(self):
508
+ self.assertFalse(ap._is_ack_result("ok, here are the 3 failing tests"))
509
+ self.assertTrue(ap._is_ack_result("OK."))
510
+ self.assertTrue(
511
+ ap._is_ack_result("File created successfully at: /src/out.py")
512
+ )
513
+
514
+
515
+ class TestDropBreadcrumb(unittest.TestCase):
516
+ def test_breadcrumb_present_bounded_and_lists_tools(self):
517
+ body = _noise_fixture()
518
+ out = ap.verbatim_compact_conversation(
519
+ copy.deepcopy(body), 8000, target_fraction=0.50, keep_last=4
520
+ )
521
+ self.assertIsNotNone(out)
522
+ crumbs = [
523
+ m["content"]
524
+ for m in out["messages"]
525
+ if isinstance(m.get("content"), str) and m["content"].startswith("[compaction:")
526
+ ]
527
+ self.assertEqual(len(crumbs), 1) # exactly one note, at the first drop
528
+ crumb = crumbs[0]
529
+ self.assertLessEqual(len(crumb), 300)
530
+ self.assertIn("noise tool pairs elided", crumb)
531
+ self.assertIn("read /src/", crumb) # names tools + paths
532
+ self.assertIn("write /src/", crumb)
533
+
534
+
535
+ class TestSupersededReadDoctrine(unittest.TestCase):
536
+ def test_intervening_write_keeps_first_read(self):
537
+ """Read A -> Write A -> Read A: the first read is the before/after
538
+ record — NOT superseded."""
539
+ middle = _pair(0, "Read", {"file_path": "/src/state.py"}, _pad("OLD-A ", "o"))
540
+ middle += _pair(1, "Write", {"file_path": "/src/state.py"},
541
+ "File created successfully at: /src/state.py")
542
+ middle += _pair(2, "Read", {"file_path": "/src/state.py"}, _pad("NEW-A ", "n"))
543
+ middle += _padding_pairs(10, 6)
544
+ body = _custom_fixture(middle)
545
+ out = ap.verbatim_compact_conversation(
546
+ copy.deepcopy(body), 8000, target_fraction=0.50, keep_last=4
547
+ )
548
+ self.assertIsNotNone(out)
549
+ blob = _all_text(out)
550
+ self.assertIn(_pad("OLD-A ", "o"), blob) # before-record kept
551
+ self.assertIn(_pad("NEW-A ", "n"), blob)
552
+
553
+ def test_later_errored_read_does_not_supersede(self):
554
+ middle = _pair(0, "Read", {"file_path": "/src/x.py"}, _pad("GOOD-READ ", "g"))
555
+ middle += _pair(1, "Read", {"file_path": "/src/x.py"}, "Error: boom")
556
+ middle[3]["content"][0]["is_error"] = True # second read errored
557
+ middle += _padding_pairs(10, 6)
558
+ body = _custom_fixture(middle)
559
+ out = ap.verbatim_compact_conversation(
560
+ copy.deepcopy(body), 8000, target_fraction=0.50, keep_last=4
561
+ )
562
+ self.assertIsNotNone(out)
563
+ self.assertIn(_pad("GOOD-READ ", "g"), _all_text(out))
564
+
565
+
566
+ class TestStage2WritePayloadImmunity(unittest.TestCase):
567
+ def test_write_pair_input_survives_stage2_verbatim(self):
568
+ """Stage 2 truncates the Write pair's RESULT but never its input —
569
+ the input payload is the model's own action history."""
570
+ write_input = {"file_path": "/src/big.py", "content": "W" * 3000}
571
+ middle = _pair(0, "Write", write_input, "WRITE-RESULT " + "r" * 3000)
572
+ middle += _padding_pairs(10, 5)
573
+ body = _custom_fixture(middle)
574
+ out = ap.verbatim_compact_conversation(
575
+ copy.deepcopy(body), 8000, target_fraction=0.50, keep_last=4
576
+ )
577
+ self.assertIsNotNone(out)
578
+ write_use = next(
579
+ b
580
+ for m in out["messages"]
581
+ for b in ap._iter_tool_use_blocks(m)
582
+ if b.get("name") == "Write"
583
+ )
584
+ self.assertEqual(write_use["input"]["content"], "W" * 3000) # intact
585
+ write_result = next(
586
+ b
587
+ for m in out["messages"]
588
+ for b in ap._iter_tool_result_blocks(m)
589
+ if b.get("tool_use_id") == "toolu_0"
590
+ )
591
+ self.assertIn("[… truncated ~", ap._extract_text(write_result["content"]))
592
+
593
+
594
+ class TestMonitorBoundaryReset(unittest.TestCase):
595
+ def test_prune_drop_count_reset_on_verbatim_success(self):
596
+ """The verbatim path re-indexes the middle — a stale legacy boundary
597
+ would force-drop kept signal on a later fallback turn."""
598
+ monitor = ap.SessionMonitor(context_window=8000)
599
+ monitor.prune_drop_count = 7
600
+ out = ap.verbatim_compact_conversation(
601
+ _noise_fixture(), 8000, monitor=monitor,
602
+ target_fraction=0.50, keep_last=4,
603
+ )
604
+ self.assertIsNotNone(out)
605
+ self.assertEqual(monitor.prune_drop_count, 0)
606
+
607
+
608
+ class TestWideFixturePerformance(unittest.TestCase):
609
+ def test_2000_pair_fixture_completes_fast(self):
610
+ """Guards the occurrence-index complexity: 1000 unique calls, each
611
+ repeated once (all stage-1 noise), must not go quadratic."""
612
+ import time
613
+
614
+ middle = []
615
+ for i in range(1000):
616
+ middle += _pair(2 * i, "Bash", {"command": f"cmd{i}"}, f"out{i} bland")
617
+ middle += _pair(2 * i + 1, "Bash", {"command": f"cmd{i}"}, f"out{i} bland")
618
+ body = _custom_fixture(middle)
619
+ start = time.monotonic()
620
+ out = ap.verbatim_compact_conversation(
621
+ copy.deepcopy(body), 40000, target_fraction=0.50, keep_last=4
622
+ )
623
+ elapsed = time.monotonic() - start
624
+ self.assertIsNotNone(out) # stage 1 drops the 1000 older repeats
625
+ self.assertLess(elapsed, 5.0)
626
+
627
+
628
+ if __name__ == "__main__":
629
+ unittest.main()