@miller-tech/uap 2.36.1 → 2.39.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/config/model-profiles/qwen38.json +3 -3
- package/config/model-registry.json +96 -6
- package/dist/.tsbuildinfo +1 -1
- package/dist/bin/cli.js +57 -0
- package/dist/bin/cli.js.map +1 -1
- package/dist/cli/deliver.d.ts +46 -0
- package/dist/cli/deliver.d.ts.map +1 -1
- package/dist/cli/deliver.js +155 -36
- package/dist/cli/deliver.js.map +1 -1
- package/dist/cli/models.d.ts +11 -1
- package/dist/cli/models.d.ts.map +1 -1
- package/dist/cli/models.js +164 -3
- package/dist/cli/models.js.map +1 -1
- package/dist/cli/workflow.d.ts +36 -0
- package/dist/cli/workflow.d.ts.map +1 -0
- package/dist/cli/workflow.js +246 -0
- package/dist/cli/workflow.js.map +1 -0
- package/dist/delivery/run-state.d.ts +1 -1
- package/dist/delivery/run-state.d.ts.map +1 -1
- package/dist/delivery/run-state.js +1 -1
- package/dist/delivery/run-state.js.map +1 -1
- package/dist/delivery/team-solve-deliver.d.ts +83 -0
- package/dist/delivery/team-solve-deliver.d.ts.map +1 -0
- package/dist/delivery/team-solve-deliver.js +191 -0
- package/dist/delivery/team-solve-deliver.js.map +1 -0
- package/dist/inference/model-files.d.ts +111 -0
- package/dist/inference/model-files.d.ts.map +1 -0
- package/dist/inference/model-files.js +172 -0
- package/dist/inference/model-files.js.map +1 -0
- package/dist/placement/registry.d.ts +650 -0
- package/dist/placement/registry.d.ts.map +1 -1
- package/dist/placement/registry.js +21 -0
- package/dist/placement/registry.js.map +1 -1
- package/dist/workflow/loader.d.ts +34 -0
- package/dist/workflow/loader.d.ts.map +1 -0
- package/dist/workflow/loader.js +232 -0
- package/dist/workflow/loader.js.map +1 -0
- package/dist/workflow/runner.d.ts +90 -0
- package/dist/workflow/runner.d.ts.map +1 -0
- package/dist/workflow/runner.js +416 -0
- package/dist/workflow/runner.js.map +1 -0
- package/dist/workflow/types.d.ts +400 -0
- package/dist/workflow/types.d.ts.map +1 -0
- package/dist/workflow/types.js +147 -0
- package/dist/workflow/types.js.map +1 -0
- package/docs/INDEX.md +1 -0
- package/docs/architecture/adr/0013-declarative-workflow-layer.md +99 -0
- package/docs/architecture/adr/0014-loop-steps-and-team-solve-deliver.md +142 -0
- package/docs/benchmarks/LCB_DROID_SMOKE.md +111 -0
- package/docs/guides/DELIVER.md +3 -1
- package/docs/guides/WORKFLOWS.md +179 -0
- package/docs/plans/model-performance-proof-options.md +39 -0
- package/docs/reference/CLI.md +33 -0
- package/docs/reference/CONFIGURATION.md +1 -0
- package/docs/reference/FEATURES.md +18 -0
- package/examples/auto-coordinator-example.js +127 -0
- package/examples/demo-order-solid.ts +19 -0
- package/examples/order-processing-strategy-pattern.ts +186 -0
- package/examples/order-processor-refactored.ts +473 -0
- package/examples/order-solid-refactor.ts +830 -0
- package/examples/refactored-order-processing.ts +106 -0
- package/examples/run-order-processing.ts +48 -0
- package/examples/solid-order-processing.ts +324 -0
- package/examples/strategy-pattern-example.ts +100 -0
- package/examples/workflows/team-solve/README.md +85 -0
- package/examples/workflows/team-solve/check-samples.sh +50 -0
- package/examples/workflows/team-solve/sample/task.md +22 -0
- package/examples/workflows/team-solve/sample/tests/1.in +2 -0
- package/examples/workflows/team-solve/sample/tests/1.out +1 -0
- package/examples/workflows/team-solve/sample/tests/2.in +2 -0
- package/examples/workflows/team-solve/sample/tests/2.out +1 -0
- package/examples/workflows/team-solve/sample/tests/3.in +2 -0
- package/examples/workflows/team-solve/sample/tests/3.out +1 -0
- package/examples/workflows/team-solve/sample/tests/4.in +2 -0
- package/examples/workflows/team-solve/sample/tests/4.out +1 -0
- package/examples/workflows/team-solve/sample/tests/5.in +1 -0
- package/examples/workflows/team-solve/sample/tests/5.out +1 -0
- package/examples/workflows/team-solve/steps/ideate-a.md +26 -0
- package/examples/workflows/team-solve/steps/ideate-b.md +28 -0
- package/examples/workflows/team-solve/steps/plan.md +20 -0
- package/examples/workflows/team-solve/steps/repair.md +25 -0
- package/examples/workflows/team-solve/steps/review.md +29 -0
- package/examples/workflows/team-solve/steps/solve.md +26 -0
- package/examples/workflows/team-solve/workflow.yaml +78 -0
- package/examples/workflows/team-solve-deliver/README.md +84 -0
- package/examples/workflows/team-solve-deliver/steps/ideate-a.md +25 -0
- package/examples/workflows/team-solve-deliver/steps/ideate-b.md +28 -0
- package/examples/workflows/team-solve-deliver/steps/plan.md +25 -0
- package/examples/workflows/team-solve-deliver/steps/repair.md +20 -0
- package/examples/workflows/team-solve-deliver/steps/review.md +31 -0
- package/examples/workflows/team-solve-deliver/steps/solve.md +28 -0
- package/examples/workflows/team-solve-deliver/steps/verify.md +9 -0
- package/examples/workflows/team-solve-deliver/workflow.yaml +74 -0
- package/package.json +2 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-311.pyc +0 -0
- package/src/policies/enforcers/__pycache__/expert_review_required.cpython-311.pyc +0 -0
- package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-311.pyc +0 -0
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-311.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-311.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +181 -15
- package/tools/agents/tests/test_signature_quality.py +51 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# team-solve-deliver — the team directs the deliver convergence loop.
|
|
2
|
+
#
|
|
3
|
+
# The combination of the two engines: team-solve's role structure and ledger
|
|
4
|
+
# discipline (fresh contexts, files as the only channel, cross-correction)
|
|
5
|
+
# wrapped around deliver's convergence machinery (tools + the project's real
|
|
6
|
+
# completion gates). Roles think; loops build and verify.
|
|
7
|
+
#
|
|
8
|
+
# plan ──┬── ideate-a ──┬── solve ── review ── repair ── verify
|
|
9
|
+
# └── ideate-b ──┘ (loop) (judge) (loop) (loop, gates-only)
|
|
10
|
+
#
|
|
11
|
+
# solve/repair/verify are `kind: loop` steps — they execute through a
|
|
12
|
+
# CALLER-INJECTED convergence loop (uap deliver wires its own machinery and
|
|
13
|
+
# auto-engages this workflow for high/critical missions). A standalone
|
|
14
|
+
# `uap workflow run team-solve-deliver` fails closed: no loop provider.
|
|
15
|
+
name: team-solve-deliver
|
|
16
|
+
description: >
|
|
17
|
+
team-solve directing the deliver convergence loop for repo missions: an
|
|
18
|
+
approach plan, two parallel ideations from different engineering families,
|
|
19
|
+
a gates-driven solve loop, a fresh-context review, a review-seeded repair
|
|
20
|
+
loop, and a verify-only verdict. Auto-engaged by `uap deliver` on
|
|
21
|
+
high/critical complexity; force with `uap deliver --team-solve`.
|
|
22
|
+
parallel: 2
|
|
23
|
+
# 0 engine-level step retries: the solve/repair loops already converge
|
|
24
|
+
# internally (until-delivered + escalation inside the mission dispatch), and
|
|
25
|
+
# a workflow-level retry of `solve` would re-run the ENTIRE mission dispatch
|
|
26
|
+
# after it already burned its turn budget — doubling the hardest-tier spend
|
|
27
|
+
# and bypassing this workflow's own review→repair discipline.
|
|
28
|
+
maxRepairsPerStep: 0
|
|
29
|
+
steps:
|
|
30
|
+
- id: plan
|
|
31
|
+
kind: agent
|
|
32
|
+
title: Approach planner (judge rail)
|
|
33
|
+
prompt: steps/plan.md
|
|
34
|
+
writes: [plan.md]
|
|
35
|
+
requires: []
|
|
36
|
+
- id: ideate-a
|
|
37
|
+
kind: agent
|
|
38
|
+
title: Ideator A (minimal-change family)
|
|
39
|
+
prompt: steps/ideate-a.md
|
|
40
|
+
reads: [plan.md]
|
|
41
|
+
writes: [proposal-a.md]
|
|
42
|
+
requires: [plan]
|
|
43
|
+
- id: ideate-b
|
|
44
|
+
kind: agent
|
|
45
|
+
title: Ideator B (structural family)
|
|
46
|
+
prompt: steps/ideate-b.md
|
|
47
|
+
reads: [plan.md]
|
|
48
|
+
writes: [proposal-b.md]
|
|
49
|
+
requires: [plan]
|
|
50
|
+
- id: solve
|
|
51
|
+
kind: loop
|
|
52
|
+
title: Solve (convergence loop, project gates)
|
|
53
|
+
prompt: steps/solve.md
|
|
54
|
+
reads: [plan.md, proposal-a.md, proposal-b.md]
|
|
55
|
+
requires: [ideate-a, ideate-b]
|
|
56
|
+
- id: review
|
|
57
|
+
kind: agent
|
|
58
|
+
title: Fresh-context reviewer (judge rail)
|
|
59
|
+
prompt: steps/review.md
|
|
60
|
+
reads: [plan.md]
|
|
61
|
+
writes: [repair.md]
|
|
62
|
+
requires: [solve]
|
|
63
|
+
- id: repair
|
|
64
|
+
kind: loop
|
|
65
|
+
title: Repair (convergence loop, review-seeded)
|
|
66
|
+
prompt: steps/repair.md
|
|
67
|
+
reads: [repair.md]
|
|
68
|
+
requires: [review]
|
|
69
|
+
- id: verify
|
|
70
|
+
kind: loop
|
|
71
|
+
title: Verdict (gates only)
|
|
72
|
+
prompt: steps/verify.md
|
|
73
|
+
verifyOnly: true
|
|
74
|
+
requires: [repair]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miller-tech/uap",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.39.0",
|
|
4
4
|
"description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -116,6 +116,7 @@
|
|
|
116
116
|
"dist",
|
|
117
117
|
"web",
|
|
118
118
|
"docs",
|
|
119
|
+
"examples",
|
|
119
120
|
"config",
|
|
120
121
|
"templates",
|
|
121
122
|
"src/policies/schemas/policies",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -697,6 +697,24 @@ _FAILURE_DECLARATION_RE = re.compile(
|
|
|
697
697
|
re.IGNORECASE,
|
|
698
698
|
)
|
|
699
699
|
|
|
700
|
+
# The harness's own zero-exit envelope: the tool CALL succeeded, whatever the
|
|
701
|
+
# output QUOTES. Droid/BYOK appends "[Process exited with code N]" to every
|
|
702
|
+
# Execute result; other harnesses print their own form. Decisive in the
|
|
703
|
+
# flag-ABSENT path only (a client that declares is_error True still wins):
|
|
704
|
+
# 2026-10-26, cognition-engine fdc94474 -- two ERROR-LOOP hard stops were
|
|
705
|
+
# served on SIX successful state-checks whose `git diff` output quoted the
|
|
706
|
+
# session's own in-flight `eprintln!("pg-server: metrics: failed to bind
|
|
707
|
+
# ...")` line; the keyword sniff turned each clean diff into the same
|
|
708
|
+
# "failure" signature and the streak escalated to a 400.
|
|
709
|
+
_ZERO_EXIT_ENVELOPE_RE = re.compile(
|
|
710
|
+
r"\[process exited with code 0\]" # Droid / BYOK Execute result footer
|
|
711
|
+
r"|\bexited with code 0\b" # generic harness prose
|
|
712
|
+
r"|\bexit (?:code|status)\s*[:=]?\s*0\b" # 'exit code 0' / 'exit status: 0'
|
|
713
|
+
r"|\bexit_code=0\b" # python subprocess env style
|
|
714
|
+
r"|\bEXIT=0\b", # shell-echoed status
|
|
715
|
+
re.IGNORECASE,
|
|
716
|
+
)
|
|
717
|
+
|
|
700
718
|
|
|
701
719
|
def _is_successful_result(text: str) -> bool:
|
|
702
720
|
"""True when the payload structurally declares success.
|
|
@@ -867,10 +885,25 @@ def _error_signature_for_result(text: str, result_error: bool | None = None) ->
|
|
|
867
885
|
(_first_meaningful_signature), so an unfamiliar error
|
|
868
886
|
shape still forms a streak; a wrapper-only payload
|
|
869
887
|
stays unnameable -- DOUBLING-DOWN owns that shape.
|
|
870
|
-
absent ->
|
|
888
|
+
absent -> keyword heuristics as before, EXCEPT a payload that
|
|
889
|
+
carries the harness's zero-exit envelope
|
|
890
|
+
(_ZERO_EXIT_ENVELOPE_RE): the call succeeded, so
|
|
891
|
+
failure-shaped words it merely QUOTES (a git diff of
|
|
892
|
+
in-flight edits, a grep over source) cannot anchor a
|
|
893
|
+
streak. 2026-10-26: two ERROR-LOOP hard stops on six
|
|
894
|
+
clean state-checks whose git-diff output quoted the
|
|
895
|
+
session's own `eprintln!("...failed to bind...")`
|
|
896
|
+
source line; a declared True flag still wins over
|
|
897
|
+
the envelope.
|
|
871
898
|
"""
|
|
872
899
|
if result_error is False:
|
|
873
900
|
return ""
|
|
901
|
+
if result_error is None and _ZERO_EXIT_ENVELOPE_RE.search(text or ""):
|
|
902
|
+
# Flag-ABSENT and the harness's own footer says the call exited 0:
|
|
903
|
+
# the result is clean whatever the bytes quote. Only the heuristic
|
|
904
|
+
# path is narrowed -- is_error True/False keep their declared
|
|
905
|
+
# precedence above.
|
|
906
|
+
return ""
|
|
874
907
|
sig = _error_signature(text)
|
|
875
908
|
if sig or result_error is not True:
|
|
876
909
|
return sig
|
|
@@ -2423,6 +2456,31 @@ PROXY_ATTRACTOR_TEMP_OVERRIDE = float(
|
|
|
2423
2456
|
PROXY_ATTRACTOR_FINALIZE_THRESHOLD = max(1, int(
|
|
2424
2457
|
os.environ.get("PROXY_ATTRACTOR_FINALIZE_THRESHOLD", "2")
|
|
2425
2458
|
))
|
|
2459
|
+
# Post-contamination low-temp clamp exemption. The standard contamination
|
|
2460
|
+
# reset clamps the next tool turn to temperature 0.10 (Cycle 15 Option 2:
|
|
2461
|
+
# deterministic output repairs malformed JSON). For DEGENERATE-CONTENT faults
|
|
2462
|
+
# (garbled args) determinism is exactly wrong: temp ~0 re-enters the same
|
|
2463
|
+
# degenerate attractor that triggered the reset (live 2026-10-30: 9
|
|
2464
|
+
# contamination-forced finalize stops in one day, the 0.10 clamp active on
|
|
2465
|
+
# every post-reset turn). Reset causes listed here keep the normal
|
|
2466
|
+
# PROXY_TOOL_TURN_TEMPERATURE instead of the 0.10 clamp. Empty string
|
|
2467
|
+
# disables the exemption entirely (always clamp).
|
|
2468
|
+
PROXY_CONTAMINATION_LOW_TEMP_EXEMPT_KINDS = {
|
|
2469
|
+
k.strip()
|
|
2470
|
+
for k in os.environ.get(
|
|
2471
|
+
"PROXY_CONTAMINATION_LOW_TEMP_EXEMPT_KINDS", "invalid_tool_args"
|
|
2472
|
+
).split(",")
|
|
2473
|
+
if k.strip()
|
|
2474
|
+
}
|
|
2475
|
+
# Late garbled-retry output cap. The deployed env pins
|
|
2476
|
+
# PROXY_TOOL_TURN_MAX_TOKENS_GARBLED high (32768, raised 2026-04-22 so legit
|
|
2477
|
+
# multi-file heredoc edits don't truncate), which also gives a degenerate
|
|
2478
|
+
# generation maximum room to run. From this retry attempt onward the budget
|
|
2479
|
+
# drops to this cap: the observed legit commands (~4.5K tokens) still fit,
|
|
2480
|
+
# while runaway degeneration is starved of room (live 2026-10-30: every
|
|
2481
|
+
# garbled call was a long inline heredoc; short commands never garbled).
|
|
2482
|
+
# 0 disables the late cap.
|
|
2483
|
+
PROXY_GARBLE_LATE_CAP = int(os.environ.get("PROXY_GARBLE_LATE_CAP", "8192"))
|
|
2426
2484
|
PROXY_AGENTIC_SUPPLEMENT_MODE = (
|
|
2427
2485
|
os.environ.get("PROXY_AGENTIC_SUPPLEMENT_MODE", "clean").strip().lower()
|
|
2428
2486
|
)
|
|
@@ -3086,6 +3144,7 @@ class SessionMonitor:
|
|
|
3086
3144
|
invalid_tool_call_streak: int = 0 # consecutive invalid tool arg payloads
|
|
3087
3145
|
required_tool_miss_streak: int = 0 # required tool turns with no tool call
|
|
3088
3146
|
contamination_resets: int = 0 # how many contamination resets were applied
|
|
3147
|
+
contamination_cause: str = "" # dominant streak behind the last reset ("invalid_tool_args"/"malformed_payload"/"required_tool_miss"/"generic")
|
|
3089
3148
|
last_fault_excerpt_hash: str = "" # hash of last TOOL RESPONSE ISSUE excerpt (attractor detection)
|
|
3090
3149
|
attractor_correction_active: bool = False # next turn uses high-temp escape sampling
|
|
3091
3150
|
forced_auto_cooldown_turns: int = 0 # temporary auto override turns remaining
|
|
@@ -9789,13 +9848,24 @@ def build_openai_request(
|
|
|
9789
9848
|
client_temp,
|
|
9790
9849
|
)
|
|
9791
9850
|
else:
|
|
9792
|
-
|
|
9851
|
+
# Cycle 15 Option 2: lower temperature after contamination resets —
|
|
9852
|
+
# EXCEPT when the reset was driven by an exempt cause: for
|
|
9853
|
+
# degenerate-content faults (garbled args) determinism re-enters
|
|
9854
|
+
# the same attractor that triggered the reset (live 2026-10-30).
|
|
9855
|
+
if (
|
|
9856
|
+
monitor.contamination_resets > 0
|
|
9857
|
+
and monitor.contamination_cause
|
|
9858
|
+
not in PROXY_CONTAMINATION_LOW_TEMP_EXEMPT_KINDS
|
|
9859
|
+
):
|
|
9793
9860
|
target_temp = min(target_temp, 0.1)
|
|
9794
9861
|
if client_temp is None or client_temp > target_temp:
|
|
9795
9862
|
openai_body["temperature"] = target_temp
|
|
9796
9863
|
extra = ""
|
|
9797
9864
|
if monitor.contamination_resets > 0:
|
|
9798
|
-
extra =
|
|
9865
|
+
extra = (
|
|
9866
|
+
f" (post-contamination reset, resets={monitor.contamination_resets},"
|
|
9867
|
+
f" cause={monitor.contamination_cause or 'generic'})"
|
|
9868
|
+
)
|
|
9799
9869
|
logger.info(
|
|
9800
9870
|
"TOOL TURN TEMP: forcing temperature=%.2f (was %s) for tool-enabled request%s",
|
|
9801
9871
|
target_temp,
|
|
@@ -10684,6 +10754,68 @@ def _hash_fault_excerpt(excerpt: str) -> str:
|
|
|
10684
10754
|
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:16]
|
|
10685
10755
|
|
|
10686
10756
|
|
|
10757
|
+
def _fault_fingerprint(
|
|
10758
|
+
excerpt: str, arg_excerpt: str, fault_tool_names: list[str]
|
|
10759
|
+
) -> str:
|
|
10760
|
+
"""Stable fingerprint of a tool-response fault for attractor-repeat detection.
|
|
10761
|
+
|
|
10762
|
+
Tool-call faults carry NO assistant text (every observed garble incident
|
|
10763
|
+
logged text_len=0), so hashing only the text excerpt yielded "" — falsy —
|
|
10764
|
+
and the attractor detector (hard reset + temp escape + lowered finalize
|
|
10765
|
+
threshold) never fired for the dominant fault class (live 2026-10-30:
|
|
10766
|
+
9 contamination-forced finalize stops, none attractor-detected). Fall
|
|
10767
|
+
back to the garbled-args excerpt — its stable command head survives
|
|
10768
|
+
normalization, so the same degenerate approach hashes equal across
|
|
10769
|
+
resets — then the failing tool names.
|
|
10770
|
+
"""
|
|
10771
|
+
if excerpt:
|
|
10772
|
+
return _hash_fault_excerpt(excerpt)
|
|
10773
|
+
if arg_excerpt:
|
|
10774
|
+
return _hash_fault_excerpt(arg_excerpt)
|
|
10775
|
+
if fault_tool_names:
|
|
10776
|
+
return _hash_fault_excerpt(
|
|
10777
|
+
"tool:" + ",".join(sorted(set(fault_tool_names)))
|
|
10778
|
+
)
|
|
10779
|
+
return ""
|
|
10780
|
+
|
|
10781
|
+
|
|
10782
|
+
def _retry_temperature(attempt: int, is_garbled: bool) -> float:
|
|
10783
|
+
"""Retry temperature ladder for malformed/garbled tool-call retries.
|
|
10784
|
+
|
|
10785
|
+
Attempt 1: configured retry temp (deterministic first try).
|
|
10786
|
+
Attempt 2: 0.5 — escape the degenerate local minimum (Cycle 15 Option 3).
|
|
10787
|
+
Attempt 3+ garbled (degenerate CONTENT): attractor-escape temperature —
|
|
10788
|
+
determinism re-enters the same degenerate trajectory (live 2026-10-30:
|
|
10789
|
+
garbled attempts 1-4 all reproduced at 0.05/0.5 while short commands
|
|
10790
|
+
never garbled); malformed-JSON retries keep the deterministic ladder
|
|
10791
|
+
because structure repair wants determinism.
|
|
10792
|
+
"""
|
|
10793
|
+
if is_garbled and attempt >= 3:
|
|
10794
|
+
return max(
|
|
10795
|
+
PROXY_ATTRACTOR_TEMP_OVERRIDE, PROXY_MALFORMED_TOOL_RETRY_TEMPERATURE
|
|
10796
|
+
)
|
|
10797
|
+
if attempt > 1:
|
|
10798
|
+
return 0.5
|
|
10799
|
+
return PROXY_MALFORMED_TOOL_RETRY_TEMPERATURE
|
|
10800
|
+
|
|
10801
|
+
|
|
10802
|
+
def _contamination_reset_cause(monitor: "SessionMonitor") -> str:
|
|
10803
|
+
"""Dominant output-quality streak behind a contamination reset.
|
|
10804
|
+
|
|
10805
|
+
Drives the post-reset temperature policy: degenerate-content faults
|
|
10806
|
+
(invalid_tool_args) are EXEMPT from the 0.10 low-temp clamp (see
|
|
10807
|
+
PROXY_CONTAMINATION_LOW_TEMP_EXEMPT_KINDS) because determinism re-enters
|
|
10808
|
+
the same attractor; malformed-JSON repairs keep the clamp.
|
|
10809
|
+
"""
|
|
10810
|
+
if monitor.invalid_tool_call_streak > monitor.malformed_tool_streak:
|
|
10811
|
+
return "invalid_tool_args"
|
|
10812
|
+
if monitor.malformed_tool_streak > 0:
|
|
10813
|
+
return "malformed_payload"
|
|
10814
|
+
if monitor.required_tool_miss_streak > 0:
|
|
10815
|
+
return "required_tool_miss"
|
|
10816
|
+
return "generic"
|
|
10817
|
+
|
|
10818
|
+
|
|
10687
10819
|
def _extract_openai_tool_calls(openai_resp: dict) -> list[dict]:
|
|
10688
10820
|
_, message = _extract_openai_choice(openai_resp)
|
|
10689
10821
|
tool_calls = message.get("tool_calls") or []
|
|
@@ -12366,10 +12498,20 @@ def _validate_tool_call_arguments(
|
|
|
12366
12498
|
allowed_tools: set[str],
|
|
12367
12499
|
) -> ToolResponseIssue:
|
|
12368
12500
|
if allowed_tools and tool_name not in allowed_tools:
|
|
12501
|
+
# 2026-10-30: name the kept alternatives — a narrowed retry (the
|
|
12502
|
+
# garbling tool excluded) observed the model emitting the excluded
|
|
12503
|
+
# tool anyway because "use the provided tool list" gave it nothing
|
|
12504
|
+
# concrete to switch to.
|
|
12505
|
+
known = ", ".join(sorted(allowed_tools)[:6])
|
|
12369
12506
|
return ToolResponseIssue(
|
|
12370
12507
|
kind="invalid_tool_args",
|
|
12371
12508
|
reason=f"unknown tool '{tool_name}'",
|
|
12372
|
-
retry_hint=
|
|
12509
|
+
retry_hint=(
|
|
12510
|
+
f"'{tool_name}' is not available on this turn. "
|
|
12511
|
+
f"Call exactly one of: {known}."
|
|
12512
|
+
if known
|
|
12513
|
+
else "Use exactly one tool from the provided tool list."
|
|
12514
|
+
),
|
|
12373
12515
|
)
|
|
12374
12516
|
|
|
12375
12517
|
if isinstance(raw_arguments, (dict, list)):
|
|
@@ -12441,7 +12583,10 @@ def _validate_tool_call_arguments(
|
|
|
12441
12583
|
reason=f"arguments for '{tool_name}' contain garbled/degenerate content",
|
|
12442
12584
|
retry_hint=(
|
|
12443
12585
|
f"Emit exactly one `{tool_name}` tool call with well-formed JSON arguments. "
|
|
12444
|
-
"
|
|
12586
|
+
"Your previous arguments degenerated (repeated digit or brace runs). "
|
|
12587
|
+
"SIMPLIFY and SHORTEN the operation: split the work into smaller steps, "
|
|
12588
|
+
"avoid long inline heredocs and large generated numeric blocks, and "
|
|
12589
|
+
"prefer short commands. Do not repeat closing braces, brackets, or digits."
|
|
12445
12590
|
),
|
|
12446
12591
|
)
|
|
12447
12592
|
|
|
@@ -13020,12 +13165,16 @@ def _build_malformed_retry_body(
|
|
|
13020
13165
|
retry_body["stream"] = False
|
|
13021
13166
|
retry_body["tool_choice"] = tool_choice
|
|
13022
13167
|
# Cycle 15 Option 3: vary temperature across retries to break degenerate patterns.
|
|
13023
|
-
#
|
|
13024
|
-
#
|
|
13025
|
-
|
|
13026
|
-
|
|
13027
|
-
|
|
13028
|
-
|
|
13168
|
+
# Ladder lives in _retry_temperature (garbled retries escalate to the
|
|
13169
|
+
# attractor-escape temperature from attempt 3 — see its docstring).
|
|
13170
|
+
retry_body["temperature"] = _retry_temperature(attempt, is_garbled)
|
|
13171
|
+
if is_garbled and attempt >= 3 and PROXY_GARBLE_LATE_CAP > 0:
|
|
13172
|
+
logger.info(
|
|
13173
|
+
"RETRY GARBLED ESCALATION: temperature=%.2f late_cap=%d for garbled retry attempt=%d",
|
|
13174
|
+
retry_body["temperature"],
|
|
13175
|
+
PROXY_GARBLE_LATE_CAP,
|
|
13176
|
+
attempt,
|
|
13177
|
+
)
|
|
13029
13178
|
|
|
13030
13179
|
if tool_choice == "required":
|
|
13031
13180
|
retry_instruction = (
|
|
@@ -13057,7 +13206,17 @@ def _build_malformed_retry_body(
|
|
|
13057
13206
|
|
|
13058
13207
|
# Option 1: Progressive garbled-cap within retries — use smaller max_tokens
|
|
13059
13208
|
# when the issue involves garbled/degenerate args to limit degeneration room.
|
|
13060
|
-
|
|
13209
|
+
# 2026-10-30: the deployed env pins PROXY_TOOL_TURN_MAX_TOKENS_GARBLED high
|
|
13210
|
+
# (32768) so legit multi-file heredoc edits don't truncate (2026-04-22) —
|
|
13211
|
+
# from attempt 3 onward, starve degeneration room with the late cap.
|
|
13212
|
+
if is_garbled and attempt >= 3 and PROXY_GARBLE_LATE_CAP > 0:
|
|
13213
|
+
retry_body["max_tokens"] = PROXY_GARBLE_LATE_CAP
|
|
13214
|
+
logger.info(
|
|
13215
|
+
"RETRY GARBLED LATE CAP: max_tokens=%d for garbled retry attempt=%d",
|
|
13216
|
+
PROXY_GARBLE_LATE_CAP,
|
|
13217
|
+
attempt,
|
|
13218
|
+
)
|
|
13219
|
+
elif is_garbled and PROXY_TOOL_TURN_MAX_TOKENS_GARBLED > 0:
|
|
13061
13220
|
retry_body["max_tokens"] = PROXY_TOOL_TURN_MAX_TOKENS_GARBLED
|
|
13062
13221
|
logger.info(
|
|
13063
13222
|
"RETRY GARBLED CAP: max_tokens=%d for garbled retry attempt=%d",
|
|
@@ -13402,17 +13561,22 @@ async def _apply_malformed_tool_guardrail(
|
|
|
13402
13561
|
excerpt = _openai_message_text(working_resp)[:220].replace("\n", " ")
|
|
13403
13562
|
# Option 2: Log garbled argument content for diagnostics
|
|
13404
13563
|
arg_excerpt = ""
|
|
13564
|
+
fault_tool_names: list[str] = []
|
|
13405
13565
|
if issue.kind == "invalid_tool_args":
|
|
13406
13566
|
for tc in (working_resp.get("choices", [{}])[0].get("message", {}).get("tool_calls") or []):
|
|
13567
|
+
fn_name = tc.get("function", {}).get("name", "")
|
|
13407
13568
|
raw_args = tc.get("function", {}).get("arguments", "")
|
|
13408
|
-
if
|
|
13569
|
+
if fn_name:
|
|
13570
|
+
fault_tool_names.append(fn_name)
|
|
13571
|
+
if raw_args and _is_garbled_tool_arguments(raw_args) and not arg_excerpt:
|
|
13409
13572
|
arg_excerpt = raw_args[:200].replace("\n", " ")
|
|
13410
|
-
break
|
|
13411
13573
|
# Attractor detection — hash the normalized fault excerpt so the
|
|
13412
13574
|
# contamination breaker can recognize the same fixed-point response
|
|
13413
13575
|
# reappearing across consecutive resets. Whitespace-normalized so trivial
|
|
13414
13576
|
# rendering differences don't break the match.
|
|
13415
|
-
monitor.last_fault_excerpt_hash =
|
|
13577
|
+
monitor.last_fault_excerpt_hash = _fault_fingerprint(
|
|
13578
|
+
excerpt, arg_excerpt, fault_tool_names
|
|
13579
|
+
)
|
|
13416
13580
|
logger.warning(
|
|
13417
13581
|
"TOOL RESPONSE ISSUE: session=%s kind=%s reason=%s malformed=%d invalid=%d required_miss=%d excerpt=%.220s args=%.200s",
|
|
13418
13582
|
session_id,
|
|
@@ -13768,6 +13932,7 @@ def _maybe_apply_session_contamination_breaker(
|
|
|
13768
13932
|
max_contamination_resets,
|
|
13769
13933
|
)
|
|
13770
13934
|
monitor.set_tool_turn_phase("finalize", reason="contamination_loop")
|
|
13935
|
+
monitor.contamination_cause = _contamination_reset_cause(monitor)
|
|
13771
13936
|
monitor.contamination_resets += 1
|
|
13772
13937
|
monitor.malformed_tool_streak = 0
|
|
13773
13938
|
monitor.invalid_tool_call_streak = 0
|
|
@@ -13890,6 +14055,7 @@ def _maybe_apply_session_contamination_breaker(
|
|
|
13890
14055
|
|
|
13891
14056
|
forced_before = monitor.consecutive_forced_count
|
|
13892
14057
|
required_miss_before = monitor.required_tool_miss_streak
|
|
14058
|
+
monitor.contamination_cause = _contamination_reset_cause(monitor)
|
|
13893
14059
|
monitor.contamination_resets += 1
|
|
13894
14060
|
# Emergency tier: the conversation itself is rewritten, so BOTH the
|
|
13895
14061
|
# forcing-loop family (via its one policy point) and the output-quality
|
|
@@ -55,6 +55,20 @@ WORKTREE_STATUS = (
|
|
|
55
55
|
SHELL_SOURCE_READ = "die() { printf 'fatal: %s\\n' \"$*\" >&2; exit 1; }\n"
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
# 2026-10-26, cognition-engine fdc94474: state-check output that QUOTES the
|
|
59
|
+
# session's own in-flight source (a numbered read of the metrics region,
|
|
60
|
+
# a git diff of the wedge fix) plus the harness's zero-exit footer. Six
|
|
61
|
+
# flag-absent results of this shape manufactured both ERROR-LOOP hard stops.
|
|
62
|
+
DIFF_QUOTING_SOURCE = (
|
|
63
|
+
" M src/rust-pg-ext/Cargo.toml\n"
|
|
64
|
+
" M src/rust-pg-ext/src/bin/pg-server.rs\n"
|
|
65
|
+
"2784→ eprintln!(\"pg-server: metrics: failed to bind {addr}: {e}"
|
|
66
|
+
" — metrics endpoint disabled\");\n"
|
|
67
|
+
"+ return None;\n"
|
|
68
|
+
"\n[Process exited with code 0]"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
58
72
|
class TracebackAnchoringTests(unittest.TestCase):
|
|
59
73
|
def test_decisive_line_anchors_not_the_header(self):
|
|
60
74
|
sig = proxy._error_signature(TRACEBACK_FULL)
|
|
@@ -225,6 +239,43 @@ class ReadOnlyContentTests(unittest.TestCase):
|
|
|
225
239
|
self.assertEqual(len(body["messages"]), 1)
|
|
226
240
|
|
|
227
241
|
|
|
242
|
+
class ZeroExitEnvelopeTests(unittest.TestCase):
|
|
243
|
+
"""A flag-ABSENT Execute result carrying the harness's zero-exit
|
|
244
|
+
envelope is a SUCCESS, whatever failure-shaped text its output QUOTES
|
|
245
|
+
(ledger 2026-10-26: two ERROR-LOOP hard stops served on six clean
|
|
246
|
+
state-checks whose output quoted an in-flight eprintln! source line)."""
|
|
247
|
+
|
|
248
|
+
def test_quoted_source_with_exit_zero_forms_no_signature(self):
|
|
249
|
+
m = proxy.SessionMonitor()
|
|
250
|
+
for _ in range(6):
|
|
251
|
+
m.note_tool_result_error(DIFF_QUOTING_SOURCE, None, tool_names=["execute"])
|
|
252
|
+
self.assertEqual(m.error_signature_streak, 0)
|
|
253
|
+
self.assertEqual(m.last_error_signature, "")
|
|
254
|
+
|
|
255
|
+
def test_exit_zero_streak_never_arms_the_guard(self):
|
|
256
|
+
m = proxy.SessionMonitor()
|
|
257
|
+
for _ in range(6):
|
|
258
|
+
m.note_tool_result_error(DIFF_QUOTING_SOURCE, None, tool_names=["execute"])
|
|
259
|
+
body = {"messages": [{"role": "user", "content": "go"}], "tool_choice": "auto"}
|
|
260
|
+
proxy._maybe_inject_error_loop_break(body, m)
|
|
261
|
+
self.assertEqual(m.error_loop_fires, 0)
|
|
262
|
+
self.assertEqual(len(body["messages"]), 1)
|
|
263
|
+
|
|
264
|
+
def test_nonzero_exit_still_keeps_heuristics(self):
|
|
265
|
+
payload = DIFF_QUOTING_SOURCE.replace(
|
|
266
|
+
"exited with code 0", "exited with code 1"
|
|
267
|
+
)
|
|
268
|
+
m = proxy.SessionMonitor()
|
|
269
|
+
m.note_tool_result_error(payload, None, tool_names=["execute"])
|
|
270
|
+
self.assertEqual(m.error_signature_streak, 1)
|
|
271
|
+
self.assertNotEqual(m.last_error_signature, "")
|
|
272
|
+
|
|
273
|
+
def test_declared_error_flag_beats_the_zero_exit_envelope(self):
|
|
274
|
+
m = proxy.SessionMonitor()
|
|
275
|
+
m.note_tool_result_error(DIFF_QUOTING_SOURCE, True, tool_names=["execute"])
|
|
276
|
+
self.assertEqual(m.error_signature_streak, 1)
|
|
277
|
+
|
|
278
|
+
|
|
228
279
|
class KnownFixCoverageTests(unittest.TestCase):
|
|
229
280
|
"""The uncovered ledger classes now carry a KNOWN FIX."""
|
|
230
281
|
|