If you have built an autonomous AI agent using OpenAI, Claude, LangChain, or CrewAI, you have inevitably encountered this scenario:
Your agent needs to look up a customer order. Instead of calling fetch_customer_order(order_id), it calls search_database(query="order"). Or worse, it hallucinates an argument like status="completed_order_v2" because the schema simply said "status": { "type": "string" } without defining permitted enum values. Or it enters an infinite loop, toggling back and forth between two sibling tools that do almost the same thing.
When this happens, developers usually react by tweaking the system prompt:
"IMPORTANT: You MUST always call fetch_customer_order when the user mentions an order! Do NOT call search_database!"
This is the Prompt Engineering Trap.
The root cause of tool-calling failures is almost never the model’s reasoning capacity. It is broken, sloppy tool contracts.
In this article, I will explain why agent tool schemas fail in production, break down the architecture of Toolsmith—a developer tool we built to lint, score, and harden AI tool definitions—and share the algorithms and engineering decisions behind it.
1. Anatomizing the Broken Tool Contract
Let’s look at a schema typical of many agent tutorials:
{
"name": "lookup_user",
"description": "Used to find information about people in the company or app.",
"parameters": {
"type": "object",
"properties": {
"user": {
"type": "string",
"description": "The user"
},
"filter": {
"description": "Optional search parameters"
}
}
}
}
Why does an LLM fail when presented with this?
- Passive, Vague Description: "Used to find information..." does not give the model an imperative trigger. When should it call this instead of a general search tool? Under what constraints?
-
Missing Imperative Verb: High-performing tool descriptions start with active verbs (
Fetch,Query,Search,Validate). This matches the instruction fine-tuning patterns of modern frontier models. -
Untyped Parameter:
"filter"has no"type". Is it a JSON object? A SQL string? A date range? -
No Enum Sets: If
"status"or"mode"only supports three valid states, failing to declare anenum: ["active", "suspended", "archived"]guarantees model hallucinations. -
Missing
requiredArray: The model has to guess whether"user"is mandatory or optional.
Multiply this by 15 or 20 tools in an enterprise agent runtime, and the model's tool-selection surface degrades into chaotic ambiguity.
2. Toolsmith System Architecture
To solve this, we designed Toolsmith: a tool that performs deterministic 0–100 linting of JSON tool schemas and provides an automated hardening loop powered by any OpenAI-compatible LLM endpoint (Ollama, Groq, OpenRouter, OpenAI) or Gemini.
Here is the high-level architecture:
┌────────────────────────────────────────────────────────────────┐
│ USER INPUT │
│ - Raw Tool Schemas (tools wrapper, bare array, single tool) │
└───────────────────────────────┬────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────┐
│ INPUT NORMALIZER │
│ - Detects shape & standardizes into NormalizedTool[] │
└───────────────────────────────┬────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────┐
│ DETERMINISTIC LINTING ENGINE │
│ (Pure TypeScript, Zero-Latency, No API calls required) │
│ │
│ ├── Description Clarity (Imperative verbs, min/max length) │
│ ├── Parameter Hygiene (Types, enums, required arrays) │
│ ├── Naming Conventions (snake_case, length, prefixing) │
│ ├── Sibling Tool Overlap (Jaccard distance, lexical matches) │
│ └── Token Economics (Byte size, token density, context cost) │
└───────┬───────────────────────┬────────────────────────┬───────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌───────────────┐ ┌───────────────┐
│ 0-100 Score │ │ Tool Category │ │ Tool │
│ & Grade Dial │ │ Breakdown │ │ Complexity │
│ (SVG arc) │ │ (Progress) │ │ (L / M / H) │
└───────┬──────┘ └───────┬───────┘ └───────┬───────┘
│ │ │
└───────────────────────┼────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────┐
│ SESSION HISTORY ENGINE (Recharts) │
│ - Tracks score improvement trajectory across analysis runs │
└───────────────────────────────┬────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────┐
│ AI CONTRACT HARDENING ENGINE │
│ - Injects typed schemas, imperative verbs, enum constraints │
│ - Connects to: Local Ollama, Groq, OpenRouter, or OpenAI API │
│ - Side-by-Side Diff Inspector with field-by-field reasoning │
└────────────────────────────────────────────────────────────────┘
3. The Deterministic Rule Engine
A critical design requirement for Toolsmith was determinism. Developers do not want to wait 8 seconds or spend $0.05 on API calls just to check if their JSON schema has typos or missing types.
The core linter runs synchronously in milliseconds:
export function runLinter(tools: NormalizedTool[]): LintReport {
const findings: Finding[] = [];
// 1. Description audits
for (const tool of tools) {
findings.push(...checkDescriptionQuality(tool));
findings.push(...checkParameterHygiene(tool));
findings.push(...checkNamingConventions(tool));
}
// 2. Cross-tool sibling overlap detection
findings.push(...checkToolOverlap(tools));
// 3. Score calculation
const totalScoreDeductions = findings.reduce(
(acc, f) => acc + f.scoreImpact,
0
);
const overallScore = Math.max(0, Math.min(100, 100 - totalScoreDeductions));
return {
tools,
findings,
overallScore,
grade: computeGrade(overallScore),
categories: computeCategoryScores(findings),
toolCount: tools.length,
};
}
Checking for Imperative Verbs
Models trigger tools most reliably when descriptions start with active, unambiguous command verbs. The linter enforces this check:
const IMPERATIVE_VERBS = new Set([
"get", "fetch", "search", "query", "find", "retrieve", "list", "read",
"create", "insert", "add", "post", "generate", "build", "make",
"update", "modify", "set", "patch", "delete", "remove", "cancel",
"send", "execute", "run", "calculate", "compute", "validate", "check"
]);
function checkDescriptionQuality(tool: NormalizedTool): Finding[] {
const findings: Finding[] = [];
const firstWord = tool.description.trim().split(/\s+/)[0]?.toLowerCase();
if (!IMPERATIVE_VERBS.has(firstWord)) {
findings.push({
id: `${tool.name}-desc-verb`,
category: "description",
severity: "warn",
toolName: tool.name,
field: "description",
message: `Description should begin with an active imperative verb (e.g., 'Fetch...', 'Search...'). Currently starts with: "${firstWord}".`,
fix: `Rewrite the first sentence to start with an active imperative command verb.`,
scoreImpact: 8,
});
}
return findings;
}
4. Measuring Tool Complexity
Not all tools carry equal cognitive load for an LLM. A tool with 8 top-level parameters and 3 layers of nested objects is significantly more likely to trigger execution errors than a simple key-value tool.
To expose this risk, we implemented a recursive tree walker that computes tool complexity:
export function computeToolComplexity(tool?: NormalizedTool): ToolComplexityInfo {
if (!tool?.parameters?.properties) {
return { level: "Low", paramCount: 0, nestedCount: 0, maxDepth: 0, summary: "0 params" };
}
let paramCount = 0;
let nestedCount = 0;
let maxDepth = 1;
function walk(props: Record<string, any>, depth: number) {
if (depth > maxDepth) maxDepth = depth;
const keys = Object.keys(props || {});
paramCount += keys.length;
for (const key of keys) {
const field = props[key];
if (field?.type === "object" || field?.properties) {
nestedCount++;
if (field.properties) walk(field.properties, depth + 1);
}
}
}
walk(tool.parameters.properties, 1);
let level: "Low" | "Medium" | "High" = "Low";
if (paramCount >= 7 || nestedCount >= 2 || maxDepth >= 3) {
level = "High";
} else if (paramCount >= 4 || nestedCount >= 1 || maxDepth >= 2) {
level = "Medium";
}
return {
level,
paramCount,
nestedCount,
maxDepth,
summary: `${paramCount} param${paramCount === 1 ? "" : "s"}, ${nestedCount} nested`,
};
}
The UI visualizes this with high-contrast badges (Low in emerald, Medium in amber, High in rose) right alongside each audited tool.
5. Visualizing Iterative Improvement with Recharts
When developers iterate on their schemas—or run our AI hardening engine—they need to see if their contracts are getting measurably better.
We integrated Recharts to render a lightweight, zero-gradient trend line:
<ResponsiveContainer width="100%" height={140}>
<LineChart data={history} margin={{ top: 10, right: 15, left: -25, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
<XAxis dataKey="label" stroke="#64748b" tick={{ fontSize: 10 }} />
<YAxis domain={[0, 100]} stroke="#64748b" tick={{ fontSize: 10 }} />
<Tooltip content={<CustomTooltip />} />
<Line
type="monotone"
dataKey="score"
stroke="#818cf8"
strokeWidth={2}
dot={{ fill: "#6366f1", r: 4, strokeWidth: 1, stroke: "#1e1b4b" }}
activeDot={{ r: 6, fill: "#a5b4fc" }}
/>
</LineChart>
</ResponsiveContainer>
This immediately surfaces the delta between an unoptimized schema (e.g. 42/100, Grade F) and its hardened variant (e.g. 96/100, Grade A).
6. Flexible AI Hardening Without .env Hassles
Most developer tools force you to clone a repo, configure a .env file, and restart containers just to test an AI model.
In Toolsmith, the OpenAI-compatible client configuration lives entirely in browser state:
-
Local Ollama: Works out of the box (
http://localhost:11434/v1, modelllama3.1). -
Groq Cloud: Blazing-fast hardening via
llama-3.3-70b-versatile. - OpenRouter / OpenAI: Plug in your personal API key and model of choice.
When the target endpoint is localhost or 127.0.0.1, Toolsmith calls it directly from the user's browser, avoiding server container networking bridges. For remote endpoints, it uses a lightweight server proxy to bypass CORS restrictions safely.
7. Key Takeaways for Production AI Agent Developers
Building robust agents requires treating tool schemas with the same rigor we apply to OpenAPI specs and gRPC protobufs:
-
Write imperative, trigger-focused descriptions: Never say
"Used for search". Say:"Search customer orders by date range or customer ID. Call when the user explicitly requests order status." -
Always define explicit
enumvalues: If a status is["pending", "shipped", "delivered"], write it out. Never leave it as an unconstrained"string". - Guard against overlapping tool scopes: If two tools have similar parameter names and verbs, merge them into a single parameterized tool or sharply distinguish their triggers.
- Audit schema token weight: Tool definitions are injected into every single prompt turn. Bloated tool schemas silently eat away your context window and inflate latency.
Code & more: https://www.dailybuild.xyz/project/252-toolsmith


Top comments (0)