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

agentloop

package module
v0.0.0-...-6272484 Latest Latest
Warning

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

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

README

agentloop

agentloop runs a language model in a loop with tools, for agents whose access has to be controlled exactly. It is a Go library with adapters for Anthropic and OpenAI, and a small command, agentloop, built on it.

An agent can do only what it was given. It has no tools, files or environment of its own. Every tool call is checked before it runs: the tool must exist, the input must match its schema, and a policy you write, if there is one, must allow it. A policy can attach labels to the session that are never removed. Labels are short provenance names such as private or mail:stranger@example.com. They record where the session's material came from, and a session can forbid a label outright: one that may not hold private never starts holding it, and a tool result that carries it is refused before the model sees it.

agentloop never holds a call for a person to approve. What an agent can reach is decided by the tools it is given and by the walls around them.

A session is an append-only log, written to disk entry by entry. If the process crashes or is killed at any point, the next run carries on from the log and never runs a tool call twice. A call that was running when the process died is reported to the model as possibly having taken effect.

The core knows no vendor. Each provider gets its own earlier turns back unchanged, so reasoning carries across turns, and failures come back sorted by what to do about them: rate limited, overloaded, unauthorized and so on.

DESIGN.md has the whole design, including what it does not guarantee.

The library

go get github.com/kirahowe/agentloop
clock := agentloop.Func(agentloop.ToolSpec{
    Name:        "clock",
    Description: "The current time.",
    Parameters:  json.RawMessage(`{"type":"object","properties":{},"additionalProperties":false}`),
}, func(ctx context.Context, input json.RawMessage) (agentloop.Result, error) {
    return agentloop.Result{Content: "09:30"}, nil
})

agent := &agentloop.Agent{
    Model:   anthropic.New(anthropic.Config{APIKey: key}),
    ModelID: "claude-opus-5-5",
    System:  "You answer briefly.",
    Tools:   []agentloop.Tool{clock},
    Limits:  agentloop.Limits{MaxTurns: 20},
}

session, err := agentloop.OpenSession(agentloop.NewFileStore("session.jsonl"))
// ...
outcome, err := agent.Run(ctx, session, "What time is it?")

The packages:

Package What it is
agentloop Messages, the Model and Tool interfaces, policies, sessions, the loop
agentloop/anthropic Anthropic's Messages API
agentloop/openai OpenAI's Responses API
agentloop/command A tool that runs a program, with exactly the environment you give it
agentloop/mcp The tools of one MCP server, run as a child process over stdio or reached over streamable HTTP
agentloop/fake A scripted model for tests

The command

agentloop run request.json runs one agent to an outcome. The request names the provider and model, the system prompt and prompt, the session file, the tools, and the command each tool call runs. It prints every session entry as a JSON line and then an outcome line, which also gives the run's cost in US dollars as cost_usd when the model is priced. agentloop acp request.json serves the same agent to an Agent Client Protocol client over stdin and stdout, where a client can also list, load, resume and close the sessions kept in the request's session_dir (with "new_sessions_only": true, only open new ones and close them). It asks the provider for the model's display name and context window, and shows the model to the client as a session config option, so nothing in the request can drift from the model. The request format is in DESIGN.md.

