Skip to content

Run a round's tool calls at once in every provider - #375

Open
gtokman wants to merge 1 commit into
huggingface:mainfrom
gtokman:tools-concurrently
Open

gtokman wants to merge 1 commit into
huggingface:mainfrom
gtokman:tools-concurrently

Conversation

@gtokman

@gtokman gtokman commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Resolves #359.

The calls a model makes in one round now run concurrently, in every provider whose tool loop runs in the library, through one shared resolver (resolveToolRound in Shared/ToolRound.swift). It replaces the seven near-identical copies in Anthropic, Gemini, Ollama, OpenAI, Open Responses, MLX and llama.cpp.

As discussed in the issue:

  • Always on. This matches Foundation Models, which requires tools to be Sendable and may call a tool concurrently with itself or with other tools. A tool that isn't safe to run alongside others can serialize its own work. There's no setting.
  • Errors. When a call throws, the others are cancelled and the first error is reported, as a throwing task group does. Calls cancelled because of it aren't reported to the delegate as failures.
  • parallel_tool_calls is unchanged. It governs what the model may ask for, not how the calls run.
  • Order and delegate. Results keep the order the model gave the calls in. The delegate hears about the round and decides each call in order, as before, and a .stop still ends the round before any call runs.

Two 300 ms calls in one round now take about 0.3 s instead of 0.6 s.

Tests

ToolRoundTests covers:

  • calls running at once;
  • results in the model's order;
  • the first error cancelling the others, reported once;
  • .stop;
  • provided output and a missing tool.

The full suite has the same failures as main here: the 13 tests that need live services.

The README's Tool Calling section notes that calls run at once.

🤖 Generated with Claude Code

https://claude.ai/code/session_01B4WBsc1Jp8fGufe89MhQFB

The calls a model makes in one round now run concurrently, through one
shared resolver (`resolveToolRound`) that every provider with its own
tool loop uses: Anthropic, Gemini, Ollama, OpenAI, Open Responses, MLX
and llama.cpp. This matches Foundation Models, which requires tools to
be `Sendable` and may call a tool concurrently with itself or with other
tools; a tool that isn't safe to run alongside others can serialize its
own work. There's no setting to turn it on.

Results keep the order the model gave the calls in. The delegate hears
about the round and decides each call in order, as before, and a `.stop`
still ends the round before any call runs. When a call throws, the
others are cancelled and the first error is reported, as a throwing task
group does; calls cancelled because of it aren't reported as failures.
`parallel_tool_calls` is unchanged: it governs what a model may ask for,
not how the calls run.

Two 300 ms calls in one round take about 0.3 s instead of 0.6 s.

Resolves huggingface#359.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B4WBsc1Jp8fGufe89MhQFB
@gtokman
gtokman force-pushed the tools-concurrently branch from cc8bd87 to d69ecf9 Compare October 29, 2026 13:38

@mattt mattt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks, @gtokman. It's great to have seven copies of the tool loop down to one. Two things before we can merge this:

  1. When a call throws, invokeToolCall awaits didFailToolCall before it throws, so the group can't cancel the other calls until the delegate returns. If another call fails during that wait, its error can reach the group first, and the delegate hears about both failures. Could the failure cancel the round first, and then report to the delegate only the error that the round throws? A test with a slow didFailToolCall and two failing tools would cover it.
  2. didExecuteToolCall now runs as each call finishes, so the delegate hears about calls in the order they complete, and the callbacks can overlap. Before, it heard about them in the model's order, one at a time, and I'd like to keep that. Could the calls still run at once, but report to the delegate in the model's order, one at a time? A test where the calls finish in reverse order would cover it.

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.

Run a round's tool calls at once

2 participants