@miller-tech/uap 1.175.3 → 1.175.4
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/package.json +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/templates/hooks/__pycache__/deliver_autoroute.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 +29 -1
- package/tools/agents/tests/test_error_loop_ignores_correctives.py +92 -0
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -300,19 +300,47 @@ _ERROR_LINE_RE = re.compile(
|
|
|
300
300
|
)
|
|
301
301
|
|
|
302
302
|
|
|
303
|
+
# Harness-generated CORRECTIVES, not tool failures.
|
|
304
|
+
#
|
|
305
|
+
# The delivery executor emits its own control lines, and some carry the word
|
|
306
|
+
# "error" purely as a channel label -- e.g.
|
|
307
|
+
#
|
|
308
|
+
# [agent r6 error] write-nudge injected after 5 read-only rounds
|
|
309
|
+
#
|
|
310
|
+
# which is the harness telling the model it has been READING too much and must
|
|
311
|
+
# now WRITE. _ERROR_LINE_RE matched it on "error", so the corrective became an
|
|
312
|
+
# error signature; after three repeats the ERROR-LOOP guard fired and injected
|
|
313
|
+
# "Do NOT make another edit yet. FIRST re-read the ENTIRE failing file" -- which
|
|
314
|
+
# tells a model already stuck in a read-only loop to read more, directly fighting
|
|
315
|
+
# the nudge it was reacting to. Observed live (2026-07-31): fired six times in a
|
|
316
|
+
# row against a run that was progressing.
|
|
317
|
+
#
|
|
318
|
+
# A corrective is the harness talking to the model about its BEHAVIOUR; an error
|
|
319
|
+
# is a tool reporting that something did not work. Only the latter belongs in the
|
|
320
|
+
# failure streak.
|
|
321
|
+
_HARNESS_CORRECTIVE_RE = re.compile(
|
|
322
|
+
r"(?:write-nudge|nudge)\s+injected|read-only rounds|forced[- ]write round|"
|
|
323
|
+
r"deferral[- ]break|stuck[- ]break|cycle[- ]break",
|
|
324
|
+
re.IGNORECASE,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
303
328
|
def _error_signature(text: str) -> str:
|
|
304
329
|
"""Edit-invariant signature of the first error line in a tool result.
|
|
305
330
|
|
|
306
331
|
Normalizes away paths, line:col numbers, hex, and bare digits so that the
|
|
307
332
|
SAME underlying failure produces the SAME signature across turns even as the
|
|
308
333
|
model edits different code around it. Returns "" when no error line is found
|
|
309
|
-
(a passing result resets the streak)
|
|
334
|
+
(a passing result resets the streak), and "" for the harness's own
|
|
335
|
+
correctives -- see _HARNESS_CORRECTIVE_RE."""
|
|
310
336
|
if not text:
|
|
311
337
|
return ""
|
|
312
338
|
m = _ERROR_LINE_RE.search(text)
|
|
313
339
|
if not m:
|
|
314
340
|
return ""
|
|
315
341
|
line = m.group(0)
|
|
342
|
+
if _HARNESS_CORRECTIVE_RE.search(line):
|
|
343
|
+
return ""
|
|
316
344
|
line = re.sub(r"(/[^\s:]+)+", "<path>", line) # unix paths
|
|
317
345
|
line = re.sub(r"\b[0-9a-fA-F]{6,}\b", "<hex>", line) # hashes/addresses
|
|
318
346
|
line = re.sub(r"\d+", "#", line) # line numbers, counts
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""The ERROR-LOOP guard must not fire on the harness's own correctives.
|
|
2
|
+
|
|
3
|
+
Observed live (opencode, 2026-07-31). The delivery executor emits control lines
|
|
4
|
+
on an "error" channel:
|
|
5
|
+
|
|
6
|
+
[agent r6 error] write-nudge injected after 5 read-only rounds
|
|
7
|
+
|
|
8
|
+
which is the harness telling the model it has been READING too much and must now
|
|
9
|
+
WRITE. `_ERROR_LINE_RE` matched it on the word "error", so the corrective entered
|
|
10
|
+
the failure streak; after three repeats the ERROR-LOOP guard injected
|
|
11
|
+
|
|
12
|
+
"Do NOT make another edit yet. FIRST re-read the ENTIRE failing file ..."
|
|
13
|
+
|
|
14
|
+
i.e. it told a model already stuck in a read-only loop to read more, fighting the
|
|
15
|
+
very nudge it was reacting to. It fired six times in a row against a run that was
|
|
16
|
+
making progress.
|
|
17
|
+
|
|
18
|
+
The distinction the fix encodes: a CORRECTIVE is the harness talking to the model
|
|
19
|
+
about its behaviour; an ERROR is a tool reporting that something did not work.
|
|
20
|
+
Only the latter belongs in the streak.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import re
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
PROXY = Path(__file__).resolve().parents[1] / "scripts" / "anthropic_proxy.py"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _load_signature():
|
|
30
|
+
"""Extract _error_signature and its patterns without importing the server."""
|
|
31
|
+
src = PROXY.read_text()
|
|
32
|
+
ns = {"re": re}
|
|
33
|
+
for name in ("_ERROR_LINE_RE", "_HARNESS_CORRECTIVE_RE"):
|
|
34
|
+
start = src.index(f"{name} = re.compile(")
|
|
35
|
+
end = src.index(")\n", src.index("re.", start + 20)) + 1
|
|
36
|
+
exec(src[start:end], ns) # noqa: S102 - reading our own source, not input
|
|
37
|
+
start = src.index("def _error_signature")
|
|
38
|
+
end = src.index("\n# ---", start)
|
|
39
|
+
exec(src[start:end], ns) # noqa: S102
|
|
40
|
+
return ns["_error_signature"]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_error_signature = _load_signature()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestHarnessCorrectivesAreNotFailures:
|
|
47
|
+
def test_write_nudge_does_not_enter_the_failure_streak(self):
|
|
48
|
+
# The exact line from the incident.
|
|
49
|
+
assert _error_signature("[agent r6 error] write-nudge injected after 5 read-only rounds") == ""
|
|
50
|
+
|
|
51
|
+
def test_other_correctives_are_ignored_too(self):
|
|
52
|
+
for line in (
|
|
53
|
+
"[agent r9 error] forced-write round engaged",
|
|
54
|
+
"[agent r4 error] nudge injected after 5 read-only rounds",
|
|
55
|
+
"deferral-break: model deferred instead of acting",
|
|
56
|
+
"cycle-break engaged",
|
|
57
|
+
):
|
|
58
|
+
assert _error_signature(line) == "", line
|
|
59
|
+
|
|
60
|
+
def test_a_corrective_repeated_never_builds_a_streak(self):
|
|
61
|
+
# The streak is what arms the guard; an empty signature can never match
|
|
62
|
+
# the previous one, so repetition alone cannot fire it.
|
|
63
|
+
line = "[agent r7 error] write-nudge injected after 5 read-only rounds"
|
|
64
|
+
assert {_error_signature(line) for _ in range(6)} == {""}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestRealFailuresStillTracked:
|
|
68
|
+
def test_tool_errors_still_produce_a_signature(self):
|
|
69
|
+
for line in (
|
|
70
|
+
"ERROR: TypeError: x is not a function at /a/b.js:12",
|
|
71
|
+
"FAILED tests/foo.test.ts > it works",
|
|
72
|
+
"SyntaxError: Unexpected token",
|
|
73
|
+
"bash: command not found: pytest",
|
|
74
|
+
):
|
|
75
|
+
assert _error_signature(line) != "", line
|
|
76
|
+
|
|
77
|
+
def test_the_same_failure_still_produces_a_stable_signature(self):
|
|
78
|
+
# Edit-invariance is the whole point of the signature: the same failure
|
|
79
|
+
# must match across turns even as line numbers and paths move.
|
|
80
|
+
a = _error_signature("ERROR: TypeError: x is not a function at /src/a.js:12")
|
|
81
|
+
b = _error_signature("ERROR: TypeError: x is not a function at /lib/b.js:940")
|
|
82
|
+
assert a == b != ""
|
|
83
|
+
|
|
84
|
+
def test_a_passing_result_resets_the_streak(self):
|
|
85
|
+
assert _error_signature("all tests passed") == ""
|
|
86
|
+
|
|
87
|
+
def test_a_corrective_wrapped_around_a_REAL_error_still_counts(self):
|
|
88
|
+
# Only the matched line is inspected, so a genuine failure elsewhere in
|
|
89
|
+
# the same payload must still register — otherwise the exclusion would
|
|
90
|
+
# become a way to hide real errors behind a nudge.
|
|
91
|
+
text = "ERROR: ReferenceError: foo is not defined\n[agent r6 error] write-nudge injected"
|
|
92
|
+
assert _error_signature(text) != ""
|