A request may give a policy: the session's starting labels, the labels each tool adds when it is called, and the labels the session may never hold (forbid_labels). Without a policy every tool the request gives is allowed. With a policy, command tools speak the envelope protocol: JSON in with the session's labels, JSON out with the labels of what the tool returned. A request that uses a field from an older version (resolve, quarantine, workbench, policy.unrestricted, or class, always_hold or consent in a tool's policy entry) is refused with a message that says the field was removed. agentloop acp never asks the client for permission, and shows the session's labels on every tool call.

What agentloop reads from another process (the ACP client, tool envelopes, local MCP servers, request files) goes through one strict JSON reader, the same dialect relay and relay-tui read: valid UTF-8, one value, no name given twice, names matched exactly. See Reading JSON.

A tool result can carry a display: a title, a Markdown body, labels and sources for the operator. agentloop keeps it in the session file and sends it to an ACP client, and never gives the model any of it. The model is told only that the operator was shown something, under the ref display_ref_prefix gives it.

A request can also take tools from MCP servers it runs over stdio. The request is the allowlist: it names each tool it wants from a server, under its own name for it, and the policy gives the tool its labels, as for any other tool. The server gives the description and schema. agentloop pins each definition by its SHA-256 in the session, and a later run on that session refuses a definition that has changed, ending with reason tools_unavailable. A tool's entry may also give the digest it must have. A server gets only PATH and HOME. A secret it needs comes from a credentials_command, whose output is written to a private file that the server's arguments can name as {credentials_file}. A server sees the session's labels only with send_labels, and a result can add labels through _meta but never remove one. Images and other non-text content fail the call unless the tool allows them.

"mcp_servers": {"files": {
  "command": ["/usr/local/bin/files-mcp", "--token-file", "{credentials_file}"],
  "credentials_command": ["security", "find-generic-password", "-s", "files", "-w"]
}},
"tools": [{"name": "file_read", "protocol": "mcp-stdio", "server": "files", "tool": "read"}],
"policy": {"tools": {"file_read": {"class": "read", "labels": ["private"]}}}

A server can also be remote, reached over MCP's streamable HTTP transport. It gives a url in place of command, and its tools have protocol mcp-http. The URL must be https, or http to this machine for tests. What the credentials_command prints is the bearer token, kept in memory only, and sent only to the server's origin. When the server answers 401, the command is run again and the request sent once more, so a command that refreshes an OAuth token keeps a long session going. A redirect to another origin fails the request. No proxy is used.

A tool's entry can narrow its arguments. fixed_args sets arguments to the given values on every call, and hidden_args names arguments the model may not give. Both are taken out of the schema the model sees, and a call that names one is refused before the server is sent anything. The fixed values are added to the call before it is sent.

"mcp_servers": {"mail": {
  "url": "https://mcp.example.com/mcp",
  "credentials_command": ["relay", "mcp-token", "mail"]
}},
"tools": [{"name": "mail_send", "protocol": "mcp-http", "server": "mail", "tool": "send_campaign",
           "fixed_args": {"send_now": false}, "hidden_args": ["reply_to"]}]

agentloop mcp-tools lists every tool of a remote server, with the digest each definition would be pinned at. It reads {"url": ..., "token": ...} on stdin, so the token never shows in a process list, and prints {"tools": [...]}, each with its name, title, description, annotations, schemas and digest. It is for an operator choosing which tools to allow.

The API key is fetched by a command named in the request, such as a keychain lookup, and reaches only the model. The model clients ignore the proxy environment variables (HTTPS_PROXY and the rest), so the key and the prompts go to the provider directly or not at all.

Traces

agentloop otel SESSION.jsonl turns a session file into OpenTelemetry traces, offline. It prints one OTLP/JSON ExportTraceServiceRequest on one line, or writes it to --out FILE. Nothing is sent anywhere, and the session is not changed, so old sessions can be exported at any time and the same file always gives the same bytes.

--conversation-id ID sets gen_ai.conversation.id; the default is the file's name without its extension. --parent-trace and --parent-span (hex ids) put every run under a span made elsewhere, such as an eval's workflow span. The file may also be a run's transcript (what agentloop run prints); its closing outcome line is skipped. Flags may come before or after the file. A usage error exits 2.

Each run is one invoke_agent span, with a chat span per model call and an execute_tool span per tool call. The model calls a tool made are chat spans under the tool span. A run with no run_ended (a crash) ends with status ERROR and reason crashed. The export has no content: no messages, tool inputs or results, policy reasons or tool logs. Labels are exported, so treat the telemetry backend as inside the policy.

The OpenTelemetry GenAI conventions are at Development stability and nothing in them is stable. The command follows the pinned commit and schema URL named in the otel package documentation, which also lists every span and attribute. Expect renames when the pin moves.

Building

Go 1.27 or later.

make build    # bin/agentloop
make test     # gofmt, vet, and every test with the race detector
make update   # move every dependency to its latest version, then test
make tck      # the ACP conformance kit against agentloop acp (needs uv)

The tests need no network and no key. Live tests against the real APIs spend a little money and run only with make live, which passes -live and a key command to the anthropic and openai test packages. The OpenAI test skips unless you add -openai-key-command.

License

MIT

Documentation

Overview

Example

An agent with one tool and a policy, on a session kept in a file.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"

	"github.com/kirahowe/agentloop"
	"github.com/kirahowe/agentloop/anthropic"
)

func main() {
	clock := agentloop.Func(agentloop.ToolSpec{
		Name:        "clock",
		Description: "The current time.",
		Parameters:  json.RawMessage(`{"type":"object","properties":{},"additionalProperties":false}`),
	}, func(ctx context.Context, input json.RawMessage) (agentloop.Result, error) {
		return agentloop.Result{Content: "09:30"}, nil
	})

	agent := &agentloop.Agent{
		Model:   anthropic.New(anthropic.Config{APIKey: "sk-..."}),
		ModelID: "claude-opus-5-5",
		System:  "You answer briefly.",
		Tools:   []agentloop.Tool{clock},
		Policy: agentloop.PolicyFunc(func(ctx context.Context, call agentloop.ToolCall, view agentloop.View) agentloop.Decision {
			return agentloop.Decision{Verdict: agentloop.Allow}
		}),
		Limits: agentloop.Limits{MaxTurns: 20},
	}

	session, err := agentloop.OpenSession(agentloop.NewFileStore("session.jsonl"))
	if err != nil {
		log.Fatal(err)
	}
	outcome, err := agent.Run(context.Background(), session, "What time is it?")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(outcome.Reason, outcome.Text)
}

Index

Examples

Constants

View Source
const (
	LimitTurns     = "max_turns"
	LimitToolCalls = "max_tool_calls"
	LimitTokens    = "max_tokens"
)

Names of the limits, as reported in Outcome.Limit.

View Source
const (
	MaxDisplayTitle   = 200 // characters
	MaxDisplayBody    = 200_000
	MaxDisplaySources = 50
	MaxSourceRef      = 2000
)

The bounds on a display.

View Source
const (
	// LabelSeed: the agent's starting labels.
	LabelSeed = "seed"
	// LabelTool: a tool call's decision or result.
	LabelTool = "tool"
)

Where a label can come from, other than a tool call.

View Source
const ForbiddenDecision = "This call would give the session a label it may not hold, so it was not run."

ForbiddenDecision is the reason recorded for a call whose decision would have given the session a label it may not hold.

View Source
const MaxLabelBytes = 100

MaxLabelBytes is the longest a label may be.

View Source
const RefusedResult = "This result was refused: it carries a label this session may not hold."

RefusedResult is what the model is told in place of a tool result that carried a label the session may not hold.

Variables

View Source
var DefaultRetry = RetryPolicy{Retries: 3, Backoff: 2 * time.Second, MaxWait: 60 * time.Second}

DefaultRetry is used when an Agent sets no Retry.

Functions

func CheckDisplay

func CheckDisplay(d Display) error

CheckDisplay says what is wrong with a display a tool gave, or returns nil: a title of 1 to MaxDisplayTitle characters, a body of at most MaxDisplayBody bytes, labels that pass CheckLabels, and at most MaxDisplaySources sources, each with a title and a one-line ref.

func CheckLabel

func CheckLabel(l string) error

CheckLabel says why l is not a label, or returns nil. Labels are short provenance names such as "private" or "contaminated": 1 to MaxLabelBytes bytes, starting with a lower-case letter or digit and continuing with lower-case letters, digits and the characters _ . : @ / + -. Nothing else is allowed, so case, spacing, invisible, bidi and lookalike variants of a name cannot exist, and a forbid list cannot miss one by a byte.

func CheckLabels

func CheckLabels(labels []string) error

CheckLabels returns the first error CheckLabel gives for labels.

func DisplayID

func DisplayID(callID string) string

DisplayID is the name a call's display is kept under: the call id in lower case when that is a short run of letters, digits, _ and -, and otherwise "call-" and the start of the id's sha256. It is a safe path segment either way.

func DisplayNote

func DisplayNote(ref string) string

DisplayNote is what the model is told in place of a display, after the result's content. It holds nothing of the display, which may carry labels the result does not: only the ref, which the agent made.

func Forbidden

func Forbidden(labels, forbid []string) []string

Forbidden is the labels in labels that forbid names, sorted, with no repeats: what a session given labels would hold that it may not. It is empty, never nil, when there are none.

func UnionLabels

func UnionLabels(ls ...[]string) []string

UnionLabels is the sorted set of the labels in all of ls.

Types

type Agent

type Agent struct {
	Model   Model
	ModelID string
	System  string
	Tools   []Tool
	// Labels are added to the session whenever a run starts, before
	// anything else happens in it.
	Labels []string
	// Forbid is the labels the session may never hold. A run on a session
	// that holds one, or would from Labels, does not start (ReasonForbidden).
	// A tool result carrying one is refused: the model is told only that
	// (RefusedResult) and the session does not take the label. An allow
	// whose decision would add one is a deny.
	Forbid []string
	// Policy decides each call after the tool and its input check out. Nil
	// allows every call to a tool the agent has.
	Policy          Policy
	Effort          Effort
	MaxOutputTokens int
	Limits          Limits
	// Retry is how failed model calls are retried. Nil means DefaultRetry.
	Retry *RetryPolicy
	// ToolTimeout bounds one tool call. Zero means no bound.
	ToolTimeout time.Duration
	// Pins are digests of tool definitions, by tool name, that the session
	// must keep. A run records the ones the session lacks, and a run on a
	// session that pinned a different digest for a tool does not start:
	// Run returns a *PinError and records nothing. The digests are opaque
	// here; the mcp package makes them.
	Pins map[string]string
	// DisplayRefPrefix, when set, names where a result's display is kept:
	// its ref is the prefix and DisplayID of the call, such as
	// "shown:2026-10-18/" and "toolu_01abc". The model is told the ref.
	DisplayRefPrefix string
	// Observe sees every entry as it is appended and every streamed delta.
	// It runs on the loop's goroutine; keep it quick.
	Observe func(Event)

	// Now and Sleep stand in for the clock, so tests can exercise retries
	// without waiting. Nil means the real ones. Sleep returns early with
	// the context's error if the context ends.
	Now   func() time.Time
	Sleep func(ctx context.Context, d time.Duration) error
}

An Agent is a model, the tools it was given and the rules around them. Its zero value runs nothing: Model is required, and the only tools are the ones in Tools.

func (*Agent) Run

func (a *Agent) Run(ctx context.Context, s *Session, prompt string) (*Outcome, error)

Run carries the session forward until the model is done or something stops it. It first settles the calls an earlier run left open, so it resumes a session from any point. A non-empty prompt is added as a user message once those are settled.

Run returns an error only if the agent is set up wrongly, the store fails, or another Run is already in progress on the session (at most one runs at a time). Anything else is an Outcome.

type Call

type Call struct {
	ID    string
	Input json.RawMessage
	// Labels are the session's labels as the call runs, which include
	// the labels its decision added.
	Labels []string
}

A Call is one call as a CallTool sees it.

type CallInfo

type CallInfo struct {
	Call ToolCall
	// Decision is the latest decision on the call, nil until it is checked.
	Decision *Decision
	Started  bool
	Finished bool
	// Result is set once Finished.
	Result *ToolResult
}

CallInfo is what the session knows about one tool call.

type CallStartedEntry

type CallStartedEntry struct {
	CallID string `json:"call_id"`
}

CallStartedEntry is written immediately before a tool runs. If it is the last word on a call, the call may or may not have taken effect.

type CallTool

type CallTool interface {
	Tool
	CallWith(ctx context.Context, call Call) (Result, error)
}

A CallTool is a Tool that is told which call it serves and what the session holds. The agent uses CallWith instead of Call.

type Decision

type Decision struct {
	Verdict Verdict `json:"verdict"`
	// Reason is shown to the model on deny, and recorded always.
	Reason string `json:"reason,omitempty"`
	// Labels are added to the session when the call is allowed, before
	// its result exists.
	Labels []string `json:"labels,omitempty"`
}

A Decision is a verdict with the reasons and consequences that go with it.

type DecisionEntry

