@miller-tech/uap 2.36.4 → 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.
Files changed (100) hide show
  1. package/config/model-profiles/qwen38.json +3 -3
  2. package/config/model-registry.json +96 -6
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/bin/cli.js +57 -0
  5. package/dist/bin/cli.js.map +1 -1
  6. package/dist/cli/deliver.d.ts +46 -0
  7. package/dist/cli/deliver.d.ts.map +1 -1
  8. package/dist/cli/deliver.js +155 -36
  9. package/dist/cli/deliver.js.map +1 -1
  10. package/dist/cli/models.d.ts +11 -1
  11. package/dist/cli/models.d.ts.map +1 -1
  12. package/dist/cli/models.js +164 -3
  13. package/dist/cli/models.js.map +1 -1
  14. package/dist/cli/workflow.d.ts +36 -0
  15. package/dist/cli/workflow.d.ts.map +1 -0
  16. package/dist/cli/workflow.js +246 -0
  17. package/dist/cli/workflow.js.map +1 -0
  18. package/dist/delivery/run-state.d.ts +1 -1
  19. package/dist/delivery/run-state.d.ts.map +1 -1
  20. package/dist/delivery/run-state.js +1 -1
  21. package/dist/delivery/run-state.js.map +1 -1
  22. package/dist/delivery/team-solve-deliver.d.ts +83 -0
  23. package/dist/delivery/team-solve-deliver.d.ts.map +1 -0
  24. package/dist/delivery/team-solve-deliver.js +191 -0
  25. package/dist/delivery/team-solve-deliver.js.map +1 -0
  26. package/dist/inference/model-files.d.ts +111 -0
  27. package/dist/inference/model-files.d.ts.map +1 -0
  28. package/dist/inference/model-files.js +172 -0
  29. package/dist/inference/model-files.js.map +1 -0
  30. package/dist/placement/registry.d.ts +650 -0
  31. package/dist/placement/registry.d.ts.map +1 -1
  32. package/dist/placement/registry.js +21 -0
  33. package/dist/placement/registry.js.map +1 -1
  34. package/dist/workflow/loader.d.ts +34 -0
  35. package/dist/workflow/loader.d.ts.map +1 -0
  36. package/dist/workflow/loader.js +232 -0
  37. package/dist/workflow/loader.js.map +1 -0
  38. package/dist/workflow/runner.d.ts +90 -0
  39. package/dist/workflow/runner.d.ts.map +1 -0
  40. package/dist/workflow/runner.js +416 -0
  41. package/dist/workflow/runner.js.map +1 -0
  42. package/dist/workflow/types.d.ts +400 -0
  43. package/dist/workflow/types.d.ts.map +1 -0
  44. package/dist/workflow/types.js +147 -0
  45. package/dist/workflow/types.js.map +1 -0
  46. package/docs/INDEX.md +1 -0
  47. package/docs/architecture/adr/0013-declarative-workflow-layer.md +99 -0
  48. package/docs/architecture/adr/0014-loop-steps-and-team-solve-deliver.md +142 -0
  49. package/docs/benchmarks/LCB_DROID_SMOKE.md +111 -0
  50. package/docs/guides/DELIVER.md +3 -1
  51. package/docs/guides/WORKFLOWS.md +179 -0
  52. package/docs/plans/model-performance-proof-options.md +39 -0
  53. package/docs/reference/CLI.md +33 -0
  54. package/docs/reference/CONFIGURATION.md +1 -0
  55. package/docs/reference/FEATURES.md +18 -0
  56. package/examples/auto-coordinator-example.js +127 -0
  57. package/examples/demo-order-solid.ts +19 -0
  58. package/examples/order-processing-strategy-pattern.ts +186 -0
  59. package/examples/order-processor-refactored.ts +473 -0
  60. package/examples/order-solid-refactor.ts +830 -0
  61. package/examples/refactored-order-processing.ts +106 -0
  62. package/examples/run-order-processing.ts +48 -0
  63. package/examples/solid-order-processing.ts +324 -0
  64. package/examples/strategy-pattern-example.ts +100 -0
  65. package/examples/workflows/team-solve/README.md +85 -0
  66. package/examples/workflows/team-solve/check-samples.sh +50 -0
  67. package/examples/workflows/team-solve/sample/task.md +22 -0
  68. package/examples/workflows/team-solve/sample/tests/1.in +2 -0
  69. package/examples/workflows/team-solve/sample/tests/1.out +1 -0
  70. package/examples/workflows/team-solve/sample/tests/2.in +2 -0
  71. package/examples/workflows/team-solve/sample/tests/2.out +1 -0
  72. package/examples/workflows/team-solve/sample/tests/3.in +2 -0
  73. package/examples/workflows/team-solve/sample/tests/3.out +1 -0
  74. package/examples/workflows/team-solve/sample/tests/4.in +2 -0
  75. package/examples/workflows/team-solve/sample/tests/4.out +1 -0
  76. package/examples/workflows/team-solve/sample/tests/5.in +1 -0
  77. package/examples/workflows/team-solve/sample/tests/5.out +1 -0
  78. package/examples/workflows/team-solve/steps/ideate-a.md +26 -0
  79. package/examples/workflows/team-solve/steps/ideate-b.md +28 -0
  80. package/examples/workflows/team-solve/steps/plan.md +20 -0
  81. package/examples/workflows/team-solve/steps/repair.md +25 -0
  82. package/examples/workflows/team-solve/steps/review.md +29 -0
  83. package/examples/workflows/team-solve/steps/solve.md +26 -0
  84. package/examples/workflows/team-solve/workflow.yaml +78 -0
  85. package/examples/workflows/team-solve-deliver/README.md +84 -0
  86. package/examples/workflows/team-solve-deliver/steps/ideate-a.md +25 -0
  87. package/examples/workflows/team-solve-deliver/steps/ideate-b.md +28 -0
  88. package/examples/workflows/team-solve-deliver/steps/plan.md +25 -0
  89. package/examples/workflows/team-solve-deliver/steps/repair.md +20 -0
  90. package/examples/workflows/team-solve-deliver/steps/review.md +31 -0
  91. package/examples/workflows/team-solve-deliver/steps/solve.md +28 -0
  92. package/examples/workflows/team-solve-deliver/steps/verify.md +9 -0
  93. package/examples/workflows/team-solve-deliver/workflow.yaml +74 -0
  94. package/package.json +2 -1
  95. package/src/policies/enforcers/__pycache__/_common.cpython-311.pyc +0 -0
  96. package/src/policies/enforcers/__pycache__/expert_review_required.cpython-311.pyc +0 -0
  97. package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-311.pyc +0 -0
  98. package/templates/hooks/__pycache__/deliver_autoroute.cpython-311.pyc +0 -0
  99. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-311.pyc +0 -0
  100. package/tools/agents/scripts/anthropic_proxy.py +147 -14
