Tool Streams
Some tools return quickly but keep doing work in the background. For example,look_out_for starts a perception loop and waits minutes for a match;
follow_person returns “started following” right away and then keeps publishing
status until the target is lost or the skill is cancelled.
Tool streams are the channel those background updates travel on. Every update is
routed to the MCP client that made the original tools/call so it can display
the progress alongside the tool’s response.
Under the hood, tool streams turn into MCP notifications/progress frames bound
to the client’s progressToken.
For the dimos-internal McpClient, each update lands on the agent’s message
queue as a HumanMessage tagged [tool:<tool_name>] so the model can reason
about it. If a client didn’t send a progressToken (raw curl, older tools), the
stream falls back to notifications/message log frames so updates still arrive,
just without the per-call UI affordance.
Skills never touch the wire format. Module exposes three helpers:
start_tool, tool_update, stop_tool. The framework handles the rest.
Quick example: inline updates
tool_update shows up in the MCP client as a progress notification bound
to the original count_to call. The skill’s return value is still the final
tools/call result; the streamed updates are in addition to it, not instead
of it.
Background-thread example
Most real skills don’t block theirtools/call response. They kick off
background work and return immediately. The background thread publishes updates
for as long as the work is running. This is the follow_person / look_out_for
shape.
"Started streaming 3 updates.". The
background loop then fires tool_update calls from a worker thread, and each
one reaches the client as a progress frame attached to the original call. When
the loop finishes (or errors), stop_tool tears the channel down.
The rules
-
start_toolmust run on the skill’s main thread. The@skillwrapper establishes a per-call context on the thread where the skill is invoked;start_toolreads that context to capture the caller’sprogressToken. Constructing a tool stream from a detached thread raisesRuntimeErrorwith a message that names the invariant. -
start_tool("x")while another"x"is already open on the same module raises. Two concurrent streams for the same logical tool is almost always a bug. If you really need to restart, callstop_tool("x")first. -
tool_updateandstop_toolare thread-safe. Background workers can fire updates from any thread oncestart_toolhas registered the channel.
What a client sees
When an MCP client callstools/call with a progressToken, every
tool_update on that call is delivered as a notifications/progress JSON-RPC
frame:
progressToken is the one the client supplied on its tools/call request.
progress is a per-stream monotonic counter (1, 2, 3, …). The dimos-specific
_meta.tool_name hint is used by our internal McpClient to route the update
into the agent transcript as [tool:start_streaming] Update 1 of 3; external
clients that don’t recognize it simply pass it through.