Skip to content

docs: expand tool runner documentation with internal mechanics#881

Open
karpetrosyan wants to merge 1 commit intoanthropics:nextfrom
karpetrosyan:improve-tool-runner-docs
Open

docs: expand tool runner documentation with internal mechanics#881
karpetrosyan wants to merge 1 commit 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

@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

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
@karpetrosyan karpetrosyan force-pushed the improve-tool-runner-docs branch from cfe2e29 to 3dcf68a Compare March 4, 2026 18:48
@karpetrosyan karpetrosyan force-pushed the improve-tool-runner-docs branch from 3dcf68a to 627a4ca Compare March 4, 2026 18:53
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.

2 participants