Chapter 23

Anthropic: The Messages API and Content Blocks

Anthropic's Messages API is stateless, and that defines the whole adapter. Claude returns tool_use content blocks; the application saves the entire assistant message, runs the tools, and sends one user message with the corresponding tool_result blocks. No server-side continuation id - you keep the history.

TypeScript
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();
const model = process.env.ANTHROPIC_MODEL ?? "claude-sonnet-5";

type AnthropicState = {
  messages: any[];
  instructions: string;
  tools: ToolDefinition[];
};

function toAnthropicTool(tool: ToolDefinition) {
  return {
    name: tool.name,
    description: tool.description,
    input_schema: tool.parameters,
    strict: true
  };
}

The shared request appends the assistant message to the history and normalizes the content blocks into our ProviderTurn.

TypeScript
async function request(state: AnthropicState): Promise<ProviderTurn> {
  const message = await client.messages.create({
    model,
    max_tokens: 2_048,
    system: state.instructions,
    tools: state.tools.map(toAnthropicTool),
    messages: state.messages
  });

  const nextState = {
    ...state,
    messages: [
      ...state.messages,
      { role: "assistant", content: message.content }
    ]
  };

  const calls = message.content
    .filter(function isToolUse(block: any) {
      return block.type === "tool_use";
    })
    .map(function normalize(block: any): ToolCall {
      return { id: block.id, name: block.name, arguments: block.input };
    });

  if (calls.length > 0) return { kind: "tool_calls", calls, state: nextState };

  const text = message.content
    .filter(function isText(block: any) { return block.type === "text"; })
    .map(function takeText(block: any) { return block.text; })
    .join("");
  return { kind: "text", text, state: nextState };
}

And returning the results packs the tool_result blocks into one user message.

TypeScript
async function resume(rawState: unknown, results: ToolResult[]) {
  const state = anthropicStateSchema.parse(rawState);
  const toolResults = results.map(function toToolResult(result) {
    return {
      type: "tool_result" as const,
      tool_use_id: result.callId,
      content: JSON.stringify({ ok: !result.isError, value: result.output }),
      is_error: result.isError
    };
  });

  return request({
    ...state,
    messages: [...state.messages, { role: "user", content: toolResults }]
  });
}

Two details here are easy to break out of habit. The first is about the model.

Claude Sonnet 5. The model uses adaptive thinking by default. Don't carry over the old manual extended-thinking setting and don't set a non-default temperature, top_p, or top_k: the current migration guide reports that Sonnet 5 rejects such parameters.

The second is about the shape of the results message.

Parallel results in one message. If Claude called several tools, return one tool_result per tool_use in the single next user message. Don't insert text before the result blocks.

The direct Messages API is a stable point of understanding. But Anthropic also has a convenient automatic loop - the Tool Runner.

Knowledge check

How should you handle temperature and manual thinking for Claude Sonnet 5?

Links