Tool
A tool is a handler on the scroll. It subscribes to tool.dispatch events and publishes tool.result events — nothing more. Everything
else, including where the handler physically runs, is an
implementation detail.
A handler on the scroll
When the model wants to call a tool, the runtime commits a tool.dispatch event. A handler subscribed to that
topic picks it up, does its work, and commits the result as tool.result. Both events are first-class — the same
kind of commit as ai.request and ai.response, captured on the same scroll.
There is no tool manager, no separate execution context. The scroll is the substrate; handlers are subscribers. Everything weave knows about tools is visible in the scroll and in the handler registrations.
One pattern, one hop
Every tool call goes through the server. The runtime emits a tool.dispatch event; a subscriber picks it up, runs
the handler, and emits tool.result. There is no
direct execution path that bypasses the scroll — which is the
feature, not the bug. Every call is captured, every result is
replayable, every flow is identical regardless of where the
handler lives.
What varies is where the subscriber runs. Today the SDK subscribes in your app process, so the handler itself is in-process with your code — even though the dispatch round-trips through the server to get there:
// Your SDK subscribes to tool.dispatch in the caller's process.
// The handler runs locally; the dispatch round-trips through the
// server. This is the one pattern weave ships today.
ai.WithTool("calc", "adds two numbers",
http.HandlerFunc(calcHandler),
CalcArgs{},
)Because the contract is event-based, a subscriber running in a separate service — or behind an MCP endpoint — could fulfill the same dispatch just as well. Those are designed paths, not shipped ones; see the status table below for what's real today.
Handlers are http.Handler-shaped
A weave tool handler is a standard http.Handler. The runtime encodes the dispatch
payload as the request body and writes the result out the
response. If you've written a web handler, you've written a
weave tool:
// A tool handler is a standard http.Handler.
// Weave hydrates the dispatch payload into the request body.
type CalcArgs struct {
A int `json:"a"`
B int `json:"b"`
}
func calcHandler(w http.ResponseWriter, r *http.Request) {
var args CalcArgs
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
_ = json.NewEncoder(w).Encode(map[string]int{
"result": args.A + args.B,
})
}The handler is a blunt function: parse args, do work, write result. No weave-specific lifecycle, no framework APIs, no subclassing. When you need to move the same handler to a service, you point a URL at it — no code change.
LLMs narrate, code decides
The model's tool call is an intent, committed as tool.dispatch. The handler decides what happens
next. It has full agency: fulfill the call, transform the
arguments, refuse, delegate to another service, or emit a
different event entirely. The LLM does not execute the tool —
the runtime does, and the handler has the final word.
This is the load-bearing philosophical claim of weave. Models are good at narrating what should happen next. Code is good at deciding whether it does. A tool handler is the place those two responsibilities meet — cleanly, with an event boundary between them that the scroll captures for later scrutiny.
What a tool call looks like on the scroll
Zooming in on the relevant events from a run that uses calc:
scroll: run-abc123
[0] ai.request derived { prompt: "add 137 + 488" }
[1] ai.response external { tool_call: { name: "calc", args: { a: 137, b: 488 } } }
[2] tool.dispatch derived { name: "calc", args: { a: 137, b: 488 } }
[3] tool.result external { result: 625 }
[4] ai.request derived { prompt: "…given result 625, answer the user" }
[5] ai.response external { text: "625" }Replay treats tool.result as
external. On replay, the recorded result is played back
and the handler does not re-run. That is why tests on tool-using
workflows become golden files instead of mocks — you are feeding
the same real external fact back into the same runtime.
Status
In-process tools are the most mature surface in weave — handler shape, typed args, and replay semantics are all real today. The remote-service and MCP paths work for common cases; the rough edges are in the auxiliary concerns (retries, streaming, auth, remote transports) rather than the core contract.
Not to be confused with
- MCP. Model Context Protocol is a transport for moving tool calls between processes. Weave doesn't ship MCP support today, but when it does it will be one more subscriber location — not a different tool model. Weave's contract stays the scroll-event pair regardless of what's on the wire.
- LangChain / LlamaIndex tools. Those frameworks treat tools as in-process Python objects wired into an agent loop. Weave treats tools as event-driven handlers that can live anywhere. Same word, different primitive.
- OpenAI function calling. Function calling is
the model-facing API — the model emits a tool_call in its
response. A weave tool is the runtime-facing side: what
happens when that tool_call lands on the scroll. Every
provider's function-calling format lowers into
tool.dispatchhere.