Repository navigation
Run a round's tool calls at once in every provider - #375
Conversation
cc8bd87 to
d69ecf9
Compare
mattt
left a comment
There was a problem hiding this comment.
Thanks, @gtokman. It's great to have seven copies of the tool loop down to one. Two things before we can merge this:
- When a call throws,
invokeToolCallawaitsdidFailToolCallbefore 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 slowdidFailToolCalland two failing tools would cover it. didExecuteToolCallnow 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.
The calls a model makes in one round now run concurrently, in the shared resolver every provider's tool loop goes through (`resolveProfileToolCalls`). MLX and llama.cpp now use it too, in place of their own copies. 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, and `parallel_tool_calls` is unchanged: it governs what a model may ask for, not how the calls run. The delegate and the profile still decide each call in order before any runs, and a `.stop` ends the round first. Once the calls are done, the profile's tool-output callbacks and the delegate's `didExecuteToolCall` hear about them in the model's order, one at a time, and the results keep that order. Each call sees the history as the round began. When a call throws, the round is cancelled first: the other calls are cancelled and waited for, and only then does the delegate hear about the error, and only the one the round throws. Resolves huggingface#359. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
d69ecf9 to
a3c09eb
Compare
|
Thanks, @mattt. I rebased on
The tests now coordinate with gates rather than timing the round, which is what failed on CI. Two things worth a look:
|
|
Thanks, @gtokman! I merged The first fixes a bug with MLX and llama.cpp. Each resolver made a The second updates the docs. A Tool's |
Resolves #359.
The calls a model makes in one round now run concurrently, in the shared resolver every provider's tool loop goes through (
resolveProfileToolCallsinProfileToolExecution.swift). MLX and llama.cpp now use it too, in place of their own copies.As discussed in the issue:
Sendableand 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.parallel_tool_callsis unchanged. It governs what the model may ask for, not how the calls run..stopstill ends the round first. Once the calls are done, the profile's tool-output callbacks anddidExecuteToolCallhear about them in the model's order, one at a time. Results keep that order.Each call sees the history as the round began, since the calls before it in the round may still be running.
With MLX and llama.cpp on the shared resolver, a dynamic profile's tool callbacks run for them too.
Tests
ToolRoundTestsuses gates rather than timings, so they don't depend on how fast the runner is:.stop. A stop decision ends the round before any call runs.The full suite has the same failures as
mainhere: the 13 tests that need live services. It builds with--traits MLX,Llama.The README's Tool Calling section notes that calls run at once and when the delegate hears about them.
🤖 Generated with Claude Code