Skip to content

docs: expand tool runner documentation with internal mechanics#881

Open
karpetrosyan wants to merge 28 commits intoanthropics:nextfrom
karpetrosyan:improve-tool-runner-docs
Open

docs: expand tool runner documentation with internal mechanics#881
karpetrosyan wants to merge 28 commits intoanthropics:nextfrom
karpetrosyan:improve-tool-runner-docs

Conversation

@karpetrosyan
Copy link
Collaborator

@karpetrosyan karpetrosyan commented Jan 15, 2026

How the Tool Runner Works

Iteration Lifecycle

On each iteration, the tool runner performs three key operations:

  1. State update (only if unchanged): The tool runner appends the last message from the API response (the one yielded to the client) to its internal state only if the state wasn't modified during that iteration via pushMessages() or setMessagesParams(). If the state was mutated, it ignores that message and continues using the user-mutated state.

  2. Tool handling (always): The tool runner inspects the last message. If it contains any tool_use blocks, it handles them and appends an appropriate message containing the corresponding tool_result blocks — regardless of whether the state was mutated.

  3. Next request + repeat: It sends a new request to the API using the current internal state, yields the new message to the user, and repeats the loop.

generateToolResponse()

The generateToolResponse() method is a helper that reads the tool_use blocks, calls the tools, and generates a message containing the corresponding tool_result blocks. Note that:

  • It does not mutate state — calling generateToolResponse alone won’t prevent the loop from adding its message to state
  • It caches results to avoid redundant calls — if you pass the same state, it returns the cached result

If you push both the last message and the result of generateToolResponse() into the state, the tool runner will effectively do nothing except send the next request:

for await (const message of runner) {
  const defaultResponse = await runner.generateToolResponse();

  if (defaultResponse) {
    runner.pushMessages(
      {
        role: message.role,
        content: message.content,
      },
      defaultResponse,
    );
  }
}

Execution Flow Diagram

Note: If the Mermaid diagram below doesn't render in your environment, view it on GitHub: https://github.com/anthropics/anthropic-sdk-typescript/blob/main/helpers.md

sequenceDiagram
  autonumber
  participant U as User
  participant TR as ToolRunner
  participant API as Model API
  participant Tools as Tools

  loop Repeat until done
    TR->>API: Send request (using current state)
    API-->>TR: Message
    TR-->>U: Yield message

    note over U: User can read message<br/>and optionally change state via<br/>pushMessages or setMessagesParams
    U->>TR: Resume iteration

    alt User did not change state
      TR->>TR: Append message to history
    else User changed state
      TR->>TR: Keep user state (no auto-append)
    end

    alt Message contains tool request
      TR->>Tools: Run tools (with generateToolResponse)
      Tools-->>TR: Tool results
      TR->>TR: Append tool results
    else No tool request
      TR->>TR: Finish
    end
  end
Loading

sd-st and others added 19 commits November 18, 2025 15:51
fix(client): don't strip path from filename for skills endpoint
docs: update README with Claude branding

- Add Claude sparkle logo to header
- Rename to "Claude SDK for TypeScript"
- Use "Claude API" terminology instead of "Anthropic API"
- Link to TypeScript SDK docs at platform.claude.com
- Simplify and consolidate documentation sections

Co-Authored-By: Claude <noreply@anthropic.com>
* "Claude PR Assistant workflow"

* "Claude Code Review workflow"
Co-authored-by: Vibeke Tengroth <vibeketengroth@gmail.com>
Co-authored-by: Packy Gallagher <packy@anthropic.com>
@karpetrosyan karpetrosyan requested a review from a team as a code owner January 15, 2026 15:38
helpers.md Outdated

#### Iteration Lifecycle

On each iteration, the tool runner performs three key operations:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we clarify "on" — is it before or after the user's loop block is executed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve changed the step order instead and mentioned that the first step is before the yield—hope that makes it clearer

dtmeadows and others added 2 commits January 26, 2026 09:09
* feat(api): update via SDK Studio

* feat(api): update via SDK Studio

* codegen metadata

* feat: migrate output_format to output_config.format for structured outputs

- Update beta header from structured-outputs-2025-11-13 to structured-outputs-2025-12-15
- Transform deprecated output_format parameter to output_config.format
- Add warning when both output_format and output_config.format are provided
- Update beta-parser to support parsing from either location
- Update tests to expect new output_config.format structure

Co-Authored-By: Claude Code (/Users/davidmeadows/stainless/stainless) <noreply@anthropic.com>

* feat: add @deprecated JSDoc tag and update example to use output_config

- Add @deprecated JSDoc tag to output_format parameter in BetaParseableMessageCreateParams
- Update parsing-zod.ts example to use output_config.format instead of output_format

Co-Authored-By: Claude Code (/Users/davidmeadows/stainless/stainless/dist/customer-repos/stainless-sdks/anthropic-typescript) <noreply@anthropic.com>

* fix: throw error instead of warning when both output params provided

Co-Authored-By: Claude Code (/Users/davidmeadows/stainless/stainless/dist/customer-repos/stainless-sdks/anthropic-typescript) <noreply@anthropic.com>

* Remove note about beta header injection

* clean up things a bit

* revert changes

* undo a change

* undo changes from next

* fixup!

* fixes

* fixup!

* fix: move structured output formatting to dedicated function and add to token counting

* feat: Test exception case in structured output

* feat: improve types and add other unit test for structured output transformation

---------

Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
Co-authored-by: Claude Code (/Users/davidmeadows/stainless/stainless) <noreply@anthropic.com>
Co-authored-by: Cameron McAteer <246350779+cameron-mcateer@users.noreply.github.com>
Add helper functions for integrating with the [Model Context Protocol
(MCP) SDK](https://github.com/modelcontextprotocol/sdk). These helpers
reduce the boilerplate required to convert MCP types to Anthropic API
types from ~100+ lines to single function calls.

**New helpers:**
- `mcpTool(tool, mcpClient)` - Convert single MCP tool to
`BetaRunnableTool`
- `mcpTools(tools, mcpClient)` - Convert array of MCP tools to
`BetaRunnableTool[]` for use with `toolRunner()`
- `mcpMessage(message)` - Convert single MCP `PromptMessage` to
`BetaMessageParam`
- `mcpMessages(messages)` - Convert array of MCP `PromptMessage` to
`BetaMessageParam[]`
- `mcpContent(content)` - Convert single MCP content block to Anthropic
content block
- `mcpResourceToContent(resource)` - Convert MCP resource to content
block (document or image)
- `mcpResourceToFile(resource)` - Convert MCP resource to `File` for
`files.upload()`
helpers.md Outdated

1. **State update (only if unchanged)**: The tool runner appends the last message from the API response (the one yielded to the client) to its internal state only if the state wasn't modified during that iteration via `pushMessages()` or `setMessagesParams()`. If the state was mutated, it ignores that message and continues using the user-mutated state.

2. **Tool handling (always)**: The tool runner inspects the last message. If it contains any `tool_use` blocks, it handles them and appends an appropriate message containing the corresponding `tool_result` blocks — regardless of whether the state was mutated.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we be precise here about where they are appended to?

Suggested change
2. **Tool handling (always)**: The tool runner inspects the last message. If it contains any `tool_use` blocks, it handles them and appends an appropriate message containing the corresponding `tool_result` blocks — regardless of whether the state was mutated.
2. **Tool handling (always)**: The tool runner inspects the last message. If it contains any `tool_use` blocks, it handles them and appends an appropriate message containing the corresponding `tool_result` blocks to `runner.params.messages` — regardless of whether the `runner.params` was mutated by the consumer.

helpers.md Outdated

2. **Tool handling (always)**: The tool runner inspects the last message. If it contains any `tool_use` blocks, it handles them and appends an appropriate message containing the corresponding `tool_result` blocks — regardless of whether the state was mutated.

3. **Next request + repeat**: It sends a new request to the API using the current internal state, yields the new message to the user, and repeats the loop.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does "repeat the loop" mean yielding control to the consumer (executing the loop block)?

helpers.md Outdated

#### generateToolResponse()

The `generateToolResponse()` method is a helper that reads the `tool_use` blocks, calls the tools, and generates a message containing the corresponding `tool_result` blocks. Note that:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we mention whether this is what the tool runner calls internally on each iteration?

The `generateToolResponse()` method is a helper that reads the `tool_use` blocks, calls the tools, and generates a message containing the corresponding `tool_result` blocks. Note that:

- It **does not mutate state** — calling generateToolResponse alone won’t prevent the loop from adding its message to state
- It **caches results** to avoid redundant calls — if you pass the same state, it returns the cached result
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: this is interesting because it prevents a user from implementing retries of tools when they fail

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe there’s a way to do retries. If we could wrap the tool with a function that handles retries, we’d get retry functionality even without caching—though I’m not sure how flexible it is

On each iteration, the tool runner performs three key operations:

1. **Next request:** It sends a new request to the API using the current internal state (before yield), and yields the new message to the user.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we insert a step here that is "The code inside the user's for loop block runs, with access to the current message response"

@karpetrosyan karpetrosyan force-pushed the improve-tool-runner-docs branch from 7dcc12a to 31220e0 Compare January 27, 2026 14:07
@stainless-app stainless-app bot force-pushed the next branch 2 times, most recently from eeb7fab to 7b4849b Compare January 29, 2026 17:24
@stainless-app stainless-app bot force-pushed the next branch 2 times, most recently from 6bcd8a5 to 883bbb6 Compare February 7, 2026 02:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants