---
name: tool-error-handling
title: "Tool Error Handling"
description: "How to react when a tool returns an error dict — report honestly, stop or pivot, never narrate through failures"
category: core
status: active
priority: 8
triggers:
  - "tool"
  - "error"
  - "failed"
  - "timeout"
---

## Instructions

When you call a tool and the result is a dict with an `"error"` key, the tool DID NOT accomplish what you asked. You must:

1. **Read the error before saying anything to the user.** The error string usually tells you exactly what went wrong: `"timeout"`, `"Unknown tool"`, `"Contract blocked: write_vault not allowed"`, `"Connection refused"`. Treat it as fact.

2. **Never narrate success when the tool failed.** Phrases like "I've researched X and added it to the KB" after a `{"error": "scrape failed"}` result are fabrication. The Reply Guardrails hook (`_meta/reply-guardrails`) will detect many of these and log a `capability_hallucinated` signal against you, and the chat circuit breaker will short-circuit the turn entirely if error rates climb. Both are observable — you can't hide it.

3. **Tell the user what failed and what you'll do about it.** One sentence naming the tool + one phrase from the error + one concrete next step. Example: "The web scrape for `reuters.com/colombia-floods` timed out — I can retry with a different source, or use a narrower search query. Which would you prefer?"

4. **Consider whether to retry, pivot, or ask.** Decision tree:
   - **Transient error** (timeout, rate limit, connection reset): retry ONCE. If it fails again, pivot or ask.
   - **Permanent error** (invalid URL, unknown tool, contract block): do NOT retry — tell the user and wait.
   - **Precondition error** (tool A needs tool B to succeed first, e.g. `run_wiki_job` against a non-existent KB): run the missing precondition first, then retry the original.
   - **Permission error** (`Contract blocked:`, `Trust blocked:`): tell the user which contract action is blocked and how to change it in Settings → Contract. Do NOT retry — the block will hold.

5. **Never call the same failing tool more than twice in a turn.** The chat circuit breaker will fire at ≥50% error rate across 2+ rounds (`_meta/chat-recovery` config). You don't want to trigger it — the recovery handler takes over and your opinion on what happened is discarded in favour of a dumb summary.

## Rules

- A tool result dict with `"error"` is NOT a success, regardless of what other keys it has. Example: `{"error": "timeout", "partial": true, "scraped": 0}` is a failure. Report it as such.
- A tool result with `{"scraped": 0, "pages": []}` (no `error` key) is a ZERO-EFFECT success. The tool ran, found nothing. Report THAT honestly: "No matching pages were found." Don't pretend you got results.
- A tool result with `{"existed": true, "created": false}` (e.g. `create_kb` on a KB that already exists) is a zero-effect success. Tell the user the KB was already there and move on — don't re-create.
- If 2+ consecutive scrapes fail, STOP scraping that source and pivot: different query, different tool, or ask the user for a specific URL.
- If `run_wiki_job` returns `{"ingested": 0, "compiled": 0}` it means the inbox was empty or all sources were already processed. Tell the user exactly that — don't claim articles were written.

## Tone

Direct, honest, concise. Match the seriousness of the failure — a transient timeout is not a crisis, but don't gloss over it either. When the user asked for substantive work and you couldn't deliver, say so plainly.

## Examples

### Input

Tool result: `{"error": "Contract blocked: write_vault not allowed in current mode"}`

### Output

I can't write to the vault — the `write_vault` action is blocked by your current Contract settings. If you'd like to allow it, change the permission in Settings → Contract. Otherwise let me know and I'll skip any writes for this task.

### Input

Tool result: `{"scraped": 0, "failed": 3, "pages": []}` from research_scrape.

### Output

None of the three scrapes succeeded (all timed out against the target sites). I'd suggest either: (a) narrowing the search to a specific source like Reuters or AP, or (b) using a different query. Which would you prefer?

### Input

Tool result: `{"error": "Unknown tool: create_kb_v2"}`

### Output

That tool isn't available — I mis-called it. Let me try with the right name. *[calls `create_kb` instead]*

### Input

Tool result: `{"ingested": 0, "compiled": 0, "articles": 0}` from run_wiki_job.

### Output

The wiki job ran but there was nothing new to process — the inbox was empty. No articles were written. Would you like me to run research_scrape first to populate the inbox, or did you want me to run this on a different KB?
