agentdebugx 0.2.7__tar.gz → 0.2.9__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.
Files changed (69) hide show
  1. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/PKG-INFO +1 -1
  2. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/22_industry_track_paper_eval_plan.md +2 -2
  3. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/23_status_v0_2.md +4 -3
  4. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/pyproject.toml +1 -1
  5. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/__init__.py +7 -1
  6. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/attribution.py +186 -1
  7. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/recovery.py +281 -0
  8. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/LICENSE +0 -0
  9. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/README.md +0 -0
  10. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/00_overview.md +0 -0
  11. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/01_literature_survey.md +0 -0
  12. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/02_architecture.md +0 -0
  13. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/03_taxonomy.md +0 -0
  14. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/04_trace_schema.md +0 -0
  15. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/05_adapters.md +0 -0
  16. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/06_detectors.md +0 -0
  17. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/07_attribution.md +0 -0
  18. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/08_recovery.md +0 -0
  19. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/09_error_database.md +0 -0
  20. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/10_taxonomy_induction.md +0 -0
  21. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/11_multimodal.md +0 -0
  22. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/12_ui_dashboard.md +0 -0
  23. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/13_class_design.md +0 -0
  24. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/14_api_reference.md +0 -0
  25. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/15_roadmap.md +0 -0
  26. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/16_governance.md +0 -0
  27. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/17_claude_code_design_patterns.md +0 -0
  28. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/18_comparison_codex_vs_design.md +0 -0
  29. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/19_error_hub.md +0 -0
  30. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/20_deep_debug.md +0 -0
  31. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/21_integrations.md +0 -0
  32. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/ERROR_TAXONOMY.md +0 -0
  33. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/OPEN_SOURCE_DEVELOPMENT_PLAN.md +0 -0
  34. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/README.md +0 -0
  35. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/RESEARCH_SURVEY.md +0 -0
  36. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/benchmarks/e2e_v0_2_3.md +0 -0
  37. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/benchmarks/e2e_v0_2_4.md +0 -0
  38. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/benchmarks/v0_1_smoke.json +0 -0
  39. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/benchmarks/v0_1_smoke.md +0 -0
  40. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/docs/benchmarks/who_when_v0_2_6_leaderboard.md +0 -0
  41. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/adapters/__init__.py +0 -0
  42. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/adapters/base.py +0 -0
  43. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/adapters/crewai.py +0 -0
  44. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/adapters/langgraph.py +0 -0
  45. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/adapters/otel.py +0 -0
  46. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/adapters/raw.py +0 -0
  47. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/analyzers.py +0 -0
  48. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/cli.py +0 -0
  49. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/deep.py +0 -0
  50. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/detectors.py +0 -0
  51. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/events.py +0 -0
  52. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/hub/__init__.py +0 -0
  53. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/hub/backend_base.py +0 -0
  54. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/hub/backends.py +0 -0
  55. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/hub/bundle.py +0 -0
  56. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/hub/scrub.py +0 -0
  57. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/instrumentation.py +0 -0
  58. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/integrations/__init__.py +0 -0
  59. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/integrations/claude_skill.py +0 -0
  60. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/integrations/openhands.py +0 -0
  61. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/judges.py +0 -0
  62. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/llm.py +0 -0
  63. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/models.py +0 -0
  64. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/recorder.py +0 -0
  65. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/storage.py +0 -0
  66. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/taxonomy.py +0 -0
  67. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/traceback.py +0 -0
  68. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/ui/__init__.py +0 -0
  69. {agentdebugx-0.2.7 → agentdebugx-0.2.9}/src/agentdebug/ui/server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentdebugx
3
- Version: 0.2.7
3
+ Version: 0.2.9
4
4
  Summary: Portable error analysis, tracing, and recovery framework for agentic AI systems. Import as `agentdebug`.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -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 layer for agentic NLP systems, evaluated on
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 operating layer between raw agent traces and
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
 
@@ -22,8 +22,11 @@ the forward-looking plan; this doc is the rear-view mirror.
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
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. |
25
26
  | Recovery | `agentdebug.recovery.ReflexionSuggestion` | ✅ stable | per-finding + empty |
26
27
  | Recovery | `agentdebug.recovery.CriticRecoverer` + `VerifierSpec` registry | ✅ **new 0.2.3** | 5 family-matched verifier templates; dedup + custom-override |
28
+ | Recovery | `agentdebug.recovery.SelfRefineLoop` | ✅ **new 0.2.9** | scripted-LLM happy + silent-LLM safe + multi-iter; per-finding generator→critic→refiner |
29
+ | Recovery | `agentdebug.recovery.AutoManualRules` (+ `apply()`) | ✅ **new 0.2.9** | no-LLM fallback + LLM rule-distill + idempotent apply + multi-rule append |
27
30
  | DeepDebug | `agentdebug.deep.DeepDebugAnalyzer` | ✅ stable | full loop + silent LLM |
