Help us shape the future of Go! Take the 2026 Go Developer Survey

agentx

package module
v1.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 31, 2026 License: MIT Imports: 0 Imported by: 0

README

agentx

English | 简体中文

agentx is a Go framework for agents that call tools, maintain conversations and resume interrupted work. It provides a streaming agent loop, typed tools, permissions, checkpoints and context management through composable packages.

  • Small dependency boundary. The module uses the Go standard library and kitx for shared primitives. It holds the contracts and one in-memory reference implementation of each; model providers, file and SQL stores, OpenTelemetry, logging and retries live in agentx-contrib.
  • Small interfaces. model.Model, tool.Tool, hook.Hook, session.Store and the other core types are small interfaces, so you can replace any one of them on its own.
  • Iterator-based streaming. Runs are iter.Seq2[*loop.Event, error] and model streams iter.Seq2[*model.StreamEvent, error], so you read them with a plain for range loop.

Requires Go 1.27.3 or later. The current release is v1.4.0. Patch releases preserve the public API; minor releases may contain breaking changes, each listed with its migration in CHANGELOG.md.

Installation

Go 1.27.3 is the toolchain requirement; library versions follow semantic versioning independently. The core uses kitx v1.0.0 from the Go module proxy; no sibling checkout or replacement directive is needed. The command below also installs the independently versioned provider module used in the quick start.

Run it inside your application's Go module. For a new application:

mkdir agentx-demo
cd agentx-demo
go mod init example.com/agentx-demo
go get github.com/nostosx/agentx@v1.3.0 github.com/nostosx/agentx-contrib/provider@v1.2.0

The core module defines model.Model; the OpenAI, Anthropic, Gemini and Bedrock implementations are in the provider module of agentx-contrib, which calls provider APIs directly over net/http without provider SDKs.

Quick start

Save the following program as main.go in that directory:

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/nostosx/agentx-contrib/provider/anthropic"
	"github.com/nostosx/agentx/agent"
)

