Problem
When a provider features server-side (provider-executed) tools, there's no way to change session state while one of those tools is running — for example, playing a background "thinking" sound for exactly as long as a web search takes, then stopping it when the search returns.
Provider tools run inside a single LLM stream, so the agent state machine never sees them start or finish, and preemptive_generation decouples the timing further. The agent thinking state isn't a reliable proxy.
#5869 adds a terminal function_tools_called event that fires once at the end of a turn. That's useful for post-turn analysis, but it comes too late to drive anything during execution, and it doesn't give a distinct start signal.
Proposal
Add a provider-tool lifecycle that mirrors the existing local-tool one (ToolCallStarted / ToolCallEnded via tool_execution_updated):
- A plugin translates its provider stream (e.g. Mistral's
ToolExecutionStarted / ToolExecutionDone) into a generic provider_tool_call event, emitted on the LLM's EventEmitter
- The framework bridges that onto the session as
provider_tool_execution_updated, carrying ProviderToolCallStarted | ProviderToolCallEnded.
- Consumers subscribe once:
session.on("provider_tool_execution_updated", ...).
Any provider with server-side tools gets the session event for free once it adds the translation in its plugin.
Draft PR
Implementation (pipeline path, with tests, mypy strict + ruff clean): #6973
It could complement #5869 (keep that as the per-turn summary, this as the lifecycle) or replace it — happy to converge on naming and shape. Realtime path and the remote_session protobuf relay are natural follow-ups.
Problem
When a provider features server-side (provider-executed) tools, there's no way to change session state while one of those tools is running — for example, playing a background "thinking" sound for exactly as long as a web search takes, then stopping it when the search returns.
Provider tools run inside a single LLM stream, so the agent state machine never sees them start or finish, and
preemptive_generationdecouples the timing further. The agentthinkingstate isn't a reliable proxy.#5869 adds a terminal
function_tools_calledevent that fires once at the end of a turn. That's useful for post-turn analysis, but it comes too late to drive anything during execution, and it doesn't give a distinct start signal.Proposal
Add a provider-tool lifecycle that mirrors the existing local-tool one (
ToolCallStarted/ToolCallEndedviatool_execution_updated):ToolExecutionStarted/ToolExecutionDone) into a genericprovider_tool_callevent, emitted on the LLM's EventEmitterprovider_tool_execution_updated, carryingProviderToolCallStarted | ProviderToolCallEnded.session.on("provider_tool_execution_updated", ...).Any provider with server-side tools gets the session event for free once it adds the translation in its plugin.
Draft PR
Implementation (pipeline path, with tests, mypy strict + ruff clean): #6973
It could complement #5869 (keep that as the per-turn summary, this as the lifecycle) or replace it — happy to converge on naming and shape. Realtime path and the
remote_sessionprotobuf relay are natural follow-ups.