type DecisionEntry struct {
	CallID  string   `json:"call_id"`
	Tool    string   `json:"tool"`
	Verdict Verdict  `json:"verdict"`
	Reason  string   `json:"reason,omitempty"`
	Labels  []string `json:"labels,omitempty"`
}

DecisionEntry records the verdict on one call.

type Delta

type Delta struct {
	Type PartType `json:"type"`
	Text string   `json:"text"`
}

A Delta is a piece of a response as it streams in, for display.

type Describer

type Describer interface {
	Describe(ctx context.Context, model string) (ModelInfo, error)
}

A Describer is a Model whose provider can describe a model. It is optional: a Model that does not implement it is described by its id alone.

Describe makes one request, with no retries, and returns a *ModelError for anything the provider refused or could not do.

type Display

type Display struct {
	Title string `json:"title"`
	Body  string `json:"body"`
	// Labels are what the display's text carries. The agent adds the
	// result's labels and the session's to them, so a display is never
	// labelled less than the call that made it.
	Labels  []string `json:"labels"`
	Sources []Source `json:"sources"`
	// Ref is where the display is kept, as kind:id, set by the agent
	// when it has a DisplayRefPrefix.
	Ref string `json:"ref,omitempty"`
}

A Display is what a tool result shows the operator alone: a title, a Markdown body and its sources. The agent keeps it in the tool_result entry and sends it to a client, and never puts any of it in the model's context. The model is told only that the operator was shown something, and under which ref (see DisplayNote).

type Effort

type Effort string

Effort is a provider-neutral reasoning level. An adapter maps it to the nearest level its provider has.

const (
	EffortLow    Effort = "low"
	EffortMedium Effort = "medium"
	EffortHigh   Effort = "high"
	EffortXHigh  Effort = "xhigh"
	EffortMax    Effort = "max"
)

type Entry

type Entry struct {
	Seq  int       `json:"seq"`
	Time time.Time `json:"time"`
	Type EntryType `json:"type"`

	RunStarted  *RunStartedEntry  `json:"run_started,omitempty"`
	Message     *MessageEntry     `json:"message,omitempty"`
	Decision    *DecisionEntry    `json:"decision,omitempty"`
	CallStarted *CallStartedEntry `json:"call_started,omitempty"`
	ToolResult  *ToolResultEntry  `json:"tool_result,omitempty"`
	ModelError  *ModelErrorEntry  `json:"model_error,omitempty"`
	RunEnded    *Outcome          `json:"run_ended,omitempty"`
	Labels      *LabelsEntry      `json:"labels,omitempty"`
	Pins        *PinsEntry        `json:"pins,omitempty"`
}

An Entry is one line of a session's log. Exactly one payload field is set, the one Type names. Entries are never edited or removed.

type EntryType

type EntryType string

EntryType says which payload of an Entry is set.

const (
	EntryRunStarted  EntryType = "run_started"
	EntryMessage     EntryType = "message"
	EntryDecision    EntryType = "decision"
	EntryCallStarted EntryType = "call_started"
	EntryToolResult  EntryType = "tool_result"
	EntryModelError  EntryType = "model_error"
	EntryRunEnded    EntryType = "run_ended"
	EntryLabels      EntryType = "labels"
	EntryPins        EntryType = "pins"
)

type ErrorKind

type ErrorKind string

ErrorKind sorts what can go wrong with a model call by what to do about it.

const (
	// RateLimited: wait until RetryAfter, or ResetsAt for the whole limit.
	RateLimited ErrorKind = "rate_limited"
	// Overloaded: the provider failed on its side. Retrying may work.
	Overloaded ErrorKind = "overloaded"
	// Unreachable: the request never got an answer.
	Unreachable ErrorKind = "unreachable"
	// Unauthorized: the credentials were refused. Someone has to act.
	Unauthorized ErrorKind = "unauthorized"
	// Billing: the account cannot pay. Someone has to act.
	Billing ErrorKind = "billing"
	// NotFound: the model or endpoint does not exist. Fix the settings.
	NotFound ErrorKind = "not_found"
	// InvalidRequest: the provider rejected the request itself.
	InvalidRequest ErrorKind = "invalid_request"
)

type Event

type Event struct {
	Entry *Entry
	Delta *Delta
}

An Event is one thing a run reports to Observe: either an entry just appended or a streamed delta.

type FileStore

type FileStore struct {
	// contains filtered or unexported fields
}

FileStore keeps entries as JSON lines in a file, syncing each append.

func NewFileStore

func NewFileStore(path string) *FileStore

NewFileStore makes a store on path. The file is created by the first append; its directory must exist.

func (*FileStore) Append

func (f *FileStore) Append(e Entry) error

Append writes one line and syncs it.

func (*FileStore) Load

func (f *FileStore) Load() ([]Entry, error)

Load reads the file. A final line that is not complete JSON is what a write cut short by a crash leaves, and is ignored. A bad line anywhere else means the log is damaged, and is an error.

The log is agentloop's own, written by encoding/json, so it is read with encoding/json and not the strict reader (relay#58 covers JSON another process wrote). Earlier versions logged tool call inputs and provider data with a repeated name or invalid UTF-8 inside, which the strict reader refuses; such a line is complete, so it must neither stop the session from loading nor be cut off as a torn write.

type InputChecker

type InputChecker interface {
	Tool
	CheckInput(input json.RawMessage) string
}

An InputChecker is a Tool with a check of its own on input that already matches its schema. The agent runs it before the policy, and a reason it returns denies the call, as a schema mismatch does.

type InputSchema

type InputSchema struct {
	// contains filtered or unexported fields
}

An InputSchema is a tool's parameters compiled, to check input as the agent checks a call's input.