func main() {
	m, err := anthropic.New() // reads ANTHROPIC_API_KEY
	if err != nil {
		log.Fatal(err)
	}
	a, err := agent.New("assistant",
		agent.WithModel(m),
		agent.WithInstructions("You are a concise assistant."),
	)
	if err != nil {
		log.Fatal(err)
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
	defer cancel()
	answer, err := a.Ask(ctx, "What is an agent loop?")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(answer)
}

After saving the file, resolve its imported dependencies:

go mod tidy

Set ANTHROPIC_API_KEY in your shell, then run go run .. This program makes a real Anthropic API request and may incur charges. ANTHROPIC_BASE_URL selects a different endpoint when needed; leave it unset for Anthropic. An account with access to the provider's default model is required. Select another model with anthropic.WithModel("your-model-id") when constructing it.

The feature examples below are fragments: they reuse ctx, m and other values from the surrounding application, and omit imports. Contrib packages need their own module installed; see agentx-contrib.

Features

Tools · Runs · Permissions · Sessions · RAG · Workflows · MCP · Serving

Typed tools

tool.Func builds the JSON Schema from the argument struct and checks the model's arguments against it before your function runs. The jsonschema struct tag refines the schema in either of the Go ecosystem's two forms. A plain tag is the field's description, as in google/jsonschema-go, which the official MCP Go SDK and ADK Go use. A tag that starts with a keyword uses the key=value form of invopop/jsonschema: title, description, enum (repeatable), default, format, pattern, minimum, maximum, exclusiveMinimum, exclusiveMaximum, minLength, maxLength, minItems, maxItems and the flags required and nullable. Escape a comma inside a value as \\,; jsonschema:"-" leaves a field out.

type weatherArgs struct {
	City string `json:"city" jsonschema:"City name, e.g. Tokyo"`
	Unit string `json:"unit,omitempty" jsonschema:"enum=celsius,enum=fahrenheit,default=celsius"`
	Days int    `json:"days,omitempty" jsonschema:"minimum=1,maximum=7,description=Forecast days\\, 1 for today"`
}

weather := tool.Func("get_weather", "Get the current weather for a city.",
	func(ctx context.Context, in weatherArgs) (string, error) {
		return in.City + ": 22°C, partly cloudy", nil
	}, tool.ReadOnly())

a := agent.MustNew("assistant", agent.WithModel(m), agent.WithTools(weather))
  • Calls to ReadOnly or Concurrent tools that come back to back in one model response run concurrently. A Concurrent tool may have side effects, so a resumed run does not repeat a call that was cut off.
  • Errors, invalid arguments and panics come back to the model as error results. They do not stop the run.
  • Arguments must be I-JSON (RFC 7493): JSON that repeats a member name or holds invalid UTF-8 is invalid, since hooks, permission rules, tools and MCP servers could otherwise read it differently. For the same reason a member that spells a declared property in another case, such as Command next to command, is invalid: encoding/json would decode it into the field while hooks and rules read the exact key.
  • tool.Media(text, parts...) returns images or documents with the text; see Multimodal content. Result.Metadata reaches model.ToolResult.Metadata and events, not the model.
Built-in tools

tool/builtin provides general-purpose tools modeled on Claude Code's and Codex's:

Tool Purpose Approval
read_file Read a text file with line numbers, page by page; images (PNG, JPEG, GIF, WebP up to 5 MB) and PDFs (up to 20 MB) come back as parts for the model to view No (read-only)
write_file Create or overwrite a file; an overwrite returns the diff Yes
edit_file Replace exact text; the match must be unique unless replace_all is set. edits makes several replacements in order, all or none. Returns the diff Yes
apply_patch Apply a Codex-format patch that adds, updates, renames and deletes files, all or none. Returns a summary and the diffs Yes
glob Find files by pattern, with ** and {a,b}; newest first No (read-only)
grep Search contents with a regular expression, with optional context lines; skips binary files No (read-only)
bash Run a shell command with a timeout, or in the background with run_in_background; long output keeps its head and tail Yes, unless sandboxed
bash_output Read a background command's new output, optionally filtered by a regular expression, and its status No
kill_shell Stop a background command and every process it started No
web_fetch Fetch a URL and convert HTML to text No (read-only)
todo_write Maintain a task list the application can display No
todos := &builtin.TodoList{}
a := agent.MustNew("coder", agent.WithModel(m),
	agent.WithTools(builtin.Tools(builtin.Config{Dir: ".", Todos: todos})...))
  • File confinement. File tools use os.Root, so neither .. nor a symbolic link can reach outside Config.Dir.

  • Read before write. The tools returned by builtin.Tools share a FileTracker: write_file, edit_file and apply_patch change or delete an existing file only after read_file has read it, and refuse if it changed since then.

  • Atomic edits. Files are replaced through a temporary file in the same directory, keeping their permissions; a symbolic link is written through in place. apply_patch checks every operation before writing anything and restores the files it changed if a later write fails.

  • Edit formats. edit_file follows Claude Code's Edit tool; its edits argument replaces the former MultiEdit tool. apply_patch takes the patch format GPT models are trained on and locates changes like Codex: exactly, then ignoring trailing whitespace, surrounding whitespace and typographic punctuation. The OpenAI Responses API receives it as a freeform tool constrained by Codex's Lark grammar; other providers send the patch as the JSON argument input. Config.Edit selects builtin.EditAll (default), EditReplace (only edit_file) or EditPatch (only apply_patch). Use tool.Freeform for freeform tools of your own.

  • Ignored files. glob and grep always skip .git and node_modules, and skip paths excluded by .gitignore unless the model sets include_ignored. They also honor .git/info/exclude and the .gitignore files of parent directories up to the repository root. A .git file or commondir is followed only to a local git directory containing HEAD, and network paths, such as UNC shares, are never looked up.

  • Shell access. bash runs with bash when it is on PATH, otherwise sh (cmd.exe on Windows). It starts in Config.Dir but, without a sandbox, is not confined to it. It requires approval unless you set Config.NoApproval. When the command exits or times out, its whole process tree is killed, including background processes. As in Claude Code, a cd carries over to later calls while it stays inside Config.Dir (links resolved); otherwise the next call starts in Config.Dir and the result says Shell cwd was reset to <dir>. The shell reports its directory from an EXIT trap on descriptor 3; Config.ResetDir turns this off, and Windows never tracks it.

  • Background commands. With run_in_background, bash returns an ID such as bash_1 at once. bash_output returns what the command wrote since the last read and whether it is running or how it ended; kill_shell kills its process tree. A builtin.Shells holds them (Tools creates one): at most 8 run at once, each keeps its last 1 MiB of output and is killed after 30 minutes unless timeout_ms asks for up to 2 hours. Call builtin.Close(tools) or Shells.Close when the agent is done. They match Claude Code's BashOutput/TaskOutput and KillShell/TaskStop in hooks and permission rules.

  • Sandbox. Config.Sandbox runs every bash command, background ones included, inside an OS sandbox. builtin.NewSandbox(policy) uses Seatbelt (/usr/bin/sandbox-exec) on macOS with a deny-by-default profile modeled on Codex's, and bubblewrap (bwrap) on Linux with Codex's mounts (--ro-bind / /, writable roots bound back, fresh user, PID, IPC and network namespaces, --die-with-parent). Commands read everything (or only ReadableRoots) except DenyRead, write only to Config.Dir, WritableRoots and the temporary directories, never to .git, .agents, .codex, .aws or .claude at the top of those roots, and have no network unless AllowNetwork is set or AllowedDomains lists hosts, which a local HTTP/CONNECT proxy enforces by name through HTTP_PROXY and HTTPS_PROXY (socat bridges it on Linux). Like Claude Code's auto-allow mode, sandboxed commands need no approval (AskSandboxed restores it); a command retried with dangerously_disable_sandbox runs outside and always needs approval, which NoSandboxEscape forbids. Permission rules still apply, for example Bash(dangerously_disable_sandbox:true). When no backend works, SandboxRequired (the default) fails the command and SandboxBestEffort runs it unsandboxed after approval. Limitations: the proxy does not inspect TLS, programs that ignore the proxy variables have no network, setuid programs such as ps do not run on macOS, Linux uses no seccomp or Landlock, so only container daemon sockets are hidden from commands and only existing paths are protected. Linux installs protection mounts after every writable bind, including overlapping parent roots. Windows has no backend.

    sb := builtin.NewSandbox(builtin.SandboxPolicy{
        DenyRead:       []string{"~/.ssh", "~/.aws"},
        AllowedDomains: []string{"proxy.golang.org", "*.githubusercontent.com"},
    })
    defer sb.Close()
    tools := builtin.Tools(builtin.Config{Dir: ".", Sandbox: sb})
    defer builtin.Close(tools)
    
  • Network access. web_fetch refuses loopback, private, link-local and shared (CGNAT) addresses, including after redirects and, for direct connections, DNS changes. A proxy from the environment still works, but then destinations are checked by name before each request only, since the proxy resolves them again. Fetches time out after 60 seconds. Set Config.AllowPrivateNetwork to allow these addresses, or pass your own HTTPClient, which replaces these checks.

  • Read-only subset. builtin.ReadOnlyTools returns only the tools without side effects, which suits research sub-agents.

  • File history. With Config.History, write_file, edit_file and apply_patch save each file's state before changing it, so a session can be rewound; see Checkpoints and rewind. Changes made by bash, sandboxed or not, are not tracked.

The agent loop and events

Run returns a stream of events:

  • run_start and turn_start
  • compacted, when the conversation was compacted before the model call
  • delta, only when streaming is enabled
  • message
  • tool_call and tool_result
  • output_retry, when the final answer did not match the output schema and was sent back (Event.OutputRetry holds the problems and attempt)
  • handoff, when the run moves to another agent (see Handoffs)
  • interrupted
  • run_end
for ev, err := range a.Run(ctx, agent.Text("Plan a trip to Kyoto")) {
	if err != nil {
		log.Fatal(err)
	}
	switch ev.Type {
	case loop.EventDelta:
		fmt.Print(ev.Text())
	case loop.EventToolCall:
		fmt.Println("->", ev.ToolCall.Name)
	case loop.EventRunEnd:
		fmt.Println("\nstatus:", ev.Result.Status)
	}
}

loop.Collect drains the events and returns a *loop.Result. The result holds the output text, the full conversation, token usage and the number of turns.

Structured output

agent.WithOutput[T] asks the model for JSON matching the schema of the struct T and checks the final answer against it. agent.AskAs[T] runs a prompt and decodes the answer; the methods Result.DecodeAs[T] and Result.Decode of loop.Result decode the result of any run.

type Forecast struct {
	City  string `json:"city"`
	TempC int    `json:"temp_c" jsonschema:"Temperature in Celsius"`
}

a := agent.MustNew("weather", agent.WithModel(m), agent.WithTools(weather),
	agent.WithOutput[Forecast]())
f, err := agent.AskAs[Forecast](ctx, a, "What's the weather in Kyoto?")
  • Request. The schema is sent as the response format in strict mode, even when tools are present, unless WithResponseFormat sets one.
  • Provider subsets. Constraints a provider rejects or ignores, such as minimum on Anthropic or minLength on Gemini, are moved into the field's description; strict tool schemas are adapted the same way. Claude models before 4.5, and Gemini 2.x when tools are present, get the schema as a system instruction instead. The answer is still checked against the full schema.
  • Retries. An answer that does not match goes back to the model with the problems found, twice by default (agent.OutputRetries(n)); each attempt takes a turn and emits an output_retry event. After that the run fails with a *loop.OutputError, and resuming it grants the retries again.
  • Code fences. An answer that is a single Markdown code block, such as ```json, is checked and decoded without the fence. Any other text around the JSON fails the check.
  • Not checked. Answers cut off by the token limit, withheld by a content filter or refused end the run as usual; decoding them returns an error.
Multimodal content

Messages and tool results carry images, documents, audio and video. Parts hold data, a URL or a provider file ID; checkpoints and sessions store data as base64.

msg := model.NewMessage(model.RoleUser,
	model.TextPart("Compare the report with the chart."),
	model.DocumentPart("application/pdf", pdf, "report.pdf"),
	model.ImagePart("image/png", chart))

shot := tool.Func("screenshot", "Capture the screen.",
	func(ctx context.Context, _ struct{}) (*tool.Result, error) {
		return tool.Media("Main display", model.ImagePart("image/png", capture())), nil
	})
  • Providers. Which content each provider accepts and returns is listed in the agentx-contrib README.
  • Errors. Content a provider or model cannot take fails before the request with a permanent error, such as audio on Anthropic or a document URL on Chat Completions.
  • Options. FileID names a Files API upload (OpenAI, Anthropic); Gemini files go in URL by their URI. Detail is OpenAI's detail and Gemini 3's per-part mediaResolution. Citations enables citations on Anthropic and Bedrock documents; Anthropic text keeps its citations in Part.Raw and sends them back.
  • Context. Token estimates count media; ClearToolResults clears a result's media with its text.
Image generation

Generated assets have a separate artifact.Store contract with scoped binary values and Put/Get/Delete operations. File and S3 implementations live in agentx-contrib, with S3 reusing servex-contrib object storage. No storage backend or servex dependency is introduced into core. Configure Artifacts and ArtifactScope on imagegen.ToolOptions to persist generated images and return their keys in tool metadata. imagegen.ArtifactError retains generated output and saved references on persistence failure, avoiding a new model request.

imagegen separates image generation from the conversation model.Model: Generator.Generate, Editor.Edit, Streamer.Stream and EditStreamer.StreamEdit are independent capabilities. Requests control the prompt, model, count, dimensions, quality, output format and background. Responses contain image model.Part values, request IDs and image/text token details. Streaming previews are distinct from the final Response.

The implementations are openai.NewImageGenerator and gemini.NewImageGenerator in agentx-contrib. Gemini uses ratio/resolution settings and rejects OpenAI-specific quality/mask options; see provider docs. Wrap it with imagegen.Tool(images, imagegen.ToolOptions{}) and pass that tool to agent.WithTools. The default output limit is one image; MaxImages explicitly increases it. Editing uses an application-provided ResolveImage function to authorize and resolve image references. Model, safety identifier and provider options remain application-controlled.

Generated images are returned in tool result Parts, with usage and request IDs in Metadata; Ask returns text, so consumers display/save images from run events or conversation tool results. Storage and publishing remain the application's responsibility. Existing session/checkpoint serialization can preserve the parts, including their image bytes. Image-generation charges are separate from the conversation model's token limits.

See the complete OpenAI examples. Image contracts are available in core v1.3.0. Image adapters require provider v1.3.0 or later; the provider v1.2.0 quick start above covers conversation APIs.

Usage limits

agent.WithLimits bounds each run; Input.Limits and Input.MaxTurns override the limits and the turn limit for one run, as do agent.RunLimits and agent.RunMaxTurns passed to Runner.Run or Runner.RunText. Zero fields are unlimited.

agent.WithLimits(loop.Limits{Requests: 20, ToolCalls: 50, TotalTokens: 200_000}),
agent.WithBudget(func(u model.Usage) error {
	if cost(u) > 0.50 {
		return errors.New("over $0.50")
	}
	return nil
}),
  • Checks. Requests is checked before each model call, ToolCalls before each batch of tool calls, and token limits and the budget before both. A batch that would exceed ToolCalls does not start.
  • Counting. Usage.Requests and Usage.ToolCalls count model calls and executed tool calls. A sub-agent's usage counts toward the parent's limits once the sub-agent returns.
  • Ending. The run ends with status limit_exceeded, and Result.Reason names the limit. Calls that did not run are answered as such, and a final answer that crossed a token limit is kept. Like max_turns, the status is final: continue the conversation with a new run.
Stop conditions

agent.WithStopAtTools("submit") ends the run as completed once the submit tool returns a result that is not an error. The result is the run's Output, and the model is not called again. agent.WithStopWhen takes conditions that see each loop.Step after its tool calls ran, such as func(s *loop.Step) bool { return s.Turn >= 5 }; the output is then the text of the model's last message. If the consumer stops reading after the tool results, resuming the run ends it the same way without calling the model.

Hooks

Hooks follow Claude Code's lifecycle events:

Event Purpose
SessionStart Add context when a session starts: startup, resume, clear, fork (via Runner) or after compaction
SessionEnd Clean up when Runner.EndSession ends a session; cannot block
UserPromptSubmit Check user input before it reaches the model, or attach context to it
PreModel / PostModel Modify a model request, or inspect the response
PreToolUse Allow, deny, ask or defer before a tool call, or rewrite its arguments
PostToolUse Replace the result of a successful tool call or append to it
PostToolUseFailure Append to the result of a tool call that failed
Stop / SubagentStop Block a stop so the agent or sub-agent keeps working (see Multi-agent)
SubagentStart Add context to a sub-agent's task before it starts
PreCompact / PostCompact Skip automatic compaction, or see its summary
Notification Be notified when a run is interrupted for tool approval

Matchers follow Claude Code's rules:

  • An empty matcher or * matches everything.
  • A matcher of only letters, digits, _, -, spaces, , and | lists exact names, such as Edit|Write or Edit, Write.
  • Any other matcher is a Go regular expression that may match anywhere: ^Edit$ matches only Edit, and mcp__github__.* every tool of the github MCP server.

Tool events match the tool name. Claude Code's built-in names also match the agentx tools with the same role: Bash, Read, Write, Edit and MultiEdit, Glob, Grep, WebFetch and TodoWrite match bash, read_file, write_file, edit_file, glob, grep, web_fetch and todo_write, so Edit|Write matches edit_file and write_file. Edit and Write also match apply_patch; BashOutput and TaskOutput match bash_output, and KillShell and TaskStop match kill_shell. Other events match Event.Reason: the source for SessionStart, the reason (clear, logout, prompt_input_exit, other, ...) for SessionEnd, the sub-agent's name for SubagentStart and SubagentStop, auto for PreCompact and PostCompact, and approval or permission_prompt for Notification.

Matching hooks run in parallel, as in Claude Code (at most 12 Prompt and Agent hooks at once; hook.Parallelism(n) bounds all of them), and identical handlers run once. Results merge in registration order: the most restrictive decision wins (deny, then defer, then ask, then allow), contexts accumulate, and the last UpdatedInput wins. PreModel hooks run one at a time because they modify the request. Stop and SubagentStop hooks can keep the agent working 8 times in a row (hook.StopBlockCap(n), 0 for no limit); a turn with tool calls resets the count. Calls a hook or approval denied fire neither PostToolUse nor PostToolUseFailure.

A Runner fires SessionStart once per session: startup for a new session, resume the first time it runs or resumes an existing one, and clear or fork for sessions made by Runner.Clear and Runner.Fork. Runner.EndSession(ctx, id, reason) fires SessionEnd within a 1.5-second budget (hook.SessionEndTimeout). After automatic compaction, SessionStart fires with compact and its context follows the summary. WithHooks copies the manager you pass, so hooks added to it later do not reach the agent.

agent.WithHook(hook.PreToolUse, "bash", hook.Func(
	func(ctx context.Context, ev *hook.Event) (*hook.Result, error) {
		if strings.Contains(string(ev.ToolCall.Arguments), "rm -rf") {
			return &hook.Result{Decision: hook.Deny, Reason: "destructive command"}, nil
		}
		return nil, nil
	}))

hook.Command runs a command as a hook, the way Claude Code command hooks work:

  • The event is written to the command's stdin as JSON with Claude Code's fields, such as cwd, tool_input, source, trigger and last_assistant_message. tool_name and tool_input are agentx's, and tool_response is {"content": ..., "is_error": ...}.
  • CLAUDE_PROJECT_DIR is set to the hook's Dir, or else the working directory. With Args, the command runs without a shell and ${CLAUDE_PROJECT_DIR} is replaced in it and in Args.
  • Exit code 2 denies the action, with stderr as the reason.
  • JSON on stdout can return a decision, extra context, rewritten arguments or updatedToolOutput. systemMessage is logged as a warning, and defer asks for approval like ask.
  • The timeout defaults to 10 minutes, 30 seconds for UserPromptSubmit and 1.5 seconds for SessionEnd.
  • Background processes the command leaves behind are killed once it exits on Unix. On Windows they keep running unless the hook times out or is cancelled.

The other Claude Code handler types are hooks too:

  • hook.HTTP POSTs the same JSON to a URL. $VAR in Headers expands only for AllowedEnvVars. A 2xx JSON body is read like command output; other bodies, non-2xx statuses and failures are non-blocking errors.
  • hook.Prompt asks a model.Model with $ARGUMENTS replaced by the event JSON; {"ok": false, "reason": ...} blocks as Claude Code does per event (ContinueOnBlock, impossible). Default timeout: 30 seconds.
  • hook.Agent hands the prompt to a hook.Verifier, such as agent.HookVerifier(m, agent.WithTools(builtin.ReadOnlyTools(cfg)...)), which works with tools for up to 50 turns. Default timeout: 60 seconds.
  • hook.MCPTool calls an MCP tool through a hook.MCPCaller, such as mcp.HookCaller(clients), with ${tool_input.file_path}-style input.
  • hook.Async runs a hook in the background. Its additionalContext and systemMessage reach the model with the next tool result or run input; with Rewake, exit code 2 also calls hook.OnAsyncResult so you can wake the session. Manager.Close cancels them.

hook.LoadSettings reads the hooks block of Claude Code settings files, so existing hook configurations work unchanged:

hooks, warnings, err := hook.LoadSettings(
	filepath.Join(home, ".claude", "settings.json"),
	".claude/settings.json",
	".claude/settings.local.json",
)
if err != nil {
	return err
}
for _, w := range warnings {
	log.Print(w) // handlers agentx skipped
}
a := agent.MustNew("assistant", agent.WithModel(m), agent.WithHooks(hooks))

Files that do not exist are skipped, a handler repeated across files runs once, disableAllHooks is honored, onFailure: "block" makes a failing hook deny the action, and async/asyncRewake commands run in the background. command and http handlers always run; prompt, agent and mcp_tool handlers need hook.SettingsOptions{Model, Verifier, MCP}.Load(...). CLAUDE_CODE_STOP_HOOK_BLOCK_CAP and CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS are honored. Handlers without their dependency, types an event does not support in Claude Code, if and PowerShell hooks, and events agentx does not fire are skipped with a warning.

Tool approval and resumable runs

You can mark a tool with tool.RequireApproval(), or a hook can return hook.Ask. A tool implementing tool.CallApprover decides per call from the arguments, as sandboxed bash does. Either way the call needs approval before it runs:

  • With an Approver. If the agent has an Approver, it decides right away. This suits interactive terminals.
  • Without one. The run stops with status interrupted and saves its state through the Checkpointer. Resume it after you have decisions. The example keeps checkpoints and sessions in files with the filestore module of agentx-contrib; loop.NewMemoryCheckpointer and session.NewMemoryStore keep them in memory:
cp, _ := filestore.Checkpoints(".agentx/checkpoints")
store, _ := filestore.Sessions(".agentx/sessions")
a := agent.MustNew("ops", agent.WithModel(m), agent.WithTools(deploy),
	agent.WithCheckpointer(cp))
runner := agent.NewRunner(a, store)

res, _ := runner.RunText(ctx, "s1", "Deploy the latest build")
if res.Status == loop.StatusInterrupted {
	approvals := map[string]loop.Approval{}
	for _, req := range res.Interrupt.Requests {
		approvals[req.Call.ID] = loop.Approve // or loop.Deny
	}
	for ev, err := range runner.Resume(ctx, "s1", approvals) {
		// ...
	}
}

Resuming also covers runs that failed, for example on a provider outage, and runs whose process stopped mid-way: Runtime.Resume and LLMAgent.Resume accept every status for which Status.Resumable is true. runner.Resume resumes the session's interrupted run, or else its last failed run (see agent.FailedRunKey). A run whose process crashed is resumed by run ID with the agent's Resume, which needs a Checkpointer. Without a Checkpointer, the runner resumes interrupted runs from state it keeps in memory, which does not survive a restart; failed runs then cannot be resumed.

  • Stopping early. If you stop iterating a run's events, the run stops before its next step. With a Checkpointer it can be resumed from where it stopped; if it stopped right after the model's final answer, resuming handles that answer instead of calling the model again. Result.Reason, as AfterRun middleware sees it, says whether the run can be resumed. Stopping at the first event runs nothing and leaves a resumed run's checkpoint as it was.
  • Approvals on resume. Approvals passed to Resume answer only the requests of the interrupt being resumed. Resuming a run that was not interrupted ignores them, and requests raised later go to the Approver or interrupt the run again.
  • Tool calls are not repeated. The state is checkpointed before each batch of tool calls, together with the results that are already in. Finished calls keep their results. A read-only call that was cut off runs again. A call with side effects that was cut off is reported to the model as possibly done instead of running twice.
  • Discarding. runner.Discard(ctx, "s1") gives up the session's interrupted or failed run, for example when its checkpoint is gone. It answers the pending calls as not run, so the session can continue.
  • Status. A final answer cut off by the token limit or withheld by a content filter ends with status incomplete. Result.FinishReason tells the cases apart and also reports refusals.
  • Changed arguments. The model's tool calls stay as it sent them. When a hook or an approval changes the arguments, the tool result tells the model which arguments actually ran.
Permissions

The permission package decides tool calls with Claude Code's permission rules and modes. Load the permissions block of Claude Code settings files, or write the rules yourself:

s, warnings, _ := permission.LoadSettings(
	filepath.Join(home, ".claude/settings.json"), ".claude/settings.json")
cfg := s.Config(dir) // allow, ask, deny, defaultMode, additionalDirectories
cfg.Rules = append(cfg.Rules, permission.Rules{
	Allow: []string{"Bash(npm run *)", "Edit(src/**)", "WebFetch(domain:go.dev)"},
	Deny:  []string{"Read(./.env)", "Bash(git push *)", "mcp__github__*"},
})
p, _ := permission.New(cfg)
a := agent.MustNew("dev", agent.WithModel(m),
	agent.WithTools(builtin.Tools(builtin.Config{Dir: dir, CheckRedirect: p.CheckRedirect})...),
	agent.WithPermissions(p))
  • Rules. Deny wins over ask, ask over allow. Bash, Read, Edit and WebFetch select the built-in tools of that role; Edit rules check every file an apply_patch call touches. Bash rules match each command of a compound command, including those in subshells and $(...). Read and Edit rules are gitignore patterns anchored with //, ~/, / or ./. Deny and ask rules of the form Tool(param:value) match another parameter, such as Bash(dangerously_disable_sandbox:true), which asks before every command that leaves the sandbox.
  • Recursive searches. With WithPermissions, glob and grep omit discovered files denied by path rules or requiring approval, even with include_ignored. Name a file explicitly to request its approval. Path carve-outs still apply to files beneath restricted directories.
  • Modes. default leaves undecided calls to the tool, but runs read-only commands such as ls and git status whose paths are inside the working directories; cat ~/.ssh/id_rsa asks. acceptEdits also allows edits inside the working directories, plan denies everything that is not read-only, dontAsk denies whatever would ask, and bypassPermissions allows all but deny and ask rules. agent.RunValue(permission.ModeKey, "plan") sets one run's mode; middleware can call p.SetMode. Hooks of the same agent receive the mode as permission_mode.
  • Plan mode and agents. Like Claude Code, which runs BashOutput, KillShell and TodoWrite without permission, plan allows bash_output, kill_shell and todo_write, which change only the agent's own state. Handoff tools are read-only, so a planning agent can hand off; the mode is kept in the run value permission.mode, which the handoff carries, so the agent handed off to plans too if it has a policy. Sub-agent tools are not read-only and stay denied.
  • Always allow. Approve with loop.Approval{Approved: true, Remember: loop.RememberSession} and later calls like it run without approval for the session (RememberRun: for the run). The Runner keeps these grants with the session.
  • Limits. The shell lexer is not a shell parser: commands it cannot split are never auto-approved, and rules match command text, not a program run by path or through sh -c. Use a sandbox where that matters.
Sessions

agent.Runner stores the conversation in a session.Store and runs each session's runs one at a time, within that Runner. Three stores are available:

  • session.NewMemoryStore() keeps sessions in memory.
  • filestore.Sessions(dir) of agentx-contrib writes each session to a JSON file atomically.
  • sqlstore.Sessions(db) of agentx-contrib keeps them in a SQL database; see File and SQL storage.

All detect concurrent modification: if another Runner or process changed the session, Save returns session.ErrConflict. Reload before deciding how to recover. A failed save does not roll back model requests or tool side effects; do not replay the entire run automatically.

Use separate built-in tool instances for separate users or sessions: their working directory, file tracker and todo list are mutable state. Close tools with builtin.Close when their owner stops. Custom models, tools, hooks and middleware shared across runs must support concurrent calls and honor context cancellation.

Generated identifiers use the standard uuid package. Session IDs from session.NewID, which the Runner and server use for sessions without an ID, are random version 4 UUIDs. Run, workflow run, A2A task and message, memory entry and rewind IDs are version 7 UUIDs behind a short prefix such as run_, so they sort by creation time.

The session is saved when a run ends, even if the caller's context was cancelled. If the caller stops iterating early, the run is cancelled and the session is saved once it ends. Unfinished tool calls are answered, so the conversation stays valid.

Tools, hooks, middleware and instruction functions read and write the session's state during a Runner's run with agent.SessionState(ctx). The state is saved with the session when the run ends, including when it fails or is interrupted, and is shared with sub-agents. Store JSON values and read them back typed with session.State.Value[T]; keys starting with agentx. belong to the Runner. Outside a Runner, agent.ContextWithSessionState supplies a state. Middleware keeps typed per-run state with loop.Run.EncodeValue and loop.Run.DecodeValue[T], which store JSON in run values and so survive checkpoints.

Instructions and dependencies
  • Dynamic instructions. WithInstructionsFunc(f) appends the text f returns to the static instructions. It runs once per run, so the system prompt stays cacheable and a resumed run reuses it; agent.EveryTurn() runs it before each model call instead. An error fails the run.
  • Templates. WithInstructionTemplate() fills {key} in the static instructions from the session state when a run starts, like Google ADK: strings as they are, other values as JSON. A missing {key} fails the run and a missing {key?} becomes empty. Only names (a letter or _, then letters, digits, _ . : -) are placeholders, so JSON stays as it is; {{ and }} are literal braces. Function output is not templated; call agent.RenderTemplate there.
  • Dependencies. Typed values, such as a database handle or the current user, travel in the context: loop.WithDeps(ctx, v), agent.WithDeps(v) as a default and agent.RunDeps(v) per Runner run. Tools, hooks, middleware and instruction functions read them with loop.Deps[T](ctx). They are never sent to the model or saved in checkpoints: pass them again when resuming, for example runner.Resume(ctx, id, approvals, agent.RunDeps(db)).
a := agent.MustNew("assistant",
	agent.WithModel(m),
	agent.WithInstructions("Answer in {lang?}."),
	agent.WithInstructionTemplate(),
	agent.WithInstructionsFunc(func(ctx context.Context, run *loop.Run) (string, error) {
		u, _ := loop.Deps[*User](ctx)
		return "The user is " + u.Name + ".", nil
	}),
)
res, err := runner.RunText(ctx, sessionID, "Hi", agent.RunDeps(currentUser))
Checkpoints and rewind

Like Claude Code's checkpointing, a Runner records a checkpoint before the user message of each run. Runner.Checkpoints lists them, and Runner.Rewind returns a session to one: agent.RewindCode restores the files, RewindConversation removes the message and everything after it and returns the message to send again, and RewindBoth does both. The returned message is the caller's original input, before context providers and middleware append reminders. A checkpoint's ID is its run's ID, which the run's events carry.

hist, err := builtin.NewFileHistory(builtin.FileHistoryConfig{Dir: ".agentx/history"})
tools := builtin.Tools(builtin.Config{Dir: ".", History: hist})
a := agent.MustNew("coder", agent.WithModel(m), agent.WithTools(tools...),
	agent.WithFileHistory(hist))
runner := agent.NewRunner(a, store)

cps, err := runner.Checkpoints(ctx, id)
res, err := runner.Rewind(ctx, id, cps[len(cps)-1].ID, agent.RewindBoth)
// res.Message is the removed prompt; res.Restored lists the restored files.
  • What is tracked. Before changing a file, write_file, edit_file and apply_patch save its state: content by SHA-256, mode and symbolic link target, for creations, deletions and moves too. If the save fails, the tool changes nothing. Edits by sub-agents count for the calling session. As in Claude Code, changes made by bash, by other tools or outside the agent are not tracked.
  • Restore. Rewinding the code returns each file changed since the checkpoint to its state before the first change after it, through os.Root and all or nothing, like apply_patch. A deleted symbolic link is not created again, and files larger than MaxFileSize are reported in Skipped. The model must read restored files again before changing them. The files' state before the rewind is kept as the checkpoint RewindResult.Undo, which History.Restore restores.
  • Rules. Rewind returns ErrSessionBusy while the session runs and refuses a session with an interrupted run until it is resumed or discarded; a failed run is given up. After compaction removed a checkpoint's message, only the code can be rewound to it. Checkpoints identify messages independently of their content, so repeated prompts cannot match an older compacted checkpoint.
  • Session state. Rewinding the conversation makes the agent that was active at the checkpoint (Checkpoint.Agent) active again, undoing later handoffs. Other session state, such as agent.SessionState values and remembered permission grants, is not rewound, as Claude Code keeps permissions across a rewind.
  • Storage and retention. FileHistory stores each content once under objects/ and each session's checkpoints in sessions/<id>.json, with mode 0600. As Claude Code does by default, it keeps a session's 100 most recent checkpoints and removes its history 30 days after the last change; it also keeps at most 1 GiB per session and files up to 16 MiB. FileHistoryConfig changes the limits.
  • Forks. Runner.Fork copies the checkpoints and the history, so either session can rewind to checkpoints before the fork, and later changes are recorded only for the session that made them. Both sessions still work on the same files.
Context engineering

actx builds the system prompt and a reminder for each new input from providers, which run once per run, before it starts. It also tracks the context window and compacts the conversation once it passes a threshold.

import "github.com/nostosx/agentx/actx"

agent.WithContext(actx.Config{
	Window: actx.Window{Limit: 200_000, ReserveOutput: 16_000},
	Strategy: actx.Chain(
		actx.ClearToolResults{KeepRecent: 3},   // clear old tool results first
		actx.ClearToolArguments{KeepRecent: 3}, // then large old tool arguments
		actx.Summarize{Model: m},               // then summarize older turns
		actx.Truncate{},                        // finally drop the oldest turns
	),
}),
agent.WithContextProvider(actx.Static("rules", "Always answer in English.", actx.PlaceSystem)),

Sections are placed in one of two ways:

  • PlaceSystem appends stable content to the system prompt, which keeps the prompt prefix cacheable.
  • PlaceReminder puts content that changes per request into a <system-reminder> block on the current user input.

actx.InstructionFiles loads project instruction files the way Codex and Claude Code do (agent.WithInstructionFiles(dir) uses its defaults):

agent.WithContextProvider(actx.InstructionFiles(actx.InstructionConfig{
	Dir:       ".",                             // default
	UserFiles: []string{"~/.codex/AGENTS.md"}, // optional personal files, read first
}))
  • The project root is the nearest ancestor of Dir containing a RootMarkers entry (default .git). From the root down to Dir, each directory contributes the first file of Names that exists (default AGENTS.override.md, AGENTS.md, CLAUDE.md), then any LocalNames (none by default; for example CLAUDE.local.md). Without a root, only Dir is searched.
  • Each file becomes a PlaceSystem section labeled with its path; the files are read again on each run.
  • @path imports outside code spans and fenced code blocks are expanded after the importing file, relative to it, up to ImportDepth hops (default 4) and once per file.
  • The files are untrusted: they and their imports are read only inside the project root (a user file's imports only inside its directory; both may also read inside ImportDirs), and paths that leave these directories through .. or symbolic links are skipped.
  • MaxBytes bounds the total (default 32 KiB); the rest is dropped with a note.

Without a Strategy, compaction uses ClearToolResults, then ClearToolArguments, then Truncate. It keeps the task a run is working on:

  • Reminders added mid-run, such as a Stop hook's continuation, stay in the turn they continue.
  • The turn in progress keeps its signed reasoning, so Anthropic extended thinking stays valid.
  • Tool results the model has not seen yet are never cleared, so a single oversized batch can exceed the window; the middleware then logs a warning. Bound tool output, for example with tool.TruncateOutput.

Editing earlier history invalidates Anthropic thinking replayed after it on models with the thinking prefix check (Claude Fable 5.1, Opus 5.5, Sonnet 5.5 and Haiku 5.5). The Anthropic provider of agentx-contrib asks the API to drop those blocks, so requests succeed and the model thinks again.

Skills

Skills use the same SKILL.md format as Claude Code: YAML front matter with a description and an optional name, which defaults to the directory name, followed by a Markdown body. The model sees only the name and description at first and loads the full text through a tool when it needs it. This is progressive disclosure: the full text enters the context only when the model uses it.

  • Front matter. All fields of Claude Code's frontmatter reference are typed in skill.Metadata, including the Agent Skills fields license, compatibility and metadata; unknown keys stay in Extra. The YAML subset covers nested block and flow maps and lists, quoted scalars that span lines, block scalars and comments.
  • Names. Validate follows Claude Code, which also accepts names such as Release Notes; the skill.Strict() load option enforces the Agent Skills specification.
  • Discovery. skill.Discover loads .claude/skills and .agents/skills from the working directory up to the project root. Managed, home (opt-in), additional and nested directories are optional. Higher precedence wins: managed, user, project, then additional; conflicts are reported.
  • Invocation. The skill tool takes arguments and expands $ARGUMENTS, $N, $name (from arguments), ${CLAUDE_SKILL_DIR}, ${CLAUDE_SESSION_ID} and the other substitutions. !`command` injection runs only with skill.Config{Shell: true}, because it executes shell commands written in skill files; otherwise the text stays as is.
  • Behavior. disable-model-invocation skills are hidden from the model; users invoke them with agent.RunSkill or Registry.Invoke. Until the run ends, allowed-tools skip approval, disallowed-tools are removed, and model and effort apply. context: fork runs the skill in a sub-agent: general-purpose by default, or an agent passed to WithSkillAgents. The default sub-agent runs the parent's tools under the parent's permission policy and tool guardrails. Skill hooks last for the session, and skills with paths appear once a matching file is read or edited.
  • Base directory. Skills from disk show the model their directory, so it can find bundled files. Embedded skills have none.
//go:embed skills
var skillFS embed.FS

skills, err := skill.LoadFS(skillFS, "skills", "")
if err != nil {
	log.Fatal(err)
}
found, err := skill.Discover(skill.DiscoverConfig{}) // project skills
if err != nil {
	log.Print(err) // skills that failed to load; the others are in found
}
a := agent.MustNew("dev", agent.WithModel(m),
	agent.WithSkills(append(skills, found.Skills...)...))
res, err := agent.NewRunner(a, nil).RunText(ctx, "", "ship it", agent.RunSkill("deploy", "staging"))
Long-term memory

memory stores facts across sessions. Retrieval is keyword-based by default. With an Embedder configured, memory.Hybrid combines keyword and vector results. If one retriever fails, it uses the others and logs the error with slog.Default unless you set OnError. memory.NewMemoryStore keeps entries in memory; filestore.Memory and sqlstore.Memory of agentx-contrib persist them.

store, _ := filestore.Memory(".agentx/memory.json")
mem := memory.New(store)
// Recall 3 memories per input, scoped to user-42.
a := agent.MustNew("assistant", agent.WithModel(m), agent.WithMemory(mem, "user-42", 3))

By default the model also gets tools to remember, recall and forget facts (memory_remember, memory_recall and memory_forget). agent.WithoutMemoryTools() hides them.

WithMemory gives every run of the agent the same scope. An agent that serves several users, for example behind server.New, must choose the scope per run instead: agent.WithMemoryScopeFunc(mem, scope, 3) calls scope(ctx) for each recall and memory tool call, with the context the run was started with, such as the request context where an HTTP middleware stored the authenticated user. When it returns an error, the call fails, so a run whose user is unknown reads and writes no memories.

The providers of agentx-contrib ship embedders: openai.NewEmbedder (/embeddings, including compatible services such as openai.Ollama()) and gemini.NewEmbedder (batchEmbedContents). retry.Embedder of agentx-contrib retries them like model calls. Gemini can embed queries and stored entries for different tasks:

docs, _ := gemini.NewEmbedder(gemini.WithTaskType(gemini.TaskRetrievalDocument))
queries, _ := gemini.NewEmbedder(gemini.WithTaskType(gemini.TaskRetrievalQuery))
policy := retry.Policy{}
mem := memory.New(store,
	memory.WithEmbedder(retry.Embedder(policy, docs)),
	memory.WithQueryEmbedder(retry.Embedder(policy, queries)))
Retrieval-augmented generation

rag loads documents, splits them into chunks, embeds and indexes the chunks, and connects the index to agents as a search tool and as automatic retrieval for each run.

docs, _ := (&rag.FSLoader{FS: os.DirFS("docs"), Include: []string{"**/*.md"}, GitIgnore: true}).Load(ctx)
emb, _ := openai.NewEmbedder()
store, _ := filestore.Vectors(".agentx/rag")
ix := rag.NewIndex(rag.Config{Store: store, Embedder: emb})
// Embed new and changed sources only, and delete removed ones.
if _, err := ix.Sync(ctx, docs); err != nil {
	log.Fatal(err)
}
a := agent.MustNew("assistant", agent.WithModel(m),
	agent.WithTools(rag.Tool(ix, rag.ToolOptions{Description: "Search the product docs."})),
	agent.WithContextProvider(rag.Provider(ix, 3)))
  • Loaders. FSLoader reads any fs.FS with glob filters and optional .gitignore rules; URLLoader fetches URLs. Parsers cover text, Markdown, HTML (with the web_fetch converter), JSON, JSON Lines and CSV. PDF needs a library outside the standard library: implement rag.Parser and register it in FSLoader.Parsers.
  • Splitters. Sizes are measured in tokens with the estimate of the actx package, or any LengthFunc. RecursiveSplitter splits at separators with overlap, MarkdownSplitter keeps the header path in MetaHeaders, CodeSplitter splits Go, Python and JavaScript at declarations, and AutoSplitter, the default, picks one per document. Chunk IDs hash the source, document and offset, so they are stable.
  • Indexing. Index embeds chunks in batches and stores them in a VectorStore: rag.NewMemoryStore(), or filestore.Vectors of agentx-contrib with one JSON file per source. A content hash per source lets Sync skip unchanged sources; change Config.Version after changing the splitter or embedder.
  • Retrieval. By default, Hybrid fuses BM25 (Keyword) and cosine similarity (Vector) with reciprocal rank fusion. Config.Reranker adds a second stage such as LLMReranker. ContextualEnricher implements contextual retrieval: a model writes a short, cached context that is embedded and searched with each chunk.
  • Agents. rag.Tool is read-only and returns numbered chunks with their sources, IDs and scores, bounded in size; the result metadata carries the citations.
File and SQL storage

The core keeps state in memory only: session.NewMemoryStore, loop.NewMemoryCheckpointer, workflow.NewMemoryCheckpointer, memory.NewMemoryStore, rag.NewMemoryStore and mcp.NewMemoryTokenStore. Persistent stores are in agentx-contrib:

  • filestore writes JSON files: Sessions, Checkpoints, WorkflowCheckpoints, Memory, Vectors and MCPTokens.
  • sqlstore keeps sessions, run checkpoints, workflow checkpoints and memories in SQLite, PostgreSQL or MySQL through database/sql, with the driver of your choice.
sessions, _ := filestore.Sessions(".agentx/sessions")
runner := agent.NewRunner(a, sessions)

See the agentx-contrib README for their options, schema and tests.

Conformance suites check any store against the contract the built-in stores follow: session/sessiontest, loop/checkpointtest, workflow/checkpointtest, memory/memorytest and rag/ragtest, whose ragtest.Embedder is a deterministic embedder for tests.

func TestStore(t *testing.T) {
	sessiontest.Run(t, func(t *testing.T) session.Store { return newStore(t) })
}
Multi-agent

Pass sub-agents to WithSubAgents and the parent can call them like tools.

  • Hooks. The parent's SubagentStart hooks fire before a sub-agent starts a task; their AdditionalContext is added in front of the task. SubagentStop hooks fire when a sub-agent finishes. One that denies with a reason, or adds context, makes the sub-agent continue, under the same cap as Stop. Both match the sub-agent's name, and Event.RunID is the sub-agent's run.
  • Usage. A sub-agent's token usage, requests and tool calls are added to the parent run's Usage, so they count toward its limits.
  • Approvals. When a sub-agent without its own Approver needs approval, the parent's Approver decides. Without one, the parent run is interrupted with the sub-agent's requests; ApprovalRequest.Agent names the agent that asked. Resuming the parent resumes the sub-agent with the decisions. The sub-agent needs no checkpointer of its own.
  • Events. WithSubAgentEvents() forwards the events of sub-agents, at every depth, into the parent's stream. Forwarded events keep the sub-agent's RunID and Agent, have Depth 1 for a sub-agent, 2 for its sub-agents, and ParentCallID set to the tool call that started the sub-agent. Their run_end ends the sub-agent, not the parent, so check Depth == 0 when you switch on Type. Off by default.
  • Concurrency. Sub-agents run one at a time by default, since they may have side effects. With WithConcurrentSubAgents() sub-agent calls in one model response run in parallel, bounded by WithParallelism. Sub-agents that need approval at the same time interrupt the parent together and resume together.
  • Depth. Sub-agents can be nested agent.DefaultMaxDepth (5) levels deep; WithMaxDepth changes the bound, and the smallest bound above a sub-agent applies. A call beyond it returns an error to the model.
coordinator := agent.MustNew("coordinator",
	agent.WithModel(m),
	agent.WithSubAgents(researcher, writer),
	agent.WithSubAgentEvents(),
	agent.WithConcurrentSubAgents(),
)

for ev, err := range coordinator.Run(ctx, agent.Text("Write about MCP")) {
	if err != nil {
		log.Fatal(err)
	}
	if ev.Depth > 0 && ev.Type == loop.EventToolCall {
		fmt.Printf("%s (via %s) -> %s\n", ev.Agent, ev.ParentCallID, ev.ToolCall.Name)
	}
}

agent.AsTool builds the tool for a single agent with its own name, description and these options, for use with WithTools.

Handoffs

WithHandoffs lets the model hand the conversation to another agent, as in the OpenAI Agents SDK and Google ADK. Each agent becomes a transfer_to_<name> tool. When the model calls one, the calls of its turn run and the run continues on the other agent with its own instructions, tools, guardrails and middleware.

  • One run. The run keeps its ID, usage and turn count, so limits cover every agent. Events of the next agent follow a loop.EventHandoff (Event.Handoff names From, To and the call), and Result.Agent names the agent that answered.
  • Sessions. A Runner saves the active agent in the session state (agent.ActiveAgentKey) and sends the next message to it. Rewinding the conversation restores the agent active at the checkpoint.
  • Permissions. Handoff tools are read-only, so plan mode allows them; see Permissions.
  • History. The next agent sees the whole conversation and a note about the handoff. A filter (HandoffInputFilter) rewrites what its model sees, not what the session saves; agent.RemoveToolCalls and agent.NestHandoffHistory are built in.
  • Options. WithHandoff(a, ...) takes HandoffName, HandoffDescription, HandoffInputFilter and HandoffInput[T](fn), a typed argument the model sends, checked by fn. agent.Ref(name, desc) refers to an agent built later, such as the one handing off, so agents can hand back. WithMaxHandoffs bounds handoffs per run (10 by default), and only the first handoff call of a turn counts.
  • Approvals. An agent that was handed the run can be interrupted like any other. Resume continues on that agent, located through the checkpointers of the agent graph. Agents that hand off to each other should share a checkpointer.
  • Hooks. No hook event is specific to handoffs. PreToolUse and PostToolUse with the matcher transfer_to_.* observe and can deny them.
billing := agent.MustNew("billing", agent.WithModel(m),
	agent.WithDescription("Invoices and refunds."),
	agent.WithHandoffs(agent.Ref("triage", "Anything else.")))
triage := agent.MustNew("triage", agent.WithModel(m),
	agent.WithHandoff(billing, agent.HandoffInputFilter(agent.RemoveToolCalls)))
res, err := agent.NewRunner(triage, nil).RunText(ctx, id, "My invoice is wrong")
fmt.Println(res.Agent, res.Output) // billing ...
Guardrails

Package guard checks the input, the final answer and tool calls. A check returns guard.Result{Tripwire, Info, Message}. A tripped guardrail ends the run with StatusBlocked and Result.Guardrail, and the result's Output is empty. An error fails the run.

  • Input. WithInputGuardrail(g) runs before the run starts, before hooks. With guard.Parallel() it runs alongside the first model call instead: a trip cancels the call, the first response waits for all parallel guardrails, so no tool runs first, and a completed call's usage is still counted. A resumed run that had not passed them runs them again. Input guardrails do not run for the agent a run is handed off to.
  • Output. WithOutputGuardrail(g) checks the answer after Stop hooks let the run finish. A blocked answer is withheld: the result, the checkpoint and the session keep [response withheld by guardrail <name>] in its place, which keeps the conversation valid for the next run. The answer streams as the model writes it, before the check, unless WithOutputGuardrail(g, guard.HoldOutput()) holds each response's deltas and message until it calls tools (then they stream as usual) or the guardrails pass; a held answer arrives at once when its response ends. A held answer that does not end the run is withheld from output: one a Stop hook continues, one the provider paused, one that fails the output schema or one of a run that stops or fails first. Its text is absent from Result.Output, OutputError.Output and output_retry; schema diagnostics are generic because details may quote unchecked text. Drafts remain in the conversation, including Result.Messages, Result.NewMessages and checkpoints, for continuation and recovery. Treat those conversation fields as internal state when displaying output.
  • Tools. WithToolGuardrail(g) checks calls before approval (Before) and results after they ran (After), before tracing, hooks and events see them. A tripped After replaces the result, its media and metadata included, so the withheld content is neither sent nor kept in the conversation, and PostToolUse hooks do not run for it.
agent.WithInputGuardrail(guard.InputGuardrail{
	Name: "topic",
	Check: func(ctx context.Context, run *loop.Run, in *model.Message) (guard.Result, error) {
		return guard.Result{Tripwire: isHomework(in.Text()), Message: "Not supported."}, nil
	},
}, guard.Parallel())
Workflows

workflow runs agents and functions as a graph. It supports:

  • parallel branches
  • joins that wait for every incoming branch
  • conditional routes, and loops back through them
  • dynamic fan-out over a list with Map
  • per-key reducers for merging state
  • retries with exponential backoff, and fallback nodes
  • persistence, crash recovery and human-in-the-loop suspensions
  • streaming events and Mermaid rendering
  • {{key}} templates filled from the shared state
g := workflow.NewGraph().
	AddNode("draft", workflow.AgentNode(writer, workflow.Template("Write about {{topic}}"), "draft")).
	AddNode("review", workflow.AgentNode(reviewer, workflow.Template("{{draft}}"), "final"),
		workflow.Retry(2, time.Second)).
	AddChain(workflow.Start, "draft", "review", workflow.End)

wf, err := g.Compile()
if err != nil {
	log.Fatal(err)
}
st, err := wf.Run(ctx, workflow.NewState(map[string]any{"topic": "Go iterators"}))
if err != nil {
	log.Fatal(err)
}
fmt.Println(st.String("final"))

State merging. Each node works on its own view of the state. Its writes are committed together when it succeeds, and dropped when it fails or suspends. Two nodes that the graph does not order must not set the same key unless the key has a reducer; otherwise the run fails with a ConflictError. State.Update changes are replayed and never conflict.

Persistence and resuming. With WithCheckpointer, the run is saved when a node attempt starts and after each node, as a Checkpoint. Use NewMemoryCheckpointer, or filestore.WorkflowCheckpoints or sqlstore.WorkflowCheckpoints of agentx-contrib. State values must then be encodable as JSON; restored values are plain JSON values, which State.Value[T] reads back typed. Resume continues a run that crashed, failed or suspended:

  • Finished nodes do not run again.
  • Nodes that were running run again with the same attempt number. Node code therefore runs at least once, but state writes take effect exactly once.

Checkpoint save or cleanup failures fail the run and are returned by Run/Resume and the stream, including failures during suspension or completion. An existing execution error is preserved alongside the storage error; a failed suspension save is not reported as a resumable suspension.

Human in the loop. A node calls Suspend and returns its error. The run then ends with a *Suspension that lists the Pending keys. Resume the run with a value for each key. The node runs again from its start, and this time Suspend returns the value.

An AgentNode whose agent needs tool approvals suspends the same way. Its Pending.Interrupt holds the requests. Resume it with map[string]loop.Approval. Each agent run gets a predictable ID, SubRunID(run, node, attempt, item). After a crash, an agent with a Checkpointer therefore resumes its own run, and finished tool calls are not repeated.

wf := g.MustCompile(
	workflow.WithCheckpointer(cp),
	workflow.Reducer("summaries", workflow.Append()),
)
_, err := wf.Run(ctx, st, workflow.WithRunID("job-42"))
if s, ok := errors.AsType[*workflow.Suspension](err); ok {
	approvals := map[string]loop.Approval{}
	for _, req := range s.Pending[0].Interrupt.Requests {
		approvals[req.Call.ID] = loop.Approve
	}
	st, err = wf.Resume(ctx, "job-42", map[string]any{s.Pending[0].Key: approvals})
}

Fan-out, loops and retries.

  • Map("docs", body, MapConcurrency(4)) runs body once per list element. Each element is visible as item. Writes are merged in element order, and finished elements are saved one by one.
  • A branch may route back to an earlier node. The loop then restarts from that node, and MaxSteps bounds the run with ErrStepLimit.
  • WithRetry(RetryPolicy{RetryIf: workflow.Transient}) adds exponential backoff with jitter. OnError("fallback") routes a failed node to a fallback node.

Events. Stream yields node, state-delta, suspension, agent and Emit events. Mermaid() renders the graph.

MCP

The mcp package is a client and a server for the Model Context Protocol (revision 2026-07-28, falling back to 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05). The client:

  • Version negotiation: Connect probes with server/discover. A modern server is used statelessly, with the protocol version in each request's _meta. Any other error, an HTTP 4xx without a modern error, or on stdio no answer within 10 seconds (mcp.WithProbeTimeout) means an older server: Connect then sends initialize offering 2025-11-25 and accepts any of the versions above. On stdio a late answer to the probe still counts, so a slow-starting modern server stays modern. mcp.WithProtocolVersion pins one version.
  • Older servers: over Streamable HTTP the client keeps the session ID, sends it with MCP-Protocol-Version on every request, starts a new session when the server answers 404 (requests issued meanwhile wait for it), and sends DELETE on Close. Broken SSE streams are resumed with GET and Last-Event-ID, honoring the server's retry, including servers that poll by closing streams. Missing capabilities give empty lists, or an error wrapping errors.ErrUnsupported.
  • Client features: mcp.WithSampling(handler) (mcp.ModelSampler(m) samples with an agentx model; mcp.ApproveSampling and mcp.ReviewSampling let a human check requests and replies), mcp.WithElicitation(handler, modes...) for form and URL elicitation, and mcp.WithRoots(fn) with Client.RootsChanged. Older servers send these as requests; modern ones return input_required, and Call runs the same handlers and retries. Without a handler, older servers get method not found and modern ones an *mcp.InputRequiredError.
  • Logging and ping: mcp.WithLogHandler, mcp.WithLogLevel and Client.SetLogLevel (logging/setLevel, or the per-request _meta level with modern servers); Client.Ping (server/discover with modern servers, which have no ping).
  • Transports: stdio and Streamable HTTP.
  • Authorization: mcp.WithOAuth(cfg) handles a 401 by reading the WWW-Authenticate challenge, fetching the Protected Resource Metadata (RFC 9728) and the authorization server metadata (RFC 8414 or OpenID Connect, issuer checked), obtaining a client ID (pre-registered, Client ID Metadata Document, or Dynamic Client Registration) and running the authorization code flow with PKCE S256, resource (RFC 8707) and iss validation (RFC 9207). cfg.Authorize shows the URL to the user; mcp.NewLoopbackRedirect receives the redirect on 127.0.0.1. Tokens are refreshed, kept in a TokenStore (NewMemoryTokenStore, or filestore.MCPTokens of agentx-contrib with mode 0600), and a 403 insufficient_scope steps the scopes up. Grant: mcp.GrantClientCredentials authorizes machines with a secret or mcp.PrivateKeyJWT.
  • Notifications: Listen opens a subscriptions/listen stream for list and resource changes. With older servers it uses the HTTP GET stream (stdio needs none) and resources/subscribe, and acknowledges the subscription itself; their notifications are not filtered.
  • Tool names: server tools become agentx tools named mcp__<server>__<tool>.
  • Media: images, audio and embedded resource blobs in tool results become tool.Result.Parts (mcp.ToolResultFrom), so models that take media see them; text and resource links stay text. Sampling handlers receive and return image and audio content.
  • Approval and trust: tools require approval unless the server hints that they are not destructive (destructiveHint: false); a missing hint counts as destructive, as in the MCP spec. That hint is trusted as given, but readOnlyHint is ignored unless you pass mcp.TrustReadOnlyHint(). To decide approval without relying on annotations, pass mcp.RequireApprovalFor(fn); RequireApprovalFor(nil) turns approval off for every tool.
  • Name collisions: long or colliding tool names get a short hash suffix. Whether a name collides depends on the other tools the server lists, so such a name can change when the tool list changes.
c, err := mcp.Connect(ctx, mcp.Stdio("npx", "-y", "@modelcontextprotocol/server-filesystem", "."))
if err != nil {
	log.Fatal(err)
}
defer c.Close()
fsTools, err := mcp.Tools(ctx, c, "fs")
if err != nil {
	log.Fatal(err)
}
a := agent.MustNew("assistant", agent.WithModel(m), agent.WithTools(fsTools...))

For a remote server, use mcp.HTTP("https://example.com/mcp"):

lr, err := mcp.NewLoopbackRedirect("")
if err != nil {
	log.Fatal(err)
}
defer lr.Close()
cfg := mcp.OAuthConfig{Store: filestore.MCPTokens(tokenPath)}
lr.Configure(&cfg)
c, err := mcp.Connect(ctx, mcp.HTTP("https://example.com/mcp"), mcp.WithOAuth(cfg))

mcp.NewServer(info, opts) serves agentx tools, resources and prompts to clients of every version above:

  • Tools: the tools of a tool.Registry (ServerOptions.Registry or Tools), with their schemas, run through tool.Executor; ReadOnly becomes readOnlyHint and RequiresApproval destructiveHint. tool.Result.Parts become image and audio content, embedded resources for other media (and for audio before 2025-03-26), and resource links for media by URL (text before 2025-06-18). mcp.AgentTool(a, mcp.AgentToolOptions{}) exposes an agent, asking the client's user to approve its tool calls.
  • Resources and prompts: AddResource, AddResourceTemplate ({var} and {+var}), AddPrompt, or ResourceSource and PromptSource implementations. Changes notify subscriptions/listen streams and sessions; ResourceUpdated(uri) reaches subscribers.
  • Handlers: mcp.Sample, mcp.Elicit and mcp.ListRoots ask the client for input: older clients get a request, newer ones an input_required result whose requestState is signed and bound to the request. The handler just blocks until the retry; on another instance sharing StateKey it runs again with the earlier answers. mcp.Log, mcp.Progress and mcp.CurrentRequest work in both eras.
  • Transports: srv.ServeStdio(ctx, os.Stdin, os.Stdout) and srv.HTTPHandler(opts): modern requests are stateless and checked against their MCP-Protocol-Version, Mcp-Method, Mcp-Name and Mcp-Param-* headers; initialize starts a session with Mcp-Session-Id, resumable SSE streams (Last-Event-ID, optional polling), a GET stream and DELETE. Origins other than loopback and AllowedOrigins get 403. ModernOnly refuses the older versions.
  • Authorization: HTTPOptions.Auth validates bearer tokens with your Verify function, answers 401 and 403 insufficient_scope challenges naming the Protected Resource Metadata, which it serves, and binds sessions to the token's subject.
srv := mcp.NewServer(mcp.Implementation{Name: "files", Version: "1.0"},
	mcp.ServerOptions{Tools: []tool.Tool{readTool, mcp.AgentTool(a, mcp.AgentToolOptions{})}})
http.Handle("/mcp", srv.HTTPHandler(mcp.HTTPOptions{}))
Middleware

The core loop only calls the model, runs tools, handles approvals and saves checkpoints. Everything else plugs in as runtime middleware, including hooks and context engineering, which agent.New installs for you. A middleware implements loop.Middleware, which only has a Name, plus any of these stages:

Stage When it runs
WrapContext Once when Run or Resume starts; derives the run's context, such as a trace span, and releases it when the call returns
BeforeRun A new run starts, before the input enters the conversation
BeforeTurn Each turn, before the model request is built
BeforeModel / AfterModel Before and after each model call
WrapModel Around each model call; can skip the call or retry it
BeforeTool Each tool call: allow it, ask for approval or deny it
RevalidateTool Approved argument edits, before execution; enforce argument policies without repeating hooks
WrapTool Around each tool execution
AfterTool After each tool call; can rewrite the result
BeforeFinish The model answered without tool calls; can keep the run going
AfterRun The run ended, including when it was interrupted. If the caller stopped reading, the status is running; if it stopped at the first event, AfterRun does not run
  • Order: Before stages run in registration order and After stages in reverse. For Wrap stages, the first middleware is the outermost.
  • Errors: returning loop.Block or loop.Stop ends the run as blocked or stopped. Any other error fails the run.
  • State: values set with run.SetValue are saved with checkpoints, so they survive an interrupted run.
  • Tools: a middleware that implements loop.ToolProvider adds its tools. Middleware enforcing argument policies in BeforeTool must also implement loop.ToolRevalidator to check approved edits. Permission and tool guardrails do so. Revalidation can deny or fail the call; it must not rewrite approved arguments. An ask decision is already satisfied by the explicit approval. Hooks and unchanged approvals are not repeated.
Tool call pipeline

agent.New registers its middleware in this order: input guardrails, hooks, skills, WithMiddleware middleware in the order given, the WithPermissions policy, handoffs, context engineering, and finally tool and output guardrails. A tool call then passes these steps:

  1. BeforeTool: PreToolUse hooks, an active skill's allowed and disallowed tools, WithMiddleware middleware, the permission policy, the handoff check and tool input guardrails. The most restrictive decision wins in any order: a hook's allow does not lift a permission ask or deny. The first denial skips the remaining stages; a tripped tool input guardrail ends the run. The permission policy also sees the decision so far, which matters in dontAsk mode.
  2. Approval when a stage asked, or no stage decided and the tool requires approval. Allowed calls skip it.
  3. RevalidateTool when an approval edited the arguments. Hooks do not run again.
  4. Execution: schema validation, then WrapTool middleware (first outermost, tool output guardrails innermost), then WithToolMiddleware (first outermost), then the tool.
  5. AfterTool in reverse: guardrails, the permission policy, WithMiddleware middleware, skills, then PostToolUse hooks. Denied calls fire no hooks.
Purpose Use
Rules and modes for tools, paths and commands permission.Policy with WithPermissions
Policies kept outside the program Command or HTTP hooks with WithHooks
Checks whose failure must stop the run WithToolGuardrail
Go policies on arguments loop.Middleware with BeforeTool and loop.ToolRevalidator
Timeouts, truncation and other execution concerns WithToolMiddleware; loop.ToolWrapper when the run is needed
Who approves which calls WithApprover, tool.CallApprover, tool.Info.RequiresApproval

The middleware package provides tracing and metrics, recorded through the Tracer and Meter interfaces of the telemetry package. Logging (logging.New) and retries with exponential backoff (retry.New), which retry only transient failures, are in the middleware module of agentx-contrib. telemetry.ErrorType(err) returns the error.type value the tracing and metrics record for an error, and telemetry.StartSpan adds a span to the current run, for middleware of your own.

middleware.Tracing and middleware.Metrics follow the OpenTelemetry GenAI semantic conventions. Each run gets an invoke_agent {agent} span. Its children are a client span per model call, named like chat gpt-6-astra, and an execute_tool {tool} span per tool call; a sub-agent's run nests under the execute_tool span that started it. Spans carry gen_ai.* attributes, including token usage with cache and reasoning tokens, and the run ID, session ID, agent name and turn (the invoke_agent span has no turn). Providers report their gen_ai.provider.name through model.Describer.

  • Other spans. Hooks get a hook {event} span and automatic compaction a compact_context span; later model spans then carry gen_ai.conversation.compacted. Each retry of agentx-contrib's retry middleware is an agentx.retry event on the model call's span.
  • Handoffs and guardrails. Each agent of a handed-off run gets its own invoke_agent span; the spans after a handoff carry agentx.handoff.from. A run a guardrail blocked carries agentx.guardrail.name and agentx.guardrail.kind and an agentx.guardrail event, with the guardrail's message when content is recorded.
  • Metrics. Model call duration and streaming chunk timing, token usage counters and per-call histograms (including cache and reasoning tokens), tool call duration, and run duration with the run status and the run's own model and tool call counts. middleware.LegacyMetrics() also records the replaced gen_ai.client.token.usage and gen_ai.client.operation.duration.
  • Content. The system prompt, messages, tool names, and tool arguments and results are recorded only with middleware.RecordContent(), because they may contain sensitive data.
  • OpenTelemetry. The core uses only the standard library and kitx; the otel module of agentx-contrib adapts OpenTelemetry providers to telemetry.Tracer and telemetry.Meter. For other libraries, implement the two interfaces.

Register tracing and metrics before retries, so a model call's span and duration cover its retries:

a := agent.MustNew("assistant",
	agent.WithModel(m),
	agent.WithMiddleware(otel.Middleware(tracerProvider, meterProvider)...),
	agent.WithMiddleware(
		logging.New(slog.Default()),
		retry.New(retry.Policy{MaxAttempts: 5}),
	),
)

To write your own, use loop.Funcs or a type that implements the stages:

redact := &loop.Funcs{
	MiddlewareName: "redact",
	AfterToolFunc: func(_ context.Context, _ *loop.Run, _ *loop.ToolPlan, res *model.ToolResult) error {
		res.Content = apiKey.ReplaceAllString(res.Content, "[redacted]")
		return nil
	},
}

Some model calls don't go through the agent loop, such as the model of a actx.Summarize strategy. For those, wrap the model directly: middleware.Wrap(m, middleware.TraceModel(tracer), retry.Model(policy)).

Serving

The server package serves a Runner over HTTP, using only the standard library:

srv := server.New(runner,
	server.WithAuth(checkToken),       // func(*http.Request) error
	server.WithCheckpointer(cp),       // report interrupts after a restart
	server.WithCORS("https://app.example"))
defer srv.Close()
http.ListenAndServe(":8080", srv)

srv.Shutdown(ctx) cancels background A2A tasks and waits for them to save their sessions until ctx ends; Close waits without a deadline. Shut the HTTP server down first.

Endpoint Purpose
POST /sessions/{id}/runs Run {"input": "..."} or {"message": {...}}
POST /sessions/{id}/resume Resume with {"approvals": {"<call_id>": {"approved": true}}}
POST /sessions/{id}/discard Discard the interrupted or failed run
GET /sessions/{id} Messages and the pending run with its approval requests
DELETE /sessions/{id} Runner.EndSession; ?purge=true also deletes it
POST /sessions, GET /sessions, POST /sessions/{id}/clear, /fork Create, list, clear and fork
GET /sessions/{id}/checkpoints, POST /sessions/{id}/rewind List checkpoints; rewind with {"checkpoint_id": "...", "mode": "both"} (code, conversation)
POST /chat Vercel AI SDK useChat
POST /agui AG-UI
GET /.well-known/agent-card.json, POST /a2a A2A 1.0 (JSON-RPC)
  • Wire format. Runs stream Server-Sent Events: the event name is the event type, the ID its sequence number, and the data the loop.Event as JSON with snake_case fields. Accept: application/json returns the final Result instead, and application/x-ndjson streams JSON Lines. server.WriteJSONL(w, events) writes the same lines, for example for a command's output. The interrupt's run state and the full conversation are left out.
  • Protocols. ?protocol=vercel-ui or ag-ui, or WithProtocol, streams the AI SDK UI message stream or AG-UI events from the session endpoints too. Tool approvals map to the AI SDK's approval parts and to AG-UI interrupts; handoffs and output retries to data-handoff and data-output-retry parts and AG-UI CUSTOM events; a guardrail block to the finish reason content-filter and RUN_ERROR code blocked.
  • Files. User messages of every protocol take images, audio, video, text, PDF and Office files, inline or by URL, as model parts; other types get 415. The UI message stream sends media of answers and tool results as file parts. An answer an output guardrail blocks is never saved; with guard.HoldOutput() it is never streamed either.
  • A2A. SendMessage, SendStreamingMessage, GetTask, ListTasks, CancelTask and SubscribeToTask of the A2A specification. The contextId is the session ID; a run waiting for approvals puts its task in TASK_STATE_INPUT_REQUIRED, and a guardrail block in TASK_STATE_REJECTED.
  • Concurrency. A session runs one request at a time: another one gets 409. session.ErrConflict is 409 as well.
  • Disconnects. If the client goes away, the run stops and the session is saved. With a Checkpointer, POST /sessions/{id}/resume continues it.
  • Limits. Bodies are capped by WithMaxBodyBytes. Clients may set max_turns and limits only with AllowRunOptions. WithSessionAccess authorizes each session ID.

See the server package documentation for the full list of endpoints and error codes.

Evaluation

The eval package runs a task over a dataset and scores each output, in the style of pydantic-evals and Google ADK evaluation:

ds := &eval.Dataset[string, string]{
	Name: "weather",
	Cases: []eval.Case[string, string]{{
		Name: "kyoto", Input: "Weather in Kyoto?", Expected: new("Sunny"),
		Evaluators: []eval.Evaluator{eval.ToolTrajectory(eval.MatchExact,
			[]eval.ToolCall{{Name: "weather", Args: map[string]any{"city": "Kyoto"}}})},
	}},
	Evaluators: []eval.Evaluator{
		eval.Contains("sunny", eval.IgnoreCase()),
		eval.MaxTurns(3),
		eval.LLMJudge(judge, "The answer states the forecast without hedging.", eval.JudgeInput()),
	},
}

func TestWeather(t *testing.T) {
	eval.Run(t, ds, eval.AgentTask(a), eval.Repeat(3), eval.Threshold(0.9))
}
  • Tasks. Any func(ctx, In) (Out, *loop.Result, error). eval.AgentTask(a) runs an agent on the input text, TypedAgentTask[T] decodes structured output and AgentTaskFunc maps other inputs. SimulateUser(a, userModel) holds a multi-turn conversation with a user played by a model following a Scenario, like ADK's user simulation.
  • Evaluators. They return assertions, scores and labels with reasons: Equals, EqualsExpected, Contains, Regex, JSONSchema, IsType, DecodesAs, Completed, MaxTurns, MaxTokens, MaxToolCalls, MaxDuration, ResponseMatch (ROUGE-1) and ToolTrajectory (MatchExact, MatchInOrder, MatchAnyOrder or MatchSubset, with PartialArgs or IgnoreArgs). Write others with Check, Func or TypedCheck.
  • LLM judge. LLMJudge asks a model for a verdict (reason, pass and, with JudgeScore, a score from 0 to 1), requested and validated as structured output, at temperature 0 by default. JudgeInput and JudgeExpected show the judge the input and expected output; JudgeSeed and JudgeReasoning are optional.
  • Spans. Trace the agent with middleware.Tracing(eval.Tracer()): the spans of each run are in EvaluatorContext.Spans, and HasSpan and NoSpan match them with a SpanQuery.
  • Running. Dataset.Evaluate takes Concurrency (8 by default), Repeat, Timeout, EvaluatorTimeout, Experiment and OnResult. A task or evaluator that fails, panics or hangs fails only its case.
  • Reports. Pass rate, assertion rates, mean scores, labels, tokens and duration percentiles, with ByCase for repeated cases. WriteText prints a table, Save and LoadReport use JSON, and eval.Compare(baseline, candidate) lists regressions. eval.Run logs the report and fails the test below Threshold or MinScore, or when a case regressed from Baseline.
  • Datasets. eval.LoadDataset[In, Out](path) and Dataset.Save read and write JSON or JSON Lines (.jsonl). Evaluators are code; expected tool calls can be stored in case metadata under tool_calls.

Record and replay. eval/replay records HTTP interactions to a cassette file and replays them, so provider and agent tests run without network access. Pass its client to a provider, such as one of agentx-contrib:

rec := replay.NewTest(t, "testdata/weather.json")
m, _ := openai.New(openai.WithHTTPClient(rec.Client()))

AGENTX_REPLAY=record go test ./... records with real credentials; the default replay mode answers from the cassette, and auto records only missing cassettes. Requests match by method, URL and body (as JSON). Credential headers such as Authorization, x-api-key and x-goog-api-key, and query keys such as key, are saved as REDACTED; WithFilter scrubs bodies. Server-Sent Events are saved and replayed event by event.

Providers

model.Model is a small interface; the implementations live in the provider module of agentx-contrib, which calls the APIs directly over net/http without provider SDKs:

Package API
github.com/nostosx/agentx-contrib/provider/anthropic Anthropic Messages API, also on Vertex AI
github.com/nostosx/agentx-contrib/provider/openai OpenAI Responses and Chat Completions, Azure OpenAI and compatible services such as Ollama, DeepSeek and OpenRouter
github.com/nostosx/agentx-contrib/provider/gemini Gemini generateContent, also on Vertex AI
github.com/nostosx/agentx-contrib/provider/bedrock Amazon Bedrock Converse and ConverseStream

Each reads its key from the environment, such as OPENAI_API_KEY:

m, err := openai.New(openai.DeepSeek()) // reads DEEPSEEK_API_KEY

The agentx-contrib README lists the environment variables, the presets of OpenAI-compatible services, cloud platform options and the media each provider accepts. For request fields specific to one provider, use agent.WithProviderOptions("openai", map[string]any{...}): each adapter reads only the options under its own name, so a request can carry options for several providers. Reasoning effort and display use the typed model.Effort and model.ReasoningDisplay constants. Any other model works once it implements model.Model; model/modeltest provides a scripted model for tests.

Packages

Package Responsibility
agent Agent definition, options, Runner (sessions, resume and rewind)
loop Agent loop, tool execution, approvals, events, checkpoints and the middleware interface
model Neutral messages, requests, responses, streaming and errors; modeltest provides a scripted model for tests
tool Tool interface, JSON Schema, registry, executor and middleware
tool/builtin Built-in file, search, shell, web and todo tools; file history
actx Context assembly, token estimates, compaction strategies; runs as middleware
session Sessions and their storage
memory Long-term memory with keyword, vector and hybrid retrieval
rag Retrieval-augmented generation: loaders, splitters, indexing, retrievers, search tool and context provider
rag/ragtest Conformance tests for rag.VectorStore implementations and a deterministic test embedder
skill SKILL.md loading, discovery, registry and invocation
hook Lifecycle hooks and command hooks; runs as middleware
permission Claude Code permission rules, modes and settings; runs as middleware
guard Input, output and tool guardrails; runs as middleware
workflow Graph orchestration
middleware Runtime middleware for tracing and metrics, plus plain model decorators
mcp MCP client and server, transports, OAuth and tool adapter
server HTTP serving: session API, Vercel AI SDK, AG-UI and A2A
eval Datasets, evaluators (including LLM judges and tool trajectories), reports and comparisons
eval/replay HTTP record and replay for provider tests

agentx-contrib holds the implementations with dependencies or provider-specific code, each in its own module: provider (OpenAI, Anthropic, Gemini and Bedrock), otel, sqlstore, filestore and middleware (logging and retries). Runnable applications live in the separate agentx-examples project.

Examples

Runnable examples, from single-turn Q&A to serving an agent over HTTP with tool approval, live in the separate agentx-examples project. Clone it on its own; Go downloads the published dependencies:

git clone https://github.com/nostosx/agentx-examples.git
cd agentx-examples
go mod download
export ANTHROPIC_API_KEY=...
go run ./cmd/basic "What is an agent loop?"

Development

Use Go 1.27.3, Task v3.53.1 and Bash. task --list lists all targets.

task check      # format, modernizer and tidy checks, vet, build, dependency boundary, race tests
task lint       # golangci-lint
task vulncheck  # reachable vulnerability scan
task test       # unit tests without the race detector
task test RACE=true   # race tests with shuffled execution order
task test COVER=true  # race tests, coverage.out and function coverage

Checks do not apply formatting, modernizer or tidy changes. Run task fmt, task fix or task tidy explicitly to apply them, then review the diff. All targets set GOWORK=off. The deps-check gate included in check allows only the standard library, agentx and kitx, including in tests. Implementations that need other dependencies belong in agentx-contrib.

The public commands are check, test, build, lint, vulncheck, fmt, fix and tidy; running task selects check. RACE and COVER accept true or false, and coverage implies race. Internal quality steps are hidden from both task --list and task --list-all.

CI runs task check on Linux, macOS and Windows, with independent lint and vulnerability jobs. See CONTRIBUTING.md for tool setup and contribution requirements, SECURITY.md for vulnerability reports and the known sandbox and permission limitations, and RELEASING.md for version ordering and release gates.

License

MIT

Web, browser and source ingestion

Core web.Reader/Extractor defines attributed readable content, Markdown and links; browser.Browser/Session defines application-owned browser operations. Register web.Tool or browser.Tool with an Agent. HTTP, Readability and Chromium implementations live in agentx-contrib/web and browser; core gains no SDK or Servex dependency.

rag.SourceWriter adds non-atomic source ingestion for Qdrant/Elasticsearch while the existing VectorStore retains atomic replacement per source. Configure rag.Config{Writer: writer, Retriever: retriever}; Store and Writer are mutually exclusive. A failed replacement may leave partial chunks; an empty source hash marks unfinished work for the next Add/Sync to retry. Applications coordinate independent writers.

GitHub, Notion and Feishu connectors live in agentx-contrib/loader and expose complete rag.Loader snapshots plus rag.ChangeLoader deltas. Apply a delta with Index.ApplyChanges and persist its cursor only after full success. Never Sync an incremental/partial listing. Connectors reuse Servex HTTP, Cache and Codec facilities.

Documentation

Overview

Package agentx is a modular Go framework for building LLM agents.

The framework is organised as a set of small, composable packages that depend only on the standard library and kitx:

  • model: provider-neutral messages, requests, responses and streaming
  • model/modeltest: a scripted model for tests
  • tool: tool definitions, JSON Schema, registry and execution
  • tool/builtin: file, search, shell, web and todo tools
  • loop: the agent loop, tool scheduling, events, checkpoints and the middleware interface that everything below plugs into
  • telemetry: tracing and metrics interfaces and the spans and run state carried in contexts
  • middleware: tracing and metrics of agent runs and the model decorators other middleware builds on
  • agent: agent construction, sub-agents and the session-aware runner
  • actx: context-window management and compaction strategies
  • session: conversation sessions and their stores
  • memory: long-term memory with keyword and vector retrieval
  • rag: retrieval-augmented generation: loaders, splitters, indexes and retrievers
  • skill: Claude Code style skills loaded from SKILL.md files
  • hook: lifecycle hooks, including shell command hooks
  • permission: permission rules and modes for tool calls
  • guard: input, output and tool guardrails
  • workflow: DAG workflows that orchestrate agents and functions
  • mcp: Model Context Protocol client, server and tool adapter
  • server: HTTP serving of runners: session API, Vercel AI SDK, AG-UI and A2A
  • eval: evaluation of agents, and eval/replay to record and replay HTTP interactions

Each contract comes with an in-memory reference implementation. Model providers (OpenAI, Anthropic, Gemini, Bedrock), file and SQL stores, OpenTelemetry, logging and retry middleware live in separate modules under github.com/nostosx/agentx-contrib.

Index

Constants

View Source
const Version = "1.4.0"

Version is the framework version reported to remote services such as MCP servers.

Variables

This section is empty.

Functions

This section is empty.

Types

This section is empty.

Directories

Path Synopsis
Package actx implements context engineering for agents: assembling the system prompt and a reminder for each new input from providers, tracking the context window and compacting conversations that grow too large.
Package actx implements context engineering for agents: assembling the system prompt and a reminder for each new input from providers, tracking the context window and compacting conversations that grow too large.
Package agent provides LLM agents built on the runtime loop, composable as sub-agents, and a Runner that persists conversations in sessions.
Package agent provides LLM agents built on the runtime loop, composable as sub-agents, and a Runner that persists conversations in sessions.
Package artifact defines storage for named binary outputs of agents and tools.
Package artifact defines storage for named binary outputs of agents and tools.
artifacttest
Package artifacttest tests implementations of artifact.Store.
Package artifacttest tests implementations of artifact.Store.
Package browser defines session-bound browser actions without a driver dependency.
Package browser defines session-bound browser actions without a driver dependency.
Package eval evaluates agents and other LLM tasks against datasets of cases.
Package eval evaluates agents and other LLM tasks against datasets of cases.
replay
Package replay records HTTP interactions to a cassette file and replays them, so tests of providers, agents and evaluations run deterministically without network access or API keys.
Package replay records HTTP interactions to a cassette file and replays them, so tests of providers, agents and evaluations run deterministically without network access or API keys.
Package guard implements guardrails: checks of a run's input, its final output and its tool calls that end the run when their tripwire triggers, in the style of the OpenAI Agents SDK's guardrails.
Package guard implements guardrails: checks of a run's input, its final output and its tool calls that end the run when their tripwire triggers, in the style of the OpenAI Agents SDK's guardrails.
Package hook implements lifecycle hooks that observe and control the agent loop, in the style of Claude Code hooks.
Package hook implements lifecycle hooks that observe and control the agent loop, in the style of Claude Code hooks.
Package imagegen defines independent image generation and editing contracts.
Package imagegen defines independent image generation and editing contracts.
internal
diff
Package diff computes line diffs and formats them as unified diffs.
Package diff computes line diffs and formats them as unified diffs.
fileaccess
Package fileaccess carries per-file permission checks into recursive tools without coupling built-in tools to a permission policy.
Package fileaccess carries per-file permission checks into recursive tools without coupling built-in tools to a permission policy.
fsmatch
Package fsmatch matches slash-separated paths against glob patterns and .gitignore rules.
Package fsmatch matches slash-separated paths against glob patterns and .gitignore rules.
htmltext
Package htmltext extracts readable text from HTML.
Package htmltext extracts readable text from HTML.
httpclient
Package httpclient assembles agentx HTTP client requests and selects defaults.
Package httpclient assembles agentx HTTP client requests and selects defaults.
media
Package media converts model parts for agentx protocols.
Package media converts model parts for agentx protocols.
process
Package process implements agentx tool and hook command execution, including platform-specific process-tree cleanup.
Package process implements agentx tool and hook command execution, including platform-specific process-tree cleanup.
toolnames
Package toolnames maps Claude Code's built-in tool names to the agentx built-in tools that play the same role.
Package toolnames maps Claude Code's built-in tool names to the agentx built-in tools that play the same role.
Package loop implements the agent loop: it calls the model, executes tool calls, asks for approvals and checkpoints state so interrupted runs can be resumed.
Package loop implements the agent loop: it calls the model, executes tool calls, asks for approvals and checkpoints state so interrupted runs can be resumed.
checkpointtest
Package checkpointtest checks loop.Checkpointer implementations against the contract the built-in checkpointers follow.
Package checkpointtest checks loop.Checkpointer implementations against the contract the built-in checkpointers follow.
Package mcp implements a Model Context Protocol client and server over stdio and Streamable HTTP, with OAuth authorization, and adapts MCP tools to agentx tools and agentx tools to MCP.
Package mcp implements a Model Context Protocol client and server over stdio and Streamable HTTP, with OAuth authorization, and adapts MCP tools to agentx tools and agentx tools to MCP.
Package memory provides long-term memory that persists across sessions, with keyword, vector and hybrid retrieval.
Package memory provides long-term memory that persists across sessions, with keyword, vector and hybrid retrieval.
memorytest
Package memorytest checks memory.Store implementations against the contract the built-in stores follow.
Package memorytest checks memory.Store implementations against the contract the built-in stores follow.
Package middleware provides the tracing and metrics of agent runs, which plug into an agent with agent.WithMiddleware, and model decorators that other middleware builds on.
Package middleware provides the tracing and metrics of agent runs, which plug into an agent with agent.WithMiddleware, and model decorators that other middleware builds on.
Package model defines the provider-neutral interface to large language models: messages, requests, responses, streaming and token usage.
Package model defines the provider-neutral interface to large language models: messages, requests, responses, streaming and token usage.
modeltest
Package modeltest provides a scripted model for tests.
Package modeltest provides a scripted model for tests.
Package permission decides tool calls with permission rules and modes in Claude Code's syntax and semantics; see https://code.claude.com/docs/en/permissions and https://code.claude.com/docs/en/permission-modes.
Package permission decides tool calls with permission rules and modes in Claude Code's syntax and semantics; see https://code.claude.com/docs/en/permissions and https://code.claude.com/docs/en/permission-modes.
rag
Package rag implements retrieval-augmented generation: loading documents, splitting them into chunks, embedding and indexing the chunks, and retrieving them for agents through a search tool or a context provider.
Package rag implements retrieval-augmented generation: loading documents, splitting them into chunks, embedding and indexing the chunks, and retrieving them for agents through a search tool or a context provider.
ragtest
Package ragtest checks rag.VectorStore implementations against the contract the built-in store follows.
Package ragtest checks rag.VectorStore implementations against the contract the built-in store follows.
Package search defines external web search and its Agent tool adapter.
Package search defines external web search and its Agent tool adapter.
Package server serves an agent.Runner over HTTP: a REST-style session API that streams run events, adapters for the Vercel AI SDK UI message stream and AG-UI, and the server side of the A2A protocol.
Package server serves an agent.Runner over HTTP: a REST-style session API that streams run events, adapters for the Vercel AI SDK UI message stream and AG-UI, and the server side of the A2A protocol.
Package session stores agent conversations and per-session state.
Package session stores agent conversations and per-session state.
sessiontest
Package sessiontest checks session.Store implementations against the contract the built-in stores follow.
Package sessiontest checks session.Store implementations against the contract the built-in stores follow.
Package skill implements Agent Skills: folders of instructions that a model discovers by name and description and loads on demand.
Package skill implements Agent Skills: folders of instructions that a model discovers by name and description and loads on demand.
Package telemetry defines the tracing and metrics interfaces of agentx and carries spans and run state through contexts.
Package telemetry defines the tracing and metrics interfaces of agentx and carries spans and run state through contexts.
Package tool defines tools that models can call, their JSON Schemas, a registry and an executor with middleware support.
Package tool defines tools that models can call, their JSON Schemas, a registry and an executor with middleware support.
builtin
Package builtin provides general-purpose tools modeled on those of coding agents such as Claude Code and Codex: file reading, writing and editing, patches, glob and grep search, shell commands, web fetching and a todo list.
Package builtin provides general-purpose tools modeled on those of coding agents such as Claude Code and Codex: file reading, writing and editing, patches, glob and grep search, shell commands, web fetching and a todo list.
Package web defines readable web content, extraction and Agent adapters.
Package web defines readable web content, extraction and Agent adapters.
Package workflow composes nodes such as agents and functions into graphs with conditional branches, loops and dynamic fan-out.
Package workflow composes nodes such as agents and functions into graphs with conditional branches, loops and dynamic fan-out.
checkpointtest
Package checkpointtest checks workflow.Checkpointer implementations against the contract the built-in checkpointers follow.
Package checkpointtest checks workflow.Checkpointer implementations against the contract the built-in checkpointers follow.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL