<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged model-context-protocol</title>
<link>https://coderlegion.com/tag/model-context-protocol</link>
<description>Powered by Question2Answer</description>
<item>
<title>How to Design Tool Errors AI Agents Can Recover From</title>
<link>https://coderlegion.com/24993/how-to-design-tool-errors-ai-agents-can-recover-from</link>
<description>&lt;p&gt;“Something went wrong” is not an error contract. It is a request for another blind guess.&lt;/p&gt;
&lt;p&gt;That is expensive inside an AI agent loop. The controller has to decide whether to retry the same call, repair an argument, ask for authorization, choose another tool, or stop. A human can infer some of that from a stack trace. A machine needs the next action to be explicit.&lt;/p&gt;
&lt;p&gt;Human strategy, benchmark design, interpretation, and final editorial judgment led this article; AI assisted with research organization, drafting, and code review.&lt;/p&gt;
&lt;p&gt;I built a deterministic benchmark around 12,000 synthetic tool operations to test three response formats: a generic message, an error code, and an actionable error envelope. The test does not measure model intelligence. It measures a smaller systems question: how much useful recovery can a controller perform when the error contract tells it what kind of failure occurred?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=8843500240625635739&quot; alt=&quot;An architecture diagram showing a tool failure converted into an actionable error envelope before the controller chooses retry, repair, or stop.&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Original architecture visual. The raw message remains useful for people, while structured fields make the next machine action explicit.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;a-tool-error-is-part-of-the-agent-s-control-plane&quot;&gt;A tool error is part of the agent's control plane&lt;/h3&gt;
&lt;p&gt;In a normal application, an error often ends a request. In an agent, the error becomes input to the next turn.&lt;/p&gt;
&lt;p&gt;That changes the design requirement. The message should still help an operator, but it also needs to support a routing decision:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Retry:&lt;/strong&gt; the same operation may succeed after backoff.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Repair:&lt;/strong&gt; the operation can succeed after changing an argument.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authorize:&lt;/strong&gt; the call needs a credential or human approval.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Replace:&lt;/strong&gt; another tool or resource should be used.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stop:&lt;/strong&gt; another attempt would waste time or repeat a side effect.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Model Context Protocol makes a related distinction between protocol errors and tool execution errors. Malformed requests and unknown tools belong at the protocol layer. Validation failures, API failures, and business-logic errors can be returned as tool results with &lt;code&gt;isError: true&lt;/code&gt;, giving the model actionable feedback it can use to adjust a later call (&lt;a rel=&quot;nofollow&quot; href=&quot;https://modelcontextprotocol.io/specification/draft/server/tools&quot;&gt;MCP tool error handling&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;OpenAI's Agents SDK exposes the same underlying control problem from the runner side. An unresolved tool name can either raise an exception or be returned to the model so the loop can recover and select an available tool (&lt;a rel=&quot;nofollow&quot; href=&quot;https://openai.github.io/openai-agents-python/running_agents/&quot;&gt;OpenAI Agents SDK runner documentation&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;The protocol can carry an error. The application still has to decide whether that error is useful.&lt;/p&gt;
&lt;h3 id=&quot;i-tested-three-error-formats-across-12-000-operations&quot;&gt;I tested three error formats across 12,000 operations&lt;/h3&gt;
&lt;p&gt;The benchmark generated 60 seeded runs with 200 initial operations in each run. Forty percent of operations failed under an explicit scenario assumption. Failures were divided evenly among four types:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;transient;&lt;/li&gt;
&lt;li&gt;validation;&lt;/li&gt;
&lt;li&gt;permission;&lt;/li&gt;
&lt;li&gt;not found.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each strategy received the same generated operations and the same recovery rolls.&lt;/p&gt;
&lt;p&gt;The three strategies were:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Generic message:&lt;/strong&gt; retry every failure once because the controller cannot classify it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error code only:&lt;/strong&gt; retry transient failures, attempt to repair validation failures, and stop on permanent failures.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Actionable envelope:&lt;/strong&gt; use the same routing, but include a field and repair suggestion that improves the modeled validation-repair probability.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Run the benchmark with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx tsx scripts/run-agent-tool-error-contract-benchmark.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The scenario assumptions are part of the result, not hidden setup. A transient retry succeeds 70% of the time. A validation repair succeeds 45% of the time when the controller receives only a code and 90% when it receives the failing field and a repair hint.&lt;/p&gt;
&lt;p&gt;Those percentages are illustrative assumptions. They are not measurements of a production model.&lt;/p&gt;
&lt;h3 id=&quot;the-actionable-envelope-cut-failed-retries-by-87-7-&quot;&gt;The actionable envelope cut failed retries by 87.7%&lt;/h3&gt;
&lt;p&gt;Here are the aggregate results:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Strategy &lt;/th&gt;&lt;th&gt;Resolution rate &lt;/th&gt;&lt;th&gt;Attempts per operation &lt;/th&gt;&lt;th&gt;Failed retry attempts &lt;/th&gt;&lt;th&gt;Permanent-failure retries&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Generic message &lt;/td&gt;&lt;td&gt;67.1% &lt;/td&gt;&lt;td&gt;1.402 &lt;/td&gt;&lt;td&gt;3,953 &lt;/td&gt;&lt;td&gt;2,449&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Error code only &lt;/td&gt;&lt;td&gt;71.2% &lt;/td&gt;&lt;td&gt;1.198 &lt;/td&gt;&lt;td&gt;1,005 &lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Actionable envelope &lt;/td&gt;&lt;td&gt;75.5% &lt;/td&gt;&lt;td&gt;1.198 &lt;/td&gt;&lt;td&gt;487 &lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=1541979796479906598&quot; alt=&quot;A bar chart comparing resolution rate and wasted retry attempts for generic messages, error codes, and actionable error envelopes.&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Original deterministic benchmark. Sixty seeded runs contained 200 initial operations each. Resolution is simulated controller success within one recovery attempt; no language model was called.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The generic strategy made 16,823 total attempts for 12,000 operations. It retried permission and not-found failures 2,449 times even though the retry could not change the outcome.&lt;/p&gt;
&lt;p&gt;Adding a code eliminated those permanent-failure retries. Total attempts fell to 14,374, while the resolution rate increased from 67.1% to 71.2% because the controller could treat validation as a repair problem instead of repeating the same invalid call.&lt;/p&gt;
&lt;p&gt;The actionable envelope used the same number of attempts as the code-only strategy but resolved 75.5% of operations. Its failed retry count fell to 487—87.7% below the generic-message strategy.&lt;/p&gt;
&lt;p&gt;The benchmark does not prove that every model will recover at that rate. It shows what the controller can do when recovery policy is encoded rather than guessed.&lt;/p&gt;
&lt;h3 id=&quot;give-the-controller-a-small-stable-error-envelope&quot;&gt;Give the controller a small, stable error envelope&lt;/h3&gt;
&lt;p&gt;The error contract does not need to become a second API specification. It needs enough information to route the failure safely.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type ToolError = {
  code: &quot;TRANSIENT&quot; | &quot;VALIDATION&quot; | &quot;PERMISSION&quot; | &quot;NOT_FOUND&quot;;
  message: string;
  retryable: boolean;
  field?: string;
  suggestion?: string;
  retryAfterMs?: number;
  traceId: string;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each field has one job.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;code&lt;/code&gt; selects a known failure class.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;message&lt;/code&gt; explains the problem to the model and operator.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;retryable&lt;/code&gt; stops the controller from inventing its own retry rule.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;field&lt;/code&gt; identifies the argument that needs repair.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;suggestion&lt;/code&gt; supplies a bounded correction rather than an open-ended rewrite.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;retryAfterMs&lt;/code&gt; prevents an immediate retry from amplifying throttling.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;traceId&lt;/code&gt; connects the agent-visible failure to logs and traces.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Do not put secrets, raw database errors, or internal stack traces into a model-visible message. The public contract should expose the recovery surface, not the server's internals.&lt;/p&gt;
&lt;h3 id=&quot;separate-routing-policy-from-error-prose&quot;&gt;Separate routing policy from error prose&lt;/h3&gt;
&lt;p&gt;A controller can route the envelope without asking a model to interpret every sentence.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function nextAction(error: ToolError): &quot;retry&quot; | &quot;repair&quot; | &quot;authorize&quot; | &quot;stop&quot; {
  if (error.code === &quot;TRANSIENT&quot; &amp;amp;&amp;amp; error.retryable) return &quot;retry&quot;;
  if (error.code === &quot;VALIDATION&quot; &amp;amp;&amp;amp; error.field &amp;amp;&amp;amp; error.suggestion) return &quot;repair&quot;;
  if (error.code === &quot;PERMISSION&quot;) return &quot;authorize&quot;;
  return &quot;stop&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is intentionally boring. A deterministic router is easier to test than a prompt that says, “Use your judgment and try again if appropriate.”&lt;/p&gt;
&lt;p&gt;The model still matters when a validation error requires semantic repair. It may need to convert “next Friday” into an ISO-8601 date or select a replacement resource. But the controller should decide whether a repair turn is allowed before spending another model call.&lt;/p&gt;
&lt;p&gt;GitHub's MCP Scripts specification takes this idea further for execution errors by requiring a &lt;code&gt;data.recoverable&lt;/code&gt; boolean. It limits &lt;code&gt;recoverable: true&lt;/code&gt; to transient failures where the same invocation may succeed on a later attempt (&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.github.com/gh-aw/reference/mcp-scripts-specification/&quot;&gt;GitHub MCP Scripts error handling&lt;/a&gt;). That is the kind of field a controller can enforce without semantic guesswork.&lt;/p&gt;
&lt;h3 id=&quot;the-prompt-trace-should-preserve-the-boundary&quot;&gt;The prompt trace should preserve the boundary&lt;/h3&gt;
&lt;p&gt;Here is the exact interpretation constraint used for the benchmark summary:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Prompt:
Compare the three tool-error strategies using only summary.json. Separate
measured simulation output from scenario assumptions. Do not claim LLM accuracy.

Input:
Generic message: 67.1% resolved, 3,953 failed retry attempts
Error code only: 71.2% resolved, 1,005 failed retry attempts
Actionable envelope: 75.5% resolved, 487 failed retry attempts

Output constraint:
Report resolution rate, attempts per operation, failed retries, and permanent-
failure retries. State that all recovery probabilities are explicit synthetic
assumptions and that no model was called.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That last line prevents a common reporting error. A controller simulation can test policy logic. It cannot tell you how reliably a model will interpret a repair hint.&lt;/p&gt;
&lt;h3 id=&quot;retry-safety-needs-more-than-a-boolean&quot;&gt;Retry safety needs more than a boolean&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;retryable: true&lt;/code&gt; is necessary and insufficient.&lt;/p&gt;
&lt;p&gt;A production retry path should also answer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Is the operation idempotent?&lt;/li&gt;
&lt;li&gt;Has this workflow already produced a side effect?&lt;/li&gt;
&lt;li&gt;What is the maximum attempt budget?&lt;/li&gt;
&lt;li&gt;Should the controller use exponential backoff and jitter?&lt;/li&gt;
&lt;li&gt;Did the server provide a rate-limit reset time?&lt;/li&gt;
&lt;li&gt;Does the repaired call need a new idempotency key or the old one?&lt;/li&gt;
&lt;li&gt;Which failures require a human decision?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A retryable payment lookup and a retryable payment creation are different risks. The first may waste capacity. The second may charge twice.&lt;/p&gt;
&lt;p&gt;For state-changing tools, store the operation ID and side-effect status outside the model context. A model should not be the source of truth for whether a transaction already happened.&lt;/p&gt;
&lt;h3 id=&quot;where-this-benchmark-stops&quot;&gt;Where this benchmark stops&lt;/h3&gt;
&lt;p&gt;The benchmark is deterministic and reproducible, but its scope is narrow.&lt;/p&gt;
&lt;p&gt;It does not measure latency, tokens, model interpretation, parallel tool calls, ambiguous repair hints, or side-effect safety. The failure mix is synthetic. The recovery probabilities are declared assumptions. Resolution means success within one modeled recovery attempt, not completion of a full agent task.&lt;/p&gt;
&lt;p&gt;Real evaluations should replay production-shaped traces, vary models and prompts, measure incorrect repairs, and test that terminal failures really stop. They should also include adversarial errors: misleading suggestions, mismatched codes, missing trace IDs, and a server that marks everything retryable.&lt;/p&gt;
&lt;p&gt;The small benchmark is still useful because it tests the first layer without model noise: whether the controller has enough structure to choose a permitted next action.&lt;/p&gt;
&lt;h3 id=&quot;make-failure-actionable-before-making-it-eloquent&quot;&gt;Make failure actionable before making it eloquent&lt;/h3&gt;
&lt;p&gt;Start with four routes: retry, repair, authorize, and stop. Define the codes that lead to each route. Add an explicit retry flag, the smallest useful repair hint, and a trace ID. Then test the controller with fixed seeds before asking a model to interpret the message.&lt;/p&gt;
&lt;p&gt;The practical test is simple: if the model were unavailable, could the runtime still avoid repeating a permission error, respect backoff, and identify the argument that needs repair?&lt;/p&gt;
&lt;p&gt;If not, the error contract is still prose. Give it enough structure to become control flow.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24993/how-to-design-tool-errors-ai-agents-can-recover-from</guid>
<pubDate>Thu, 20 Aug 2026 14:34:07 +0000</pubDate>
</item>
</channel>
</rss>