func CompileInput

func CompileInput(spec ToolSpec) (*InputSchema, error)

CompileInput compiles a tool's parameters as the agent does: no reference is loaded from anywhere.

func (*InputSchema) Check

func (s *InputSchema) Check(input json.RawMessage) string

Check returns why input is not acceptable, or "".

type LabelSource

type LabelSource struct {
	// Kind is LabelSeed or LabelTool.
	Kind string `json:"kind"`
	// Tool and CallID name the call, for LabelTool.
	Tool   string `json:"tool,omitempty"`
	CallID string `json:"call_id,omitempty"`
	// Seq is the entry's sequence number.
	Seq int `json:"seq"`
}

A LabelSource is the entry that first gave the session a label, so a client can show why the session holds it.

type LabelsEntry

type LabelsEntry struct {
	Labels []string `json:"labels"`
	// Source says where they came from: LabelSeed.
	Source string `json:"source,omitempty"`
}

LabelsEntry records labels added to the session from outside any call: an agent's starting labels.

type Limits

type Limits struct {
	// MaxTurns is the number of model calls in one run.
	MaxTurns int `json:"max_turns,omitempty"`
	// MaxToolCalls is the number of tool calls run in one run. Calls the
	// model makes beyond it are refused and the run ends.
	MaxToolCalls int `json:"max_tool_calls,omitempty"`
	// MaxTokens is Usage.Total across one run's model calls. It is checked
	// before each call, so the last call can overshoot it.
	MaxTokens int64 `json:"max_tokens,omitempty"`
}

Limits stop a run that spends too much. A zero field is no limit.

type MemoryStore

type MemoryStore struct {
	// contains filtered or unexported fields
}

MemoryStore keeps entries in memory. Entries pass through JSON on the way in and out, so it behaves like a file would, without sharing pointers with the caller.

func NewMemoryStore

func NewMemoryStore(entries ...Entry) *MemoryStore

NewMemoryStore makes a store, optionally holding entries already.

func (*MemoryStore) Append

func (m *MemoryStore) Append(e Entry) error

func (*MemoryStore) Load

func (m *MemoryStore) Load() ([]Entry, error)

type Message

type Message struct {
	Role   Role    `json:"role"`
	Parts  []Part  `json:"parts"`
	Native *Native `json:"native,omitempty"`
}

A Message is one turn of a conversation in a form no provider owns.

Parts is the neutral reading of the turn: what policies, logs and other providers see. Native, set only on assistant turns, is the provider's own record of the same turn, kept byte for byte. A provider replays its own turns from Native, so reasoning and anything else the neutral form cannot express goes back to it unchanged; history is never rewritten.

func Text

func Text(role Role, text string) Message

Text builds a message holding only text.

func (Message) Text

func (m Message) Text() string

Text joins the message's text parts.

func (Message) ToolCalls

func (m Message) ToolCalls() []ToolCall

ToolCalls lists the message's tool calls in order.

type MessageEntry

type MessageEntry struct {
	Message Message `json:"message"`
	Stop    Stop    `json:"stop,omitempty"`
	Usage   Usage   `json:"usage"`
	// Model is the model that served the turn.
	Model   string `json:"model,omitempty"`
	Refusal string `json:"refusal,omitempty"`
}

MessageEntry records a message joining the conversation. For an assistant turn it also carries why the turn ended and what it cost.

type Model

type Model interface {
	// Provider names the provider, as recorded in Native.Provider.
	Provider() string
	Generate(ctx context.Context, req *Request, observe func(Delta)) (*Response, error)
}

A Model is one provider's API, adapted. Adapters live in their own packages (anthropic, openai) and are the only code that knows a vendor.

Generate makes exactly one request: no retries, which belong to the Agent so they are visible and bounded. It returns a *ModelError for anything the provider refused or could not do.

type ModelError

type ModelError struct {
	Kind    ErrorKind `json:"kind"`
	Status  int       `json:"status,omitempty"`
	Message string    `json:"message"`
	// RetryAfter is the earliest time the provider asked to be retried.
	RetryAfter time.Time `json:"retry_after,omitzero"`
	// ResetsAt is when the exhausted limit fully resets, if the provider
	// said, which can be well after RetryAfter.
	ResetsAt time.Time `json:"resets_at,omitzero"`
	Err      error     `json:"-"`
}

A ModelError is a model call that failed, sorted by kind.

func AsModelError

func AsModelError(err error) *ModelError

AsModelError finds the ModelError in err, wrapping any other error as Unreachable.

func (*ModelError) Error

func (e *ModelError) Error() string

func (*ModelError) Transient

func (e *ModelError) Transient() bool

Transient reports whether the same request could succeed later.

func (*ModelError) Unwrap

func (e *ModelError) Unwrap() error

type ModelErrorEntry

type ModelErrorEntry struct {
	Error ModelError `json:"error"`
	// Retry says whether the loop went on to try again, after WaitMillis.
	Retry      bool  `json:"retry"`
	WaitMillis int64 `json:"wait_ms,omitempty"`
}

ModelErrorEntry records one failed model call.

type ModelInfo

type ModelInfo struct {
	// ID is the model's identifier, as the provider gives it.
	ID string
	// DisplayName is the provider's name for people. Empty means it gave
	// none.
	DisplayName string
	// ContextWindow is how many input tokens the model takes. Zero means
	// the provider did not say.
	ContextWindow int64
	// Prices is what the model costs. Nil means the provider's adapter
	// does not know, and then no cost is reported for it, not zero.
	Prices *Prices
}

