keel
Model

Tool capabilities

Keep tool calls and their results usable when a chain changes tiers.

A tier serving a tool-carrying turn must understand tool definitions, emit calls, and replay tool results. Returning text alone does not satisfy that contract.

agenttools: [lookup]primarysupportsTools: truefallbacksupportsTools: falsedefineAgent → KeelError: every tier must support tools
An agent with tools requires tool support from every tier. One text-only fallback rejects the configuration before any turn runs.

Declare capabilities honestly

Adapter fieldContract
supportsToolsExposes available tools, emits call events, and handles their transcript results.
canForceToolCan constrain a provider request to a specific tool when forceTool is supplied.

Known models in the shipped catalog declare both. Custom model definitions and custom adapters must declare the behavior they actually support.

Require tools across the whole chain

When an agent declares tools, every tier must declare supportsTools: true. defineAgent throws KeelError if even one fallback is incompatible. The message names the agent, chain, and offending tiers. A text-only chain remains valid for an agent with tools: [].

TypeScript rejects known incompatibilities too. customModel, provider classes and factories, and defineChain preserve literal capability values. Everything in this block compiles except the final defineAgent call — the error it demonstrates:

import { z } from "zod";
import {
  customModel,
  defineAgent,
  defineChain,
  defineTool,
} from "@keel-dev/core";
import { OpenAIProvider } from "@keel-dev/core/providers/openai";

const lookupTool = defineTool({
  id: "lookup_order",
  description: "Look up an order by its ID.",
  access: "public",
  input: z.object({ orderId: z.string() }),
  execute: (input) => ({ result: { found: false, query: input } }),
});

const primary = new OpenAIProvider({
  apiKey: process.env.OPENAI_API_KEY,
  model: "gpt-5.1",
});

const textModel = customModel({
  provider: "openai",
  id: "my-text-model",
  windowTokens: 32_000,
  maxOutputTokens: 4_000,
  supportsTools: false,
  canForceTool: false,
});

const chain = defineChain({
  id: "assistant-chain",
  tiers: [
    primary,
    new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY, model: textModel }),
  ],
});

// TypeScript error: every tier must declare supportsTools: true.
defineAgent({
  id: "assistant",
  chain,
  prompt: "Help the user.",
  reads: [],
  emits: [],
  tools: [lookupTool],
});

For hand-written adapters, use satisfies ModelAdapter to check their shape while preserving literal capabilities. An explicit ModelAdapter or ChainDef annotation can widen that information. Dynamic booleans and tool arrays are validated when defineAgent executes, even if TypeScript cannot prove incompatibility.

Keep the execution guard

The low-level chain runner also checks the actual available tool list. It skips incompatible tiers loudly, emitting a fallback chain event whose detail.reason is "no-tool-support"; the skipped tier is also recorded in fallbacksTaken. This remains a backstop for direct chain calls and runtime-added submission tools; it does not allow a mixed chain through defineAgent for an agent declaring tools.

Forced tool selection and delivery

For a required deliverable, the runtime can request a particular submission tool during bounded recovery rounds. Adapters that implement forced selection translate forceTool into provider configuration.

A capability flag alone does not prove delivery. The close gate still checks whether the required deliverable was submitted. See Failure and recovery and Turn.

Inspect in Studio

First verify that a tool-using agent with a text-only fallback fails during configuration. With a compatible chain, run a turn in Studio and inspect MODEL to confirm the selected tier receives tools and replays their results. Custom adapters should cover both behaviors in their tests.

Next: Limits and usage.