agents

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Jul 20, 2026 License: MIT Imports: 4 Imported by: 0

README

agents

A small, provider-agnostic foundation for building AI tool-calling agents in Go. It gives you an engine boundary, tools described by JSON Schema, a named agent (a harness), and a runner that drives the tool loop, with hooks to authorize a state-changing tool and to pause for human approval before tools run.

It carries no web framework, auth system, or storage. The host supplies authorization and persists the transcript however it likes.

Install

go get github.com/whilesmartgo/agents

Quickstart

package main

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

	"github.com/whilesmartgo/agents"
	"github.com/whilesmartgo/agents/engine/openai"
)

func main() {
	tools := agents.NewRegistry(agents.Tool{
		Name:        "get_time",
		Description: "Return the current time in a timezone.",
		Parameters: map[string]any{
			"type":       "object",
			"properties": map[string]any{"tz": map[string]any{"type": "string"}},
			"required":   []string{"tz"},
		},
		Handler: func(_ context.Context, args json.RawMessage) (string, error) {
			var in struct{ TZ string `json:"tz"` }
			_ = json.Unmarshal(args, &in)
			return "12:00 in " + in.TZ, nil
		},
	})

	runner := agents.Runner{
		// Works with OpenAI, Azure OpenAI, Ollama, vLLM, LM Studio, and most gateways.
		Engine: openai.New("https://api.openai.com/v1", "gpt-4o-mini", openai.WithAPIKey("sk-...")),
		Harness: agents.Harness{
			Name:         "assistant",
			SystemPrompt: "You are a helpful assistant.",
			Model:        "gpt-4o-mini",
			MaxSteps:     5,
			Tools:        tools,
		},
	}

	conv := agents.NewConversation(runner.Harness.SystemPrompt)
	answer, err := runner.Turn(context.Background(), conv, "What time is it in UTC?")
	if err != nil {
		panic(err)
	}
	fmt.Println(answer)
}

Authorization and human-in-the-loop

A tool marked Mutates changes state. Set Runner.Authorize to gate it; a non-nil error stops that one tool and its text is returned to the model as the tool result, so the model can adapt rather than the run failing.

runner.Authorize = func(ctx context.Context, tool agents.Tool, call agents.ToolCall) error {
	if !userMayRun(ctx, tool.Name) {
		return fmt.Errorf("you do not have permission to run %q", tool.Name)
	}
	return nil
}

Set Runner.Approve to pause before any tools run. Returning false makes Turn return agents.ErrAwaitingApproval with the pending calls recorded as the last assistant turn; call Resume once the operator has decided.

Resume takes a per-call decision map (call ID to approved). A call runs only if approved; the rest are declined and the model continues without them. Pass nil to approve every pending call.

runner.Approve = func(ctx context.Context, calls []agents.ToolCall) (bool, error) {
	return askOperator(calls) // block for a decision, or return false to defer
}

answer, err := runner.Turn(ctx, conv, "restart the database")
if errors.Is(err, agents.ErrAwaitingApproval) {
	// show conv's pending tool calls, collect a decision per call, then:
	answer, err = runner.Resume(ctx, conv, map[string]bool{"call_1": true})
}

Status

v0. The core (engine, tools, harness, runner) and an OpenAI-compatible engine are here. A durable, owner-scoped action ledger is planned as a separate module.

License

MIT.

Documentation

Overview

Package agents is a small, provider-agnostic foundation for building AI tool-calling agents in Go.

It has four parts:

  • Engine: the boundary to a language model. Complete takes a Request (the transcript plus the tools the model may call) and returns the model's reply, which is either final content or a set of tool calls. An OpenAI-compatible implementation lives in engine/openai.
  • Tool: a function the model may call, described by a JSON Schema. A tool marked Mutates changes state, so the host can gate it.
  • Harness: a named agent, being a system prompt, a model, a Registry of tools, and a cap on tool rounds per turn.
  • Runner: drives a Harness against an Engine, running the tool loop until the model returns a final answer, with optional hooks to authorize a mutating tool and to pause for human approval before tools run.

The library carries no web framework, auth system, or storage. The host supplies authorization through Runner.Authorize and persists a Conversation however it likes; a Conversation is just a slice of Messages.

Index

Examples

Constants

View Source
const (
	RoleSystem    = "system"
	RoleUser      = "user"
	RoleAssistant = "assistant"
	RoleTool      = "tool"
)

Message roles in a transcript.

Variables

View Source
var ErrAwaitingApproval = errors.New("agents: awaiting tool approval")

ErrAwaitingApproval is returned by Turn when the Approve hook withheld approval for the model's requested tools. The pending calls are recorded as the last assistant turn; call Resume once the host has approval.

Functions

This section is empty.

Types

type Conversation

type Conversation struct {
	Messages []Message
}

Conversation is the running transcript. Persist it however you like; it is just a slice of Messages.

func NewConversation

func NewConversation(systemPrompt string) *Conversation

NewConversation starts a conversation, seeding the system prompt when set.

type Engine

type Engine interface {
	Complete(ctx context.Context, req Request) (*Response, error)
}

Engine is the model-agnostic boundary. Implementations translate a Request to a provider's wire format and back. See engine/openai for one.

type Harness

type Harness struct {
	Name         string
	SystemPrompt string
	Model        string
	MaxSteps     int
	Tools        *Registry

	// StepLimitMessage, when set, replaces the default assistant message a turn
	// returns after it exhausts MaxSteps without a final answer.
	StepLimitMessage string
}

Harness is a named agent: a system prompt, a model, a tool set, and a cap on how many tool rounds a single turn may take before it must answer.

type Message

