@miller-tech/uap 1.130.2 → 1.132.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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/cli/deliver.js +45 -29
- package/dist/cli/deliver.js.map +1 -1
- package/dist/cli/handsfree.d.ts.map +1 -1
- package/dist/cli/handsfree.js +12 -1
- package/dist/cli/handsfree.js.map +1 -1
- package/dist/delivery/context-budget.d.ts +25 -3
- package/dist/delivery/context-budget.d.ts.map +1 -1
- package/dist/delivery/context-budget.js +74 -3
- package/dist/delivery/context-budget.js.map +1 -1
- package/dist/delivery/decompose.d.ts.map +1 -1
- package/dist/delivery/decompose.js +4 -1
- package/dist/delivery/decompose.js.map +1 -1
- package/dist/delivery/epic-controller.d.ts +20 -1
- package/dist/delivery/epic-controller.d.ts.map +1 -1
- package/dist/delivery/epic-controller.js +21 -10
- package/dist/delivery/epic-controller.js.map +1 -1
- package/dist/delivery/persistence-profile.d.ts.map +1 -1
- package/dist/delivery/persistence-profile.js +19 -2
- package/dist/delivery/persistence-profile.js.map +1 -1
- package/package.json +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +148 -6
- package/tools/agents/tests/test_prune_preserve_force_write.py +209 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Tests for the huge-project hands-free fixes:
|
|
2
|
+
|
|
3
|
+
#3 FORCE-WRITE: at the hard recon-convergence tier the proxy strips the
|
|
4
|
+
exploration tools (read-only class + escape hatches) and forces
|
|
5
|
+
tool_choice='required' so the model MUST write instead of reading again.
|
|
6
|
+
#2 STATE CARRYOVER: the contamination breaker reconstructs the plan + files
|
|
7
|
+
written from the message stream and re-injects them after the reset, so a
|
|
8
|
+
prune no longer destroys mission state.
|
|
9
|
+
"""
|
|
10
|
+
import importlib.util
|
|
11
|
+
import unittest
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
proxy_path = Path(__file__).resolve().parents[3] / "tools" / "agents" / "scripts" / "anthropic_proxy.py"
|
|
15
|
+
spec = importlib.util.spec_from_file_location("anthropic_proxy", proxy_path)
|
|
16
|
+
ap = importlib.util.module_from_spec(spec)
|
|
17
|
+
spec.loader.exec_module(ap)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _tool(name):
|
|
21
|
+
return {"type": "function", "function": {"name": name, "parameters": {}}}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _names(tools):
|
|
25
|
+
return {(t.get("function", {}) or {}).get("name", "") for t in tools}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ForceWriteHardTierTest(unittest.TestCase):
|
|
29
|
+
"""#3: hard tier removes read/exploration tools and forces a write."""
|
|
30
|
+
|
|
31
|
+
def _hard_monitor(self):
|
|
32
|
+
m = ap.SessionMonitor(context_window=132096)
|
|
33
|
+
m.consecutive_no_write_turns = (
|
|
34
|
+
int(ap.PROXY_RECON_HARD_MULTIPLIER * ap.PROXY_RECON_CONVERGENCE_THRESHOLD) + 1
|
|
35
|
+
)
|
|
36
|
+
return m
|
|
37
|
+
|
|
38
|
+
def test_strips_reads_and_forces_required(self):
|
|
39
|
+
m = self._hard_monitor()
|
|
40
|
+
tools = [_tool("Read"), _tool("Grep"), _tool("Bash"), _tool("Write"), _tool("Edit")]
|
|
41
|
+
body = {"messages": [{"role": "user", "content": "reading"}], "tools": list(tools)}
|
|
42
|
+
ap._maybe_inject_recon_convergence(body, m, full_tools=tools)
|
|
43
|
+
remaining = _names(body["tools"])
|
|
44
|
+
self.assertNotIn("Read", remaining)
|
|
45
|
+
self.assertNotIn("Grep", remaining)
|
|
46
|
+
self.assertNotIn("Bash", remaining) # exploration escape hatch also stripped
|
|
47
|
+
self.assertIn("Write", remaining)
|
|
48
|
+
self.assertIn("Edit", remaining)
|
|
49
|
+
self.assertEqual(body.get("tool_choice"), "required")
|
|
50
|
+
|
|
51
|
+
def test_keeps_deliver_when_writes_are_gated(self):
|
|
52
|
+
m = self._hard_monitor()
|
|
53
|
+
tools = [_tool("Read"), _tool("Bash"), _tool("deliver")]
|
|
54
|
+
body = {"messages": [{"role": "user", "content": "reading"}], "tools": list(tools)}
|
|
55
|
+
ap._maybe_inject_recon_convergence(body, m, full_tools=tools)
|
|
56
|
+
remaining = _names(body["tools"])
|
|
57
|
+
self.assertIn("deliver", remaining) # deliver is a write path — survives
|
|
58
|
+
self.assertNotIn("Read", remaining)
|
|
59
|
+
self.assertEqual(body.get("tool_choice"), "required")
|
|
60
|
+
|
|
61
|
+
def test_floor_invariant_no_write_path_left_untouched(self):
|
|
62
|
+
"""If stripping reads would leave NO write path, do not strip (never
|
|
63
|
+
strand). Falls back to the hard tier's 'auto' release."""
|
|
64
|
+
m = self._hard_monitor()
|
|
65
|
+
tools = [_tool("Read"), _tool("Grep")] # read-only, no write tool
|
|
66
|
+
body = {"messages": [{"role": "user", "content": "reading"}], "tools": list(tools)}
|
|
67
|
+
ap._maybe_inject_recon_convergence(body, m, full_tools=tools)
|
|
68
|
+
remaining = _names(body["tools"])
|
|
69
|
+
self.assertIn("Read", remaining) # untouched — no write path to force toward
|
|
70
|
+
self.assertNotEqual(body.get("tool_choice"), "required")
|
|
71
|
+
|
|
72
|
+
def test_firm_tier_does_not_force_write(self):
|
|
73
|
+
"""The firm tier (below the hard multiplier) must keep reads available."""
|
|
74
|
+
m = ap.SessionMonitor(context_window=132096)
|
|
75
|
+
m.consecutive_no_write_turns = ap.PROXY_RECON_CONVERGENCE_THRESHOLD # firm
|
|
76
|
+
tools = [_tool("Read"), _tool("Write")]
|
|
77
|
+
body = {"messages": [{"role": "user", "content": "reading"}],
|
|
78
|
+
"tools": list(tools), "tool_choice": "required"}
|
|
79
|
+
ap._maybe_inject_recon_convergence(body, m, full_tools=tools)
|
|
80
|
+
self.assertIn("Read", _names(body["tools"])) # reads still allowed at firm
|
|
81
|
+
self.assertEqual(body.get("tool_choice"), "auto")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class StateCarryoverExtractTest(unittest.TestCase):
|
|
85
|
+
"""#2: _extract_state_carryover reconstructs plan + files from the stream."""
|
|
86
|
+
|
|
87
|
+
def _convo(self):
|
|
88
|
+
return [
|
|
89
|
+
{"role": "user", "content": "build the app"},
|
|
90
|
+
{"role": "assistant", "content": [
|
|
91
|
+
{"type": "tool_use", "name": "TodoWrite", "input": {"todos": [
|
|
92
|
+
{"content": "scaffold project", "status": "completed"},
|
|
93
|
+
{"content": "add auth", "status": "in_progress"},
|
|
94
|
+
{"content": "write tests", "status": "pending"},
|
|
95
|
+
]}},
|
|
96
|
+
]},
|
|
97
|
+
{"role": "assistant", "content": [
|
|
98
|
+
{"type": "tool_use", "name": "Write", "input": {"file_path": "src/index.ts"}},
|
|
99
|
+
]},
|
|
100
|
+
{"role": "assistant", "content": [
|
|
101
|
+
{"type": "tool_use", "name": "Edit", "input": {"file_path": "src/auth.ts"}},
|
|
102
|
+
]},
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
def test_extracts_plan_and_files(self):
|
|
106
|
+
out = ap._extract_state_carryover(self._convo())
|
|
107
|
+
self.assertIsNotNone(out)
|
|
108
|
+
self.assertIn("STATE CARRYOVER", out)
|
|
109
|
+
self.assertIn("scaffold project", out)
|
|
110
|
+
self.assertIn("add auth", out)
|
|
111
|
+
self.assertIn("[x] scaffold project", out)
|
|
112
|
+
self.assertIn("[~] add auth", out)
|
|
113
|
+
self.assertIn("src/index.ts", out)
|
|
114
|
+
self.assertIn("src/auth.ts", out)
|
|
115
|
+
|
|
116
|
+
def test_returns_none_on_fresh_session(self):
|
|
117
|
+
convo = [{"role": "user", "content": "hi"},
|
|
118
|
+
{"role": "assistant", "content": [
|
|
119
|
+
{"type": "tool_use", "name": "Read", "input": {"file_path": "x"}}]}]
|
|
120
|
+
self.assertIsNone(ap._extract_state_carryover(convo))
|
|
121
|
+
|
|
122
|
+
def test_keeps_most_recent_plan_only(self):
|
|
123
|
+
convo = self._convo()
|
|
124
|
+
convo.append({"role": "assistant", "content": [
|
|
125
|
+
{"type": "tool_use", "name": "TodoWrite", "input": {"todos": [
|
|
126
|
+
{"content": "final polish", "status": "pending"}]}}]})
|
|
127
|
+
out = ap._extract_state_carryover(convo)
|
|
128
|
+
self.assertIn("final polish", out)
|
|
129
|
+
self.assertNotIn("scaffold project", out) # superseded by the later plan
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ContaminationBreakerPreservesStateTest(unittest.TestCase):
|
|
133
|
+
"""#2 end-to-end: a standard contamination reset re-injects the carryover."""
|
|
134
|
+
|
|
135
|
+
def test_standard_reset_includes_carryover(self):
|
|
136
|
+
m = ap.SessionMonitor(context_window=132096)
|
|
137
|
+
m.malformed_tool_streak = max(1, ap.PROXY_SESSION_CONTAMINATION_THRESHOLD)
|
|
138
|
+
keep = max(2, ap.PROXY_SESSION_CONTAMINATION_KEEP_LAST)
|
|
139
|
+
filler = [{"role": "user", "content": f"m{i}"} for i in range(keep + 4)]
|
|
140
|
+
messages = (
|
|
141
|
+
[{"role": "user", "content": "build the app"}]
|
|
142
|
+
+ [{"role": "assistant", "content": [
|
|
143
|
+
{"type": "tool_use", "name": "TodoWrite", "input": {"todos": [
|
|
144
|
+
{"content": "step one", "status": "completed"},
|
|
145
|
+
{"content": "step two", "status": "pending"}]}}]}]
|
|
146
|
+
+ [{"role": "assistant", "content": [
|
|
147
|
+
{"type": "tool_use", "name": "Write", "input": {"file_path": "app/main.py"}}]}]
|
|
148
|
+
+ filler
|
|
149
|
+
)
|
|
150
|
+
body = {"messages": messages, "tools": [_tool("Write")]}
|
|
151
|
+
out = ap._maybe_apply_session_contamination_breaker(body, m, "fp:test")
|
|
152
|
+
blob = "\n".join(
|
|
153
|
+
b if isinstance(b, str) else str(b)
|
|
154
|
+
for msg in out["messages"] for b in [msg.get("content", "")]
|
|
155
|
+
)
|
|
156
|
+
self.assertIn("STATE CARRYOVER", blob)
|
|
157
|
+
self.assertIn("app/main.py", blob)
|
|
158
|
+
self.assertIn("step two", blob)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class PrunerStateCarryoverTest(unittest.TestCase):
|
|
162
|
+
"""#2 companion: the CONTEXT PRUNER (not just the contamination breaker)
|
|
163
|
+
must carry the plan + files-written across a drop, or a long build re-reads
|
|
164
|
+
its own output after every prune (observed live: `cat lib.rs` ×16)."""
|
|
165
|
+
|
|
166
|
+
def test_prune_marker_carries_written_files(self):
|
|
167
|
+
long = "x " * 800 # ~400 tok each — large middle, small head/tail
|
|
168
|
+
messages = [{"role": "user", "content": "build the org-model crate"}]
|
|
169
|
+
# a WRITE in the middle (will be dropped) — the breadcrumb never
|
|
170
|
+
# captures write actions, only tool_result reads.
|
|
171
|
+
messages.append({"role": "assistant", "content": [
|
|
172
|
+
{"type": "tool_use", "name": "Write",
|
|
173
|
+
"input": {"file_path": "org-model/src/person.rs"}}]})
|
|
174
|
+
for _ in range(6): # large read results fill the middle
|
|
175
|
+
messages.append({"role": "user", "content": [
|
|
176
|
+
{"type": "tool_result", "content": "read " + long}]})
|
|
177
|
+
for t in ["a", "b", "c", "d"]: # small recent tail (protected, fits)
|
|
178
|
+
messages.append({"role": "user", "content": t})
|
|
179
|
+
|
|
180
|
+
body = {"messages": messages}
|
|
181
|
+
m = ap.SessionMonitor(context_window=4000)
|
|
182
|
+
out = ap.prune_conversation(body, context_window=4000, monitor=m,
|
|
183
|
+
target_fraction=0.4, keep_last=4)
|
|
184
|
+
blob = "\n".join(
|
|
185
|
+
b if isinstance(b, str) else str(b)
|
|
186
|
+
for msg in out["messages"] for b in [msg.get("content", "")]
|
|
187
|
+
)
|
|
188
|
+
# the dropped Write action survives as a carryover manifest entry
|
|
189
|
+
self.assertIn("STATE CARRYOVER", blob)
|
|
190
|
+
self.assertIn("org-model/src/person.rs", blob)
|
|
191
|
+
|
|
192
|
+
def test_no_carryover_on_fresh_prune_still_prunes(self):
|
|
193
|
+
"""A prune with no plan/writes to carry still works (breadcrumb only)."""
|
|
194
|
+
long = "y " * 800
|
|
195
|
+
messages = [{"role": "user", "content": "explore"}]
|
|
196
|
+
for _ in range(6):
|
|
197
|
+
messages.append({"role": "user", "content": [
|
|
198
|
+
{"type": "tool_result", "content": "read " + long}]})
|
|
199
|
+
for t in ["a", "b", "c", "d"]:
|
|
200
|
+
messages.append({"role": "user", "content": t})
|
|
201
|
+
m = ap.SessionMonitor(context_window=4000)
|
|
202
|
+
out = ap.prune_conversation({"messages": messages}, context_window=4000,
|
|
203
|
+
monitor=m, target_fraction=0.4, keep_last=4)
|
|
204
|
+
# no crash, and it did drop (fewer messages than input)
|
|
205
|
+
self.assertLess(len(out["messages"]), len(messages))
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
if __name__ == "__main__":
|
|
209
|
+
unittest.main()
|