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.
Files changed (50) hide show
  1. triage_agent-0.3.0/.gitignore +36 -0
  2. triage_agent-0.3.0/PKG-INFO +555 -0
  3. triage_agent-0.3.0/README.md +503 -0
  4. triage_agent-0.3.0/examples/anthropic_agent.py +172 -0
  5. triage_agent-0.3.0/examples/durable_checkpoints.py +160 -0
  6. triage_agent-0.3.0/examples/groq_agent.py +188 -0
  7. triage_agent-0.3.0/examples/huggingface_agent.py +196 -0
  8. triage_agent-0.3.0/examples/langgraph_agent.py +94 -0
  9. triage_agent-0.3.0/examples/llm_classifier.py +114 -0
  10. triage_agent-0.3.0/examples/ollama_agent.py +173 -0
  11. triage_agent-0.3.0/examples/raw_openai.py +166 -0
  12. triage_agent-0.3.0/pyproject.toml +77 -0
  13. triage_agent-0.3.0/tests/__init__.py +0 -0
  14. triage_agent-0.3.0/tests/test_adapter_crewai.py +146 -0
  15. triage_agent-0.3.0/tests/test_adapter_langchain.py +175 -0
  16. triage_agent-0.3.0/tests/test_adapter_langgraph.py +188 -0
  17. triage_agent-0.3.0/tests/test_adapter_openai_agents.py +189 -0
  18. triage_agent-0.3.0/tests/test_agent.py +607 -0
  19. triage_agent-0.3.0/tests/test_checkpoint.py +89 -0
  20. triage_agent-0.3.0/tests/test_checkpoint_redis.py +140 -0
  21. triage_agent-0.3.0/tests/test_checkpoint_sqlite.py +127 -0
  22. triage_agent-0.3.0/tests/test_classifier_hybrid.py +177 -0
  23. triage_agent-0.3.0/tests/test_classifier_llm.py +303 -0
  24. triage_agent-0.3.0/tests/test_classifier_rules.py +184 -0
  25. triage_agent-0.3.0/tests/test_policy.py +154 -0
  26. triage_agent-0.3.0/tests/test_taxonomy.py +111 -0
  27. triage_agent-0.3.0/triage/__init__.py +50 -0
  28. triage_agent-0.3.0/triage/adapters/__init__.py +5 -0
  29. triage_agent-0.3.0/triage/adapters/crewai.py +64 -0
  30. triage_agent-0.3.0/triage/adapters/langchain.py +89 -0
  31. triage_agent-0.3.0/triage/adapters/langgraph.py +71 -0
  32. triage_agent-0.3.0/triage/adapters/openai_agents.py +69 -0
  33. triage_agent-0.3.0/triage/agent.py +239 -0
  34. triage_agent-0.3.0/triage/checkpoint/__init__.py +11 -0
  35. triage_agent-0.3.0/triage/checkpoint/base.py +92 -0
  36. triage_agent-0.3.0/triage/checkpoint/memory.py +37 -0
  37. triage_agent-0.3.0/triage/checkpoint/redis.py +81 -0
  38. triage_agent-0.3.0/triage/checkpoint/sqlite.py +100 -0
  39. triage_agent-0.3.0/triage/classifier/__init__.py +16 -0
  40. triage_agent-0.3.0/triage/classifier/base.py +22 -0
  41. triage_agent-0.3.0/triage/classifier/hybrid.py +46 -0
  42. triage_agent-0.3.0/triage/classifier/llm.py +159 -0
  43. triage_agent-0.3.0/triage/classifier/rules.py +75 -0
  44. triage_agent-0.3.0/triage/policy.py +151 -0
  45. triage_agent-0.3.0/triage/strategies/__init__.py +13 -0
  46. triage_agent-0.3.0/triage/strategies/replan.py +28 -0
  47. triage_agent-0.3.0/triage/strategies/retry.py +32 -0
  48. triage_agent-0.3.0/triage/strategies/rollback.py +19 -0
  49. triage_agent-0.3.0/triage/taxonomy.py +100 -0
  50. 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
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/)
62
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](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