ModelInfo is what a provider says about one of its models.

type ModelUsage

type ModelUsage struct {
	Model string `json:"model"`
	Usage Usage  `json:"usage"`
}

A ModelUsage is what one model call cost, and the model that served it.

type Native

type Native struct {
	Provider string          `json:"provider"`
	Model    string          `json:"model"`
	Data     json.RawMessage `json:"data"`
}

Native is a provider's own record of an assistant turn.

type Outcome

type Outcome struct {
	Reason Reason `json:"reason"`
	// Text is the last assistant text.
	Text  string      `json:"text,omitempty"`
	Error *ModelError `json:"error,omitempty"`
	Limit string      `json:"limit,omitempty"`
	// Forbidden, for forbidden, is the labels the session held or would
	// have started with that Agent.Forbid names.
	Forbidden []string `json:"forbidden,omitempty"`
	// Refused lists the tool results of this run that were refused for
	// carrying a forbidden label, whatever the reason the run ended.
	Refused []Refusal `json:"refused,omitempty"`
	// Turns and Usage count this run only.
	Turns int   `json:"turns"`
	Usage Usage `json:"usage"`
}

An Outcome is how a run ended. Everything but a setup or store failure is one.

type Part

type Part struct {
	Type PartType `json:"type"`
	// Text is the text of a text part, or the readable summary of a
	// reasoning part, which may be empty.
	Text       string      `json:"text,omitempty"`
	ToolCall   *ToolCall   `json:"tool_call,omitempty"`
	ToolResult *ToolResult `json:"tool_result,omitempty"`
}

A Part is one piece of a message.

type PartType

type PartType string

PartType says which field of a Part is set.

const (
	TextPart       PartType = "text"
	ReasoningPart  PartType = "reasoning"
	ToolCallPart   PartType = "tool_call"
	ToolResultPart PartType = "tool_result"
)

type PinError

type PinError struct {
	Tool, Pinned, Got string
}

A PinError says a tool's definition is not the one the session pinned.

func (*PinError) Error

func (e *PinError) Error() string

type PinsEntry

type PinsEntry struct {
	Pins map[string]string `json:"pins"`
}

PinsEntry records the digests of tool definitions the session has pinned (see Agent.Pins), by tool name. A pin is never changed.

type Policy

type Policy interface {
	Decide(ctx context.Context, call ToolCall, view View) Decision
}

A Policy decides whether a tool call may run. It is asked only about calls to tools the agent has, whose input already matches the schema.

type PolicyFunc

type PolicyFunc func(ctx context.Context, call ToolCall, view View) Decision

PolicyFunc makes a Policy from a function.

func (PolicyFunc) Decide

func (f PolicyFunc) Decide(ctx context.Context, call ToolCall, view View) Decision

Decide calls f.

type Prices

type Prices struct {
	Input, Output, CacheWrite, CacheRead float64
}

Prices is what a model charges, in US dollars per million tokens, for each kind of token a call counts.

func (Prices) Cost

func (p Prices) Cost(u Usage) float64

Cost is what the usage costs at these prices, in US dollars.

type Reason

type Reason string

Reason is why a run ended.

const (
	// ReasonCompleted: the model finished its turn, or there was nothing to do.
	ReasonCompleted Reason = "completed"
	// ReasonMaxTokens: the last response was cut off by its token cap.
	ReasonMaxTokens Reason = "max_tokens"
	// ReasonRefused: the model declined to answer.
	ReasonRefused Reason = "refused"
	// ReasonLimit: a Limits field was reached; see Outcome.Limit.
	ReasonLimit Reason = "limit"
	// ReasonModelError: the model call failed for good; see Outcome.Error.
	ReasonModelError Reason = "model_error"
	// ReasonCanceled: the context ended.
	ReasonCanceled Reason = "canceled"
	// ReasonForbidden: the session holds, or would start with, a label
	// Agent.Forbid names, so nothing ran; see Outcome.Forbidden.
	ReasonForbidden Reason = "forbidden"
)

type Refusal

type Refusal struct {
	CallID string   `json:"call_id"`
	Tool   string   `json:"tool"`
	Labels []string `json:"labels"`
}

A Refusal is one tool result the session was not given, because it carried labels the session may not hold.

type Request

type Request struct {
	Model    string
	System   string
	Messages []Message
	Tools    []ToolSpec
	// Effort is how hard the model should think. Empty means the
	// provider's default.
	Effort Effort
	// MaxOutputTokens caps one response. Zero means the adapter's default.
	MaxOutputTokens int
}

A Request is everything one model call is given.

type Response

type Response struct {
	// Message is the assistant turn, with Native set.
	Message Message
	Stop    Stop
	// Model is the model that served the request, which can differ from
	// the one asked for when a provider falls back.
	Model string
	Usage Usage
	// Refusal explains a Stop of StopRefusal, when the provider said why.
	Refusal string
}

A Response is one model call's answer.

type Result

type Result struct {
	Content string
	// IsError marks a result the model should read as a failure.
	IsError bool
	// Labels are added to the session for good, describing what the
	// result brought into it, such as "private" or "contaminated".
	Labels []string
	// Log is for the operator: it is kept in the session's log and never
	// shown to the model.
	Log string
	// Usage is the model calls the tool made for the run. They count toward the run's usage and its
	// MaxTokens limit, and are kept in the tool_result entry even when
	// the result is refused.
	Usage []ModelUsage
	// Display is what the result shows the operator and never the
	// model (see Display).
	Display *Display
}

A Result is what a tool call produced.

