triage-agent 0.3.0__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.
- triage_agent-0.3.0/.gitignore +36 -0
- triage_agent-0.3.0/PKG-INFO +555 -0
- triage_agent-0.3.0/README.md +503 -0
- triage_agent-0.3.0/examples/anthropic_agent.py +172 -0
- triage_agent-0.3.0/examples/durable_checkpoints.py +160 -0
- triage_agent-0.3.0/examples/groq_agent.py +188 -0
- triage_agent-0.3.0/examples/huggingface_agent.py +196 -0
- triage_agent-0.3.0/examples/langgraph_agent.py +94 -0
- triage_agent-0.3.0/examples/llm_classifier.py +114 -0
- triage_agent-0.3.0/examples/ollama_agent.py +173 -0
- triage_agent-0.3.0/examples/raw_openai.py +166 -0
- triage_agent-0.3.0/pyproject.toml +77 -0
- triage_agent-0.3.0/tests/__init__.py +0 -0
- triage_agent-0.3.0/tests/test_adapter_crewai.py +146 -0
- triage_agent-0.3.0/tests/test_adapter_langchain.py +175 -0
- triage_agent-0.3.0/tests/test_adapter_langgraph.py +188 -0
- triage_agent-0.3.0/tests/test_adapter_openai_agents.py +189 -0
- triage_agent-0.3.0/tests/test_agent.py +607 -0
- triage_agent-0.3.0/tests/test_checkpoint.py +89 -0
- triage_agent-0.3.0/tests/test_checkpoint_redis.py +140 -0
- triage_agent-0.3.0/tests/test_checkpoint_sqlite.py +127 -0
- triage_agent-0.3.0/tests/test_classifier_hybrid.py +177 -0
- triage_agent-0.3.0/tests/test_classifier_llm.py +303 -0
- triage_agent-0.3.0/tests/test_classifier_rules.py +184 -0
- triage_agent-0.3.0/tests/test_policy.py +154 -0
- triage_agent-0.3.0/tests/test_taxonomy.py +111 -0
- triage_agent-0.3.0/triage/__init__.py +50 -0
- triage_agent-0.3.0/triage/adapters/__init__.py +5 -0
- triage_agent-0.3.0/triage/adapters/crewai.py +64 -0
- triage_agent-0.3.0/triage/adapters/langchain.py +89 -0
- triage_agent-0.3.0/triage/adapters/langgraph.py +71 -0
- triage_agent-0.3.0/triage/adapters/openai_agents.py +69 -0
- triage_agent-0.3.0/triage/agent.py +239 -0
- triage_agent-0.3.0/triage/checkpoint/__init__.py +11 -0
- triage_agent-0.3.0/triage/checkpoint/base.py +92 -0
- triage_agent-0.3.0/triage/checkpoint/memory.py +37 -0
- triage_agent-0.3.0/triage/checkpoint/redis.py +81 -0
- triage_agent-0.3.0/triage/checkpoint/sqlite.py +100 -0
- triage_agent-0.3.0/triage/classifier/__init__.py +16 -0
- triage_agent-0.3.0/triage/classifier/base.py +22 -0
- triage_agent-0.3.0/triage/classifier/hybrid.py +46 -0
- triage_agent-0.3.0/triage/classifier/llm.py +159 -0
- triage_agent-0.3.0/triage/classifier/rules.py +75 -0
- triage_agent-0.3.0/triage/policy.py +151 -0
- triage_agent-0.3.0/triage/strategies/__init__.py +13 -0
- triage_agent-0.3.0/triage/strategies/replan.py +28 -0
- triage_agent-0.3.0/triage/strategies/retry.py +32 -0
- triage_agent-0.3.0/triage/strategies/rollback.py +19 -0
- triage_agent-0.3.0/triage/taxonomy.py +100 -0
- triage_agent-0.3.0/triage/trajectory.py +54 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Claude Code — local dev only
|
|
2
|
+
.claude/
|
|
3
|
+
CLAUDE.md
|
|
4
|
+
PLAN.md
|
|
5
|
+
SPEC.md
|
|
6
|
+
|
|
7
|
+
# Python
|
|
8
|
+
__pycache__/
|
|
9
|
+
*.py[cod]
|
|
10
|
+
*.pyo
|
|
11
|
+
.venv/
|
|
12
|
+
venv/
|
|
13
|
+
*.egg-info/
|
|
14
|
+
dist/
|
|
15
|
+
build/
|
|
16
|
+
.eggs/
|
|
17
|
+
|
|
18
|
+
# Testing
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
.coverage
|
|
21
|
+
htmlcov/
|
|
22
|
+
|
|
23
|
+
# Type checking
|
|
24
|
+
.mypy_cache/
|
|
25
|
+
|
|
26
|
+
# Editors
|
|
27
|
+
.vscode/
|
|
28
|
+
.idea/
|
|
29
|
+
*.swp
|
|
30
|
+
*.swo
|
|
31
|
+
|
|
32
|
+
# macOS
|
|
33
|
+
.DS_Store
|
|
34
|
+
|
|
35
|
+
# Environment
|
|
36
|
+
.env
|
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: triage-agent
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Classify why your agent failed. Recover intelligently.
|
|
5
|
+
Project-URL: Homepage, https://github.com/mattekudacy/triage
|
|
6
|
+
Project-URL: Issues, https://github.com/mattekudacy/triage/issues
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: agents,ai,llm,observability,recovery,reliability
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: anyio>=4.0
|
|
18
|
+
Requires-Dist: pydantic>=2.0
|
|
19
|
+
Requires-Dist: typing-extensions>=4.8; python_version < '3.11'
|
|
20
|
+
Provides-Extra: anthropic
|
|
21
|
+
Requires-Dist: anthropic>=0.25; extra == 'anthropic'
|
|
22
|
+
Provides-Extra: crewai
|
|
23
|
+
Requires-Dist: crewai>=0.1; extra == 'crewai'
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: aiosqlite>=0.19; extra == 'dev'
|
|
26
|
+
Requires-Dist: anthropic>=0.25; extra == 'dev'
|
|
27
|
+
Requires-Dist: fakeredis>=2.20; extra == 'dev'
|
|
28
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
29
|
+
Requires-Dist: openai>=1.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.3; extra == 'dev'
|
|
33
|
+
Provides-Extra: langchain
|
|
34
|
+
Requires-Dist: langchain-core>=0.1; extra == 'langchain'
|
|
35
|
+
Requires-Dist: langchain>=0.1; extra == 'langchain'
|
|
36
|
+
Provides-Extra: langfuse
|
|
37
|
+
Requires-Dist: langfuse>=2.0; extra == 'langfuse'
|
|
38
|
+
Provides-Extra: langgraph
|
|
39
|
+
Requires-Dist: langgraph>=0.2; extra == 'langgraph'
|
|
40
|
+
Provides-Extra: openai
|
|
41
|
+
Requires-Dist: openai>=1.0; extra == 'openai'
|
|
42
|
+
Provides-Extra: openai-agents
|
|
43
|
+
Requires-Dist: openai-agents>=0.0.3; extra == 'openai-agents'
|
|
44
|
+
Provides-Extra: otel
|
|
45
|
+
Requires-Dist: opentelemetry-api>=1.20; extra == 'otel'
|
|
46
|
+
Requires-Dist: opentelemetry-sdk>=1.20; extra == 'otel'
|
|
47
|
+
Provides-Extra: redis
|
|
48
|
+
Requires-Dist: redis[asyncio]>=5.0; extra == 'redis'
|
|
49
|
+
Provides-Extra: sqlite
|
|
50
|
+
Requires-Dist: aiosqlite>=0.19; extra == 'sqlite'
|
|
51
|
+
Description-Content-Type: text/markdown
|
|
52
|
+
|
|
53
|
+
# triage
|
|
54
|
+
|
|
55
|
+
**Classify why your agent failed. Recover intelligently.**
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
pip install triage-agent
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
[](https://www.python.org/)
|
|
62
|
+
[](LICENSE)
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## The problem
|
|
67
|
+
|
|
68
|
+
Current agent frameworks know *that* your agent failed. They don't know *why* — and without knowing why, every failure gets the same blunt response: retry from scratch or give up.
|
|
69
|
+
|
|
70
|
+
`triage` adds a classification-and-routing layer between the failure and the recovery:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
agent fails → classify failure type → route to matching strategy → recover
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
It works with any async agent callable — OpenAI, LangGraph, CrewAI, raw LLM loops — without requiring you to change your framework.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Installation
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Core only
|
|
84
|
+
pip install triage-agent
|
|
85
|
+
|
|
86
|
+
# With framework adapters
|
|
87
|
+
pip install "triage-agent[langgraph]"
|
|
88
|
+
pip install "triage-agent[crewai]"
|
|
89
|
+
pip install "triage-agent[openai-agents]"
|
|
90
|
+
pip install "triage-agent[langchain]"
|
|
91
|
+
|
|
92
|
+
# With LLM-based classifier
|
|
93
|
+
pip install "triage-agent[anthropic]"
|
|
94
|
+
|
|
95
|
+
# With durable checkpoint storage
|
|
96
|
+
pip install "triage-agent[sqlite]"
|
|
97
|
+
pip install "triage-agent[redis]"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Python 3.10+ required. Core dependencies: `anyio>=4.0`, `pydantic>=2.0`.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Quick start
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
import triage
|
|
108
|
+
from triage.strategies.retry import retry_with_tool_manifest, backoff_and_retry
|
|
109
|
+
from triage.strategies.replan import replan
|
|
110
|
+
from triage.strategies.rollback import rollback_to_checkpoint
|
|
111
|
+
from triage.taxonomy import Step
|
|
112
|
+
|
|
113
|
+
# 1. Define your agent — it receives record_step and update_state callbacks
|
|
114
|
+
async def my_agent(task: str, *, record_step, update_state, _triage_hint=None, **kwargs):
|
|
115
|
+
# ... your agent logic ...
|
|
116
|
+
data = fetch_data(task)
|
|
117
|
+
record_step(Step(index=0, action="called search", tool_called="search",
|
|
118
|
+
tool_input={"q": task}, tool_output=data))
|
|
119
|
+
update_state({"data": data}) # persisted into checkpoints; restored on rollback
|
|
120
|
+
return "done"
|
|
121
|
+
|
|
122
|
+
# 2. Declare a recovery policy
|
|
123
|
+
policy = triage.FailurePolicy(
|
|
124
|
+
WRONG_TOOL_CALLED = retry_with_tool_manifest(max_attempts=3),
|
|
125
|
+
EXTERNAL_FAULT = backoff_and_retry(max_attempts=5),
|
|
126
|
+
LOOP_DETECTED = replan(hint="Try a different approach."),
|
|
127
|
+
HALLUCINATED_STATE = rollback_to_checkpoint(),
|
|
128
|
+
default = triage.FailurePolicy.escalate_by_default(),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# 3. Wrap and run
|
|
132
|
+
agent = triage.Agent(my_agent, policy=policy)
|
|
133
|
+
result = await agent.run("search for recent AI papers")
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Or use the decorator form:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
@triage.agent(policy=policy)
|
|
140
|
+
async def my_agent(task: str, *, record_step, **kwargs):
|
|
141
|
+
...
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Framework adapters
|
|
147
|
+
|
|
148
|
+
Drop-in wrappers let you add triage to an existing agent without changing its internals.
|
|
149
|
+
|
|
150
|
+
### LangGraph
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from triage.adapters.langgraph import wrap_langgraph
|
|
154
|
+
|
|
155
|
+
agent = wrap_langgraph(compiled_graph, policy=policy)
|
|
156
|
+
result = await agent.run("your task")
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Streams events via `graph.astream_events(..., version="v2")` to capture tool calls and LLM turns.
|
|
160
|
+
|
|
161
|
+
### CrewAI
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from triage.adapters.crewai import wrap_crewai
|
|
165
|
+
|
|
166
|
+
agent = wrap_crewai(crew, policy=policy)
|
|
167
|
+
result = await agent.run("your task")
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Patches `crew.step_callback` for each run (original restored in `finally`).
|
|
171
|
+
|
|
172
|
+
### OpenAI Agents SDK
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from triage.adapters.openai_agents import wrap_openai_agents
|
|
176
|
+
|
|
177
|
+
agent = wrap_openai_agents(sdk_agent, policy=policy)
|
|
178
|
+
result = await agent.run("your task")
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Uses `Runner.run_streamed` and iterates `stream_events()`.
|
|
182
|
+
|
|
183
|
+
### LangChain
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from triage.adapters.langchain import wrap_langchain
|
|
187
|
+
|
|
188
|
+
agent = wrap_langchain(executor, policy=policy)
|
|
189
|
+
result = await agent.run("your task")
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Injects a fresh `BaseCallbackHandler` per call via `config={"callbacks": [...]}`.
|
|
193
|
+
|
|
194
|
+
All adapters accept the same optional kwargs as `triage.Agent`: `classifier`, `checkpoint_store`, `max_recovery_attempts`, `auto_checkpoint`.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## How it works
|
|
199
|
+
|
|
200
|
+
### 1. Record steps
|
|
201
|
+
|
|
202
|
+
Your agent calls `record_step(Step(...))` for each observable action. `triage` injects the callback — you don't need to import or construct anything:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
async def my_agent(task: str, *, record_step, **kwargs):
|
|
206
|
+
result = call_tool("search", {"q": task})
|
|
207
|
+
record_step(Step(
|
|
208
|
+
index=0,
|
|
209
|
+
action="called search tool",
|
|
210
|
+
tool_called="search",
|
|
211
|
+
tool_input={"q": task},
|
|
212
|
+
tool_output=result,
|
|
213
|
+
))
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### 2. Classify the failure
|
|
217
|
+
|
|
218
|
+
When your agent raises an exception, `triage` runs the classifier over the recorded trajectory and returns one of 10 `FailureType` values:
|
|
219
|
+
|
|
220
|
+
| FailureType | Trigger | Default recovery |
|
|
221
|
+
|---|---|---|
|
|
222
|
+
| `WRONG_TOOL_CALLED` | Error matches `"tool not found"` / `"no tool named"` | Retry with correct manifest |
|
|
223
|
+
| `CONSTRAINT_IGNORED` | LLM output contains a forbidden string | Replan with constraint reminder |
|
|
224
|
+
| `LOOP_DETECTED` | Last 3 steps identical tool + input | Replan or rollback |
|
|
225
|
+
| `HALLUCINATED_STATE` | Agent asserts facts contradicting tool output | Rollback to checkpoint |
|
|
226
|
+
| `PLAN_INCOMPLETE` | Success declared but sub-goals incomplete | Resume from subgoal |
|
|
227
|
+
| `SCHEMA_MISMATCH` | Error matches `"validation error"` / JSON parse failure | Retry with schema hint |
|
|
228
|
+
| `CONTEXT_OVERFLOW` | Agent lost earlier context | Replan with compressed context |
|
|
229
|
+
| `GOAL_DRIFT` | Agent making progress toward the wrong goal | Replan with goal restatement |
|
|
230
|
+
| `EXTERNAL_FAULT` | HTTP 429 / 500 / 502 / 503 in error | Exponential backoff + retry |
|
|
231
|
+
| `UNKNOWN` | None of the above | Escalate to human |
|
|
232
|
+
|
|
233
|
+
The default `RulesClassifier` is pattern-based and makes zero API calls. For semantic classification use `LLMClassifier`, or use `HybridClassifier` to get the best of both:
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from triage.classifier.llm import LLMClassifier
|
|
237
|
+
from triage.classifier.hybrid import HybridClassifier
|
|
238
|
+
|
|
239
|
+
# LLM only — every failure classified by Claude
|
|
240
|
+
agent = triage.Agent(
|
|
241
|
+
my_agent,
|
|
242
|
+
policy=policy,
|
|
243
|
+
classifier=LLMClassifier(model="claude-haiku-4-5-20251001"),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Hybrid — rules first, LLM only when rules return UNKNOWN (~20% of failures)
|
|
247
|
+
agent = triage.Agent(
|
|
248
|
+
my_agent,
|
|
249
|
+
policy=policy,
|
|
250
|
+
classifier=HybridClassifier(llm=LLMClassifier()),
|
|
251
|
+
)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`LLMClassifier` supports Anthropic and any OpenAI-compatible provider. Configure via constructor args or env vars:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
# Anthropic (default)
|
|
258
|
+
ANTHROPIC_API_KEY=sk-ant-... python my_agent.py
|
|
259
|
+
|
|
260
|
+
# Ollama (local, no key)
|
|
261
|
+
TRIAGE_LLM_BASE_URL=http://localhost:11434/v1 TRIAGE_LLM_MODEL=llama3.2 python my_agent.py
|
|
262
|
+
|
|
263
|
+
# Groq
|
|
264
|
+
TRIAGE_LLM_BASE_URL=https://api.groq.com/openai/v1 TRIAGE_LLM_API_KEY=gsk_... TRIAGE_LLM_MODEL=llama-3.1-8b-instant python my_agent.py
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Or pass explicitly:
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
LLMClassifier(base_url="http://localhost:11434/v1", model="llama3.2")
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
`LLMClassifier` falls back to `UNKNOWN` silently on any error. Requires `pip install "triage-agent[anthropic]"` for Anthropic, or `pip install openai` for any OpenAI-compatible provider.
|
|
274
|
+
|
|
275
|
+
### 3. Dispatch to a strategy
|
|
276
|
+
|
|
277
|
+
The policy maps each `FailureType` to a strategy callable. The strategy returns a `RecoveryAction` that tells `triage` what to do next.
|
|
278
|
+
|
|
279
|
+
### 4. Execute the recovery
|
|
280
|
+
|
|
281
|
+
`triage` executes the action and re-runs your agent with injected context:
|
|
282
|
+
|
|
283
|
+
| Action | What happens |
|
|
284
|
+
|---|---|
|
|
285
|
+
| `RETRY` | Re-runs the agent; injects `_triage_hint` into kwargs |
|
|
286
|
+
| `REPLAN` | Re-runs the agent; injects `_triage_hint` with new plan instruction |
|
|
287
|
+
| `ROLLBACK` | Restores trajectory from checkpoint, re-runs agent |
|
|
288
|
+
| `RESUME` | Re-runs agent; injects `_triage_subgoal` pointing at incomplete subgoal |
|
|
289
|
+
| `ESCALATE` | Raises `TriageEscalationError(message, context)` |
|
|
290
|
+
| `ABORT` | Raises `TriageAbortError(reason, context)` |
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Failure policy
|
|
295
|
+
|
|
296
|
+
`FailurePolicy` is a plain dataclass — one field per `FailureType`:
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
policy = triage.FailurePolicy(
|
|
300
|
+
WRONG_TOOL_CALLED = retry_with_tool_manifest(max_attempts=3),
|
|
301
|
+
CONSTRAINT_IGNORED = replan(hint="Re-read the task constraints carefully."),
|
|
302
|
+
LOOP_DETECTED = replan(max_replans=2),
|
|
303
|
+
HALLUCINATED_STATE = rollback_to_checkpoint(),
|
|
304
|
+
PLAN_INCOMPLETE = resume_from_subgoal(),
|
|
305
|
+
SCHEMA_MISMATCH = retry_with_tool_manifest(max_attempts=2),
|
|
306
|
+
EXTERNAL_FAULT = backoff_and_retry(max_attempts=5),
|
|
307
|
+
default = triage.FailurePolicy.escalate_by_default(),
|
|
308
|
+
)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Any `FailureType` not explicitly listed falls through to `default`. If `default` is also unset, triage escalates automatically.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Built-in strategies
|
|
316
|
+
|
|
317
|
+
### `triage.strategies.retry`
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
from triage.strategies.retry import retry_with_tool_manifest, backoff_and_retry
|
|
321
|
+
|
|
322
|
+
# Retry with a hint to use the correct tool manifest
|
|
323
|
+
retry_with_tool_manifest(max_attempts=3)
|
|
324
|
+
|
|
325
|
+
# Retry with exponential backoff (2^attempt seconds). Good for rate limits.
|
|
326
|
+
backoff_and_retry(max_attempts=5)
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### `triage.strategies.replan`
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
from triage.strategies.replan import replan, resume_from_subgoal
|
|
333
|
+
|
|
334
|
+
# Restart with a new plan, optionally injecting a hint
|
|
335
|
+
replan(hint="The previous approach used the wrong API endpoint.")
|
|
336
|
+
|
|
337
|
+
# Continue from the first incomplete sub-goal
|
|
338
|
+
resume_from_subgoal()
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### `triage.strategies.rollback`
|
|
342
|
+
|
|
343
|
+
```python
|
|
344
|
+
from triage.strategies.rollback import rollback_to_checkpoint
|
|
345
|
+
|
|
346
|
+
# Restore to latest checkpoint (or a named one)
|
|
347
|
+
rollback_to_checkpoint()
|
|
348
|
+
rollback_to_checkpoint(checkpoint_id="before-api-call")
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## Checkpoints
|
|
354
|
+
|
|
355
|
+
Save agent state at key points so triage can roll back to them on failure.
|
|
356
|
+
|
|
357
|
+
### In-memory (default)
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
from triage.checkpoint import InMemoryCheckpointStore
|
|
361
|
+
|
|
362
|
+
store = InMemoryCheckpointStore()
|
|
363
|
+
agent = triage.Agent(my_agent, policy=policy, checkpoint_store=store)
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### SQLite (persistent, single-process)
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
pip install "triage-agent[sqlite]"
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
```python
|
|
373
|
+
from triage.checkpoint.sqlite import SQLiteCheckpointStore
|
|
374
|
+
|
|
375
|
+
store = SQLiteCheckpointStore("runs/checkpoints.db")
|
|
376
|
+
agent = triage.Agent(my_agent, policy=policy, checkpoint_store=store)
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Redis (distributed)
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
pip install "triage-agent[redis]"
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
```python
|
|
386
|
+
import redis.asyncio as aioredis
|
|
387
|
+
from triage.checkpoint.redis import RedisCheckpointStore
|
|
388
|
+
|
|
389
|
+
client = aioredis.Redis.from_url("redis://localhost:6379")
|
|
390
|
+
store = RedisCheckpointStore(client)
|
|
391
|
+
agent = triage.Agent(my_agent, policy=policy, checkpoint_store=store)
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Auto-checkpoint
|
|
395
|
+
|
|
396
|
+
Enable automatic checkpointing after every successful step:
|
|
397
|
+
|
|
398
|
+
```python
|
|
399
|
+
agent = triage.Agent(my_agent, policy=policy, checkpoint_store=store, auto_checkpoint=True)
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Checkpoints are always awaited before `run()` returns or any recovery action executes, so a `ROLLBACK` always has a checkpoint available.
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## Recovery context in your agent
|
|
407
|
+
|
|
408
|
+
Two callbacks are always injected, plus recovery context on retry:
|
|
409
|
+
|
|
410
|
+
```python
|
|
411
|
+
async def my_agent(
|
|
412
|
+
task: str,
|
|
413
|
+
*,
|
|
414
|
+
record_step,
|
|
415
|
+
update_state,
|
|
416
|
+
_triage_hint=None,
|
|
417
|
+
_triage_subgoal=None,
|
|
418
|
+
_triage_state=None,
|
|
419
|
+
**kwargs,
|
|
420
|
+
):
|
|
421
|
+
# On rollback, _triage_state contains the state saved at the checkpoint
|
|
422
|
+
if _triage_state:
|
|
423
|
+
data = _triage_state["data"] # skip re-fetching, use restored state
|
|
424
|
+
else:
|
|
425
|
+
data = fetch_data(task)
|
|
426
|
+
|
|
427
|
+
record_step(Step(index=0, action="fetch", tool_output=data))
|
|
428
|
+
update_state({"data": data}) # saved into every auto_checkpoint
|
|
429
|
+
|
|
430
|
+
if _triage_hint:
|
|
431
|
+
print(f"Recovery hint: {_triage_hint}")
|
|
432
|
+
if _triage_subgoal:
|
|
433
|
+
task = _triage_subgoal
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
| Key | Set when |
|
|
437
|
+
|---|---|
|
|
438
|
+
| `record_step` | Always — injected on every call |
|
|
439
|
+
| `update_state` | Always — injected on every call |
|
|
440
|
+
| `_triage_hint` | `RETRY`, `REPLAN`, or `ROLLBACK` action |
|
|
441
|
+
| `_triage_subgoal` | `RESUME` action |
|
|
442
|
+
| `_triage_state` | `ROLLBACK` action, when checkpoint has non-empty state |
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Attempt history
|
|
447
|
+
|
|
448
|
+
Strategies can inspect everything that was tried before they were called:
|
|
449
|
+
|
|
450
|
+
```python
|
|
451
|
+
async def smart_strategy(ctx: triage.FailureContext) -> triage.RecoveryAction:
|
|
452
|
+
# ctx.attempt_history is a list of (FailureType, action_kind) tuples
|
|
453
|
+
replan_count = sum(1 for _, kind in ctx.attempt_history if kind == "replan")
|
|
454
|
+
|
|
455
|
+
if replan_count >= 2:
|
|
456
|
+
return triage.RecoveryAction.ESCALATE(message="Replanned twice, still failing.")
|
|
457
|
+
return triage.RecoveryAction.REPLAN(hint="Try a different approach.")
|
|
458
|
+
|
|
459
|
+
policy = triage.FailurePolicy(GOAL_DRIFT=smart_strategy)
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
`attempt_history` is empty on the first failure and grows by one entry per recovery attempt. Each entry is `(failure_type, action_kind)` where `action_kind` is one of `"retry"`, `"replan"`, `"rollback"`, `"resume"`, `"escalate"`, `"abort"`.
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## Handling escalation and abort
|
|
467
|
+
|
|
468
|
+
```python
|
|
469
|
+
try:
|
|
470
|
+
result = await agent.run(task)
|
|
471
|
+
except triage.TriageEscalationError as exc:
|
|
472
|
+
# exc.context is a FailureContext with the full trajectory and failure type
|
|
473
|
+
print(f"Needs human review: {exc}")
|
|
474
|
+
print(f"Failure type: {exc.context.failure_type.value}")
|
|
475
|
+
print(f"Failed at step: {exc.context.critical_step_index}")
|
|
476
|
+
except triage.TriageAbortError as exc:
|
|
477
|
+
print(f"Hard stop: {exc}")
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## Custom classifier
|
|
483
|
+
|
|
484
|
+
Any class implementing `classify(trajectory, task) -> FailureType` satisfies the protocol:
|
|
485
|
+
|
|
486
|
+
```python
|
|
487
|
+
from triage.classifier.base import Classifier
|
|
488
|
+
from triage.taxonomy import FailureType
|
|
489
|
+
from triage.trajectory import Trajectory
|
|
490
|
+
|
|
491
|
+
class MyClassifier:
|
|
492
|
+
def classify(self, trajectory: Trajectory, task: str) -> FailureType:
|
|
493
|
+
...
|
|
494
|
+
|
|
495
|
+
agent = triage.Agent(my_agent, policy=policy, classifier=MyClassifier())
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
## Example: OpenAI tool-calling loop
|
|
501
|
+
|
|
502
|
+
See [`examples/raw_openai.py`](examples/raw_openai.py) for a full working example. It deliberately triggers a `WRONG_TOOL_CALLED` failure on the first attempt and shows triage catching and recovering it automatically:
|
|
503
|
+
|
|
504
|
+
```bash
|
|
505
|
+
OPENAI_API_KEY=sk-... python examples/raw_openai.py
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Expected output:
|
|
509
|
+
|
|
510
|
+
```
|
|
511
|
+
Task: What is 42 * 17?
|
|
512
|
+
|
|
513
|
+
[triage] wrong_tool_called detected at step 0
|
|
514
|
+
[triage] Dispatching: RecoveryAction.RETRY(hint='Re-run using only tools in the current manifest.', inject={'max_attempts': 3})
|
|
515
|
+
[triage] Attempt 1...
|
|
516
|
+
|
|
517
|
+
Result: 714
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
## Project layout
|
|
523
|
+
|
|
524
|
+
```
|
|
525
|
+
triage/
|
|
526
|
+
taxonomy.py FailureType enum, Step, FailureContext
|
|
527
|
+
trajectory.py Trajectory (append / replay_from / last_n_steps)
|
|
528
|
+
checkpoint/
|
|
529
|
+
base.py Checkpoint, CheckpointStore protocol, serialization helpers
|
|
530
|
+
memory.py InMemoryCheckpointStore
|
|
531
|
+
sqlite.py SQLiteCheckpointStore (requires aiosqlite)
|
|
532
|
+
redis.py RedisCheckpointStore (requires redis[asyncio])
|
|
533
|
+
policy.py RecoveryAction (6 constructors), FailurePolicy
|
|
534
|
+
agent.py Agent class, TriageEscalationError, TriageAbortError, @agent decorator
|
|
535
|
+
classifier/
|
|
536
|
+
base.py Classifier protocol
|
|
537
|
+
rules.py RulesClassifier — 6 rules, sync, zero API calls
|
|
538
|
+
llm.py LLMClassifier — Anthropic or OpenAI-compatible backend
|
|
539
|
+
hybrid.py HybridClassifier — rules first, LLM fallback on UNKNOWN
|
|
540
|
+
strategies/
|
|
541
|
+
retry.py retry_with_tool_manifest(), backoff_and_retry()
|
|
542
|
+
replan.py replan(), resume_from_subgoal()
|
|
543
|
+
rollback.py rollback_to_checkpoint()
|
|
544
|
+
adapters/
|
|
545
|
+
langgraph.py wrap_langgraph() (requires langgraph)
|
|
546
|
+
crewai.py wrap_crewai() (requires crewai)
|
|
547
|
+
openai_agents.py wrap_openai_agents() (requires openai-agents)
|
|
548
|
+
langchain.py wrap_langchain() (requires langchain)
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## License
|
|
554
|
+
|
|
555
|
+
MIT
|