agentdebugx 0.2.11__tar.gz → 0.2.12__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.11 → agentdebugx-0.2.12}/PKG-INFO +2 -1
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/23_status_v0_2.md +2 -1
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/pyproject.toml +3 -1
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/__init__.py +7 -1
- agentdebugx-0.2.12/src/agentdebug/adapters/openai_agents.py +239 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/cli.py +6 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/recovery.py +187 -1
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/LICENSE +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/README.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/00_overview.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/01_literature_survey.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/02_architecture.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/03_taxonomy.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/04_trace_schema.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/05_adapters.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/06_detectors.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/07_attribution.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/08_recovery.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/09_error_database.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/10_taxonomy_induction.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/11_multimodal.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/12_ui_dashboard.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/13_class_design.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/14_api_reference.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/15_roadmap.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/16_governance.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/17_claude_code_design_patterns.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/18_comparison_codex_vs_design.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/19_error_hub.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/20_deep_debug.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/21_integrations.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/22_industry_track_paper_eval_plan.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/ERROR_TAXONOMY.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/OPEN_SOURCE_DEVELOPMENT_PLAN.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/README.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/RESEARCH_SURVEY.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/benchmarks/e2e_v0_2_3.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/benchmarks/e2e_v0_2_4.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/benchmarks/v0_1_smoke.json +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/benchmarks/v0_1_smoke.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/docs/benchmarks/who_when_v0_2_6_leaderboard.md +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/adapters/__init__.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/adapters/base.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/adapters/crewai.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/adapters/langgraph.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/adapters/otel.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/adapters/raw.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/analyzers.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/attribution.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/deep.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/detectors.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/events.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/hub/__init__.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/hub/backend_base.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/hub/backends.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/hub/bundle.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/hub/scrub.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/instrumentation.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/integrations/__init__.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/integrations/claude_skill.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/integrations/openhands.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/judges.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/llm.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/models.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/recorder.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/storage.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/taxonomy.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/traceback.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/src/agentdebug/ui/__init__.py +0 -0
- {agentdebugx-0.2.11 → agentdebugx-0.2.12}/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.
|
|
3
|
+
Version: 0.2.12
|
|
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
|
|
@@ -29,6 +29,7 @@ Provides-Extra: all
|
|
|
29
29
|
Provides-Extra: crewai
|
|
30
30
|
Provides-Extra: hub-hf
|
|
31
31
|
Provides-Extra: langgraph
|
|
32
|
+
Provides-Extra: openai-agents
|
|
32
33
|
Provides-Extra: openhands
|
|
33
34
|
Provides-Extra: otel
|
|
34
35
|
Provides-Extra: ui
|
|
@@ -28,6 +28,8 @@ the forward-looking plan; this doc is the rear-view mirror.
|
|
|
28
28
|
| Recovery | `agentdebug.recovery.CriticRecoverer` + `VerifierSpec` registry | ✅ **new 0.2.3** | 5 family-matched verifier templates; dedup + custom-override |
|
|
29
29
|
| Recovery | `agentdebug.recovery.SelfRefineLoop` | ✅ **new 0.2.9** | scripted-LLM happy + silent-LLM safe + multi-iter; per-finding generator→critic→refiner |
|
|
30
30
|
| Recovery | `agentdebug.recovery.AutoManualRules` (+ `apply()`) | ✅ **new 0.2.9** | no-LLM fallback + LLM rule-distill + idempotent apply + multi-rule append |
|
|
31
|
+
| Recovery | `agentdebug.recovery.SagaRollback` + `Compensator` registry | ✅ **new 0.2.12** | reverse-order suggestion + skip-errored-tools + apply() invokes registered compensation + raises on unknown tool / missing event |
|
|
32
|
+
| Adapter | `agentdebug.adapters.openai_agents.OpenAIAgentsBridge` (TracingProcessor) | ✅ **new 0.2.12** | doctor-status when sdk absent + import-error path + bridge translates function/generation/handoff/unknown spans + context manager attach/detach |
|
|
31
33
|
| DeepDebug | `agentdebug.deep.DeepDebugAnalyzer` | ✅ stable | full loop + silent LLM |
|
|
32
34
|
| Cascade view | `agentdebug.traceback.format_traceback` | ✅ stable | cascade + step-order + ANSI + empty |
|
|
33
35
|
| Detectors | `agentdebug.detectors.RepeatedToolCall / RepeatedState / StepCountLimit` | ✅ **new 0.2.2** | threshold + window + budget |
|
|
@@ -57,7 +59,6 @@ across 32 source files.
|
|
|
57
59
|
| [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 |
|
|
58
60
|
| [07_attribution.md](./07_attribution.md) | `DeltaDebugAttributor` (Zeller) | same replay constraint | v0.3 |
|
|
59
61
|
| [08_recovery.md](./08_recovery.md) | `LangGraphRewind` | depends on LangGraph checkpointer; ships when we have a real LangGraph user | v0.3 |
|
|
60
|
-
| [08_recovery.md](./08_recovery.md) | `SagaRollback` | needs compensation registry on tool definitions; new schema | v0.3 |
|
|
61
62
|
| [08_recovery.md](./08_recovery.md) | `MCTSBranchExploration` (LATS) | heavy; v2 feature | v2.0 |
|
|
62
63
|
| [09_error_database.md](./09_error_database.md) | DuckDB analytical + Parquet archive | optional; Hub bundles already give per-project corpus | v0.3 |
|
|
63
64
|
| [09_error_database.md](./09_error_database.md) | Vector similarity search | needs embedding model + index choice | v0.3 |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "agentdebugx"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.12"
|
|
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"
|
|
@@ -53,6 +53,7 @@ httpx = ">=0.24,<1.0"
|
|
|
53
53
|
[tool.poetry.extras]
|
|
54
54
|
langgraph = ["langchain-core"]
|
|
55
55
|
crewai = ["crewai"]
|
|
56
|
+
openai-agents = ["openai-agents"]
|
|
56
57
|
otel = ["opentelemetry-api", "opentelemetry-sdk"]
|
|
57
58
|
ui = ["fastapi", "uvicorn"]
|
|
58
59
|
hub-hf = ["huggingface_hub"]
|
|
@@ -60,6 +61,7 @@ openhands = ["openhands-ai"]
|
|
|
60
61
|
all = [
|
|
61
62
|
"langchain-core",
|
|
62
63
|
"crewai",
|
|
64
|
+
"openai-agents",
|
|
63
65
|
"opentelemetry-api",
|
|
64
66
|
"opentelemetry-sdk",
|
|
65
67
|
"fastapi",
|
|
@@ -47,10 +47,13 @@ from agentdebug.recorder import AgentDebug, TraceSession
|
|
|
47
47
|
from agentdebug.recovery import (
|
|
48
48
|
DEFAULT_VERIFIERS,
|
|
49
49
|
AutoManualRules,
|
|
50
|
+
CompensationSpec,
|
|
51
|
+
Compensator,
|
|
50
52
|
CriticRecoverer,
|
|
51
53
|
FixProposal,
|
|
52
54
|
Recoverer,
|
|
53
55
|
ReflexionSuggestion,
|
|
56
|
+
SagaRollback,
|
|
54
57
|
SelfRefineLoop,
|
|
55
58
|
VerifierSpec,
|
|
56
59
|
)
|
|
@@ -72,6 +75,8 @@ __all__ = [
|
|
|
72
75
|
'AutoManualRules',
|
|
73
76
|
'BinarySearchAttributor',
|
|
74
77
|
'CascadeFrame',
|
|
78
|
+
'CompensationSpec',
|
|
79
|
+
'Compensator',
|
|
75
80
|
'CounterfactualAttributor',
|
|
76
81
|
'CriticRecoverer',
|
|
77
82
|
'DEFAULT_VERIFIERS',
|
|
@@ -80,6 +85,7 @@ __all__ = [
|
|
|
80
85
|
'EnsembleAttributor',
|
|
81
86
|
'RepeatedStateDetector',
|
|
82
87
|
'RepeatedToolCallDetector',
|
|
88
|
+
'SagaRollback',
|
|
83
89
|
'SBFLAttributor',
|
|
84
90
|
'SelfRefineLoop',
|
|
85
91
|
'StepByStepAttributor',
|
|
@@ -110,4 +116,4 @@ __all__ = [
|
|
|
110
116
|
'get_failure_mode',
|
|
111
117
|
]
|
|
112
118
|
|
|
113
|
-
__version__ = '0.2.
|
|
119
|
+
__version__ = '0.2.12'
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""OpenAI Agents SDK adapter — bridge `agents.tracing` into AgentDebug.
|
|
2
|
+
|
|
3
|
+
The OpenAI Agents SDK (``openai-agents``) emits typed spans through its own
|
|
4
|
+
tracing system (not OpenTelemetry — though there are community OTel bridges).
|
|
5
|
+
The integration point is :class:`agents.tracing.TracingProcessor`: subclass
|
|
6
|
+
it, register via ``add_trace_processor``, and you receive ``on_span_start`` /
|
|
7
|
+
``on_span_end`` for every span the SDK creates (``generation``, ``function``,
|
|
8
|
+
``agent``, ``guardrail``, ``handoff``, audio, …).
|
|
9
|
+
|
|
10
|
+
This module ships ``OpenAIAgentsBridge``, which converts each span end into
|
|
11
|
+
an :class:`agentdebug.models.AgentEvent` and records it on a long-lived
|
|
12
|
+
:class:`AgentTrajectory`. The ``agents`` package is imported lazily, so this
|
|
13
|
+
module is safe to import without the SDK installed.
|
|
14
|
+
|
|
15
|
+
Usage::
|
|
16
|
+
|
|
17
|
+
from agentdebug import AgentDebug, SQLiteTraceStore
|
|
18
|
+
from agentdebug.adapters.openai_agents import OpenAIAgentsBridge
|
|
19
|
+
|
|
20
|
+
debugger = AgentDebug(store=SQLiteTraceStore('.agentdebug/errors.sqlite'))
|
|
21
|
+
trajectory = debugger.start_trace(goal='answer the user', framework='openai-agents')
|
|
22
|
+
|
|
23
|
+
with OpenAIAgentsBridge(debugger, trajectory):
|
|
24
|
+
from agents import Agent, Runner
|
|
25
|
+
agent = Agent(name='Assistant', instructions='Be concise.')
|
|
26
|
+
Runner.run_sync(agent, 'What is 2+2?')
|
|
27
|
+
|
|
28
|
+
debugger.finish_trace(trajectory, success=True)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import logging
|
|
34
|
+
from types import TracebackType
|
|
35
|
+
from typing import Any, List, Literal, Optional, Type
|
|
36
|
+
|
|
37
|
+
from agentdebug.adapters.base import AdapterStatus, FrameworkAdapter
|
|
38
|
+
from agentdebug.models import AgentTrajectory, EventType
|
|
39
|
+
from agentdebug.recorder import AgentDebug
|
|
40
|
+
|
|
41
|
+
LOG = logging.getLogger('agentdebug.adapters.openai_agents')
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _import_agents_tracing() -> Any:
|
|
45
|
+
try:
|
|
46
|
+
from agents import tracing as _tracing
|
|
47
|
+
except ImportError as exc:
|
|
48
|
+
raise ImportError(
|
|
49
|
+
'OpenAIAgentsBridge requires the `openai-agents` package. '
|
|
50
|
+
"Install with `pip install 'agentdebugx[openai-agents]'` or "
|
|
51
|
+
"`pip install openai-agents`."
|
|
52
|
+
) from exc
|
|
53
|
+
return _tracing
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Span-type names the SDK emits; map each to the most representative
|
|
57
|
+
# AgentDebugX EventType. Names not in this map are recorded as OBSERVATION.
|
|
58
|
+
_SPAN_TYPE_TO_EVENT: dict[str, EventType] = {
|
|
59
|
+
'agent': EventType.AGENT_STEP,
|
|
60
|
+
'generation': EventType.LLM_RESPONSE,
|
|
61
|
+
'function': EventType.TOOL_RESULT,
|
|
62
|
+
'tool': EventType.TOOL_RESULT,
|
|
63
|
+
'handoff': EventType.HANDOFF,
|
|
64
|
+
'guardrail': EventType.GUARDRAIL,
|
|
65
|
+
'response': EventType.LLM_RESPONSE,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _span_attr(span: Any, *names: str) -> Any:
|
|
70
|
+
"""Read the first non-None attribute from `span` (or `span.span_data` if
|
|
71
|
+
the SDK wraps its payload there).
|
|
72
|
+
"""
|
|
73
|
+
holders = [span, getattr(span, 'span_data', None)]
|
|
74
|
+
for holder in holders:
|
|
75
|
+
if holder is None:
|
|
76
|
+
continue
|
|
77
|
+
for name in names:
|
|
78
|
+
val = getattr(holder, name, None)
|
|
79
|
+
if val is not None:
|
|
80
|
+
return val
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class OpenAIAgentsBridge:
|
|
85
|
+
"""Bridge ``agents.tracing`` spans into an AgentDebug trajectory.
|
|
86
|
+
|
|
87
|
+
Both a context manager and a plain object with attach/detach. Holds the
|
|
88
|
+
registered ``TracingProcessor`` so the SDK doesn't GC it mid-session.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
framework = 'openai-agents'
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
debugger: AgentDebug,
|
|
96
|
+
trajectory: AgentTrajectory,
|
|
97
|
+
) -> None:
|
|
98
|
+
self.debugger = debugger
|
|
99
|
+
self.trajectory = trajectory
|
|
100
|
+
self._processor: Optional[Any] = None
|
|
101
|
+
self._attached = False
|
|
102
|
+
|
|
103
|
+
def attach(self) -> 'OpenAIAgentsBridge':
|
|
104
|
+
if self._attached:
|
|
105
|
+
return self
|
|
106
|
+
tracing = _import_agents_tracing()
|
|
107
|
+
base_cls = getattr(tracing, 'TracingProcessor', None)
|
|
108
|
+
if base_cls is None:
|
|
109
|
+
raise RuntimeError(
|
|
110
|
+
'agents.tracing.TracingProcessor not found; the installed '
|
|
111
|
+
'`openai-agents` version may be incompatible.'
|
|
112
|
+
)
|
|
113
|
+
bridge = self
|
|
114
|
+
|
|
115
|
+
class _Processor(base_cls): # type: ignore[misc, valid-type]
|
|
116
|
+
def on_trace_start(self_, trace_obj: Any) -> None:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
def on_trace_end(self_, trace_obj: Any) -> None:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
def on_span_start(self_, span: Any) -> None:
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
def on_span_end(self_, span: Any) -> None:
|
|
126
|
+
bridge._record_span(span)
|
|
127
|
+
|
|
128
|
+
def shutdown(self_) -> None:
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
def force_flush(self_) -> None:
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
self._processor = _Processor()
|
|
135
|
+
add = getattr(tracing, 'add_trace_processor', None)
|
|
136
|
+
if not callable(add):
|
|
137
|
+
raise RuntimeError(
|
|
138
|
+
'agents.tracing.add_trace_processor not found; the installed '
|
|
139
|
+
'`openai-agents` version may be incompatible.'
|
|
140
|
+
)
|
|
141
|
+
add(self._processor)
|
|
142
|
+
self._attached = True
|
|
143
|
+
return self
|
|
144
|
+
|
|
145
|
+
def detach(self) -> None:
|
|
146
|
+
# The SDK does not expose a clean unsubscribe in current releases.
|
|
147
|
+
# Dropping the strong reference + having the processor become a no-op
|
|
148
|
+
# is the safest available approach.
|
|
149
|
+
self._processor = None
|
|
150
|
+
self._attached = False
|
|
151
|
+
|
|
152
|
+
# Context-manager sugar
|
|
153
|
+
def __enter__(self) -> 'OpenAIAgentsBridge':
|
|
154
|
+
return self.attach()
|
|
155
|
+
|
|
156
|
+
def __exit__(
|
|
157
|
+
self,
|
|
158
|
+
exc_type: Optional[Type[BaseException]],
|
|
159
|
+
exc_value: Optional[BaseException],
|
|
160
|
+
tb: Optional[TracebackType],
|
|
161
|
+
) -> Literal[False]:
|
|
162
|
+
self.detach()
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
def _record_span(self, span: Any) -> None:
|
|
166
|
+
try:
|
|
167
|
+
span_type = str(_span_attr(span, 'span_type', 'type') or 'unknown')
|
|
168
|
+
event_type = _SPAN_TYPE_TO_EVENT.get(span_type, EventType.OBSERVATION)
|
|
169
|
+
error = _span_attr(span, 'error')
|
|
170
|
+
agent_name = (
|
|
171
|
+
_span_attr(span, 'agent_name', 'name', 'function_name')
|
|
172
|
+
or span_type
|
|
173
|
+
)
|
|
174
|
+
self.debugger.record_event(
|
|
175
|
+
self.trajectory,
|
|
176
|
+
event_type=event_type,
|
|
177
|
+
agent_name=str(agent_name),
|
|
178
|
+
module=self._module_for(event_type),
|
|
179
|
+
input=_summarize(_span_attr(span, 'input', 'arguments', 'prompt')),
|
|
180
|
+
output=_summarize(
|
|
181
|
+
_span_attr(span, 'output', 'response', 'result',
|
|
182
|
+
'completion')
|
|
183
|
+
),
|
|
184
|
+
error=str(error)[:500] if error else None,
|
|
185
|
+
openai_agents_span_type=span_type,
|
|
186
|
+
openai_agents_span_id=str(_span_attr(span, 'span_id') or ''),
|
|
187
|
+
)
|
|
188
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
189
|
+
LOG.warning('OpenAIAgentsBridge failed to record span: %s', exc)
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def _module_for(event_type: EventType) -> str:
|
|
193
|
+
# Pull from agentdebug.taxonomy module families: planning/action/etc.
|
|
194
|
+
if event_type in {EventType.TOOL_CALL, EventType.TOOL_RESULT}:
|
|
195
|
+
return 'action'
|
|
196
|
+
if event_type in {EventType.HANDOFF, EventType.GUARDRAIL}:
|
|
197
|
+
return 'multiagent'
|
|
198
|
+
if event_type in {EventType.LLM_CALL, EventType.LLM_RESPONSE}:
|
|
199
|
+
return 'planning'
|
|
200
|
+
return 'planning'
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _summarize(value: Any) -> Any:
|
|
204
|
+
if value is None:
|
|
205
|
+
return None
|
|
206
|
+
text = str(value)
|
|
207
|
+
return text[:1000] if len(text) > 1000 else text
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class OpenAIAgentsAdapter:
|
|
211
|
+
"""Structural :class:`FrameworkAdapter` for the doctor."""
|
|
212
|
+
|
|
213
|
+
framework = 'openai-agents'
|
|
214
|
+
|
|
215
|
+
def instrument(self, debugger: AgentDebug) -> AdapterStatus: # noqa: D401
|
|
216
|
+
try:
|
|
217
|
+
_import_agents_tracing()
|
|
218
|
+
except ImportError as exc:
|
|
219
|
+
return AdapterStatus(
|
|
220
|
+
framework=self.framework,
|
|
221
|
+
implemented=False,
|
|
222
|
+
notes=str(exc),
|
|
223
|
+
)
|
|
224
|
+
return AdapterStatus(
|
|
225
|
+
framework=self.framework,
|
|
226
|
+
implemented=True,
|
|
227
|
+
notes=(
|
|
228
|
+
'Create a trajectory, then use '
|
|
229
|
+
'`with OpenAIAgentsBridge(debugger, trajectory): Runner.run_sync(...)` '
|
|
230
|
+
'to record every Agent/Runner/Function/Handoff/Guardrail span '
|
|
231
|
+
'into AgentDebug.'
|
|
232
|
+
),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
_: FrameworkAdapter = OpenAIAgentsAdapter() # static structural check
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
__all__ = ['OpenAIAgentsAdapter', 'OpenAIAgentsBridge']
|
|
@@ -453,6 +453,12 @@ def _cmd_doctor() -> int:
|
|
|
453
453
|
statuses.append(CrewAIAdapter().instrument(_dummy_debugger()))
|
|
454
454
|
except Exception as exc: # pragma: no cover - defensive
|
|
455
455
|
statuses.append(_status('crewai', False, str(exc)))
|
|
456
|
+
try:
|
|
457
|
+
from agentdebug.adapters.openai_agents import OpenAIAgentsAdapter
|
|
458
|
+
|
|
459
|
+
statuses.append(OpenAIAgentsAdapter().instrument(_dummy_debugger()))
|
|
460
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
461
|
+
statuses.append(_status('openai-agents', False, str(exc)))
|
|
456
462
|
try:
|
|
457
463
|
from agentdebug.adapters.otel import OTelExportAdapter
|
|
458
464
|
|
|
@@ -13,9 +13,10 @@ artifacts to be surfaced (CLI/UI/PR comment) or fed back into the next run.
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
from dataclasses import dataclass, field
|
|
16
|
-
from typing import List, Optional, Protocol, Tuple
|
|
16
|
+
from typing import Any, Callable, Dict, List, Optional, Protocol, Tuple
|
|
17
17
|
|
|
18
18
|
from agentdebug.models import (
|
|
19
|
+
AgentEvent,
|
|
19
20
|
AgentTrajectory,
|
|
20
21
|
DiagnosticReport,
|
|
21
22
|
FailureFinding,
|
|
@@ -583,13 +584,198 @@ class AutoManualRules:
|
|
|
583
584
|
return rule
|
|
584
585
|
|
|
585
586
|
|
|
587
|
+
@dataclass
|
|
588
|
+
class CompensationSpec:
|
|
589
|
+
"""Pairs a tool name with the function that undoes its side effect.
|
|
590
|
+
|
|
591
|
+
The compensation receives the recorded TOOL_CALL input + TOOL_RESULT
|
|
592
|
+
output and returns a short human-readable summary of what was
|
|
593
|
+
rolled back. The function is the user's responsibility — AgentDebug
|
|
594
|
+
only orchestrates the order and provenance.
|
|
595
|
+
"""
|
|
596
|
+
|
|
597
|
+
tool_name: str
|
|
598
|
+
description: str
|
|
599
|
+
compensate: Callable[[Any, Any], str]
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
class Compensator:
|
|
603
|
+
"""Registry mapping tool_name → :class:`CompensationSpec`.
|
|
604
|
+
|
|
605
|
+
Users register compensations once per tool the agent might call with a
|
|
606
|
+
side effect (email send, DB write, API POST, file create). The Saga
|
|
607
|
+
recoverer looks up compensations by the agent_name on a TOOL_CALL /
|
|
608
|
+
TOOL_RESULT pair.
|
|
609
|
+
"""
|
|
610
|
+
|
|
611
|
+
def __init__(self) -> None:
|
|
612
|
+
self._specs: dict[str, CompensationSpec] = {}
|
|
613
|
+
|
|
614
|
+
def register(
|
|
615
|
+
self,
|
|
616
|
+
tool_name: str,
|
|
617
|
+
compensate: Callable[[Any, Any], str],
|
|
618
|
+
*,
|
|
619
|
+
description: str = '',
|
|
620
|
+
) -> CompensationSpec:
|
|
621
|
+
spec = CompensationSpec(
|
|
622
|
+
tool_name=tool_name,
|
|
623
|
+
description=description or f'compensation for {tool_name}',
|
|
624
|
+
compensate=compensate,
|
|
625
|
+
)
|
|
626
|
+
self._specs[tool_name] = spec
|
|
627
|
+
return spec
|
|
628
|
+
|
|
629
|
+
def get(self, tool_name: str) -> Optional[CompensationSpec]:
|
|
630
|
+
return self._specs.get(tool_name)
|
|
631
|
+
|
|
632
|
+
def registered_tools(self) -> list[str]:
|
|
633
|
+
return sorted(self._specs.keys())
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
@dataclass
|
|
637
|
+
class _ToolPair:
|
|
638
|
+
"""A matched TOOL_CALL / TOOL_RESULT pair from a trajectory."""
|
|
639
|
+
|
|
640
|
+
call_event_id: str
|
|
641
|
+
result_event_id: str
|
|
642
|
+
tool_name: str
|
|
643
|
+
step_index: Optional[int]
|
|
644
|
+
input: Any
|
|
645
|
+
output: Any
|
|
646
|
+
error: Optional[str]
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _pair_tool_calls(trajectory: AgentTrajectory) -> List[_ToolPair]:
|
|
650
|
+
"""Walk the trajectory and emit (call, result) pairs by agent_name +
|
|
651
|
+
proximity. Calls without a following result are dropped (the tool never
|
|
652
|
+
completed, so there's nothing to compensate)."""
|
|
653
|
+
pairs: List[_ToolPair] = []
|
|
654
|
+
pending: Dict[str, AgentEvent] = {}
|
|
655
|
+
for evt in trajectory.events:
|
|
656
|
+
et = getattr(evt.event_type, 'value', evt.event_type)
|
|
657
|
+
if et == 'tool.call':
|
|
658
|
+
pending[evt.agent_name] = evt
|
|
659
|
+
continue
|
|
660
|
+
if et == 'tool.result' and evt.agent_name in pending:
|
|
661
|
+
call = pending.pop(evt.agent_name)
|
|
662
|
+
pairs.append(_ToolPair(
|
|
663
|
+
call_event_id=call.event_id,
|
|
664
|
+
result_event_id=evt.event_id,
|
|
665
|
+
tool_name=evt.agent_name,
|
|
666
|
+
step_index=evt.step_index or call.step_index,
|
|
667
|
+
input=call.input,
|
|
668
|
+
output=evt.output,
|
|
669
|
+
error=evt.error,
|
|
670
|
+
))
|
|
671
|
+
return pairs
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
class SagaRollback:
|
|
675
|
+
"""SagaLLM-style compensating-action layer (arXiv:2503.11951).
|
|
676
|
+
|
|
677
|
+
For every successful TOOL_CALL/TOOL_RESULT pair in the trajectory whose
|
|
678
|
+
tool has a registered compensation, emit a :class:`FixProposal`. Tool
|
|
679
|
+
results with an error are skipped — there's nothing to undo.
|
|
680
|
+
|
|
681
|
+
Proposals are returned in REVERSE EXECUTION ORDER so applying them
|
|
682
|
+
walks the side effects back to a clean state. Each ``apply()`` invokes
|
|
683
|
+
the user-provided ``compensate(input, output)`` and returns the
|
|
684
|
+
function's summary string.
|
|
685
|
+
|
|
686
|
+
Unlike the suggest-only Reflexion / CRITIC / Self-Refine / AutoManual
|
|
687
|
+
recoverers, ``apply()`` here HAS REAL SIDE EFFECTS — it runs the
|
|
688
|
+
user's compensation code. Marked ``requires_human_approval=True`` by
|
|
689
|
+
default so the UI surfaces every rollback for confirmation.
|
|
690
|
+
"""
|
|
691
|
+
|
|
692
|
+
id = 'saga_rollback'
|
|
693
|
+
|
|
694
|
+
def __init__(
|
|
695
|
+
self,
|
|
696
|
+
compensator: Compensator,
|
|
697
|
+
*,
|
|
698
|
+
require_approval: bool = True,
|
|
699
|
+
) -> None:
|
|
700
|
+
self.compensator = compensator
|
|
701
|
+
self.require_approval = require_approval
|
|
702
|
+
|
|
703
|
+
def suggest(
|
|
704
|
+
self,
|
|
705
|
+
trajectory: AgentTrajectory,
|
|
706
|
+
report: DiagnosticReport,
|
|
707
|
+
) -> List[FixProposal]:
|
|
708
|
+
# Recovery suggestions are independent of the report — saga always
|
|
709
|
+
# looks at the trajectory's actual side-effect surface.
|
|
710
|
+
del report
|
|
711
|
+
pairs = _pair_tool_calls(trajectory)
|
|
712
|
+
# Reverse order so consumers can apply in sequence.
|
|
713
|
+
proposals: List[FixProposal] = []
|
|
714
|
+
for pair in reversed(pairs):
|
|
715
|
+
if pair.error:
|
|
716
|
+
continue # tool failed; no side effect to compensate
|
|
717
|
+
spec = self.compensator.get(pair.tool_name)
|
|
718
|
+
if spec is None:
|
|
719
|
+
continue # no registered compensation; can't auto-rollback
|
|
720
|
+
proposals.append(FixProposal(
|
|
721
|
+
proposal_id=new_id('fix'),
|
|
722
|
+
recoverer_id=self.id,
|
|
723
|
+
target_event_id=pair.result_event_id,
|
|
724
|
+
summary=(
|
|
725
|
+
f'Compensate {pair.tool_name} call '
|
|
726
|
+
f'at step {pair.step_index}'
|
|
727
|
+
),
|
|
728
|
+
rationale=(
|
|
729
|
+
f'SagaRollback (arXiv:2503.11951): undo the recorded '
|
|
730
|
+
f'side effect of {pair.tool_name}. '
|
|
731
|
+
f'Compensation: {spec.description}'
|
|
732
|
+
),
|
|
733
|
+
confidence=0.85,
|
|
734
|
+
suggestion_text=(
|
|
735
|
+
f'apply() will call the registered compensation for '
|
|
736
|
+
f'`{pair.tool_name}` with the recorded input/output, '
|
|
737
|
+
f'reversing the side effect.'
|
|
738
|
+
),
|
|
739
|
+
side_effects=[f'tool.compensate:{pair.tool_name}'],
|
|
740
|
+
requires_human_approval=self.require_approval,
|
|
741
|
+
))
|
|
742
|
+
return proposals
|
|
743
|
+
|
|
744
|
+
def apply(self, proposal: FixProposal, *, trajectory: AgentTrajectory) -> str:
|
|
745
|
+
"""Run the registered compensation. Returns the user's summary string.
|
|
746
|
+
|
|
747
|
+
``trajectory`` is required so apply can recover the recorded
|
|
748
|
+
input/output from the target event without the caller having to
|
|
749
|
+
carry them through.
|
|
750
|
+
"""
|
|
751
|
+
pairs_by_result = {
|
|
752
|
+
p.result_event_id: p for p in _pair_tool_calls(trajectory)
|
|
753
|
+
}
|
|
754
|
+
pair = pairs_by_result.get(proposal.target_event_id or '')
|
|
755
|
+
if pair is None:
|
|
756
|
+
raise ValueError(
|
|
757
|
+
f'proposal target_event_id={proposal.target_event_id!r} not '
|
|
758
|
+
f'found among tool-result events in the trajectory'
|
|
759
|
+
)
|
|
760
|
+
spec = self.compensator.get(pair.tool_name)
|
|
761
|
+
if spec is None:
|
|
762
|
+
raise ValueError(
|
|
763
|
+
f'no registered compensation for tool {pair.tool_name!r}; '
|
|
764
|
+
f'register it on the Compensator before applying'
|
|
765
|
+
)
|
|
766
|
+
return spec.compensate(pair.input, pair.output)
|
|
767
|
+
|
|
768
|
+
|
|
586
769
|
__all__ = [
|
|
587
770
|
'AutoManualRules',
|
|
771
|
+
'CompensationSpec',
|
|
772
|
+
'Compensator',
|
|
588
773
|
'CriticRecoverer',
|
|
589
774
|
'DEFAULT_VERIFIERS',
|
|
590
775
|
'FixProposal',
|
|
591
776
|
'Recoverer',
|
|
592
777
|
'ReflexionSuggestion',
|
|
778
|
+
'SagaRollback',
|
|
593
779
|
'SelfRefineLoop',
|
|
594
780
|
'VerifierSpec',
|
|
595
781
|
]
|
|
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
|