type Message struct {
	Role       string     `json:"role"`
	Content    string     `json:"content"`
	ToolCalls  []ToolCall `json:"tool_calls,omitempty"`
	ToolCallID string     `json:"tool_call_id,omitempty"`
	Name       string     `json:"name,omitempty"`

	// Meta is host-owned metadata, ignored by the engine and never sent to the
	// model. It round-trips through JSON so a host can persist the transcript as
	// []Message and keep its own per-message state (presentation flags, a UI
	// label, an author id) without a parallel message type.
	Meta map[string]any `json:"meta,omitempty"`
}

Message is one turn in the transcript. An assistant message may carry ToolCalls; a tool message carries the result of one call, keyed by ToolCallID.

type Registry

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

Registry is the ordered set of tools available to a harness, keyed by name. It is not safe for concurrent modification; build it once, then use it.

func NewRegistry

func NewRegistry(tools ...Tool) *Registry

NewRegistry builds a registry from the given tools.

func (*Registry) Get

func (r *Registry) Get(name string) (Tool, bool)

Get returns a tool by name.

func (*Registry) Register

func (r *Registry) Register(t Tool)

Register adds or replaces a tool. Insertion order is preserved for the schema list the model sees.

func (*Registry) Schemas

func (r *Registry) Schemas() []ToolSchema

Schemas returns every tool as advertised to the model, in registration order.

type Request

type Request struct {
	Model       string
	Messages    []Message
	Tools       []ToolSchema
	MaxTokens   int
	Temperature float64
}

Request is one call to the model.

type Response

type Response struct {
	Model     string
	Content   string
	ToolCalls []ToolCall
	Usage     Usage
}

Response is the model's reply. When ToolCalls is non-empty the model wants those run before it continues; otherwise Content is the final answer.

type Runner

type Runner struct {
	Engine  Engine
	Harness Harness

	// Authorize, when set, runs before a Mutates tool. A non-nil error stops
	// that one tool; its text becomes the tool result the model sees, so the
	// model can adapt rather than the whole run failing.
	Authorize func(ctx context.Context, tool Tool, call ToolCall) error

	// Approve, when set, runs when the model requests tools. Returning false
	// pauses the turn (human-in-the-loop): Turn returns ErrAwaitingApproval and
	// the pending calls are the last assistant turn. When nil, tools run
	// automatically.
	Approve func(ctx context.Context, calls []ToolCall) (bool, error)
}

Runner drives a Harness against an Engine.

func (Runner) Advance added in v0.4.0

func (r Runner) Advance(ctx context.Context, conv *Conversation) (string, error)

Advance drives the tool loop from the conversation's current state, without adding a user message: it calls the model, runs any requested tools (or pauses via Approve), and repeats until a final answer or the step budget runs out. A host that records its own richer user turn appends it, then calls Advance.

func (Runner) Resume

func (r Runner) Resume(ctx context.Context, conv *Conversation, decisions map[string]bool) (string, error)

Resume runs the tools of a turn paused by Approve, then advances. It errors if nothing is pending.

decisions maps a pending call's ID to whether the operator approved it. When decisions is non-nil, a call runs only if its ID maps to true; every other pending call is declined and its result records the refusal, so the model continues without it. A nil map approves every pending call.

func (Runner) Turn

func (r Runner) Turn(ctx context.Context, conv *Conversation, userMessage string) (string, error)

Turn appends a user message and advances until the model gives a final answer or the step budget runs out, returning the final content.

Example

ExampleRunner_Turn drives a tool loop against a fake engine: the model asks for a tool, the runner runs it, and the model gives a final answer.

eng := &scriptedEngine{responses: []Response{
	{ToolCalls: []ToolCall{{ID: "c1", Name: "echo", Arguments: `{"text":"world"}`}}},
	{Content: "The tool said echo:world"},
}}
runner := Runner{Engine: eng, Harness: Harness{Model: "demo", Tools: NewRegistry(echoTool())}}

conv := NewConversation("You are a demo.")
answer, err := runner.Turn(context.Background(), conv, "echo world")
if err != nil {
	panic(err)
}
fmt.Println(answer)
Output:
The tool said echo:world

type Tool

type Tool struct {
	Name        string
	Description string
	Parameters  map[string]any
	Mutates     bool
	Handler     func(ctx context.Context, args json.RawMessage) (string, error)
}

Tool is a function the model may call. Handler runs it and returns the result text that is fed back to the model. Parameters is a JSON Schema for the arguments. Mutates marks a state-changing tool, which the host can gate through Runner.Authorize.

type ToolCall

type ToolCall struct {
	ID        string `json:"id"`
	Name      string `json:"name"`
	Arguments string `json:"arguments"`
}

ToolCall is one tool invocation the model requested. Arguments is a JSON object string, as produced by the model.

type ToolSchema

type ToolSchema struct {
	Name        string         `json:"name"`
	Description string         `json:"description"`
	Parameters  map[string]any `json:"parameters"`
}

ToolSchema advertises a tool to the model. Parameters is a JSON Schema object describing the tool's arguments.

type Usage

type Usage struct {
	PromptTokens     int `json:"prompt_tokens"`
	CompletionTokens int `json:"completion_tokens"`
}

Usage reports token counts when the engine provides them.

Directories

Path Synopsis
engine
openai
Package openai is an Engine that speaks the OpenAI chat-completions wire format, which OpenAI, Azure OpenAI, Ollama, vLLM, LM Studio, and most gateways accept.
Package openai is an Engine that speaks the OpenAI chat-completions wire format, which OpenAI, Azure OpenAI, Ollama, vLLM, LM Studio, and most gateways accept.

Jump to

Keyboard shortcuts

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