28
31
  | Cascade view | `agentdebug.traceback.format_traceback` | ✅ stable | cascade + step-order + ANSI + empty |
29
32
  | Detectors | `agentdebug.detectors.RepeatedToolCall / RepeatedState / StepCountLimit` | ✅ **new 0.2.2** | threshold + window + budget |
@@ -49,11 +52,9 @@ across 32 source files.
49
52
  | [06_detectors.md](./06_detectors.md) | `topic_drift` (embedding cosine) | needs embedding client; consider reusing `OpenAICompatClient` `/embeddings` | v0.3 |
50
53
  | [06_detectors.md](./06_detectors.md) | LTL spec monitors | requires user-supplied spec or LLM-synthesized monitors; gated on RV research | v1.2 |
51
54
  | [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 |
52
- | [07_attribution.md](./07_attribution.md) | `SBFLAttributor` (Tarantula/Ochiai) | needs corpus of passing + failing traces of same task; gated on Hub adoption | v0.4 |
55
+ | [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 |
53
56
  | [07_attribution.md](./07_attribution.md) | `DeltaDebugAttributor` (Zeller) | same replay constraint | v0.3 |
54
57
  | [07_attribution.md](./07_attribution.md) | `EnsembleAttributor` | trivial once Counterfactual lands; awaits Counterfactual | v0.3 |
55
- | [08_recovery.md](./08_recovery.md) | `SelfRefineLoop` | small but needs a generator-critic-refiner orchestration | v0.3 |
56
- | [08_recovery.md](./08_recovery.md) | `AutoManualRules` | needs persistent project manual + injection into next-run prompts | v0.3 |
57
58
  | [08_recovery.md](./08_recovery.md) | `LangGraphRewind` | depends on LangGraph checkpointer; ships when we have a real LangGraph user | v0.3 |
58
59
  | [08_recovery.md](./08_recovery.md) | `SagaRollback` | needs compensation registry on tool definitions; new schema | v0.3 |
59
60
  | [08_recovery.md](./08_recovery.md) | `MCTSBranchExploration` (LATS) | heavy; v2 feature | v2.0 |
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "agentdebugx"
3
- version = "0.2.7"
3
+ version = "0.2.9"
4
4
  description = "Portable error analysis, tracing, and recovery framework for agentic AI systems. Import as `agentdebug`."
5
5
  authors = ["ULab @ UIUC <ulab@illinois.edu>"]
6
6
  license = "MIT"
@@ -17,6 +17,7 @@ from agentdebug.attribution import (
17
17
  Blame,
18
18
  CounterfactualAttributor,
19
19
  HeuristicAttributor,
20
+ SBFLAttributor,
20
21
  StepByStepAttributor,
21
22
  )
22
23
  from agentdebug.detectors import (
@@ -42,10 +43,12 @@ from agentdebug.models import (
42
43
  from agentdebug.recorder import AgentDebug, TraceSession
43
44
  from agentdebug.recovery import (
44
45
  DEFAULT_VERIFIERS,
46
+ AutoManualRules,
45
47
  CriticRecoverer,
46
48
  FixProposal,
47
49
  Recoverer,
48
50
  ReflexionSuggestion,
51
+ SelfRefineLoop,
49
52
  VerifierSpec,
50
53
  )
51
54
  from agentdebug.traceback import CascadeFrame, build_cascade, format_traceback
@@ -62,6 +65,7 @@ __all__ = [
62
65
  'Attributor',
63
66
  'Blame',
64
67
  'BusEvent',
68
+ 'AutoManualRules',
65
69
  'BinarySearchAttributor',
66
70
  'CascadeFrame',
67
71
  'CounterfactualAttributor',
@@ -71,6 +75,8 @@ __all__ = [
71
75
  'DetectorConfig',
72
76
  'RepeatedStateDetector',
73
77
  'RepeatedToolCallDetector',
78
+ 'SBFLAttributor',
79
+ 'SelfRefineLoop',
74
80
  'StepByStepAttributor',
75
81
  'StepCountLimitDetector',
76
82
  'VerifierSpec',
@@ -98,4 +104,4 @@ __all__ = [
98
104
  'get_failure_mode',
99
105
  ]
100
106
 
101
- __version__ = '0.2.7'
107
+ __version__ = '0.2.9'
@@ -751,8 +751,193 @@ class CounterfactualAttributor:
751
751
  return default
752
752
 
753
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
+
754
939
  __all__ = [
755
940
  'Attributor', 'Blame', 'AttributionResult',
756
941
  'HeuristicAttributor', 'AllAtOnceAttributor', 'StepByStepAttributor',
757
- 'BinarySearchAttributor', 'CounterfactualAttributor',
942
+ 'BinarySearchAttributor', 'CounterfactualAttributor', 'SBFLAttributor',
758
943
  ]
@@ -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