Your first tool-using agent
We'll build a tiny agent that knows it can't read a clock on its own. It has to call a tool to find out the current time, then use the answer to structure its response. Forty lines of Go, start to finish.
What you'll build
An agent called clock with one tool, now.
When asked "what's the current UTC time, and is it morning,
afternoon, evening, or night?" the model will dispatch now, receive the current time, and return a typed
answer.
By the end you'll have:
- A runnable agent binary that uses a tool
- An inspectable state-machine description of that agent
- A clear picture of which events land on the scroll during the run
Prerequisites
- Go 1.22+ installed and on your PATH.
- The
weaveCLI. For now, run it viago run ./cmd/weavefrom a weave checkout — published binaries ship with beta access. - An OpenAI API key exported as
OPENAI_API_KEY. Other providers land later; for this guide we assume OpenAI.
The whole thing
Create clock.go with the file below. We'll walk
through each piece after:
// clock.go — an agent that asks for the current time via a tool.
package main
import (
"encoding/json"
"fmt"
"time"
"github.com/zilfi-io/weave/pkg/weave"
)
// NoArgs is an empty parameter type — the tool takes no inputs,
// but we still need a struct so the model has a schema.
type NoArgs struct{}
// Answer is the structured output the agent produces.
type Answer struct {
UTCTime string `json:"utc_time"`
Period string `json:"period" jsonschema:"description=morning, afternoon, evening, or night"`
}
// The tool. Returns the current UTC time in RFC3339 format.
var nowTool = weave.Tool("now",
weave.WithDescription("Returns the current UTC time in RFC3339 format."),
weave.WithParams(NoArgs{}),
weave.WithHandler(func(w *weave.ToolResponse, req *weave.ToolRequest) {
// Decode args even when there are none — keeps the handler
// shape consistent across tools.
var p NoArgs
if err := json.NewDecoder(req.Args).Decode(&p); err != nil {
w.Error(err)
return
}
fmt.Fprint(w, time.Now().UTC().Format(time.RFC3339))
}),
)
func main() {
agent := weave.Agent("clock",
weave.WithSystem("You are a helpful assistant. Use the provided tool to answer questions about time."),
weave.WithPrompt("What is the current UTC time, and is it morning, afternoon, evening, or night?"),
weave.WithTool(nowTool),
weave.WithOutput(Answer{}),
)
weave.NewApp(agent).Run()
}Walking through it
The file has three pieces: parameter and output types, the tool, and the agent.
Parameter and output types
NoArgs is an empty struct — the now tool takes no input, but the registration still expects a
schema. An empty struct is the right thing here. Answer is the shape weave will constrain the model to produce; the jsonschema tag becomes part of the schema the model
sees, so it knows what values period accepts.
The tool
var nowTool = weave.Tool("now",
weave.WithDescription("Returns the current UTC time in RFC3339 format."),
weave.WithParams(NoArgs{}),
weave.WithHandler(func(w *weave.ToolResponse, req *weave.ToolRequest) {
var p NoArgs
if err := json.NewDecoder(req.Args).Decode(&p); err != nil {
w.Error(err)
return
}
fmt.Fprint(w, time.Now().UTC().Format(time.RFC3339))
}),
)Three options, in order: a description (for the model), a
parameter schema (for the model to constrain its arguments), and
a handler (for the runtime to invoke). The handler takes a *weave.ToolResponse (io.Writer plus Error(err)) and a *weave.ToolRequest (io.Reader of arguments plus a context). The shape is inspired
by http.Handler — if you've written a web handler,
this reads the same way.
The handler runs in your app process. Under the hood the
runtime commits a tool.dispatch event; the SDK
subscribes, invokes your handler, and commits the result as tool.result. See the Tool concept page for the full
picture.
The agent
agent := weave.Agent("clock",
weave.WithSystem("You are a helpful assistant. Use the provided tool to answer questions about time."),
weave.WithPrompt("What is the current UTC time, and is it morning, afternoon, evening, or night?"),
weave.WithTool(nowTool),
weave.WithOutput(Answer{}),
)
weave.NewApp(agent).Run()weave.Agent returns a node. Tools you want the
agent to have access to get passed in with weave.WithTool — one call per tool. The agent is
then handed to weave.NewApp(...).Run(), which sets
up an in-memory scroll, wires up the runner, and blocks on
completion.
Run it
Export your API key, then run through the weave CLI. The CLI compiles and runs the file as a subprocess, captures its event stream, and prints a live summary:
$ export OPENAI_API_KEY=sk-...
$ weave run ./clock.go
[agent.started]
[ai.cost] gpt-4o — 142 tokens, $0.0011
[ai.cost] gpt-4o — 218 tokens, $0.0018
[agent.completed]
--- summary ---
AI calls: 2
Tokens: 360
Cost: $0.0029
Latency: 1.847s
Events: 14Why two AI calls? The first one produces a tool_call for now; the runtime
dispatches the tool, gets the result, and loops back to the
model with the result included. The second call produces the
final structured answer.
Inspect the graph
A weave agent is a workflow definition, not imperative code. You can ask the CLI to show you the state machine it compiled from your options:
$ weave inspect ./clock.go
{
"name": "clock",
"entry": "start",
"states": ["done", "processing", "processing.thinking", "processing.tool", "start"],
"transitions": [
{ "from": "start", "to": "processing", "event": "begin" },
{ "from": "processing", "to": "processing.thinking", "event": "think" },
{ "from": "processing.thinking", "to": "processing.tool", "event": "tool_call" },
{ "from": "processing.tool", "to": "processing.thinking", "event": "tool_result" },
{ "from": "processing.thinking", "to": "done", "event": "answer" }
],
"tools": [
{ "name": "now", "description": "Returns the current UTC time in RFC3339 format." }
]
}The five states describe the full loop: start → processing.thinking (model call) → processing.tool (handler runs) → processing.thinking (model call with tool result)
→ done. Every tool-using agent compiles to this
shape. See the Workflow
concept page for why this matters.
What just happened
In order, tied back to concepts:
- Registration. Your
mainbuilt a workflow definition describing the agent and its tools. The runner will interpret this against a scroll. - First AI call. The runner emitted
ai.request(derived). OpenAI returned atool_call, captured asai.response(external commit). - Tool dispatch. The runner emitted
tool.dispatch(derived). Your SDK subscriber picked it up, invokednowTool, and committedtool.result(external commit) with the timestamp. - Second AI call. The runner emitted another
ai.request(derived) including the tool result. OpenAI returned the structured answer, captured asai.response(external commit). - Done. The runner emitted
workflow.stepwith iddoneand the run ended.
All four concepts showed up: the scroll holds the events, the events split into external commits and deriveds, the workflow definition drove the loop, and the tool was a handler on the scroll.
Where this goes
The scroll for this run lived in memory and disappeared when the process exited. The next steps — persistent scrolls, replay-driven testing, multi-agent composition — each get their own guide. For now, the fact that you can write a weave agent in 40 lines of Go and watch the runtime orchestrate a real tool call is the point.