Guide · 1

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 weave CLI. For now, run it via go run ./cmd/weave from 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:    14

Why 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: startprocessing.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:

  1. Registration. Your main built a workflow definition describing the agent and its tools. The runner will interpret this against a scroll.
  2. First AI call. The runner emitted ai.request (derived). OpenAI returned a tool_call, captured as ai.response (external commit).
  3. Tool dispatch. The runner emitted tool.dispatch (derived). Your SDK subscriber picked it up, invoked nowTool, and committed tool.result (external commit) with the timestamp.
  4. Second AI call. The runner emitted another ai.request (derived) including the tool result. OpenAI returned the structured answer, captured as ai.response (external commit).
  5. Done. The runner emitted workflow.step with id done and 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.