agentdebugx 0.2.6__tar.gz → 0.2.8__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.6 → agentdebugx-0.2.8}/PKG-INFO +1 -1
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/22_industry_track_paper_eval_plan.md +2 -2
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/23_status_v0_2.md +4 -2
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/pyproject.toml +1 -1
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/__init__.py +5 -1
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/attribution.py +371 -1
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/LICENSE +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/README.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/00_overview.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/01_literature_survey.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/02_architecture.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/03_taxonomy.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/04_trace_schema.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/05_adapters.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/06_detectors.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/07_attribution.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/08_recovery.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/09_error_database.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/10_taxonomy_induction.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/11_multimodal.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/12_ui_dashboard.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/13_class_design.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/14_api_reference.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/15_roadmap.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/16_governance.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/17_claude_code_design_patterns.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/18_comparison_codex_vs_design.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/19_error_hub.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/20_deep_debug.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/21_integrations.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/ERROR_TAXONOMY.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/OPEN_SOURCE_DEVELOPMENT_PLAN.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/README.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/RESEARCH_SURVEY.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/benchmarks/e2e_v0_2_3.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/benchmarks/e2e_v0_2_4.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/benchmarks/v0_1_smoke.json +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/benchmarks/v0_1_smoke.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/docs/benchmarks/who_when_v0_2_6_leaderboard.md +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/adapters/__init__.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/adapters/base.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/adapters/crewai.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/adapters/langgraph.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/adapters/otel.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/adapters/raw.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/analyzers.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/cli.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/deep.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/detectors.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/events.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/hub/__init__.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/hub/backend_base.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/hub/backends.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/hub/bundle.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/hub/scrub.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/instrumentation.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/integrations/__init__.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/integrations/claude_skill.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/integrations/openhands.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/judges.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/llm.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/models.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/recorder.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/recovery.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/storage.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/taxonomy.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/traceback.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/ui/__init__.py +0 -0
- {agentdebugx-0.2.6 → agentdebugx-0.2.8}/src/agentdebug/ui/server.py +0 -0
|
@@ -37,14 +37,14 @@ Examples worth emulating:
|
|
|
37
37
|
|
|
38
38
|
For AgentDebugX, the paper should therefore be framed as:
|
|
39
39
|
|
|
40
|
-
> A deployment-oriented debugging
|
|
40
|
+
> A deployment-oriented debugging framework for agentic NLP systems, evaluated on
|
|
41
41
|
> whether it makes failures observable, attributable, shareable, and easier for
|
|
42
42
|
> humans to fix.
|
|
43
43
|
|
|
44
44
|
The central claim should not be "we beat every attributor." A stronger claim
|
|
45
45
|
for the Industry Track is:
|
|
46
46
|
|
|
47
|
-
> AgentDebugX provides the missing
|
|
47
|
+
> AgentDebugX provides the missing operational bridge between raw agent traces and
|
|
48
48
|
> actionable debugging workflows: aligned native/error traces, taxonomy-backed
|
|
49
49
|
> reports, Error Hub bundles, and cost-aware analysis profiles.
|
|
50
50
|
|
|
@@ -21,6 +21,8 @@ the forward-looking plan; this doc is the rear-view mirror.
|
|
|
21
21
|
| Attribution | `agentdebug.attribution.AllAtOnceAttributor` | ✅ stable | mocked LLM + fallback |
|
|
22
22
|
| Attribution | `agentdebug.attribution.StepByStepAttributor` | ✅ **new 0.2.2** | scripted-LLM + fallback |
|
|
23
23
|
| Attribution | `agentdebug.attribution.BinarySearchAttributor` | ✅ **new 0.2.3** | oracle-LLM logarithmic convergence + fallback + render elision |
|
|
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
|
+
| 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. |
|
|
24
26
|
| Recovery | `agentdebug.recovery.ReflexionSuggestion` | ✅ stable | per-finding + empty |
|
|
25
27
|
| Recovery | `agentdebug.recovery.CriticRecoverer` + `VerifierSpec` registry | ✅ **new 0.2.3** | 5 family-matched verifier templates; dedup + custom-override |
|
|
26
28
|
| DeepDebug | `agentdebug.deep.DeepDebugAnalyzer` | ✅ stable | full loop + silent LLM |
|
|
@@ -47,8 +49,8 @@ across 32 source files.
|
|
|
47
49
|
| [06_detectors.md](./06_detectors.md) | `trajectory_perplexity` (TrajAD) | needs token-level LM perplexity API or embedding model + baseline calibration | v0.3 |
|
|
48
50
|
| [06_detectors.md](./06_detectors.md) | `topic_drift` (embedding cosine) | needs embedding client; consider reusing `OpenAICompatClient` `/embeddings` | v0.3 |
|
|
49
51
|
| [06_detectors.md](./06_detectors.md) | LTL spec monitors | requires user-supplied spec or LLM-synthesized monitors; gated on RV research | v1.2 |
|
|
50
|
-
| [07_attribution.md](./07_attribution.md) | `CounterfactualAttributor` |
|
|
51
|
-
| [07_attribution.md](./07_attribution.md) | `SBFLAttributor`
|
|
52
|
+
| [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
|
+
| [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 |
|
|
52
54
|
| [07_attribution.md](./07_attribution.md) | `DeltaDebugAttributor` (Zeller) | same replay constraint | v0.3 |
|
|
53
55
|
| [07_attribution.md](./07_attribution.md) | `EnsembleAttributor` | trivial once Counterfactual lands; awaits Counterfactual | v0.3 |
|
|
54
56
|
| [08_recovery.md](./08_recovery.md) | `SelfRefineLoop` | small but needs a generator-critic-refiner orchestration | v0.3 |
|
|
@@ -15,7 +15,9 @@ from agentdebug.attribution import (
|
|
|
15
15
|
Attributor,
|
|
16
16
|
BinarySearchAttributor,
|
|
17
17
|
Blame,
|
|
18
|
+
CounterfactualAttributor,
|
|
18
19
|
HeuristicAttributor,
|
|
20
|
+
SBFLAttributor,
|
|
19
21
|
StepByStepAttributor,
|
|
20
22
|
)
|
|
21
23
|
from agentdebug.detectors import (
|
|
@@ -63,12 +65,14 @@ __all__ = [
|
|
|
63
65
|
'BusEvent',
|
|
64
66
|
'BinarySearchAttributor',
|
|
65
67
|
'CascadeFrame',
|
|
68
|
+
'CounterfactualAttributor',
|
|
66
69
|
'CriticRecoverer',
|
|
67
70
|
'DEFAULT_VERIFIERS',
|
|
68
71
|
'Detector',
|
|
69
72
|
'DetectorConfig',
|
|
70
73
|
'RepeatedStateDetector',
|
|
71
74
|
'RepeatedToolCallDetector',
|
|
75
|
+
'SBFLAttributor',
|
|
72
76
|
'StepByStepAttributor',
|
|
73
77
|
'StepCountLimitDetector',
|
|
74
78
|
'VerifierSpec',
|
|
@@ -96,4 +100,4 @@ __all__ = [
|
|
|
96
100
|
'get_failure_mode',
|
|
97
101
|
]
|
|
98
102
|
|
|
99
|
-
__version__ = '0.2.
|
|
103
|
+
__version__ = '0.2.8'
|
|
@@ -566,8 +566,378 @@ def _EVENT_ELLIPSIS(count: int) -> _EllipsisEvent:
|
|
|
566
566
|
return _EllipsisEvent(count=count)
|
|
567
567
|
|
|
568
568
|
|
|
569
|
+
_COUNTERFACTUAL_SYSTEM_PROMPT = """You are AgentDebugX-Attributor running an
|
|
570
|
+
LLM-simulated counterfactual replay (AgenTracer-style, arXiv:2509.03312).
|
|
571
|
+
|
|
572
|
+
You will be given the goal, the full trajectory, and ONE CANDIDATE STEP. Your
|
|
573
|
+
job is to estimate whether the agent would have succeeded if THAT step had
|
|
574
|
+
been done correctly — leaving everything else the same. This isolates the
|
|
575
|
+
step's causal contribution to the failure.
|
|
576
|
+
|
|
577
|
+
CRITICAL OUTPUT RULES (these maximize the chance your reply parses):
|
|
578
|
+
1. Output ONLY a JSON object. No prose before/after. No markdown fences.
|
|
579
|
+
2. Keep "rationale" to ONE short sentence (<= 200 chars).
|
|
580
|
+
3. Do NOT include newlines inside string values.
|
|
581
|
+
4. Emit the JSON object COMPLETE.
|
|
582
|
+
|
|
583
|
+
Schema:
|
|
584
|
+
{
|
|
585
|
+
"rescue_probability": <0..1>,
|
|
586
|
+
"confidence": <0..1>,
|
|
587
|
+
"rationale": "<short>",
|
|
588
|
+
"would_block_downstream_failures": true | false
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
Higher rescue_probability = correcting this step would more likely have
|
|
592
|
+
rescued the run; this step is therefore more responsible for the failure.
|
|
593
|
+
"""
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
class CounterfactualAttributor:
|
|
597
|
+
"""LLM-simulated counterfactual replay.
|
|
598
|
+
|
|
599
|
+
For each of K candidate steps (top-K from prior findings, or
|
|
600
|
+
error-bearing events, or the tail of the trajectory) ask the LLM:
|
|
601
|
+
"if this step had been correct, would the rest of the trajectory still
|
|
602
|
+
fail?" Steps with the highest rescue-probability become the top blame
|
|
603
|
+
hypotheses. Costs O(K) LLM calls — comparable to AllAtOnce, with a
|
|
604
|
+
stronger causal claim per probe.
|
|
605
|
+
|
|
606
|
+
This is *simulated* counterfactual, not real re-rollout — strictly
|
|
607
|
+
weaker than AgenTracer's actual replay, but framework-independent and
|
|
608
|
+
runnable today against any LLM. When the underlying framework gains a
|
|
609
|
+
real replay surface (LangGraph checkpointer, OpenHands rewind), wire
|
|
610
|
+
that in as an alternative ``replay_fn`` and the algorithm carries over.
|
|
611
|
+
"""
|
|
612
|
+
|
|
613
|
+
id = 'counterfactual'
|
|
614
|
+
|
|
615
|
+
def __init__(
|
|
616
|
+
self,
|
|
617
|
+
llm: LLMClient,
|
|
618
|
+
*,
|
|
619
|
+
max_candidates: int = 5,
|
|
620
|
+
max_tokens: int = 2048,
|
|
621
|
+
fallback: Optional[Attributor] = None,
|
|
622
|
+
) -> None:
|
|
623
|
+
self.llm = llm
|
|
624
|
+
self.max_candidates = max_candidates
|
|
625
|
+
self.max_tokens = max_tokens
|
|
626
|
+
self.fallback: Attributor = fallback or HeuristicAttributor()
|
|
627
|
+
|
|
628
|
+
def attribute(
|
|
629
|
+
self,
|
|
630
|
+
trajectory: AgentTrajectory,
|
|
631
|
+
findings: List[FailureFinding],
|
|
632
|
+
) -> AttributionResult:
|
|
633
|
+
candidates = self._pick_candidates(trajectory, findings)
|
|
634
|
+
if not candidates:
|
|
635
|
+
return self.fallback.attribute(trajectory, findings)
|
|
636
|
+
ranked: List[tuple[AgentEvent, Dict[str, Any]]] = []
|
|
637
|
+
for evt in candidates:
|
|
638
|
+
verdict = self._ask_counterfactual(trajectory, evt)
|
|
639
|
+
if verdict is None:
|
|
640
|
+
continue
|
|
641
|
+
ranked.append((evt, verdict))
|
|
642
|
+
if not ranked:
|
|
643
|
+
return self.fallback.attribute(trajectory, findings)
|
|
644
|
+
# Sort by rescue_probability desc, tie-break by confidence.
|
|
645
|
+
ranked.sort(
|
|
646
|
+
key=lambda r: (
|
|
647
|
+
-self._coerce_float(r[1].get('rescue_probability'), 0.0),
|
|
648
|
+
-self._coerce_float(r[1].get('confidence'), 0.0),
|
|
649
|
+
)
|
|
650
|
+
)
|
|
651
|
+
hypotheses: List[Blame] = []
|
|
652
|
+
for evt, verdict in ranked:
|
|
653
|
+
hypotheses.append(Blame(
|
|
654
|
+
span_id=evt.event_id,
|
|
655
|
+
step_index=evt.step_index,
|
|
656
|
+
agent_name=evt.agent_name,
|
|
657
|
+
confidence=self._coerce_float(verdict.get('rescue_probability'), 0.0),
|
|
658
|
+
rationale=(
|
|
659
|
+
str(verdict.get('rationale') or 'no rationale')
|
|
660
|
+
+ f' [rescue_probability={verdict.get("rescue_probability")}]'
|
|
661
|
+
),
|
|
662
|
+
evidence=[
|
|
663
|
+
f'event_id={evt.event_id}',
|
|
664
|
+
f'step={evt.step_index}',
|
|
665
|
+
],
|
|
666
|
+
sources=[self.id],
|
|
667
|
+
))
|
|
668
|
+
return AttributionResult(
|
|
669
|
+
method=self.id,
|
|
670
|
+
hypotheses=hypotheses,
|
|
671
|
+
raw={'candidates_probed': len(ranked)},
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
def _pick_candidates(
|
|
675
|
+
self,
|
|
676
|
+
trajectory: AgentTrajectory,
|
|
677
|
+
findings: List[FailureFinding],
|
|
678
|
+
) -> List[AgentEvent]:
|
|
679
|
+
events_by_id = {e.event_id: e for e in trajectory.events}
|
|
680
|
+
candidates: List[AgentEvent] = []
|
|
681
|
+
seen: set[str] = set()
|
|
682
|
+
# 1. Prior findings (the judge already nominated suspects).
|
|
683
|
+
for f in findings:
|
|
684
|
+
evt = events_by_id.get(f.event_id) if f.event_id else None
|
|
685
|
+
if evt is not None and evt.event_id not in seen:
|
|
686
|
+
candidates.append(evt)
|
|
687
|
+
seen.add(evt.event_id)
|
|
688
|
+
if len(candidates) >= self.max_candidates:
|
|
689
|
+
return candidates
|
|
690
|
+
# 2. Events that recorded an error directly.
|
|
691
|
+
for evt in trajectory.events:
|
|
692
|
+
if evt.error and evt.event_id not in seen:
|
|
693
|
+
candidates.append(evt)
|
|
694
|
+
seen.add(evt.event_id)
|
|
695
|
+
if len(candidates) >= self.max_candidates:
|
|
696
|
+
return candidates
|
|
697
|
+
# 3. Fallback: tail of the trajectory (failure most often manifests there).
|
|
698
|
+
for evt in reversed(trajectory.events):
|
|
699
|
+
if evt.event_id not in seen:
|
|
700
|
+
candidates.append(evt)
|
|
701
|
+
seen.add(evt.event_id)
|
|
702
|
+
if len(candidates) >= self.max_candidates:
|
|
703
|
+
return candidates
|
|
704
|
+
return candidates
|
|
705
|
+
|
|
706
|
+
def _ask_counterfactual(
|
|
707
|
+
self, trajectory: AgentTrajectory, candidate: AgentEvent,
|
|
708
|
+
) -> Optional[Dict[str, Any]]:
|
|
709
|
+
events_doc = '\n'.join(
|
|
710
|
+
f'event_id={e.event_id} step={e.step_index} agent={e.agent_name} '
|
|
711
|
+
f'type={getattr(e.event_type, "value", e.event_type)} '
|
|
712
|
+
f'output={str(e.output)[:200]} error={str(e.error)[:200]}'
|
|
713
|
+
for e in trajectory.events
|
|
714
|
+
)
|
|
715
|
+
user = (
|
|
716
|
+
f'GOAL: {trajectory.goal!r}\n'
|
|
717
|
+
f'FRAMEWORK: {trajectory.framework!r}\n\n'
|
|
718
|
+
f'FULL TRAJECTORY:\n{events_doc}\n\n'
|
|
719
|
+
f'CANDIDATE STEP TO COUNTERFACTUALLY CORRECT:\n'
|
|
720
|
+
f' event_id={candidate.event_id}\n'
|
|
721
|
+
f' step={candidate.step_index} agent={candidate.agent_name}\n'
|
|
722
|
+
f' module={candidate.module}\n'
|
|
723
|
+
f' input={str(candidate.input)[:300]}\n'
|
|
724
|
+
f' output={str(candidate.output)[:300]}\n'
|
|
725
|
+
f' error={str(candidate.error)[:300]}\n\n'
|
|
726
|
+
f'Question: if this step had been DONE CORRECTLY, what is the '
|
|
727
|
+
f'probability the run would have succeeded?'
|
|
728
|
+
)
|
|
729
|
+
try:
|
|
730
|
+
result = self.llm.complete(
|
|
731
|
+
messages=[
|
|
732
|
+
{'role': 'system', 'content': _COUNTERFACTUAL_SYSTEM_PROMPT},
|
|
733
|
+
{'role': 'user', 'content': user},
|
|
734
|
+
],
|
|
735
|
+
max_tokens=self.max_tokens,
|
|
736
|
+
)
|
|
737
|
+
except Exception as exc: # pragma: no cover
|
|
738
|
+
LOG.warning('counterfactual probe failed at event=%s: %s',
|
|
739
|
+
candidate.event_id, exc)
|
|
740
|
+
return None
|
|
741
|
+
parsed = extract_json_block(result.text)
|
|
742
|
+
if parsed is None:
|
|
743
|
+
return None
|
|
744
|
+
return cast(Dict[str, Any], parsed)
|
|
745
|
+
|
|
746
|
+
@staticmethod
|
|
747
|
+
def _coerce_float(value: Any, default: float) -> float:
|
|
748
|
+
try:
|
|
749
|
+
return float(value)
|
|
750
|
+
except (TypeError, ValueError):
|
|
751
|
+
return default
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
class SBFLAttributor:
|
|
755
|
+
"""Spectrum-based fault localization (Tarantula / Ochiai / DStar).
|
|
756
|
+
|
|
757
|
+
Model-free. Costs ZERO LLM calls at inference time. The price: it needs
|
|
758
|
+
a corpus of *other* traces of the same task (a mix of passing and
|
|
759
|
+
failing). For each step in the failing trace under attribution, compute
|
|
760
|
+
suspiciousness from how often that step's signature appears in failing
|
|
761
|
+
vs passing traces of the same task.
|
|
762
|
+
|
|
763
|
+
Signature design — the cross-trace identity of a "step":
|
|
764
|
+
|
|
765
|
+
(agent_name, event_type, module, normalized_io_hash)
|
|
766
|
+
|
|
767
|
+
The normalized I/O hash collapses whitespace + truncates to 120 chars
|
|
768
|
+
so semantically-equivalent step executions match across runs (a
|
|
769
|
+
`search` call with the same query produces the same signature even if
|
|
770
|
+
the wrapping prompt differs trace-to-trace).
|
|
771
|
+
|
|
772
|
+
Suspiciousness formulas:
|
|
773
|
+
|
|
774
|
+
Tarantula = (ef / nf) / (ef / nf + ep / np)
|
|
775
|
+
Ochiai = ef / sqrt((ef + ep) * (nf))
|
|
776
|
+
DStar* = ef^* / (ep + (nf - ef)) [* = exponent, default 2]
|
|
777
|
+
|
|
778
|
+
where for the candidate step:
|
|
779
|
+
ef = # failing corpus traces containing this signature
|
|
780
|
+
ep = # passing corpus traces containing this signature
|
|
781
|
+
nf = total failing traces in the corpus
|
|
782
|
+
np = total passing traces in the corpus
|
|
783
|
+
|
|
784
|
+
Reference: Jones & Harrold (Tarantula, ICSE 2002); Abreu et al.
|
|
785
|
+
(Ochiai, TR 2007); Wong et al. (DStar, IEEE TR 2014).
|
|
786
|
+
"""
|
|
787
|
+
|
|
788
|
+
id = 'sbfl'
|
|
789
|
+
|
|
790
|
+
def __init__(
|
|
791
|
+
self,
|
|
792
|
+
*,
|
|
793
|
+
passing_corpus: List[AgentTrajectory],
|
|
794
|
+
failing_corpus: List[AgentTrajectory],
|
|
795
|
+
formula: str = 'ochiai',
|
|
796
|
+
dstar_exponent: float = 2.0,
|
|
797
|
+
top_k: int = 5,
|
|
798
|
+
fallback: Optional[Attributor] = None,
|
|
799
|
+
) -> None:
|
|
800
|
+
if formula not in {'tarantula', 'ochiai', 'dstar'}:
|
|
801
|
+
raise ValueError(
|
|
802
|
+
f"Unknown SBFL formula {formula!r}; "
|
|
803
|
+
"use 'tarantula', 'ochiai', or 'dstar'"
|
|
804
|
+
)
|
|
805
|
+
self.passing_corpus = list(passing_corpus)
|
|
806
|
+
self.failing_corpus = list(failing_corpus)
|
|
807
|
+
self.formula = formula
|
|
808
|
+
self.dstar_exponent = dstar_exponent
|
|
809
|
+
self.top_k = top_k
|
|
810
|
+
self.fallback: Attributor = fallback or HeuristicAttributor()
|
|
811
|
+
# Precompute signature presence per trace so attribution is O(N) over
|
|
812
|
+
# the candidate's events × O(corpus_size).
|
|
813
|
+
self._sigs_in_failing: List[set[str]] = [
|
|
814
|
+
_signatures_for(t) for t in self.failing_corpus
|
|
815
|
+
]
|
|
816
|
+
self._sigs_in_passing: List[set[str]] = [
|
|
817
|
+
_signatures_for(t) for t in self.passing_corpus
|
|
818
|
+
]
|
|
819
|
+
|
|
820
|
+
def attribute(
|
|
821
|
+
self,
|
|
822
|
+
trajectory: AgentTrajectory,
|
|
823
|
+
findings: List[FailureFinding],
|
|
824
|
+
) -> AttributionResult:
|
|
825
|
+
nf = len(self._sigs_in_failing)
|
|
826
|
+
np = len(self._sigs_in_passing)
|
|
827
|
+
if nf == 0 and np == 0:
|
|
828
|
+
return self.fallback.attribute(trajectory, findings)
|
|
829
|
+
|
|
830
|
+
scored: List[tuple[AgentEvent, float, int, int]] = []
|
|
831
|
+
for evt in trajectory.events:
|
|
832
|
+
sig = _signature_for(evt)
|
|
833
|
+
if not sig:
|
|
834
|
+
continue
|
|
835
|
+
ef = sum(1 for s in self._sigs_in_failing if sig in s)
|
|
836
|
+
ep = sum(1 for s in self._sigs_in_passing if sig in s)
|
|
837
|
+
score = _suspiciousness(
|
|
838
|
+
ef=ef, ep=ep, nf=max(nf, 1), np=max(np, 1),
|
|
839
|
+
formula=self.formula, dstar_exponent=self.dstar_exponent,
|
|
840
|
+
)
|
|
841
|
+
scored.append((evt, score, ef, ep))
|
|
842
|
+
|
|
843
|
+
if not scored:
|
|
844
|
+
return self.fallback.attribute(trajectory, findings)
|
|
845
|
+
|
|
846
|
+
# Sort by suspiciousness desc, tie-break by step index asc (earlier wins).
|
|
847
|
+
scored.sort(key=lambda r: (-r[1], r[0].step_index or 10**9))
|
|
848
|
+
hypotheses: List[Blame] = []
|
|
849
|
+
for evt, score, ef, ep in scored[: self.top_k]:
|
|
850
|
+
if score <= 0.0:
|
|
851
|
+
continue # nothing suspicious about this step
|
|
852
|
+
hypotheses.append(Blame(
|
|
853
|
+
span_id=evt.event_id,
|
|
854
|
+
step_index=evt.step_index,
|
|
855
|
+
agent_name=evt.agent_name,
|
|
856
|
+
confidence=min(1.0, max(0.0, score)),
|
|
857
|
+
rationale=(
|
|
858
|
+
f'{self.formula} suspiciousness={score:.3f} '
|
|
859
|
+
f'(ef={ef}/{nf}, ep={ep}/{np})'
|
|
860
|
+
),
|
|
861
|
+
evidence=[
|
|
862
|
+
f'signature occurs in {ef}/{nf} failing and {ep}/{np} '
|
|
863
|
+
f'passing corpus traces',
|
|
864
|
+
],
|
|
865
|
+
sources=[self.id],
|
|
866
|
+
))
|
|
867
|
+
if not hypotheses:
|
|
868
|
+
return self.fallback.attribute(trajectory, findings)
|
|
869
|
+
return AttributionResult(
|
|
870
|
+
method=self.id,
|
|
871
|
+
hypotheses=hypotheses,
|
|
872
|
+
raw={
|
|
873
|
+
'formula': self.formula,
|
|
874
|
+
'corpus_size': {'failing': nf, 'passing': np},
|
|
875
|
+
'scored_events': len(scored),
|
|
876
|
+
},
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def _signature_for(event: AgentEvent) -> Optional[str]:
|
|
881
|
+
"""Cross-trace identity of a step. Returns None for events that should
|
|
882
|
+
not contribute to suspiciousness:
|
|
883
|
+
|
|
884
|
+
* ``run.start`` / ``run.end`` — trajectory lifecycle markers.
|
|
885
|
+
* ``error`` events emitted by ``agent_name='system'`` — these are the
|
|
886
|
+
synthetic markers from ``AgentDebug.finish_trace(success=False)`` and
|
|
887
|
+
are present in every failed trace; they would saturate suspiciousness
|
|
888
|
+
without revealing anything about the cause.
|
|
889
|
+
"""
|
|
890
|
+
et = getattr(event.event_type, 'value', event.event_type)
|
|
891
|
+
if et in {'run.start', 'run.end'}:
|
|
892
|
+
return None
|
|
893
|
+
if et == 'error' and (event.agent_name or '') == 'system':
|
|
894
|
+
return None
|
|
895
|
+
payload = ' '.join(str(event.output or event.input or '').split())[:120]
|
|
896
|
+
return '|'.join([
|
|
897
|
+
str(et),
|
|
898
|
+
str(event.agent_name or ''),
|
|
899
|
+
str(event.module or ''),
|
|
900
|
+
payload,
|
|
901
|
+
])
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
def _signatures_for(trajectory: AgentTrajectory) -> set[str]:
|
|
905
|
+
out: set[str] = set()
|
|
906
|
+
for evt in trajectory.events:
|
|
907
|
+
sig = _signature_for(evt)
|
|
908
|
+
if sig:
|
|
909
|
+
out.add(sig)
|
|
910
|
+
return out
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
def _suspiciousness(
|
|
914
|
+
*, ef: int, ep: int, nf: int, np: int,
|
|
915
|
+
formula: str, dstar_exponent: float,
|
|
916
|
+
) -> float:
|
|
917
|
+
import math
|
|
918
|
+
if formula == 'tarantula':
|
|
919
|
+
# Defined as 0 when the step never appears in failing traces.
|
|
920
|
+
if ef == 0:
|
|
921
|
+
return 0.0
|
|
922
|
+
# Defensive: nf>=1 by caller.
|
|
923
|
+
f_rate = ef / nf
|
|
924
|
+
p_rate = (ep / np) if np > 0 else 0.0
|
|
925
|
+
denom = f_rate + p_rate
|
|
926
|
+
return f_rate / denom if denom > 0 else 0.0
|
|
927
|
+
if formula == 'ochiai':
|
|
928
|
+
denom = math.sqrt((ef + ep) * nf)
|
|
929
|
+
return (ef / denom) if denom > 0 else 0.0
|
|
930
|
+
# dstar
|
|
931
|
+
if ep == 0 and (nf - ef) == 0:
|
|
932
|
+
return float('inf') if ef > 0 else 0.0
|
|
933
|
+
denom = ep + (nf - ef)
|
|
934
|
+
if denom == 0:
|
|
935
|
+
return 0.0
|
|
936
|
+
return float((ef ** dstar_exponent) / denom)
|
|
937
|
+
|
|
938
|
+
|
|
569
939
|
__all__ = [
|
|
570
940
|
'Attributor', 'Blame', 'AttributionResult',
|
|
571
941
|
'HeuristicAttributor', 'AllAtOnceAttributor', 'StepByStepAttributor',
|
|
572
|
-
'BinarySearchAttributor',
|
|
942
|
+
'BinarySearchAttributor', 'CounterfactualAttributor', 'SBFLAttributor',
|
|
573
943
|
]
|
|
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
|