Concept · 6 of 7

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.

Tool dispatch/result on scroll
implemented
Both events are first-class and captured on the scroll. Replay sees tool.result as an external commit.
SDK subscriber (in-process)
implemented
Register a handler with ai.WithTool; your SDK subscribes to tool.dispatch in your app process and invokes the handler when one arrives.
http.Handler shape
implemented
Handlers are standard net/http handlers. Dispatch payloads arrive as the request body, results go out the response.
Typed argument hydration
implemented
Register a struct alongside the handler; the runtime uses its schema to constrain the model and decode the dispatch payload.
Tool-level replay
implemented
Recorded tool.result events are fed back on replay — handlers do not re-run. This is what makes tests golden files.
Remote-service subscriber
designed
The scroll contract allows any subscriber anywhere to fulfill tool.dispatch. Packaged registration sugar for a different-process subscriber ships in a later milestone.
MCP integration
designed
No MCP transports today — not stdio, not HTTP. Exposing tools as MCP servers and consuming MCP servers as tools are both on the roadmap.

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.dispatch here.