Tool Calling
Lets a model call your own functions mid-turn, see the results, and continue - using each provider's native tool-calling API, not a prompted convention. The OpenAI-wire-format backends share one implementation; Anthropic and Ollama each have a different wire shape under the hood, normalized to the same interface. This is a different mechanism from runAgents()/skill-style dispatch.
import { generateWithTools, anthropic } from "@aviasole/shapecraft";
import { z } from "zod";
import type { ToolDefinition } from "@aviasole/shapecraft";
const getWeather: ToolDefinition = {
name: "get_weather",
description: "Get current weather for a city",
parameters: z.object({ city: z.string() }),
handler: async ({ city }: { city: string }) => ({ city, tempC: 18, condition: "cloudy" }),
};
const result = await generateWithTools(
anthropic({ model: "claude-haiku-4-5-20251001" }),
[getWeather],
z.object({ summary: z.string() }),
"What's the weather in Lisbon? Use the get_weather tool, then summarize it in one sentence."
);
console.log(result.data); // { summary: "It's 18°C and cloudy in Lisbon." }
console.log(result.toolCalls); // [{ type: "tool-call", call: { name: "get_weather", args: { city: "Lisbon" } }, result: {...} }]ToolDefinition.parameters reuses the same SchemaInput machinery as everywhere else in shapecraft (Zod or raw { jsonSchema } - pattern/validate/xml aren't valid tool-parameter shapes and throw clearly if used). The loop runs until the model stops requesting tools, then extracts + validates the final answer through an ordinary generate() call over the transcript - the same one-shot extraction pattern turnaround mode uses at its completion sentinel, so the structured-answer guarantee is identical to any standalone generate() call, not a weaker tool-calling-specific check.
What this does and doesn't guarantee:
- Tool argument validation is real and structural - each call's
argsgoes through the exact sameSchemaInputvalidation pipeline as anygenerate()call, before the handler ever runs. A model producing malformed args fails that step, gets fed back as an error the model can retry from - it never silently reaches your handler. - The final answer is validated exactly as strongly as a standalone
generate()call with that schema - tool-calling is a different path to the same validated-answer guarantee, not a weaker one. - Tool selection is not validated. Nothing checks the model picked the "right" tool, or that it should have called one at all - that's inherent model behavior, same structural-vs-semantic gap Guarantees documents elsewhere.
- A tool handler's own correctness is entirely your responsibility. shapecraft only guarantees the handler's return value gets fed back to the model as-is.
- No loop-prevention beyond
maxTurns(default 10) - a model that keeps requesting tools forever throwsMaxToolTurnsExceededError. A handler that throws aborts immediately withToolExecutionErrorinstead (a business-logic failure, never retried the way a bad argument is - re-prompting the model can't fix a broken handler).
Available on every cloud backend: openai(), groq(), anthropic(), ollama(), fireworks(), mistral(), openRouter(), deepseek() and gemini(). The only exception is llamaCpp() - local GGUF inference exposes no tools API. Check model.capabilities.toolCalling rather than hardcoding that list (see Backends & Guarantee Levels). Non-streaming in v1, same reasoning runAgents() gives for skipping streaming.