@@ -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.36.4",
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",
@@ -2456,6 +2456,31 @@ PROXY_ATTRACTOR_TEMP_OVERRIDE = float(
2456
2456
  PROXY_ATTRACTOR_FINALIZE_THRESHOLD = max(1, int(
2457
2457
  os.environ.get("PROXY_ATTRACTOR_FINALIZE_THRESHOLD", "2")
2458
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"))
2459
2484
  PROXY_AGENTIC_SUPPLEMENT_MODE = (
2460
2485
  os.environ.get("PROXY_AGENTIC_SUPPLEMENT_MODE", "clean").strip().lower()
2461
2486
  )
@@ -3119,6 +3144,7 @@ class SessionMonitor:
3119
3144
  invalid_tool_call_streak: int = 0 # consecutive invalid tool arg payloads
3120
3145
  required_tool_miss_streak: int = 0 # required tool turns with no tool call
3121
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")
3122
3148
  last_fault_excerpt_hash: str = "" # hash of last TOOL RESPONSE ISSUE excerpt (attractor detection)
3123
3149
  attractor_correction_active: bool = False # next turn uses high-temp escape sampling
3124
3150
  forced_auto_cooldown_turns: int = 0 # temporary auto override turns remaining
@@ -9822,13 +9848,24 @@ def build_openai_request(
9822
9848
  client_temp,
9823
9849
  )
9824
9850
  else:
9825
- if monitor.contamination_resets > 0:
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
+ ):
9826
9860
  target_temp = min(target_temp, 0.1)
9827
9861
  if client_temp is None or client_temp > target_temp:
9828
9862
  openai_body["temperature"] = target_temp
9829
9863
  extra = ""
9830
9864
  if monitor.contamination_resets > 0:
9831
- extra = f" (post-contamination reset, resets={monitor.contamination_resets})"
9865
+ extra = (
9866
+ f" (post-contamination reset, resets={monitor.contamination_resets},"
9867
+ f" cause={monitor.contamination_cause or 'generic'})"
9868
+ )
9832
9869
  logger.info(
9833
9870
  "TOOL TURN TEMP: forcing temperature=%.2f (was %s) for tool-enabled request%s",
9834
9871
  target_temp,
@@ -10717,6 +10754,68 @@ def _hash_fault_excerpt(excerpt: str) -> str:
10717
10754
  return hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:16]
10718
10755
 
10719
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
+
10720
10819
  def _extract_openai_tool_calls(openai_resp: dict) -> list[dict]:
10721
10820
  _, message = _extract_openai_choice(openai_resp)
10722
10821
  tool_calls = message.get("tool_calls") or []
@@ -12399,10 +12498,20 @@ def _validate_tool_call_arguments(
12399
12498
  allowed_tools: set[str],
12400
12499
  ) -> ToolResponseIssue:
12401
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])
12402
12506
  return ToolResponseIssue(
12403
12507
  kind="invalid_tool_args",
12404
12508
  reason=f"unknown tool '{tool_name}'",
12405
- retry_hint="Use exactly one tool from the provided tool list.",
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
+ ),
12406
12515
  )
