Every tool call is a chance to prompt
There’s a subtlety in building AI agents that took me an embarrassingly long time to appreciate: your tool responses are prompts!
So many tools and MCP servers treat outputs as mere data pipes: the model calls a function, the function returns a result or an error code, and that’s the end of it. But that mindset overlooks a powerful design opportunity. Tool responses are language, and that means responses can (and should) be crafted to deliberately nudge the model in the direction of what to do next.
Most tool errors leave the model stranded
Here’s how most of us start out writing tools:
import { createTool } from '@mastra/core/tools';import { z } from 'zod'; import { db } from '@/project/db'; export const getUser = createTool({ id: 'getUser', description: 'Fetch a user by their ID', inputSchema: z.object({ id: z.string(), }), execute: async ({ context }) => { const user = await db.getUser(context.id); if (!user) { return { error: 'User not found', }; } return user; },});
This works, but we’ve missed an opportunity to help the agent correct course when it mistakenly gives us a bad identifier. The tool response tells the model that something went wrong, but there’s no context for recovery.
The model is left to infer intent, context, and resolution from a flat and contextless error message. This doesn’t work very well, and often results in the agent wasting a few turns trying different strategies to unblock itself.
Tool responses as strategic prompts
Consider the following change:
import { createTool } from '@mastra/core/tools';import { z } from 'zod'; import { db } from '@/project/db'; export const getUser = createTool({ id: 'getUser', description: 'Fetch a user by their ID', inputSchema: z.object({ id: z.string(), }), execute: async ({ context }) => { const user = await db.getUser(context.id); if (!user) { return { error: [ 'User with ID ${context.id} not found.', 'Try calling listUsers() to see available users,', 'or use searchUsers({ query }) to find users by name.', ].join(' '), }; } return user; },});
By adjusting the error message our agent now has a clear path forward, and can use the suggested tools in the error message to get back on track.
- Self-correction without bloating the system prompt. You don’t need to enumerate every single possible failure mode in your system prompt! I find this helps maintainability a lot; I get lost in the 6+ page system prompts being used by some startups.
- Reduced token costs. Because your system prompt is slimmer, you end up sending fewer input tokens for each agent run. The improvement is more pronounced the shorter your runs are.
- Better handling of the “lost in the middle” problem. When models have long context windows, they can lose track of available tools and their usage patterns. Solutions to problems get revealed to the model when they’re needed and most timely, which plays well with the model’s attention mechanism.
Implementation tips
Just like GraphQL requires a mindset shift from REST, building tools for LLMs requires a mindset shift from traditional software design. Things are generally “squishier”; there is no deterministic switch statement on the caller’s side with carefully considered error handling logic.
Instead of returning error codes and calling it a day, engineers need to think in terms of probabilistic language-driven workflows. This means designing tool responses not merely to signal failure, but to actively guide recovery. LLMs are fully capable of course correction provided you fill their context window appropriately. In this world, helpfulness and clarity beat precision and terseness.
Here are three general principles I apply when thinking about tool output:
- Provide context. Most of the standard error communication guidance apply equally well to LLMs and humans alike. “Please try again” or “something went wrong” are poor error messages in the first place. Be more concrete.
- Reference useful tools by name. Help the model remember what’s available, and tailor your recommended tools based on the type of error encountered.
- Use conversational language. Your tool is interfacing with a language model; take advantage of the fact it understands prose, and avoid spitting out error codes.