agentdebugx 0.2.10__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.10 → agentdebugx-0.2.12}/PKG-INFO +2 -1
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/23_status_v0_2.md +5 -3
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/pyproject.toml +3 -1
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/__init__.py +9 -1
- agentdebugx-0.2.12/src/agentdebug/adapters/openai_agents.py +239 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/cli.py +6 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/detectors.py +95 -1
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/llm.py +57 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/recovery.py +187 -1
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/LICENSE +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/README.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/00_overview.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/01_literature_survey.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/02_architecture.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/03_taxonomy.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/04_trace_schema.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/05_adapters.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/06_detectors.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/07_attribution.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/08_recovery.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/09_error_database.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/10_taxonomy_induction.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/11_multimodal.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/12_ui_dashboard.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/13_class_design.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/14_api_reference.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/15_roadmap.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/16_governance.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/17_claude_code_design_patterns.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/18_comparison_codex_vs_design.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/19_error_hub.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/20_deep_debug.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/21_integrations.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/22_industry_track_paper_eval_plan.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/ERROR_TAXONOMY.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/OPEN_SOURCE_DEVELOPMENT_PLAN.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/README.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/RESEARCH_SURVEY.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/benchmarks/e2e_v0_2_3.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/benchmarks/e2e_v0_2_4.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/benchmarks/v0_1_smoke.json +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/benchmarks/v0_1_smoke.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/docs/benchmarks/who_when_v0_2_6_leaderboard.md +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/adapters/__init__.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/adapters/base.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/adapters/crewai.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/adapters/langgraph.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/adapters/otel.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/adapters/raw.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/analyzers.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/attribution.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/deep.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/events.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/hub/__init__.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/hub/backend_base.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/hub/backends.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/hub/bundle.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/hub/scrub.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/instrumentation.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/integrations/__init__.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/integrations/claude_skill.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/integrations/openhands.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/judges.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/models.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/recorder.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/storage.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/taxonomy.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/traceback.py +0 -0
- {agentdebugx-0.2.10 → agentdebugx-0.2.12}/src/agentdebug/ui/__init__.py +0 -0
- {agentdebugx-0.2.10 → 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,9 +28,13 @@ 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 |
|
|
36
|
+
| Detectors | `agentdebug.detectors.TopicDriftDetector` (embedding cosine vs goal) | ✅ **new 0.2.11** | stub-embedder ranking + no-goal short-circuit + embedder-raises safe + threshold boundary |
|
|
37
|
+
| LLM client | `agentdebug.llm.OpenAICompatClient.embed()` (+ `EmbeddingClient` Protocol) | ✅ **new 0.2.11** | mocked-httpx POST /v1/embeddings + empty-input short-circuit |
|
|
34
38
|
| Hub bundle | `agentdebug.hub.Bundle / pack_bundle / unpack_bundle` | ✅ stable | round-trip |
|
|
35
39
|
| Hub scrubber | `agentdebug.hub.Scrubber` | ✅ stable | 12 redactions + idempotent |
|
|
36
40
|
| Hub backends | `LocalHubBackend`, `GitHubBackend`, `HuggingFaceBackend` | ✅ stable | local-bare-git + local |
|
|
@@ -49,14 +53,12 @@ across 32 source files.
|
|
|
49
53
|
|
|
50
54
|
| Doc | Component | Why deferred | Realistic ship |
|
|
51
55
|
|---|---|---|---|
|
|
52
|
-
| [06_detectors.md](./06_detectors.md) | `trajectory_perplexity` (TrajAD) | needs token-level LM perplexity API
|
|
53
|
-
| [06_detectors.md](./06_detectors.md) | `topic_drift` (embedding cosine) | needs embedding client; consider reusing `OpenAICompatClient` `/embeddings` | v0.3 |
|
|
56
|
+
| [06_detectors.md](./06_detectors.md) | `trajectory_perplexity` (TrajAD) | needs token-level LM perplexity API; v0.3 |
|
|
54
57
|
| [06_detectors.md](./06_detectors.md) | LTL spec monitors | requires user-supplied spec or LLM-synthesized monitors; gated on RV research | v1.2 |
|
|
55
58
|
| [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 |
|
|
56
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 |
|
|
57
60
|
| [07_attribution.md](./07_attribution.md) | `DeltaDebugAttributor` (Zeller) | same replay constraint | v0.3 |
|
|
58
61
|
| [08_recovery.md](./08_recovery.md) | `LangGraphRewind` | depends on LangGraph checkpointer; ships when we have a real LangGraph user | v0.3 |
|
|
59
|
-
| [08_recovery.md](./08_recovery.md) | `SagaRollback` | needs compensation registry on tool definitions; new schema | v0.3 |
|
|
60
62
|
| [08_recovery.md](./08_recovery.md) | `MCTSBranchExploration` (LATS) | heavy; v2 feature | v2.0 |
|
|
61
63
|
| [09_error_database.md](./09_error_database.md) | DuckDB analytical + Parquet archive | optional; Hub bundles already give per-project corpus | v0.3 |
|
|
62
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",
|
|
@@ -28,6 +28,7 @@ from agentdebug.detectors import (
|
|
|
28
28
|
RepeatedStateDetector,
|
|
29
29
|
RepeatedToolCallDetector,
|
|
30
30
|
StepCountLimitDetector,
|
|
31
|
+
TopicDriftDetector,
|
|
31
32
|
default_detectors,
|
|
32
33
|
run_detectors,
|
|
33
34
|
)
|
|
@@ -46,10 +47,13 @@ from agentdebug.recorder import AgentDebug, TraceSession
|
|
|
46
47
|
from agentdebug.recovery import (
|
|
47
48
|
DEFAULT_VERIFIERS,
|
|
48
49
|
AutoManualRules,
|
|
50
|
+
CompensationSpec,
|
|
51
|
+
Compensator,
|
|
49
52
|
CriticRecoverer,
|
|
50
53
|
FixProposal,
|
|
51
54
|
Recoverer,
|
|
52
55
|
ReflexionSuggestion,
|
|
56
|
+
SagaRollback,
|
|
53
57
|
SelfRefineLoop,
|
|
54
58
|
VerifierSpec,
|
|
55
59
|
)
|
|
@@ -71,6 +75,8 @@ __all__ = [
|
|
|
71
75
|
'AutoManualRules',
|
|
72
76
|
'BinarySearchAttributor',
|
|
73
77
|
'CascadeFrame',
|
|
78
|
+
'CompensationSpec',
|
|
79
|
+
'Compensator',
|
|
74
80
|
'CounterfactualAttributor',
|
|
75
81
|
'CriticRecoverer',
|
|
76
82
|
'DEFAULT_VERIFIERS',
|
|
@@ -79,9 +85,11 @@ __all__ = [
|
|
|
79
85
|
'EnsembleAttributor',
|
|
80
86
|
'RepeatedStateDetector',
|
|
81
87
|
'RepeatedToolCallDetector',
|
|
88
|
+
'SagaRollback',
|
|
82
89
|
'SBFLAttributor',
|
|
83
90
|
'SelfRefineLoop',
|
|
84
91
|
'StepByStepAttributor',
|
|
92
|
+
'TopicDriftDetector',
|
|
85
93
|
'StepCountLimitDetector',
|
|
86
94
|
'VerifierSpec',
|
|
87
95
|
'build_cascade',
|
|
@@ -108,4 +116,4 @@ __all__ = [
|
|
|
108
116
|
'get_failure_mode',
|
|
109
117
|
]
|
|
110
118
|
|
|
111
|
-
__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
|
|
|
@@ -15,7 +15,7 @@ from __future__ import annotations
|
|
|
15
15
|
|
|
16
16
|
import logging
|
|
17
17
|
from dataclasses import dataclass
|
|
18
|
-
from typing import List, Optional, Protocol
|
|
18
|
+
from typing import Any, List, Optional, Protocol
|
|
19
19
|
|
|
20
20
|
from agentdebug.models import (
|
|
21
21
|
AgentEvent,
|
|
@@ -273,12 +273,106 @@ def _suggestion(mode: FailureMode) -> Optional[str]:
|
|
|
273
273
|
return None
|
|
274
274
|
|
|
275
275
|
|
|
276
|
+
class TopicDriftDetector:
|
|
277
|
+
"""Embedding-based anomaly detector for goal drift.
|
|
278
|
+
|
|
279
|
+
Embed the trajectory's goal once; embed each user-facing event payload
|
|
280
|
+
(LLM_RESPONSE / PLAN / OBSERVATION outputs); flag any step whose cosine
|
|
281
|
+
similarity with the goal drops below ``threshold``.
|
|
282
|
+
|
|
283
|
+
Maps to ``FM-2.3 task_derailment`` (MAST) / ``planning.inefficient_plan``.
|
|
284
|
+
Closes the anomaly family from doc 06 alongside the existing
|
|
285
|
+
RepeatedToolCallDetector / RepeatedStateDetector.
|
|
286
|
+
|
|
287
|
+
Skipped silently if the embedding client raises or returns no vectors —
|
|
288
|
+
the rest of the detector pipeline is unaffected.
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
id = 'topic_drift'
|
|
292
|
+
|
|
293
|
+
def __init__(
|
|
294
|
+
self,
|
|
295
|
+
embedding_client: Any,
|
|
296
|
+
*,
|
|
297
|
+
threshold: float = 0.35,
|
|
298
|
+
max_events: int = 60,
|
|
299
|
+
) -> None:
|
|
300
|
+
# embedding_client is duck-typed to EmbeddingClient to avoid an
|
|
301
|
+
# import cycle (detectors.py is imported from agentdebug/__init__.py).
|
|
302
|
+
self.embedding_client = embedding_client
|
|
303
|
+
self.threshold = threshold
|
|
304
|
+
self.max_events = max_events
|
|
305
|
+
|
|
306
|
+
def detect(self, trajectory: AgentTrajectory) -> List[FailureFinding]:
|
|
307
|
+
if not trajectory.goal:
|
|
308
|
+
return []
|
|
309
|
+
contextful = [
|
|
310
|
+
e for e in trajectory.events
|
|
311
|
+
if e.event_type in {
|
|
312
|
+
EventType.LLM_RESPONSE, EventType.PLAN, EventType.OBSERVATION,
|
|
313
|
+
EventType.LLM_RESPONSE.value, EventType.PLAN.value,
|
|
314
|
+
EventType.OBSERVATION.value,
|
|
315
|
+
}
|
|
316
|
+
and e.output is not None and str(e.output).strip()
|
|
317
|
+
]
|
|
318
|
+
if not contextful:
|
|
319
|
+
return []
|
|
320
|
+
contextful = contextful[-self.max_events:]
|
|
321
|
+
texts = [trajectory.goal] + [str(e.output)[:1000] for e in contextful]
|
|
322
|
+
try:
|
|
323
|
+
vectors = self.embedding_client.embed(texts)
|
|
324
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
325
|
+
LOG.warning('topic_drift detector embed() failed: %s', exc)
|
|
326
|
+
return []
|
|
327
|
+
if not vectors or len(vectors) != len(texts):
|
|
328
|
+
return []
|
|
329
|
+
goal_vec = vectors[0]
|
|
330
|
+
findings: List[FailureFinding] = []
|
|
331
|
+
mode = SEED_FAILURE_MODES['planning.inefficient_plan']
|
|
332
|
+
for evt, evt_vec in zip(contextful, vectors[1:]):
|
|
333
|
+
sim = _cosine(goal_vec, evt_vec)
|
|
334
|
+
if sim >= self.threshold:
|
|
335
|
+
continue
|
|
336
|
+
findings.append(FailureFinding(
|
|
337
|
+
finding_id=new_id('finding'),
|
|
338
|
+
failure_mode=mode,
|
|
339
|
+
event_id=evt.event_id,
|
|
340
|
+
agent_name=evt.agent_name,
|
|
341
|
+
step_index=evt.step_index,
|
|
342
|
+
# Confidence proportional to how far below threshold we drifted.
|
|
343
|
+
confidence=min(0.95, 0.4 + (self.threshold - sim)),
|
|
344
|
+
evidence=[
|
|
345
|
+
f'goal/output cosine={sim:.3f} < threshold={self.threshold:.2f}',
|
|
346
|
+
],
|
|
347
|
+
suggestion=_suggestion(mode),
|
|
348
|
+
metadata={
|
|
349
|
+
'source': self.id,
|
|
350
|
+
'cosine_to_goal': round(sim, 4),
|
|
351
|
+
'threshold': self.threshold,
|
|
352
|
+
},
|
|
353
|
+
))
|
|
354
|
+
return findings
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _cosine(a: List[float], b: List[float]) -> float:
|
|
358
|
+
import math
|
|
359
|
+
if not a or not b or len(a) != len(b):
|
|
360
|
+
return 0.0
|
|
361
|
+
dot = sum(x * y for x, y in zip(a, b))
|
|
362
|
+
na = math.sqrt(sum(x * x for x in a))
|
|
363
|
+
nb = math.sqrt(sum(y * y for y in b))
|
|
364
|
+
if na == 0 or nb == 0:
|
|
365
|
+
return 0.0
|
|
366
|
+
return dot / (na * nb)
|
|
367
|
+
|
|
368
|
+
|
|
276
369
|
__all__ = [
|
|
277
370
|
'Detector',
|
|
278
371
|
'DetectorConfig',
|
|
279
372
|
'RepeatedStateDetector',
|
|
280
373
|
'RepeatedToolCallDetector',
|
|
281
374
|
'StepCountLimitDetector',
|
|
375
|
+
'TopicDriftDetector',
|
|
282
376
|
'default_detectors',
|
|
283
377
|
'run_detectors',
|
|
284
378
|
]
|
|
@@ -44,6 +44,25 @@ class LLMClient(Protocol):
|
|
|
44
44
|
...
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
class EmbeddingClient(Protocol):
|
|
48
|
+
"""Subprotocol for clients that also expose ``/v1/embeddings``.
|
|
49
|
+
|
|
50
|
+
Kept separate from :class:`LLMClient` so detectors can declare a
|
|
51
|
+
narrower dependency and tests can stub embeddings without faking
|
|
52
|
+
a chat client.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
embedding_model: str
|
|
56
|
+
|
|
57
|
+
def embed(
|
|
58
|
+
self,
|
|
59
|
+
texts: List[str],
|
|
60
|
+
*,
|
|
61
|
+
timeout: float = 60.0,
|
|
62
|
+
) -> List[List[float]]:
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
|
|
47
66
|
class OpenAICompatClient:
|
|
48
67
|
"""OpenAI-compatible chat completions client.
|
|
49
68
|
|
|
@@ -63,12 +82,16 @@ class OpenAICompatClient:
|
|
|
63
82
|
base_url: str,
|
|
64
83
|
api_key: str,
|
|
65
84
|
model: str,
|
|
85
|
+
embedding_model: str = 'text-embedding-3-small',
|
|
66
86
|
default_max_tokens: int = 2048,
|
|
67
87
|
timeout: float = 60.0,
|
|
68
88
|
) -> None:
|
|
69
89
|
self.base_url = base_url.rstrip('/')
|
|
70
90
|
self.api_key = api_key
|
|
71
91
|
self.model = model
|
|
92
|
+
# Embeddings hit a separate endpoint with a separate model id; default
|
|
93
|
+
# to OpenAI's small embedding model since the gateway is OpenAI-compat.
|
|
94
|
+
self.embedding_model = embedding_model
|
|
72
95
|
self.default_max_tokens = default_max_tokens
|
|
73
96
|
self.timeout = timeout
|
|
74
97
|
|
|
@@ -132,6 +155,40 @@ class OpenAICompatClient:
|
|
|
132
155
|
return CompletionResult(text=text, raw=data)
|
|
133
156
|
|
|
134
157
|
|
|
158
|
+
def embed(
|
|
159
|
+
self,
|
|
160
|
+
texts: List[str],
|
|
161
|
+
*,
|
|
162
|
+
timeout: Optional[float] = None,
|
|
163
|
+
) -> List[List[float]]:
|
|
164
|
+
"""OpenAI-compatible ``/v1/embeddings`` POST.
|
|
165
|
+
|
|
166
|
+
Returns a list of vectors (one per input text) in the same order.
|
|
167
|
+
Empty ``texts`` short-circuits to ``[]`` to save a network round-trip.
|
|
168
|
+
"""
|
|
169
|
+
if not texts:
|
|
170
|
+
return []
|
|
171
|
+
url = f'{self.base_url}/embeddings'
|
|
172
|
+
headers = {
|
|
173
|
+
'Authorization': f'Bearer {self.api_key}',
|
|
174
|
+
'Content-Type': 'application/json',
|
|
175
|
+
}
|
|
176
|
+
body = {'model': self.embedding_model, 'input': list(texts)}
|
|
177
|
+
resp = httpx.post(
|
|
178
|
+
url, headers=headers, json=body, timeout=timeout or self.timeout
|
|
179
|
+
)
|
|
180
|
+
resp.raise_for_status()
|
|
181
|
+
data = resp.json()
|
|
182
|
+
rows = data.get('data') or []
|
|
183
|
+
out: List[List[float]] = []
|
|
184
|
+
for row in rows:
|
|
185
|
+
vec = row.get('embedding')
|
|
186
|
+
if not isinstance(vec, list):
|
|
187
|
+
continue
|
|
188
|
+
out.append([float(v) for v in vec])
|
|
189
|
+
return out
|
|
190
|
+
|
|
191
|
+
|
|
135
192
|
def extract_json_block(text: str) -> Optional[Dict[str, Any]]:
|
|
136
193
|
"""Extract the first top-level JSON object from a possibly-fenced response."""
|
|
137
194
|
if not text:
|
|
@@ -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
|