12407
12516
 
12408
12517
  if isinstance(raw_arguments, (dict, list)):
@@ -12474,7 +12583,10 @@ def _validate_tool_call_arguments(
12474
12583
  reason=f"arguments for '{tool_name}' contain garbled/degenerate content",
12475
12584
  retry_hint=(
12476
12585
  f"Emit exactly one `{tool_name}` tool call with well-formed JSON arguments. "
12477
- "Do not repeat closing braces, brackets, or digits."
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."
12478
12590
  ),
12479
12591
  )
12480
12592
 
@@ -13053,12 +13165,16 @@ def _build_malformed_retry_body(
13053
13165
  retry_body["stream"] = False
13054
13166
  retry_body["tool_choice"] = tool_choice
13055
13167
  # Cycle 15 Option 3: vary temperature across retries to break degenerate patterns.
13056
- # Attempt 1: use configured retry temp (default 0.0) for deterministic first try.
13057
- # Attempt 2+: increase to 0.5 to escape the degenerate local minimum.
13058
- if total_attempts > 1 and attempt > 1:
13059
- retry_body["temperature"] = 0.5
13060
- else:
13061
- retry_body["temperature"] = PROXY_MALFORMED_TOOL_RETRY_TEMPERATURE
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
+ )
13062
13178
 
13063
13179
  if tool_choice == "required":
13064
13180
  retry_instruction = (
@@ -13090,7 +13206,17 @@ def _build_malformed_retry_body(
13090
13206
 
13091
13207
  # Option 1: Progressive garbled-cap within retries — use smaller max_tokens
13092
13208
  # when the issue involves garbled/degenerate args to limit degeneration room.
13093
- if is_garbled and PROXY_TOOL_TURN_MAX_TOKENS_GARBLED > 0:
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:
13094
13220
  retry_body["max_tokens"] = PROXY_TOOL_TURN_MAX_TOKENS_GARBLED
13095
13221
  logger.info(
13096
13222
  "RETRY GARBLED CAP: max_tokens=%d for garbled retry attempt=%d",
@@ -13435,17 +13561,22 @@ async def _apply_malformed_tool_guardrail(
13435
13561
  excerpt = _openai_message_text(working_resp)[:220].replace("\n", " ")
13436
13562
  # Option 2: Log garbled argument content for diagnostics
13437
13563
  arg_excerpt = ""
13564
+ fault_tool_names: list[str] = []
13438
13565
  if issue.kind == "invalid_tool_args":
13439
13566
  for tc in (working_resp.get("choices", [{}])[0].get("message", {}).get("tool_calls") or []):
13567
+ fn_name = tc.get("function", {}).get("name", "")
13440
13568
  raw_args = tc.get("function", {}).get("arguments", "")
13441
- if raw_args and _is_garbled_tool_arguments(raw_args):
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:
13442
13572
  arg_excerpt = raw_args[:200].replace("\n", " ")
13443
- break
13444
13573
  # Attractor detection — hash the normalized fault excerpt so the
13445
13574
  # contamination breaker can recognize the same fixed-point response
13446
13575
  # reappearing across consecutive resets. Whitespace-normalized so trivial
13447
13576
  # rendering differences don't break the match.
13448
- monitor.last_fault_excerpt_hash = _hash_fault_excerpt(excerpt)
13577
+ monitor.last_fault_excerpt_hash = _fault_fingerprint(
13578
+ excerpt, arg_excerpt, fault_tool_names
13579
+ )
13449
13580
  logger.warning(
13450
13581
  "TOOL RESPONSE ISSUE: session=%s kind=%s reason=%s malformed=%d invalid=%d required_miss=%d excerpt=%.220s args=%.200s",
13451
13582
  session_id,
@@ -13801,6 +13932,7 @@ def _maybe_apply_session_contamination_breaker(
13801
13932
  max_contamination_resets,
13802
13933
  )
13803
13934
  monitor.set_tool_turn_phase("finalize", reason="contamination_loop")
13935
+ monitor.contamination_cause = _contamination_reset_cause(monitor)
13804
13936
  monitor.contamination_resets += 1
13805
13937
  monitor.malformed_tool_streak = 0
13806
13938
  monitor.invalid_tool_call_streak = 0
@@ -13923,6 +14055,7 @@ def _maybe_apply_session_contamination_breaker(
13923
14055
 
13924
14056
  forced_before = monitor.consecutive_forced_count
13925
14057
  required_miss_before = monitor.required_tool_miss_streak
14058
+ monitor.contamination_cause = _contamination_reset_cause(monitor)
13926
14059
  monitor.contamination_resets += 1
13927
14060
  # Emergency tier: the conversation itself is rewritten, so BOTH the
13928
14061
  # forcing-loop family (via its one policy point) and the output-quality