mkdir my-agent && cd my-agent
go mod init my-agent
go get github.com/zzir/agents-go
export OPENAI_API_KEY=sk-...
An agent is a plain struct: instructions, a name, and optional configuration such as tools or a structured output type.
import "github.com/zzir/agents-go/agents"
historyTutor := &agents.Agent{
Name: "History Tutor",
HandoffDescription: "Specialist agent for historical questions",
Instructions: agents.StaticInstructions("You provide assistance with historical queries. Explain important events and context clearly."),
}
HandoffDescription gives the routing agent extra context for deciding where to hand off.
mathTutor := &agents.Agent{
Name: "Math Tutor",
HandoffDescription: "Specialist agent for math questions",
Instructions: agents.StaticInstructions("You help with math problems. Show your reasoning step by step."),
}
Handoffs lists the agents this agent may delegate to. The model sees each as a transfer_to_<name> tool.
triage := &agents.Agent{
Name: "Triage Agent",
Instructions: agents.StaticInstructions("You determine which agent to use based on the user's question."),
Handoffs: []agents.Handoff{agents.HandoffTo(historyTutor), agents.HandoffTo(mathTutor)},
}
import (
"context"
"fmt"
"log"
"github.com/zzir/agents-go/models/openai"
)
func main() {
provider := openai.NewProvider()
res, err := agents.RunSync(context.Background(), triage, "What is the French Revolution?", agents.RunOptions{
Model: agents.ModelOptions{Provider: provider},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(res.FinalOutputString()) // answered by the History Tutor
}
Guardrails run alongside the first model call and can stop a run before it wastes tokens. One guardrail declares the stages it inspects, so a single value can cover the input, the tool arguments and the final output.
triage.Guardrails = []agents.Guardrail{{
Name: "homework_only",
Stages: []agents.GuardrailStage{agents.StageInput},
Run: func(ctx context.Context, rc *agents.RunContext, p agents.GuardrailPayload) (agents.GuardrailDecision, error) {
// Inspect p.Input (or call a cheap classifier model here).
offTopic := false
if offTopic {
return agents.Trip("not a homework question"), nil
}
return agents.Allow(nil), nil
},
}}
When a guardrail trips, Run returns an *agents.GuardrailTripwireError;
tw.Stage() says which stage fired. See Guardrails for the
other stages and for substitution.
NewTool reflects a typed Go function into a strict JSON-schema tool. Struct tags document the parameters.
type weatherArgs struct {
City string `json:"city" jsonschema:"the city to look up"`
}
weather := agents.NewTool("get_weather", "Look up the current weather for a city.",
func(ctx context.Context, tc *agents.ToolContext, args weatherArgs) (string, error) {
return "Sunny, 23°C in " + args.City, nil
})
mathTutor.Tools = []*agents.Tool{weather}
Structured output is the same idea in reverse: a Go type on the agent, the
typed value back out of the result. (From here on, ctx and opts are the
context and the agents.RunOptions from the run above.)
type answer struct {
Summary string `json:"summary"`
Sources []string `json:"sources"`
}
historyTutor.OutputType = agents.OutputType[answer]()
res, err := agents.RunSync(ctx, historyTutor, "Who built the Colosseum?", opts)
if err != nil {
log.Fatal(err)
}
if a, ok := agents.FinalOutputAs[answer](res); ok {
fmt.Println(a.Summary, a.Sources)
}
A run is an iterator. Run returns one plus a control handle; the run
advances as you consume it, so abandoning the loop stops the run instead of
leaking a goroutine.
stream, ctrl := agents.Run(ctx, triage, "tell me about the Roman Republic", opts)
for event, err := range stream {
if err != nil {
log.Fatal(err)
}
switch e := event.(type) {
case *agents.RunItemStreamEvent:
if e.Item.Kind == agents.ItemMessage {
fmt.Println(e.Item.Text())
}
case *agents.RunCompletedEvent:
fmt.Println("done:", e.Result.FinalOutputString())
}
}
_ = ctrl // StopAfterTurn, and mid-run input: Steer redirects the current
// exchange, NextTurn rides along with the next turn, FollowUp queues the next
// exchange; Pending reports what was not consumed.
See Streaming for the event types and Controlling a live run.
A tool with NeedsApproval set pauses the run instead of executing; the paused
state survives a process restart.
weather.NeedsApproval = true
res, err := agents.RunSync(ctx, mathTutor, "What's the weather in Rome?", opts)
if err != nil {
log.Fatal(err)
}
for len(res.Interruptions) > 0 {
for _, item := range res.Interruptions {
res.State.Approve(item, false) // or res.State.Reject(item, false, "no")
}
if res, err = agents.ResumeRunSync(ctx, res.State, opts); err != nil {
log.Fatal(err)
}
}
The paused state serializes to JSON (res.State.MarshalJSON()) and rebuilds
with agents.RunStateFromJSON(data, registry), so the approval can happen in
another process — see Human-in-the-loop.
See examples/handoffs and examples/tools for complete runnable programs, and: