A Go SDK for building AI agents, reimagined around a filesystem-first design inspired by Vercel's Eve. Core agent capabilities live in conventional files and folders, so projects are easy to inspect, extend, and operate — while the underlying engine remains a plain Go library with multi-provider support, tools, skills, handoffs, guardrails, memory, and tracing.
An agent is a directory. Conventional files describe its behaviour:
myagent/
agent.yaml # model, provider, temperature, tools
instructions.md # system prompt
skills/
refunds.md # on-demand knowledge (YAML front-matter + markdown body)
subagents/
billing/ # a delegated agent (handoff), same layout recursively
agent.yaml
instructions.md
schedules/
nightly.yaml # cron triggers
channels/ # integration adapters (HTTP is built in)
| Path | Purpose |
|---|---|
agent.yaml |
Model, provider, sampling params, and the tools the agent may use |
instructions.md |
The system prompt |
skills/*.md |
Procedures/knowledge surfaced to the model and loaded on demand |
subagents/<n>/ |
Specialized agents the parent can hand off to |
schedules/*.yaml |
Cron expressions that trigger the agent autonomously |
name: assistant
provider: openai # openai | anthropic | gemini | ollama
model: gpt-4o
temperature: 0.7
max_tokens: 1024
tools:
- current_time
- add---
name: weather
description: How to answer questions about the weather using a public API.
---
1. Determine the location the user is asking about.
2. Use the `http_get` tool to fetch https://wttr.in/<location>?format=3.
3. Summarize the result in one friendly sentence.Skills are not dumped into the prompt. Instead the model sees a catalog of
skill names and descriptions and pulls a skill's full body only when needed,
via the built-in load_skill tool.
Skills can be shared across projects — like a plugin marketplace — by declaring
remote sources in agent.yaml. They're resolved at load time (never by
the model at runtime), pinned to an immutable commit SHA, integrity-checked
against an optional sha256, restricted to an allowlisted host, and cached
locally. This gives you marketplace ergonomics without the prompt-injection/SSRF
risk of fetching model-supplied URLs.
# agent.yaml — local skills/ are loaded too, in addition to these
skills:
- source: github.com/acme/agent-skills/refunds.md
ref: a1b2c3d4 # commit SHA — required for github.com sources
sha256: 9f86d0… # optional integrity pin
- source: https://example.com/pinned/skill.md
sha256: 0b1c2d…Defaults are safe: only raw.githubusercontent.com / gist.githubusercontent.com
are allowed unless you widen the allowlist via loader.LoadWithOptions.
Each subagent handoff can carry context in one of three ways, set per subagent
in the parent's agent.yaml:
subagents:
researcher: forked # shared (default) | fresh | forked| Mode | Subagent sees | Returns to parent? | Use case |
|---|---|---|---|
| shared | the full live history | no (transfers control) | routing/triage |
| fresh | only the task | yes | clean delegation, no context bleed |
| forked | a copy of the history | yes | independent exploration; parent resumes unaffected |
In code: agents.WithHandoffs(sub) (shared) or
agents.WithHandoff(sub, agents.ContextFresh|ContextForked). The model triggers
a handoff by calling the auto-generated handoff_<name> tool; the runner
intercepts it and applies the configured mode.
Agents gain real, dynamic tools by connecting to MCP servers declared in
agent.yaml. Servers are connected at load time over stdio (local
subprocess) or streamable-HTTP (remote), their tools are listed and adapted
into the SDK's tool interface, and they're closed when the agent is. This is
also how a CLI-run agent gets tools beyond the three builtins — without
recompiling.
# agent.yaml
mcp:
- name: github # tools are exposed to the model as github_<tool>
transport: stdio
command: ["npx", "-y", "@modelcontextprotocol/server-github"]
env: { GITHUB_TOKEN: "${GITHUB_TOKEN}" } # $VARs are expanded
- name: search
transport: http
url: https://mcp.example.com
headers: { Authorization: "Bearer ${SEARCH_KEY}" }
tools: [web_search] # optional allowlist of tool namesBuilt on the official modelcontextprotocol/go-sdk.
Servers are declared in config, never chosen by the model; tool names are
namespaced by server to avoid collisions. From code, use mcp.ConnectAll and
add manager.Tools() to an agent, or just let the loader do it. Remember to
defer agent.Close() to release MCP connections.
Every run is instrumented. The runner emits structured logs (log/slog) and a
span tree covering the run, each turn, every LLM call, tool execution
(including skills' load_skill and MCP tools), guardrails, and handoffs/
delegations.
runner := agents.NewRunner(
agents.WithProvider(p),
agents.WithLogger(tracing.NewLogger(os.Stderr, slog.LevelDebug)), // structured logs
agents.WithTracer(tracing.NewConsoleTracer()), // indented span tree
)
result, _ := runner.Run(ctx, agent, "hello")
// The full trace tree is always captured programmatically, regardless of tracer:
for _, span := range result.Traces {
fmt.Printf("%*s%s %s\n", span.Depth*2, "", span.Name, span.Duration)
}Console trace output looks like:
[trace] agent.run (12.4ms) agent=assistant turns=2 tokens=345
[trace] turn (8.1ms) n=0 agent=assistant
[trace] llm.complete (6.0ms) model=gpt-4o tokens=210 tool_calls=1
[trace] tool.execute (1.2ms) tool=add args={"a":5,"b":3} result_chars=1
[trace] turn (3.9ms) n=1 agent=assistant
[trace] llm.complete (3.8ms) model=gpt-4o tokens=135 tool_calls=0
Tracers available in pkg/tracing: NewConsoleTracer, NewSlogTracer,
NewRecorder (programmatic capture), and NewTee to combine them. With the
CLI, enable both with -v/--verbose, or set EVE_LOG=debug / EVE_TRACE=1.
go build -o eve ./cmd/eve
eve init myagent # scaffold a new agent directory
eve validate myagent # load and report what was found (no API key needed)
eve run myagent "what time is it?" # run a single turn
eve dev myagent # serve over HTTP: POST /chat {"input": "..."}
eve schedules myagent # run the agent's cron schedulesProvider credentials come from the environment:
export PROVIDER=openai # optional; otherwise auto-detected
export OPENAI_API_KEY=... # or ANTHROPIC_API_KEY / GEMINI_API_KEY / OLLAMA_HOSTBecause Go tools are compiled functions,
agent.yamlreferences tools by name. The CLI ships a small builtin registry (current_time,add,http_get). To use your own Go tools, load the agent from your own program with a custom registry (see below).
The CLI is a thin shell over the library. Load a filesystem agent with your own tools and run it:
package main
import (
"context"
"fmt"
"github.com/ryanhill4L/agents-sdk/pkg/agents"
"github.com/ryanhill4L/agents-sdk/pkg/loader"
"github.com/ryanhill4L/agents-sdk/pkg/providers"
"github.com/ryanhill4L/agents-sdk/pkg/tools"
)
func main() {
// 1. Register your Go tools by name.
reg := tools.NewRegistry()
reg.MustRegister(tools.NewTool("add", "Adds two numbers",
func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
return args["a"].(float64) + args["b"].(float64), nil
},
tools.Param{Name: "a", Type: "number", Required: true},
tools.Param{Name: "b", Type: "number", Required: true},
))
// 2. Load the agent from the filesystem.
agent, err := loader.Load("myagent", reg)
if err != nil {
panic(err)
}
// 3. Run it with a provider resolved from the environment.
provider, _ := providers.Resolve(agent.Provider)
runner := agents.NewRunner(agents.WithProvider(provider))
result, err := runner.Run(context.Background(), agent, "What is 21 + 21?")
if err != nil {
panic(err)
}
fmt.Println(result.FinalOutput)
}You can also build agents entirely in code with functional options
(agents.NewAgent, agents.WithInstructions, agents.WithTools,
agents.WithSkills, agents.WithHandoffs, …) — the loader simply translates
files into those calls.
| Package | Responsibility |
|---|---|
pkg/agents |
Agent, Runner, the turn loop, handoffs, skills wiring |
pkg/loader |
Builds an Agent from a directory (agent.yaml, instructions.md, …) |
pkg/skills |
Skill parsing, the catalog, and the load_skill builtin tool |
pkg/tools |
Tool interface, Registry, FunctionTool (reflection), SimpleTool |
pkg/mcp |
Connects MCP servers (stdio/HTTP) and adapts their tools to tools.Tool |
pkg/channels |
Integration adapters; built-in HTTP channel (powers eve dev) |
pkg/schedules |
Cron parsing and a dependency-free scheduler |
pkg/providers |
OpenAI, Anthropic, Gemini, and Ollama integrations + Resolve |
pkg/memory |
SQLite-backed session persistence |
pkg/guardrails |
Pluggable input validation |
pkg/tracing |
Span-based observability (no-op and console tracers) |
| Provider | Env var | Notes |
|---|---|---|
| OpenAI | OPENAI_API_KEY |
GPT-4o and friends |
| Anthropic | ANTHROPIC_API_KEY |
Claude models |
| Gemini | GEMINI_API_KEY |
Google Gemini |
| Ollama | OLLAMA_HOST |
Local models (default :11434) |
examples/filesystem-agent— the filesystem-first layout, driven by theeveCLI (agent + skill + subagent + schedule).examples/basic,examples/multi-agent,examples/json,examples/event-scheduler— code-first usage of the library API.
make build # build all packages
make build-cli # build the eve CLI into bin/eve
make test # run tests
make run-fs-example # validate the filesystem-first example
make check # fmt + vet + tidyMIT — see LICENSE.