agentdebugx 0.2.8__tar.gz → 0.2.10__tar.gz
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.
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/PKG-INFO +1 -1
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/23_status_v0_2.md +3 -3
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/pyproject.toml +1 -1
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/__init__.py +9 -1
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/attribution.py +213 -4
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/recovery.py +281 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/LICENSE +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/README.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/00_overview.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/01_literature_survey.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/02_architecture.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/03_taxonomy.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/04_trace_schema.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/05_adapters.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/06_detectors.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/07_attribution.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/08_recovery.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/09_error_database.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/10_taxonomy_induction.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/11_multimodal.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/12_ui_dashboard.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/13_class_design.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/14_api_reference.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/15_roadmap.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/16_governance.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/17_claude_code_design_patterns.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/18_comparison_codex_vs_design.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/19_error_hub.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/20_deep_debug.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/21_integrations.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/22_industry_track_paper_eval_plan.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/ERROR_TAXONOMY.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/OPEN_SOURCE_DEVELOPMENT_PLAN.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/README.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/RESEARCH_SURVEY.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/benchmarks/e2e_v0_2_3.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/benchmarks/e2e_v0_2_4.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/benchmarks/v0_1_smoke.json +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/benchmarks/v0_1_smoke.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/docs/benchmarks/who_when_v0_2_6_leaderboard.md +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/adapters/__init__.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/adapters/base.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/adapters/crewai.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/adapters/langgraph.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/adapters/otel.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/adapters/raw.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/analyzers.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/cli.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/deep.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/detectors.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/events.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/hub/__init__.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/hub/backend_base.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/hub/backends.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/hub/bundle.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/hub/scrub.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/instrumentation.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/integrations/__init__.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/integrations/claude_skill.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/integrations/openhands.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/judges.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/llm.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/models.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/recorder.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/storage.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/taxonomy.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/traceback.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/ui/__init__.py +0 -0
- {agentdebugx-0.2.8 → agentdebugx-0.2.10}/src/agentdebug/ui/server.py +0 -0
|
@@ -23,8 +23,11 @@ the forward-looking plan; this doc is the rear-view mirror.
|
|
|
23
23
|
| Attribution | `agentdebug.attribution.BinarySearchAttributor` | ✅ **new 0.2.3** | oracle-LLM logarithmic convergence + fallback + render elision |
|
|
24
24
|
| Attribution | `agentdebug.attribution.CounterfactualAttributor` | ✅ **new 0.2.7** | scripted-rescue-prob ranking + candidate selection priority (findings → errors → tail) + dual fallback (no candidates / silent LLM) |
|
|
25
25
|
| Attribution | `agentdebug.attribution.SBFLAttributor` (Tarantula / Ochiai / DStar) | ✅ **new 0.2.8** | signature design + ranking on synthetic corpus + 3 formulas + dual fallback + edge cases. Model-free: zero LLM cost at inference. |
|
|
26
|
+
| Attribution | `agentdebug.attribution.EnsembleAttributor` (+ `AttributionBudget`) | ✅ **new 0.2.10** | Borda + Bayesian merge, weighted backends, walltime/backend-count budget, swallows backend exceptions, falls back when all empty, honest per-source provenance |
|
|
26
27
|
| Recovery | `agentdebug.recovery.ReflexionSuggestion` | ✅ stable | per-finding + empty |
|
|
27
28
|
| Recovery | `agentdebug.recovery.CriticRecoverer` + `VerifierSpec` registry | ✅ **new 0.2.3** | 5 family-matched verifier templates; dedup + custom-override |
|
|
29
|
+
| Recovery | `agentdebug.recovery.SelfRefineLoop` | ✅ **new 0.2.9** | scripted-LLM happy + silent-LLM safe + multi-iter; per-finding generator→critic→refiner |
|
|
30
|
+
| Recovery | `agentdebug.recovery.AutoManualRules` (+ `apply()`) | ✅ **new 0.2.9** | no-LLM fallback + LLM rule-distill + idempotent apply + multi-rule append |
|
|
28
31
|
| DeepDebug | `agentdebug.deep.DeepDebugAnalyzer` | ✅ stable | full loop + silent LLM |
|
|
29
32
|
| Cascade view | `agentdebug.traceback.format_traceback` | ✅ stable | cascade + step-order + ANSI + empty |
|
|
30
33
|
| Detectors | `agentdebug.detectors.RepeatedToolCall / RepeatedState / StepCountLimit` | ✅ **new 0.2.2** | threshold + window + budget |
|
|
@@ -52,9 +55,6 @@ across 32 source files.
|
|
|
52
55
|
| [07_attribution.md](./07_attribution.md) | `CounterfactualAttributor` — *real* replay variant | true re-rollout requires framework-specific replay surface; the v0.2.7 LLM-simulated variant ships now, the real-replay variant is gated on adapter support (LangGraph checkpointer / OpenHands rewind) | v0.4 |
|
|
53
56
|
| [07_attribution.md](./07_attribution.md) | `SBFLAttributor` — *corpus* | shipped in 0.2.8 (`tarantula`/`ochiai`/`dstar`); awaiting paired-trace adoption to gather a useful corpus in production | corpus tooling deferred to v0.4 |
|
|
54
57
|
| [07_attribution.md](./07_attribution.md) | `DeltaDebugAttributor` (Zeller) | same replay constraint | v0.3 |
|
|
55
|
-
| [07_attribution.md](./07_attribution.md) | `EnsembleAttributor` | trivial once Counterfactual lands; awaits Counterfactual | v0.3 |
|
|
56
|
-
| [08_recovery.md](./08_recovery.md) | `SelfRefineLoop` | small but needs a generator-critic-refiner orchestration | v0.3 |
|
|
57
|
-
| [08_recovery.md](./08_recovery.md) | `AutoManualRules` | needs persistent project manual + injection into next-run prompts | v0.3 |
|
|
58
58
|
| [08_recovery.md](./08_recovery.md) | `LangGraphRewind` | depends on LangGraph checkpointer; ships when we have a real LangGraph user | v0.3 |
|
|
59
59
|
| [08_recovery.md](./08_recovery.md) | `SagaRollback` | needs compensation registry on tool definitions; new schema | v0.3 |
|
|
60
60
|
| [08_recovery.md](./08_recovery.md) | `MCTSBranchExploration` (LATS) | heavy; v2 feature | v2.0 |
|
|
@@ -11,11 +11,13 @@ See ``docs/14_api_reference.md`` for the full public surface and
|
|
|
11
11
|
from agentdebug.analyzers import HeuristicAnalyzer
|
|
12
12
|
from agentdebug.attribution import (
|
|
13
13
|
AllAtOnceAttributor,
|
|
14
|
+
AttributionBudget,
|
|
14
15
|
AttributionResult,
|
|
15
16
|
Attributor,
|
|
16
17
|
BinarySearchAttributor,
|
|
17
18
|
Blame,
|
|
18
19
|
CounterfactualAttributor,
|
|
20
|
+
EnsembleAttributor,
|
|
19
21
|
HeuristicAttributor,
|
|
20
22
|
SBFLAttributor,
|
|
21
23
|
StepByStepAttributor,
|
|
@@ -43,10 +45,12 @@ from agentdebug.models import (
|
|
|
43
45
|
from agentdebug.recorder import AgentDebug, TraceSession
|
|
44
46
|
from agentdebug.recovery import (
|
|
45
47
|
DEFAULT_VERIFIERS,
|
|
48
|
+
AutoManualRules,
|
|
46
49
|
CriticRecoverer,
|
|
47
50
|
FixProposal,
|
|
48
51
|
Recoverer,
|
|
49
52
|
ReflexionSuggestion,
|
|
53
|
+
SelfRefineLoop,
|
|
50
54
|
VerifierSpec,
|
|
51
55
|
)
|
|
52
56
|
from agentdebug.traceback import CascadeFrame, build_cascade, format_traceback
|
|
@@ -63,6 +67,8 @@ __all__ = [
|
|
|
63
67
|
'Attributor',
|
|
64
68
|
'Blame',
|
|
65
69
|
'BusEvent',
|
|
70
|
+
'AttributionBudget',
|
|
71
|
+
'AutoManualRules',
|
|
66
72
|
'BinarySearchAttributor',
|
|
67
73
|
'CascadeFrame',
|
|
68
74
|
'CounterfactualAttributor',
|
|
@@ -70,9 +76,11 @@ __all__ = [
|
|
|
70
76
|
'DEFAULT_VERIFIERS',
|
|
71
77
|
'Detector',
|
|
72
78
|
'DetectorConfig',
|
|
79
|
+
'EnsembleAttributor',
|
|
73
80
|
'RepeatedStateDetector',
|
|
74
81
|
'RepeatedToolCallDetector',
|
|
75
82
|
'SBFLAttributor',
|
|
83
|
+
'SelfRefineLoop',
|
|
76
84
|
'StepByStepAttributor',
|
|
77
85
|
'StepCountLimitDetector',
|
|
78
86
|
'VerifierSpec',
|
|
@@ -100,4 +108,4 @@ __all__ = [
|
|
|
100
108
|
'get_failure_mode',
|
|
101
109
|
]
|
|
102
110
|
|
|
103
|
-
__version__ = '0.2.
|
|
111
|
+
__version__ = '0.2.10'
|
|
@@ -19,7 +19,7 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
21
|
from dataclasses import dataclass, field
|
|
22
|
-
from typing import Any, Dict, List, Optional, Protocol, cast
|
|
22
|
+
from typing import Any, Dict, List, Optional, Protocol, Tuple, cast
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
# Forward decl so BinarySearchAttributor.attribute can reference _EllipsisEvent
|
|
@@ -936,8 +936,217 @@ def _suspiciousness(
|
|
|
936
936
|
return float((ef ** dstar_exponent) / denom)
|
|
937
937
|
|
|
938
938
|
|
|
939
|
+
@dataclass
|
|
940
|
+
class AttributionBudget:
|
|
941
|
+
"""Optional cap on ensemble cost."""
|
|
942
|
+
|
|
943
|
+
max_backends: Optional[int] = None # short-circuit after N successful backends
|
|
944
|
+
max_seconds: Optional[float] = None # walltime cap; remaining backends skipped
|
|
945
|
+
|
|
946
|
+
def exceeded(
|
|
947
|
+
self, *, completed: int, elapsed_s: float,
|
|
948
|
+
) -> bool:
|
|
949
|
+
if self.max_backends is not None and completed >= self.max_backends:
|
|
950
|
+
return True
|
|
951
|
+
if self.max_seconds is not None and elapsed_s >= self.max_seconds:
|
|
952
|
+
return True
|
|
953
|
+
return False
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
class EnsembleAttributor:
|
|
957
|
+
"""Compose any subset of attributors into a single ranked result.
|
|
958
|
+
|
|
959
|
+
Two merge strategies:
|
|
960
|
+
|
|
961
|
+
* ``"borda"`` (default) — each backend's ranked list contributes
|
|
962
|
+
Borda points (top hypothesis = N, next = N-1, …). Steps are
|
|
963
|
+
ranked by total points × backend weight; ties broken by primary
|
|
964
|
+
confidence × weight.
|
|
965
|
+
* ``"bayesian"`` — combine confidences as 1 - ∏(1 - w_i × c_i)
|
|
966
|
+
across backends that nominated the same (event_id, step_index).
|
|
967
|
+
Treats each backend as an independent noisy classifier; good when
|
|
968
|
+
backends are heterogeneous and confidences are well-calibrated.
|
|
969
|
+
|
|
970
|
+
Provenance is HONEST: every output ``Blame.sources`` lists every
|
|
971
|
+
backend that contributed; the rationale aggregates source-by-source
|
|
972
|
+
so the UI can show which backends agreed.
|
|
973
|
+
|
|
974
|
+
Pair with ``AttributionBudget`` to cap wall-clock or backend count
|
|
975
|
+
when running expensive backends (e.g., BinarySearch + Counterfactual)
|
|
976
|
+
alongside cheap ones (Heuristic + SBFL).
|
|
977
|
+
"""
|
|
978
|
+
|
|
979
|
+
id = 'ensemble'
|
|
980
|
+
|
|
981
|
+
def __init__(
|
|
982
|
+
self,
|
|
983
|
+
backends: List[Attributor],
|
|
984
|
+
*,
|
|
985
|
+
weights: Optional[Dict[str, float]] = None,
|
|
986
|
+
merge: str = 'borda',
|
|
987
|
+
top_k: int = 5,
|
|
988
|
+
budget: Optional[AttributionBudget] = None,
|
|
989
|
+
fallback: Optional[Attributor] = None,
|
|
990
|
+
) -> None:
|
|
991
|
+
if not backends:
|
|
992
|
+
raise ValueError('EnsembleAttributor requires at least one backend')
|
|
993
|
+
if merge not in {'borda', 'bayesian'}:
|
|
994
|
+
raise ValueError(
|
|
995
|
+
f"Unknown merge strategy {merge!r}; use 'borda' or 'bayesian'"
|
|
996
|
+
)
|
|
997
|
+
self.backends = list(backends)
|
|
998
|
+
self.weights = weights or {b.id: 1.0 for b in backends}
|
|
999
|
+
self.merge = merge
|
|
1000
|
+
self.top_k = top_k
|
|
1001
|
+
self.budget = budget
|
|
1002
|
+
self.fallback: Attributor = fallback or HeuristicAttributor()
|
|
1003
|
+
|
|
1004
|
+
def attribute(
|
|
1005
|
+
self,
|
|
1006
|
+
trajectory: AgentTrajectory,
|
|
1007
|
+
findings: List[FailureFinding],
|
|
1008
|
+
) -> AttributionResult:
|
|
1009
|
+
import time as _time
|
|
1010
|
+
|
|
1011
|
+
per_backend: List[Tuple[str, AttributionResult]] = []
|
|
1012
|
+
started = _time.perf_counter()
|
|
1013
|
+
for backend in self.backends:
|
|
1014
|
+
if self.budget and self.budget.exceeded(
|
|
1015
|
+
completed=len(per_backend),
|
|
1016
|
+
elapsed_s=_time.perf_counter() - started,
|
|
1017
|
+
):
|
|
1018
|
+
LOG.debug('ensemble budget hit; stopping early')
|
|
1019
|
+
break
|
|
1020
|
+
try:
|
|
1021
|
+
result = backend.attribute(trajectory, findings)
|
|
1022
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
1023
|
+
LOG.warning('ensemble backend %s raised: %s', backend.id, exc)
|
|
1024
|
+
continue
|
|
1025
|
+
per_backend.append((backend.id, result))
|
|
1026
|
+
|
|
1027
|
+
# Any backend produced hypotheses? Otherwise fall back.
|
|
1028
|
+
if not any(r.hypotheses for _id, r in per_backend):
|
|
1029
|
+
return self.fallback.attribute(trajectory, findings)
|
|
1030
|
+
|
|
1031
|
+
if self.merge == 'borda':
|
|
1032
|
+
merged = self._merge_borda(per_backend)
|
|
1033
|
+
else:
|
|
1034
|
+
merged = self._merge_bayesian(per_backend)
|
|
1035
|
+
|
|
1036
|
+
merged = merged[: self.top_k]
|
|
1037
|
+
elapsed_ms = int((_time.perf_counter() - started) * 1000)
|
|
1038
|
+
return AttributionResult(
|
|
1039
|
+
method=self.id,
|
|
1040
|
+
hypotheses=merged,
|
|
1041
|
+
elapsed_ms=elapsed_ms,
|
|
1042
|
+
raw={
|
|
1043
|
+
'merge': self.merge,
|
|
1044
|
+
'backends_run': [bid for bid, _ in per_backend],
|
|
1045
|
+
'weights': dict(self.weights),
|
|
1046
|
+
'budget_exceeded': bool(
|
|
1047
|
+
self.budget
|
|
1048
|
+
and self.budget.exceeded(
|
|
1049
|
+
completed=len(per_backend),
|
|
1050
|
+
elapsed_s=_time.perf_counter() - started,
|
|
1051
|
+
)
|
|
1052
|
+
),
|
|
1053
|
+
},
|
|
1054
|
+
)
|
|
1055
|
+
|
|
1056
|
+
# ---- merge strategies ----
|
|
1057
|
+
|
|
1058
|
+
def _merge_borda(
|
|
1059
|
+
self, per_backend: List[Tuple[str, AttributionResult]],
|
|
1060
|
+
) -> List[Blame]:
|
|
1061
|
+
# Aggregator keyed by canonical step identity.
|
|
1062
|
+
agg: Dict[Tuple[Optional[str], Optional[int]], _MergeRow] = {}
|
|
1063
|
+
for backend_id, result in per_backend:
|
|
1064
|
+
w = self.weights.get(backend_id, 1.0)
|
|
1065
|
+
n = len(result.hypotheses)
|
|
1066
|
+
for rank, h in enumerate(result.hypotheses):
|
|
1067
|
+
key = (h.span_id, h.step_index)
|
|
1068
|
+
row = agg.setdefault(key, _MergeRow(span_id=h.span_id,
|
|
1069
|
+
step_index=h.step_index))
|
|
1070
|
+
# Borda points: top = N, next = N-1, … last = 1
|
|
1071
|
+
row.borda_points += (n - rank) * w
|
|
1072
|
+
row.weighted_conf_sum += w * h.confidence
|
|
1073
|
+
row.weight_sum += w
|
|
1074
|
+
row.sources.add(backend_id)
|
|
1075
|
+
if h.agent_name and not row.agent_name:
|
|
1076
|
+
row.agent_name = h.agent_name
|
|
1077
|
+
if h.rationale and backend_id not in row.rationales:
|
|
1078
|
+
row.rationales[backend_id] = h.rationale
|
|
1079
|
+
row.evidence.extend(h.evidence)
|
|
1080
|
+
ranked = sorted(
|
|
1081
|
+
agg.values(),
|
|
1082
|
+
key=lambda r: (-r.borda_points, -r.weighted_conf_sum),
|
|
1083
|
+
)
|
|
1084
|
+
return [self._row_to_blame(r) for r in ranked]
|
|
1085
|
+
|
|
1086
|
+
def _merge_bayesian(
|
|
1087
|
+
self, per_backend: List[Tuple[str, AttributionResult]],
|
|
1088
|
+
) -> List[Blame]:
|
|
1089
|
+
agg: Dict[Tuple[Optional[str], Optional[int]], _MergeRow] = {}
|
|
1090
|
+
for backend_id, result in per_backend:
|
|
1091
|
+
w = self.weights.get(backend_id, 1.0)
|
|
1092
|
+
for h in result.hypotheses:
|
|
1093
|
+
key = (h.span_id, h.step_index)
|
|
1094
|
+
row = agg.setdefault(key, _MergeRow(span_id=h.span_id,
|
|
1095
|
+
step_index=h.step_index))
|
|
1096
|
+
# P(no source supports) *= (1 - w*c)
|
|
1097
|
+
effective = max(0.0, min(1.0, w * h.confidence))
|
|
1098
|
+
row.bayesian_not_pos *= (1.0 - effective)
|
|
1099
|
+
row.weighted_conf_sum += w * h.confidence
|
|
1100
|
+
row.weight_sum += w
|
|
1101
|
+
row.sources.add(backend_id)
|
|
1102
|
+
if h.agent_name and not row.agent_name:
|
|
1103
|
+
row.agent_name = h.agent_name
|
|
1104
|
+
if h.rationale and backend_id not in row.rationales:
|
|
1105
|
+
row.rationales[backend_id] = h.rationale
|
|
1106
|
+
row.evidence.extend(h.evidence)
|
|
1107
|
+
ranked = sorted(
|
|
1108
|
+
agg.values(),
|
|
1109
|
+
key=lambda r: (-(1.0 - r.bayesian_not_pos), -r.weighted_conf_sum),
|
|
1110
|
+
)
|
|
1111
|
+
return [self._row_to_blame(r) for r in ranked]
|
|
1112
|
+
|
|
1113
|
+
def _row_to_blame(self, row: '_MergeRow') -> Blame:
|
|
1114
|
+
if self.merge == 'borda':
|
|
1115
|
+
# Confidence = weighted average of backend confidences (cap at 1).
|
|
1116
|
+
conf = min(1.0, row.weighted_conf_sum / max(row.weight_sum, 1e-9))
|
|
1117
|
+
else:
|
|
1118
|
+
conf = max(0.0, min(1.0, 1.0 - row.bayesian_not_pos))
|
|
1119
|
+
rationale = '; '.join(
|
|
1120
|
+
f'{bid}: {txt[:140]}' for bid, txt in row.rationales.items()
|
|
1121
|
+
) or 'ensemble agreement'
|
|
1122
|
+
return Blame(
|
|
1123
|
+
span_id=row.span_id,
|
|
1124
|
+
step_index=row.step_index,
|
|
1125
|
+
agent_name=row.agent_name,
|
|
1126
|
+
confidence=conf,
|
|
1127
|
+
rationale=rationale,
|
|
1128
|
+
evidence=list(dict.fromkeys(row.evidence)), # dedupe preserve order
|
|
1129
|
+
sources=sorted(row.sources),
|
|
1130
|
+
)
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
@dataclass
|
|
1134
|
+
class _MergeRow:
|
|
1135
|
+
span_id: Optional[str]
|
|
1136
|
+
step_index: Optional[int]
|
|
1137
|
+
agent_name: Optional[str] = None
|
|
1138
|
+
borda_points: float = 0.0
|
|
1139
|
+
bayesian_not_pos: float = 1.0
|
|
1140
|
+
weighted_conf_sum: float = 0.0
|
|
1141
|
+
weight_sum: float = 0.0
|
|
1142
|
+
sources: set[str] = field(default_factory=set)
|
|
1143
|
+
rationales: Dict[str, str] = field(default_factory=dict)
|
|
1144
|
+
evidence: List[str] = field(default_factory=list)
|
|
1145
|
+
|
|
1146
|
+
|
|
939
1147
|
__all__ = [
|
|
940
|
-
'
|
|
941
|
-
'
|
|
942
|
-
'
|
|
1148
|
+
'AllAtOnceAttributor', 'AttributionBudget', 'AttributionResult',
|
|
1149
|
+
'Attributor', 'BinarySearchAttributor', 'Blame',
|
|
1150
|
+
'CounterfactualAttributor', 'EnsembleAttributor', 'HeuristicAttributor',
|
|
1151
|
+
'SBFLAttributor', 'StepByStepAttributor',
|
|
943
1152
|
]
|
|
@@ -304,11 +304,292 @@ class CriticRecoverer:
|
|
|
304
304
|
)
|
|
305
305
|
|
|
306
306
|
|
|
307
|
+
_SELF_REFINE_CRITIC_PROMPT = """You are AgentDebugX-SelfRefine acting as the
|
|
308
|
+
CRITIC. You will be shown the goal of a failed agent run and a single
|
|
309
|
+
failure finding (mode + step + evidence).
|
|
310
|
+
|
|
311
|
+
Your job: in 2-4 short sentences, explain WHAT went wrong at that step and
|
|
312
|
+
WHY it caused the failure. Do not propose a fix yet — that's the next round.
|
|
313
|
+
Be concrete; reference the evidence.
|
|
314
|
+
|
|
315
|
+
Output plain text. No JSON, no markdown."""
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
_SELF_REFINE_REFINER_PROMPT = """You are AgentDebugX-SelfRefine acting as the
|
|
319
|
+
REFINER. You will be shown the goal of a failed agent run, a single failure
|
|
320
|
+
finding, AND the CRITIC's analysis of what went wrong.
|
|
321
|
+
|
|
322
|
+
Your job: produce a single concrete REFINED ACTION the agent should take
|
|
323
|
+
NEXT TIME at that step. 2-4 short sentences. Plain text. Be operational —
|
|
324
|
+
the agent will read this verbatim as guidance.
|
|
325
|
+
|
|
326
|
+
If the failure is in tool args, give the exact corrected arg shape. If the
|
|
327
|
+
failure is in planning, give the next plan step. If the failure is in
|
|
328
|
+
reflection, give the next verification check.
|
|
329
|
+
|
|
330
|
+
Output plain text. No JSON, no markdown."""
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class SelfRefineLoop:
|
|
334
|
+
"""Self-Refine (Madaan et al., NeurIPS 2023, arXiv:2303.17651) recovery.
|
|
335
|
+
|
|
336
|
+
Suggest-only — we do NOT re-execute the agent. For each finding, run a
|
|
337
|
+
bounded generator-critic-refiner cycle: CRITIC explains what went
|
|
338
|
+
wrong, REFINER proposes a concrete next-attempt action. The cycle can
|
|
339
|
+
iterate ``max_iters`` times; each iteration costs 2 LLM calls.
|
|
340
|
+
|
|
341
|
+
Output is a :class:`FixProposal` whose ``suggestion_text`` contains
|
|
342
|
+
both the critic analysis and the refiner's concrete action. Pair with
|
|
343
|
+
:class:`ReflexionSuggestion` for short verbal hints and
|
|
344
|
+
:class:`CriticRecoverer` for verifier-template recommendations.
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
id = 'self_refine'
|
|
348
|
+
|
|
349
|
+
def __init__(
|
|
350
|
+
self,
|
|
351
|
+
llm: object,
|
|
352
|
+
*,
|
|
353
|
+
max_iters: int = 1,
|
|
354
|
+
max_tokens: int = 512,
|
|
355
|
+
) -> None:
|
|
356
|
+
# llm is duck-typed to LLMClient (avoid circular import); it must have
|
|
357
|
+
# a `.complete(messages=..., max_tokens=...)` returning .text.
|
|
358
|
+
self.llm = llm
|
|
359
|
+
self.max_iters = max(1, max_iters)
|
|
360
|
+
self.max_tokens = max_tokens
|
|
361
|
+
|
|
362
|
+
def suggest(
|
|
363
|
+
self,
|
|
364
|
+
trajectory: AgentTrajectory,
|
|
365
|
+
report: DiagnosticReport,
|
|
366
|
+
) -> List[FixProposal]:
|
|
367
|
+
if not report.findings:
|
|
368
|
+
return []
|
|
369
|
+
proposals: List[FixProposal] = []
|
|
370
|
+
for finding in report.findings:
|
|
371
|
+
proposal = self._refine_one(trajectory, finding)
|
|
372
|
+
if proposal is not None:
|
|
373
|
+
proposals.append(proposal)
|
|
374
|
+
return proposals
|
|
375
|
+
|
|
376
|
+
def _refine_one(
|
|
377
|
+
self,
|
|
378
|
+
trajectory: AgentTrajectory,
|
|
379
|
+
finding: FailureFinding,
|
|
380
|
+
) -> Optional[FixProposal]:
|
|
381
|
+
evidence_block = '\n'.join(f' - {e}' for e in finding.evidence) or ' (none)'
|
|
382
|
+
finding_block = (
|
|
383
|
+
f'GOAL: {trajectory.goal!r}\n'
|
|
384
|
+
f'FAILURE MODE: {finding.failure_mode.mode_id} '
|
|
385
|
+
f'({finding.failure_mode.name})\n'
|
|
386
|
+
f'AT: agent={finding.agent_name}, step={finding.step_index}\n'
|
|
387
|
+
f'EVIDENCE:\n{evidence_block}'
|
|
388
|
+
)
|
|
389
|
+
last_critic = ''
|
|
390
|
+
last_refined = ''
|
|
391
|
+
try:
|
|
392
|
+
for _ in range(self.max_iters):
|
|
393
|
+
critic_text = self._call(
|
|
394
|
+
system=_SELF_REFINE_CRITIC_PROMPT,
|
|
395
|
+
user=finding_block + (
|
|
396
|
+
f'\n\nPRIOR REFINED ACTION (improve on this if needed):\n'
|
|
397
|
+
f'{last_refined}' if last_refined else ''
|
|
398
|
+
),
|
|
399
|
+
)
|
|
400
|
+
if not critic_text:
|
|
401
|
+
break
|
|
402
|
+
last_critic = critic_text
|
|
403
|
+
refined_text = self._call(
|
|
404
|
+
system=_SELF_REFINE_REFINER_PROMPT,
|
|
405
|
+
user=finding_block + f'\n\nCRITIC ANALYSIS:\n{critic_text}',
|
|
406
|
+
)
|
|
407
|
+
if not refined_text:
|
|
408
|
+
break
|
|
409
|
+
last_refined = refined_text
|
|
410
|
+
except Exception:
|
|
411
|
+
# Defensive: never break the host pipeline on LLM hiccup.
|
|
412
|
+
pass
|
|
413
|
+
if not last_critic and not last_refined:
|
|
414
|
+
return None
|
|
415
|
+
suggestion_text = (
|
|
416
|
+
f'Self-Refine output (after {self.max_iters} iter(s)) for '
|
|
417
|
+
f'{finding.failure_mode.mode_id} at step {finding.step_index}:\n\n'
|
|
418
|
+
f'CRITIC:\n{last_critic or "(no critic output)"}\n\n'
|
|
419
|
+
f'REFINED ACTION:\n{last_refined or "(no refined action)"}'
|
|
420
|
+
)
|
|
421
|
+
return FixProposal(
|
|
422
|
+
proposal_id=new_id('fix'),
|
|
423
|
+
recoverer_id=self.id,
|
|
424
|
+
target_event_id=finding.event_id,
|
|
425
|
+
summary=(
|
|
426
|
+
f'Self-Refine action for {finding.failure_mode.mode_id} '
|
|
427
|
+
f'at step {finding.step_index}'
|
|
428
|
+
),
|
|
429
|
+
rationale=(
|
|
430
|
+
'Self-Refine (Madaan et al., NeurIPS 2023): one LM acts as '
|
|
431
|
+
'critic, then refiner; output is a concrete next-attempt '
|
|
432
|
+
'action the agent reads verbatim.'
|
|
433
|
+
),
|
|
434
|
+
confidence=max(0.3, min(0.9, finding.confidence)),
|
|
435
|
+
suggestion_text=suggestion_text,
|
|
436
|
+
side_effects=[],
|
|
437
|
+
requires_human_approval=False,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
def _call(self, *, system: str, user: str) -> str:
|
|
441
|
+
messages = [
|
|
442
|
+
{'role': 'system', 'content': system},
|
|
443
|
+
{'role': 'user', 'content': user},
|
|
444
|
+
]
|
|
445
|
+
# Direct attribute access — llm is duck-typed (see __init__ note).
|
|
446
|
+
result = self.llm.complete( # type: ignore[attr-defined]
|
|
447
|
+
messages=messages, max_tokens=self.max_tokens
|
|
448
|
+
)
|
|
449
|
+
text = getattr(result, 'text', '') or ''
|
|
450
|
+
return str(text).strip()
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class AutoManualRules:
|
|
454
|
+
"""AutoManual-style learned rule extraction (Chen et al., NeurIPS 2024,
|
|
455
|
+
arXiv:2405.16247).
|
|
456
|
+
|
|
457
|
+
Distills each failure finding into a one-line rule. ``suggest()`` returns
|
|
458
|
+
one :class:`FixProposal` per finding whose ``suggestion_text`` holds the
|
|
459
|
+
rule. ``apply()`` appends accepted rules to a per-project Markdown manual
|
|
460
|
+
(``~/.agentdebug/manuals/<project>.md`` by default) which downstream
|
|
461
|
+
runs can prepend to their agent's system prompt to avoid re-hitting the
|
|
462
|
+
same failure.
|
|
463
|
+
|
|
464
|
+
Unlike :class:`ReflexionSuggestion` (short verbal hint per attempt) and
|
|
465
|
+
:class:`SelfRefineLoop` (heavy generator-critic-refiner per finding),
|
|
466
|
+
this is the LONG-TERM memory: small, append-only, surviving across runs.
|
|
467
|
+
Apply is opt-in because it writes to the filesystem.
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
id = 'auto_manual'
|
|
471
|
+
|
|
472
|
+
DEFAULT_MANUAL_DIR = '~/.agentdebug/manuals'
|
|
473
|
+
|
|
474
|
+
def __init__(
|
|
475
|
+
self,
|
|
476
|
+
*,
|
|
477
|
+
llm: Optional[object] = None,
|
|
478
|
+
manual_dir: Optional[str] = None,
|
|
479
|
+
project: str = 'default',
|
|
480
|
+
max_tokens: int = 256,
|
|
481
|
+
) -> None:
|
|
482
|
+
# llm is optional — if absent, we synthesize a rule from the finding's
|
|
483
|
+
# suggestion template rather than calling out.
|
|
484
|
+
self.llm = llm
|
|
485
|
+
self.manual_dir = manual_dir or self.DEFAULT_MANUAL_DIR
|
|
486
|
+
self.project = project
|
|
487
|
+
self.max_tokens = max_tokens
|
|
488
|
+
|
|
489
|
+
def suggest(
|
|
490
|
+
self,
|
|
491
|
+
trajectory: AgentTrajectory,
|
|
492
|
+
report: DiagnosticReport,
|
|
493
|
+
) -> List[FixProposal]:
|
|
494
|
+
if not report.findings:
|
|
495
|
+
return []
|
|
496
|
+
proposals: List[FixProposal] = []
|
|
497
|
+
for finding in report.findings:
|
|
498
|
+
rule = self._rule_for(trajectory, finding)
|
|
499
|
+
if not rule:
|
|
500
|
+
continue
|
|
501
|
+
proposals.append(FixProposal(
|
|
502
|
+
proposal_id=new_id('fix'),
|
|
503
|
+
recoverer_id=self.id,
|
|
504
|
+
target_event_id=finding.event_id,
|
|
505
|
+
summary=(
|
|
506
|
+
f'Add rule to {self.project} manual: '
|
|
507
|
+
f'"{rule[:80]}{"…" if len(rule) > 80 else ""}"'
|
|
508
|
+
),
|
|
509
|
+
rationale=(
|
|
510
|
+
'AutoManual (Chen et al., NeurIPS 2024): distill the '
|
|
511
|
+
'failure into a one-line rule the next run reads via its '
|
|
512
|
+
'system prompt. Long-term memory complements per-attempt '
|
|
513
|
+
'Reflexion and per-finding Self-Refine.'
|
|
514
|
+
),
|
|
515
|
+
confidence=max(0.4, min(0.9, finding.confidence)),
|
|
516
|
+
suggestion_text=rule,
|
|
517
|
+
side_effects=['manual.write'],
|
|
518
|
+
requires_human_approval=False,
|
|
519
|
+
))
|
|
520
|
+
return proposals
|
|
521
|
+
|
|
522
|
+
def apply(self, proposal: FixProposal) -> str:
|
|
523
|
+
"""Append the proposal's rule to the project manual; return the path."""
|
|
524
|
+
from pathlib import Path
|
|
525
|
+
|
|
526
|
+
target = Path(self.manual_dir).expanduser() / f'{self.project}.md'
|
|
527
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
528
|
+
existing = target.read_text(encoding='utf-8') if target.exists() else ''
|
|
529
|
+
if not existing:
|
|
530
|
+
header = f'# AgentDebugX learned manual — {self.project}\n\n'
|
|
531
|
+
existing = header + '<!-- auto-generated; safe to edit. -->\n\n'
|
|
532
|
+
# Idempotent: skip if the rule is already present (case-insensitive).
|
|
533
|
+
rule = proposal.suggestion_text.strip()
|
|
534
|
+
if rule.lower() in existing.lower():
|
|
535
|
+
return str(target)
|
|
536
|
+
target.write_text(
|
|
537
|
+
existing + f'- {rule}\n',
|
|
538
|
+
encoding='utf-8',
|
|
539
|
+
)
|
|
540
|
+
return str(target)
|
|
541
|
+
|
|
542
|
+
def _rule_for(
|
|
543
|
+
self,
|
|
544
|
+
trajectory: AgentTrajectory,
|
|
545
|
+
finding: FailureFinding,
|
|
546
|
+
) -> Optional[str]:
|
|
547
|
+
# If no LLM, fall back to the finding's suggestion template.
|
|
548
|
+
if self.llm is None:
|
|
549
|
+
if finding.suggestion:
|
|
550
|
+
return str(finding.suggestion)
|
|
551
|
+
if finding.failure_mode.suggestion_templates:
|
|
552
|
+
return str(finding.failure_mode.suggestion_templates[0])
|
|
553
|
+
return None
|
|
554
|
+
prompt_system = (
|
|
555
|
+
'You distill failure findings into ONE-LINE actionable rules for '
|
|
556
|
+
'an agent to follow next time. Output plain text — a single '
|
|
557
|
+
'sentence under 200 characters. No JSON, no markdown, no '
|
|
558
|
+
'preamble. Start with an imperative verb ("Validate ...", '
|
|
559
|
+
'"Always ...", "Before ..."). Be specific to the failure.'
|
|
560
|
+
)
|
|
561
|
+
evidence = '; '.join(finding.evidence) or '(no evidence)'
|
|
562
|
+
prompt_user = (
|
|
563
|
+
f'Goal: {trajectory.goal!r}\n'
|
|
564
|
+
f'Failure mode: {finding.failure_mode.mode_id} '
|
|
565
|
+
f'({finding.failure_mode.name})\n'
|
|
566
|
+
f'Agent: {finding.agent_name} at step {finding.step_index}\n'
|
|
567
|
+
f'Evidence: {evidence}'
|
|
568
|
+
)
|
|
569
|
+
try:
|
|
570
|
+
result = self.llm.complete( # type: ignore[attr-defined]
|
|
571
|
+
messages=[
|
|
572
|
+
{'role': 'system', 'content': prompt_system},
|
|
573
|
+
{'role': 'user', 'content': prompt_user},
|
|
574
|
+
],
|
|
575
|
+
max_tokens=self.max_tokens,
|
|
576
|
+
)
|
|
577
|
+
except Exception:
|
|
578
|
+
return str(finding.suggestion) if finding.suggestion else None
|
|
579
|
+
text = getattr(result, 'text', '') or ''
|
|
580
|
+
rule = str(text).strip().splitlines()[0] if text.strip() else ''
|
|
581
|
+
if not rule:
|
|
582
|
+
return str(finding.suggestion) if finding.suggestion else None
|
|
583
|
+
return rule
|
|
584
|
+
|
|
585
|
+
|
|
307
586
|
__all__ = [
|
|
587
|
+
'AutoManualRules',
|
|
308
588
|
'CriticRecoverer',
|
|
309
589
|
'DEFAULT_VERIFIERS',
|
|
310
590
|
'FixProposal',
|
|
311
591
|
'Recoverer',
|
|
312
592
|
'ReflexionSuggestion',
|
|
593
|
+
'SelfRefineLoop',
|
|
313
594
|
'VerifierSpec',
|
|
314
595
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|