type RetryPolicy

type RetryPolicy struct {
	// Retries is the number of further attempts after the first.
	Retries int
	// Backoff is the first wait for overloaded or unreachable errors. It
	// doubles each time.
	Backoff time.Duration
	// MaxWait is the longest single wait, rate limits included. A longer
	// wait ends the run instead. Zero means no cap: any wait is accepted.
	MaxWait time.Duration
}

RetryPolicy says how a failed model call is retried.

func (RetryPolicy) Wait

func (p RetryPolicy) Wait(me *ModelError, attempt int, now time.Time) (time.Duration, bool)

Wait says whether to try a model call again after it failed with me on the given attempt (0 for the first), and how long to wait first, as of now.

type Role

type Role string

Role is who wrote a message.

const (
	User      Role = "user"
	Assistant Role = "assistant"
)

type RunStartedEntry

type RunStartedEntry struct {
	Prompt string `json:"prompt,omitempty"`
}

RunStartedEntry records that Run began.

type Session

type Session struct {
	// contains filtered or unexported fields
}

A Session is a conversation and everything decided in it. It is the entries of a Store and nothing else: the conversation, the labels, the usage and the state of every tool call are derived from them, so a session reopened from its store is the session that was closed.

func OpenSession

func OpenSession(store Store) (*Session, error)

OpenSession rebuilds a session from the store. An empty store makes a new session.

func (*Session) AddLabels

func (s *Session) AddLabels(labels []string, source string) (*Entry, error)

AddLabels adds labels to the session from outside any call, from source (LabelSeed). It writes an entry only when one of them is new, and returns the entry it wrote, if any.

func (*Session) Call

func (s *Session) Call(id string) (CallInfo, bool)

Call returns the state of one call.

func (*Session) CheckPins

func (s *Session) CheckPins(pins map[string]string) error

CheckPins returns a *PinError for the first tool, by name, whose digest in pins differs from the one the session pinned. A tool the session has not pinned passes.

func (*Session) Entries

func (s *Session) Entries() []Entry

Entries is the log.

func (*Session) LabelSources

func (s *Session) LabelSources() map[string]LabelSource

LabelSources gives, for each label the session holds, the entry that first added it.

func (*Session) Labels

func (s *Session) Labels() []string

Labels lists the session's labels, sorted.

func (*Session) Messages

func (s *Session) Messages() []Message

Messages is the conversation. Results that follow an assistant turn are gathered into one user message, in the order of the calls. While calls are pending the conversation is not yet one a model can take.

func (*Session) Pending

func (s *Session) Pending() []ToolCall

Pending lists the calls of the last assistant turn that have no result yet, in order. A resumed run settles exactly these.

func (*Session) Pins

func (s *Session) Pins() map[string]string

Pins are the digests of tool definitions the session has pinned, by tool name.

func (*Session) Turns

func (s *Session) Turns() []Turn

Turns lists the assistant turns, with the stop of each.

func (*Session) Usage

func (s *Session) Usage() Usage

Usage is everything the session's model calls cost.

func (*Session) View

func (s *Session) View() View

View is the session as a policy sees it.

type Source

type Source struct {
	Title string `json:"title"`
	Ref   string `json:"ref"`
}

A Source is one thing a display draws on: a title and a ref, such as web:https://example.org/ or doc:notes/venues.md.

type Stop

type Stop string

Stop is why a response ended.

const (
	StopEndTurn   Stop = "end_turn"
	StopToolUse   Stop = "tool_use"
	StopMaxTokens Stop = "max_tokens"
	StopRefusal   Stop = "refusal"
	// StopPause means the provider paused a long turn and the same
	// request should be sent again to continue it.
	StopPause Stop = "pause"
)

type Store

type Store interface {
	// Load returns every entry, in order.
	Load() ([]Entry, error)
	// Append adds one entry. It is durable when it returns: a crash after
	// that cannot lose it.
	Append(Entry) error
}

A Store keeps a session's entries. It is the only thing a session needs to be remembered and resumed.

type Tool

type Tool interface {
	Spec() ToolSpec
	// Call runs the tool on input that has already been checked against
	// the spec's schema and allowed by the policy. An error means the
	// tool could not run; the model is told so.
	Call(ctx context.Context, input json.RawMessage) (Result, error)
}

A Tool is something the model may call. An Agent has no tools but the ones it is given, and runs nothing else.

func Func

func Func(spec ToolSpec, call func(ctx context.Context, input json.RawMessage) (Result, error)) Tool

Func makes a Tool from a function.

type ToolCall

type ToolCall struct {
	ID    string          `json:"id"`
	Name  string          `json:"name"`
	Input json.RawMessage `json:"input"`
}

A ToolCall is the model asking to run a tool.

type ToolResult

type ToolResult struct {
	CallID  string `json:"call_id"`
	Content string `json:"content"`
	IsError bool   `json:"is_error,omitempty"`
}

A ToolResult answers one ToolCall.

type ToolResultEntry

type ToolResultEntry struct {
	Result ToolResult `json:"result"`
	Labels []string   `json:"labels,omitempty"`
	// Log is what the tool said for the operator alone, such as a
	// program's stderr. It is never part of the conversation.
	Log string `json:"log,omitempty"`
	// Usage is what the model calls the tool made cost (see
	// Result.Usage). It counts toward the run's usage and the session's.
	Usage []ModelUsage `json:"usage,omitempty"`
	// Display is what the operator was shown and the model was not (see
	// Display). It is never part of the conversation.
	Display *Display `json:"display,omitempty"`
}

ToolResultEntry records how a call ended and what the result added to the session's labels.

type ToolSpec

type ToolSpec struct {
	Name        string `json:"name"`
	Description string `json:"description"`
	// Parameters is the JSON Schema of the tool's input, an object.
	Parameters json.RawMessage `json:"parameters"`
}

A ToolSpec is what the model is told about a tool.

type Turn

type Turn struct {
	// Message is the turn's index in Messages.
	Message int
	Stop    Stop
	// Model is the model that served the turn.
	Model string
	Usage Usage
	// Calls lists the ids of the turn's tool calls, in order.
	Calls []string
}

A Turn is one assistant message and how it ended.

type Usage

type Usage struct {
	InputTokens      int64 `json:"input_tokens"`
	OutputTokens     int64 `json:"output_tokens"`
	CacheReadTokens  int64 `json:"cache_read_tokens,omitempty"`
	CacheWriteTokens int64 `json:"cache_write_tokens,omitempty"`
}

Usage counts tokens.

func (Usage) Total

func (u Usage) Total() int64

Total is every token the provider counted.

type Verdict

type Verdict string

Verdict is a policy's answer about one tool call.

const (
	// Allow lets the call run.
	Allow Verdict = "allow"
	// Deny refuses the call. The model is told the reason.
	Deny Verdict = "deny"
)

type View

type View struct {
	// Labels held by the session, sorted.
	Labels []string
}

View is the session as a policy sees it.

func (View) Has

func (v View) Has(label string) bool

Has reports whether the session holds the label.

Directories

Path Synopsis
Package acpagent serves agentloop sessions over the Agent Client Protocol.
Package acpagent serves agentloop sessions over the Agent Client Protocol.
Package anthropic adapts the Anthropic Messages API to agentloop.Model.
Package anthropic adapts the Anthropic Messages API to agentloop.Model.
cmd
agentloop command
Command agentloop runs one agent from a JSON request.
Command agentloop runs one agent from a JSON request.
Package command provides an agentloop.Tool that runs a program.
Package command provides an agentloop.Tool that runs a program.
Package fake is a scripted agentloop.Model for tests.
Package fake is a scripted agentloop.Model for tests.
internal
acpschema
Package acpschema validates JSON against the ACP schema vendored in acp/testdata/schema.json.
Package acpschema validates JSON against the ACP schema vendored in acp/testdata/schema.json.
cmd/mcpremote command
Command mcpremote serves fakeremote, a remote MCP server and its OAuth authorization server, on 127.0.0.1 at a port of its own, for tests.
Command mcpremote serves fakeremote, a remote MCP server and its OAuth authorization server, on 127.0.0.1 at a port of its own, for tests.
cmd/mcpserver command
Command mcpserver is a small MCP stdio server for agentloop's tests.
Command mcpserver is a small MCP stdio server for agentloop's tests.
cmd/mockprovider command
Command mockprovider is a stand-in for Anthropic's Messages and Models APIs, for running agentloop against conformance suites without a key or a network.
Command mockprovider is a stand-in for Anthropic's Messages and Models APIs, for running agentloop against conformance suites without a key or a network.
fakeremote
Package fakeremote is a remote MCP server and the OAuth 2.1 authorization server in front of it, for tests: of agentloop's streamable HTTP client, and of relay connect, which runs the whole sign-in against it.
Package fakeremote is a remote MCP server and the OAuth 2.1 authorization server in front of it, for tests: of agentloop's streamable HTTP client, and of relay connect, which runs the whole sign-in against it.
httpclient
Package httpclient is the HTTP client agentloop's model adapters use when the caller gives none.
Package httpclient is the HTTP client agentloop's model adapters use when the caller gives none.
livetest
Package livetest gates the smoke tests that call the real provider APIs.
Package livetest gates the smoke tests that call the real provider APIs.
request
Package request is the JSON document that says what to run, and what it takes to turn that document into an agent: loading, validation, the API key command and agent construction.
Package request is the JSON document that says what to run, and what it takes to turn that document into an agent: loading, validation, the API key command and agent construction.
scenario
Package scenario is the harness behind the property tests, for reuse by any package that wants to drive the same generated runs.
Package scenario is the harness behind the property tests, for reuse by any package that wants to drive the same generated runs.
sessionfile
Package sessionfile opens a session file for one process at a time.
Package sessionfile opens a session file for one process at a time.
strictjson
Package strictjson is how agentloop reads JSON that another process wrote: the ACP client's messages, tool envelopes, MCP traffic and request files.
Package strictjson is how agentloop reads JSON that another process wrote: the ACP client's messages, tool envelopes, MCP traffic and request files.
strictjson/stricttest
Package stricttest loads the table of lines that relay, agentloop and relay-tui must read alike (relay#58), from testdata/strict-json/cases.jsonl.
Package stricttest loads the table of lines that relay, agentloop and relay-tui must read alike (relay#58), from testdata/strict-json/cases.jsonl.
usage
Package usage is the error for input the operator can fix: an unusable request, command line or session path.
Package usage is the error for input the operator can fix: an unusable request, command line or session path.
Package mcp gives an agent the tools of one MCP server as agentloop.Tool values.
Package mcp gives an agent the tools of one MCP server as agentloop.Tool values.
Package openai adapts OpenAI's Responses API to agentloop.Model.
Package openai adapts OpenAI's Responses API to agentloop.Model.
Package otel turns an agentloop session log into OpenTelemetry traces.
Package otel turns an agentloop session log into OpenTelemetry traces.

Jump to

Keyboard shortcuts

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