AgentFieldreference

Go SDK

Build production agents in Go with the AgentField SDK

Go SDK — go get agentfield

Build and deploy AI agents in Go with idiomatic patterns.

The Go SDK provides the same core primitives as the Python and TypeScript SDKs -- agents, reasoners, memory, AI, cross-agent calls -- with idiomatic Go patterns: functional options, explicit error returns, and context.Context propagation.

Install

go get github.com/Agent-Field/agentfield/sdk/go
import "github.com/Agent-Field/agentfield/sdk/go/agent"
import "github.com/Agent-Field/agentfield/sdk/go/ai"

Quick Start

package main

import (
    "context"
    "log"

    "github.com/Agent-Field/agentfield/sdk/go/agent"
)

func main() {
    app, err := agent.New(agent.Config{
        NodeID:        "my-agent",
        Version:       "1.0.0",
        AgentFieldURL: "http://localhost:8080",
        ListenAddress: ":8001",
    })
    if err != nil {
        log.Fatal(err)
    }

    app.RegisterReasoner("greet", func(ctx context.Context, input map[string]any) (any, error) {
        name, _ := input["name"].(string)
        return map[string]any{"message": "Hello, " + name}, nil
    })

    if err := app.Serve(context.Background()); err != nil {
        log.Fatal(err)
    }
}

Agent Constructor and Config

agent.New

func New(cfg Config) (*Agent, error)

Creates an Agent instance. NodeID and Version are required; everything else has sensible defaults.

Config struct

FieldTypeDefaultDescription
NodeIDstringrequiredUnique identifier for this agent node
VersionstringrequiredAgent version (e.g. "1.0.0")
TeamIDstring"default"Groups related agents together
AgentFieldURLstring""Control plane URL
ListenAddressstring":8001"Address the HTTP server binds to
PublicURLstring"http://localhost" + ListenAddressURL reported to the control plane
Tokenstring""Bearer token for control plane authentication
DeploymentTypestring"long_running""long_running" or "serverless"
LeaseRefreshIntervaltime.Duration2mHeartbeat interval with control plane
DisableLeaseLoopboolfalseDisable automatic heartbeats
Logger*log.Loggerstdout loggerCustom logger
AIConfig*ai.ConfignilLLM configuration
MemoryBackendMemoryBackendin-memoryPluggable storage backend
HarnessConfig*HarnessConfignilDefault config for Harness() calls. nil or a zero value runs the aforge provider; AGENTFIELD_HARNESS_PROVIDER shifts that default.
Tags[]stringnilMetadata labels for policy-based authorization
RequireOriginAuthboolfalseValidate incoming requests
InternalTokenstring""Token for origin auth
EnableDIDboolfalseAuto-register a DID identity
DIDstring""Pre-configured DID
PrivateKeyJWKstring""Ed25519 private key for DID signing
VCEnabledboolfalseGenerate Verifiable Credentials
LocalVerificationboolfalseVerify incoming DID signatures locally
VerificationRefreshIntervaltime.Duration5mCache refresh interval
CLIConfig*CLIConfignilCLI help text and formatting options
RegisterReasoner

RegisterReasoner

func (a *Agent) RegisterReasoner(name string, handler HandlerFunc, opts ...ReasonerOption)

Registers a handler function at /reasoners/{name}.

type HandlerFunc func(ctx context.Context, input map[string]any) (any, error)

ReasonerOption functions

OptionDescription
WithInputSchema(raw json.RawMessage)Override auto-generated input JSON Schema
WithOutputSchema(raw json.RawMessage)Override default output JSON Schema
WithDescription(desc string)Human-readable description for discovery
WithReasonerTags(tags ...string)Tags for tag-based authorization policies
WithCLI()Make this reasoner callable from the CLI
WithDefaultCLI()Mark as the default CLI handler
WithCLIFormatter(fn func(context.Context, any, error))Custom CLI output formatter
WithVCEnabled(enabled bool)Override agent-level VC generation
WithRequireRealtimeValidation()Force control-plane verification

Example

schema := json.RawMessage(`{
    "type": "object",
    "properties": { "query": {"type": "string"} },
    "required": ["query"]
}`)

app.RegisterReasoner("search", searchHandler,
    agent.WithInputSchema(schema),
    agent.WithDescription("Search the knowledge base"),
    agent.WithReasonerTags("search", "read-only"),
    agent.WithCLI(),
)
RegisterSession

RegisterSession

func (a *Agent) RegisterSession(name string, provider string, transport string, opts ...SessionOption)

Registers a realtime or multimodal session entrypoint. Sessions are started through the AgentField control plane; session tools route back into normal reasoners so workflow context is preserved.

app.RegisterSession("voice", "openai", "webrtc",
    agent.WithSessionModel("gpt-realtime-2"),
    agent.WithSessionModalities("audio", "text"),
    agent.WithSessionVoice("marin"),
    agent.WithSessionTools("voice-support-af.resolve_voice_turn"),
    agent.WithSessionTags("support:voice", "pii:limited"),
)

SessionOption functions

OptionDescription
WithSessionModel(model)Provider model for the session
WithSessionModalities(modalities...)Session modalities such as audio and text
WithSessionVoice(voice)Provider voice name
WithSessionTools(tools...)Tool targets exposed to the realtime session
WithSessionTags(tags...)Access-control tags proposed for the session ingress
WithSessionMetadata(metadata)Additional session metadata for registration

Provider and transport are explicit. AgentField validates the combination and does not infer or switch providers.

WithSessionTools(...) is a provider/client-visible allowlist. It does not replace handler-controlled orchestration. Use it to expose selected AgentField targets for autonomous realtime tool calls during the live session.

WithSessionTags(...) proposes access-control tags for the session ingress. Approve them like reasoner and skill tags, then use policies to control who can start the live session.

AI Methods — AI(), AIStream(), AIWithTools()

AI Methods

All AI methods require AIConfig to be set in the agent Config.

ai.Config

type Config struct {
    APIKey      string        // Required. Or set OPENAI_API_KEY / OPENROUTER_API_KEY
    BaseURL     string        // Default: "https://api.openai.com/v1"
    Model       string        // Default: "gpt-4o"
    Temperature float64       // Default: 0.7
    MaxTokens   int           // Default: 4096
    Timeout     time.Duration // Default: 30s
    SiteURL     string        // Optional (OpenRouter rankings)
    SiteName    string        // Optional (OpenRouter rankings)
}

AI

func (a *Agent) AI(ctx context.Context, prompt string, opts ...ai.Option) (*ai.Response, error)
resp, err := app.AI(ctx, "Summarize this document",
    ai.WithSystem("You are a document analyst"),
    ai.WithTemperature(0.3),
    ai.WithMaxTokens(1000),
)
text := resp.Text()

AIStream

func (a *Agent) AIStream(ctx context.Context, prompt string, opts ...ai.Option) (<-chan ai.StreamChunk, <-chan error)

AIWithTools

func (a *Agent) AIWithTools(
    ctx context.Context, prompt string, config ai.ToolCallConfig, discoveryOpts ...DiscoveryOption,
) (*ai.Response, *ai.ToolCallTrace, error)

ai.Option functions

OptionDescription
ai.WithSystem(content)System message
ai.WithModel(model)Override model
ai.WithTemperature(temp)Set temperature (0.0--2.0)
ai.WithMaxTokens(n)Set max tokens
ai.WithJSONMode()Enable JSON response format
ai.WithSchema(schema)Structured output with JSON Schema
ai.WithStream()Enable streaming
ai.WithTools(tools)Provide tool definitions
ai.WithImageFile(path)Attach image from file
ai.WithImageURL(url)Attach image from URL
Memory

Memory

func (a *Agent) Memory() *Memory

Returns the agent's hierarchical memory system. Default scope is session.

Key-value operations

err := app.Memory().Set(ctx, "user_preference", "dark_mode")
val, err := app.Memory().Get(ctx, "user_preference")
val, err := app.Memory().GetWithDefault(ctx, "theme", "light")
keys, err := app.Memory().List(ctx)
err := app.Memory().Delete(ctx, "user_preference")

Vector operations

err := app.Memory().SetVector(ctx, "doc-1", embedding, map[string]any{"source": "kb"})
embedding, metadata, err := app.Memory().GetVector(ctx, "doc-1")
results, err := app.Memory().SearchVector(ctx, queryEmbedding, agent.SearchOptions{Limit: 10, Threshold: 0.8}) // returns []VectorSearchResult
err := app.Memory().DeleteVector(ctx, "doc-1")

Scoped memory

MethodScope
Memory().WorkflowScope()Current workflow execution
Memory().SessionScope()Current session (default)
Memory().UserScope()Current user/actor across sessions
Memory().GlobalScope()Shared across all sessions and users
Memory().Scoped(scope, id)Explicit scope and scope ID

Memory backends

  • NewInMemoryBackend() -- Thread-safe in-process storage. Default.
  • NewControlPlaneMemoryBackend(url, token, nodeID) -- Delegates to control plane's distributed memory API.
Discover, Call, Harness, Note

Discover

func (a *Agent) Discover(ctx context.Context, opts ...DiscoveryOption) (*types.DiscoveryResult, error)
OptionDescription
WithAgent(id)Filter to a single agent
WithAgentIDs(ids)Filter to multiple agents
WithTags(tags)Filter by tags (supports wildcards)
WithDiscoveryInputSchema(bool)Include input schemas
WithFormat(format)"json", "xml", or "compact"
WithLimit(n)Pagination limit

Call

func (a *Agent) Call(ctx context.Context, target string, input map[string]any) (map[string]any, error)

Target uses dot notation: "agent-id.reasoner-name".

result, err := app.Call(ctx, "search-agent.query", map[string]any{"q": "machine learning", "limit": 10})

CallLocal

func (a *Agent) CallLocal(ctx context.Context, reasonerName string, input map[string]any) (any, error)

Harness

func (a *Agent) Harness(ctx context.Context, prompt string, schema map[string]any, dest any, opts harness.Options) (*harness.Result, error)

Options with no Provider runs AForge, AgentField's native harness — installed alongside the af binary, so harness.Options{} is complete once OPENROUTER_API_KEY is set. An empty Model means the provider's own default. Precedence: Options.Provider, then Config.HarnessConfig.Provider, then AGENTFIELD_HARNESS_PROVIDER, then "aforge".

var result ReviewResult
schema, _ := harness.StructToJSONSchema(result)

// Default worker — nothing to install beyond af.
hr, err := app.Harness(ctx, "Review this code for security issues", schema, &result,
    harness.Options{},
)

// Bring your own coding agent: same call, different worker.
hr, err = app.Harness(ctx, "Review this code for security issues", schema, &result,
    harness.Options{Provider: "claude-code"},
)

Note

func (a *Agent) Note(ctx context.Context, message string, tags ...string)
func (a *Agent) Notef(ctx context.Context, format string, args ...any)
Server Lifecycle, CLI, Execution Context

Server Lifecycle

Serve

func (a *Agent) Serve(ctx context.Context) error

Registers with the control plane, starts HTTP server, blocks until context cancelled or SIGTERM.

Run

func (a *Agent) Run(ctx context.Context) error

Intelligent mode router: CLI mode if CLI-enabled reasoners exist and arguments are present, otherwise falls back to Serve().

Handler

func (a *Agent) Handler() http.Handler

Returns the agent as an http.Handler for serverless or custom hosting.

HandleServerlessEvent

func (a *Agent) HandleServerlessEvent(ctx context.Context, event map[string]any, adapter func(map[string]any) map[string]any) (map[string]any, int, error)

Execution Context

func ExecutionContextFrom(ctx context.Context) ExecutionContext
FieldTypeDescription
RunIDstringUnique run identifier
ExecutionIDstringUnique execution identifier
ParentExecutionIDstringParent execution (for nested calls)
SessionIDstringSession identifier
ActorIDstringUser/actor identifier
WorkflowIDstringCurrent workflow
DepthintNesting depth
CallerDIDstringDID of the calling agent
ParentWorkflowIDstringParent workflow identifier
RootWorkflowIDstringRoot workflow identifier
AgentNodeIDstringAgent node identifier
ReasonerNamestringName of the reasoner being executed
StartedAttime.TimeExecution start time
TargetDIDstringTarget DID for this execution
AgentNodeDIDstringDID of the agent node

HTTP Endpoints

EndpointMethodDescription
/healthGETHealth check
/discoverGETCapability discovery
/executePOSTExecute a reasoner (control plane routing)
/execute/{name}POSTExecute a specific reasoner
/reasoners/{name}POSTDirect reasoner invocation

CLI Mode

app.RegisterReasoner("analyze", analyzeHandler,
    agent.WithDefaultCLI(),
    agent.WithDescription("Analyze a dataset"),
)
app.Run(context.Background())
CLI Integration

CLI Mode

The Go SDK supports building agents that double as CLI tools. Register reasoners with WithCLI() or WithDefaultCLI(), then call app.Run() -- it automatically detects whether to start the HTTP server or run the CLI handler.

CLIConfig

FieldTypeDescription
AppNamestringCLI program name (shown in usage)
AppDescriptionstringShort description for help text
DisableColorsboolDisable colored output
DefaultOutputFormatstringDefault output format
HelpPreamblestringText shown before help content
HelpEpilogstringText shown after help content
EnvironmentVars[]stringEnvironment variables shown in help
app, _ := agent.New(agent.Config{
    NodeID:  "analyzer",
    Version: "1.0.0",
    CLIConfig: &agent.CLIConfig{
        AppName:        "analyzer",
        AppDescription: "Analyze datasets from the command line",
    },
})

Registering CLI Handlers

// Default CLI handler — runs when no subcommand is specified
app.RegisterReasoner("analyze", analyzeHandler,
    agent.WithDefaultCLI(),
    agent.WithDescription("Analyze a dataset"),
)

// Named CLI subcommand
app.RegisterReasoner("summarize", summarizeHandler,
    agent.WithCLI(),
    agent.WithDescription("Summarize results"),
)

// Custom output formatting for CLI
app.RegisterReasoner("report", reportHandler,
    agent.WithCLI(),
    agent.WithCLIFormatter(func(ctx context.Context, result any, err error) {
        if err != nil {
            fmt.Fprintf(os.Stderr, "Error: %v\n", err)
            return
        }
        r := result.(map[string]any)
        fmt.Printf("Report: %s\nScore: %.1f\n", r["title"], r["score"])
    }),
)

CLI Usage

# Run as CLI (auto-detected from args)
go run . --input '{"dataset": "sales.csv"}'

# Run specific subcommand
go run . summarize --input '{"query": "Q1 results"}'

# Run as server (no args = server mode)
go run .
Media Generation — MediaProvider, OpenRouterMediaProvider, MediaRouter

MediaProvider

The MediaProvider interface abstracts media generation across providers. OpenRouterMediaProvider is the built-in implementation for OpenRouter's image, audio, and video APIs.

OpenRouterMediaProvider

import goai "github.com/Agent-Field/agentfield/sdk/go/ai"

// Pass API key directly
media, err := goai.NewOpenRouterMediaProvider("sk-or-...")

// Or read from OPENROUTER_API_KEY environment variable
media, err := goai.NewOpenRouterMediaProvider("")

Methods:

MethodDescription
Name() stringReturns "openrouter"
SupportedModalities() []stringReturns ["image", "audio", "video"]
GenerateImage(ctx, ImageRequest) (*MediaResponse, error)Generate image from text
GenerateAudio(ctx, AudioRequest) (*MediaResponse, error)Generate speech via SSE streaming
GenerateVideo(ctx, VideoRequest) (*MediaResponse, error)Generate video via async polling

Image Generation

resp, err := media.GenerateImage(ctx, goai.ImageRequest{
    Prompt: "A sunset over mountains",
    Model:  "google/gemini-3.1-flash-image-preview",
    ImageConfig: &goai.ImageConfig{
        AspectRatio: "16:9",
    },
})
if err != nil {
    return nil, err
}
fmt.Println("Image URL:", resp.Images[0].URL)

Audio Generation

resp, err := media.GenerateAudio(ctx, goai.AudioRequest{
    Text:   "Welcome to AgentField.",
    Model:  "openai/tts-1",
    Voice:  "alloy",
    Format: "pcm16",
})
if err != nil {
    return nil, err
}
fmt.Printf("Audio: %d bytes of %s data\n", len(resp.Audio.Data), resp.Audio.Format)

Video Generation

resp, err := media.GenerateVideo(ctx, goai.VideoRequest{
    Prompt:       "A cat playing with yarn",
    Model:        "kling-video/v2.0/master",
    Duration:     10,
    PollInterval: 30 * time.Second,
    Timeout:      10 * time.Minute,
})
if err != nil {
    return nil, err
}
fmt.Println("Video URL:", resp.Videos[0].URL)

Request Types

ImageRequest:

FieldTypeDescription
PromptstringText prompt (required)
ModelstringModel name
SizestringImage dimensions
QualitystringQuality level
ImageConfig*ImageConfigProvider-specific config

AudioRequest:

FieldTypeDescription
TextstringText to synthesize (required)
ModelstringTTS model
VoicestringVoice name
FormatstringAudio format

VideoRequest:

FieldTypeDescription
PromptstringText prompt (required)
ModelstringVideo model
Durationfloat64Duration in seconds
ResolutionstringOutput resolution
AspectRatiostringAspect ratio
GenerateAudio*boolInclude audio track
Seed*intReproducibility seed
PollIntervaltime.DurationPoll interval (default 30s)
Timeouttime.DurationTotal timeout (default 10m)
Extramap[string]anyAdditional parameters

MediaResponse

FieldTypeDescription
TextstringText content
Images[]ImageDataGenerated images
Audio*AudioDataGenerated audio
Files[]FileDataGenerated files
Videos[]VideoDataGenerated videos
RawResponseanyRaw provider response

VideoData

FieldTypeDescription
URLstringVideo file URL
DatastringBase64-encoded video data
MimeTypestringMIME type (e.g., "video/mp4")
FilenamestringSuggested filename
Durationfloat64Duration in seconds
ResolutionstringOutput resolution
AspectRatiostringAspect ratio
HasAudioboolWhether video has audio track
CostUSDfloat64Generation cost in USD

MediaRouter

Route model names to the correct provider by prefix.

router := goai.NewMediaRouter()
orProvider, _ := goai.NewOpenRouterMediaProvider("")
router.Register("openrouter/", orProvider)

// Resolve provider by model prefix
provider, err := router.Resolve("openrouter/kling-video/v2.0/master", "video")
if err != nil {
    log.Fatal(err) // no provider matches
}

resp, err := provider.GenerateVideo(ctx, goai.VideoRequest{
    Prompt: "A cat playing with yarn",
    Model:  "kling-video/v2.0/master",  // prefix stripped by router
})
Convenience Methods — CallLocal, Initialize

CallLocal

func (a *Agent) CallLocal(ctx context.Context, reasonerName string, input map[string]any) (any, error)

Invoke a reasoner registered on the same agent without going through the network. Useful for internal composition.

// Call a local reasoner directly (no HTTP, no control plane)
result, err := app.CallLocal(ctx, "validate", map[string]any{
    "document": document,
    "rules":    validationRules,
})

Initialize

func (a *Agent) Initialize(ctx context.Context) error

Manually initialize the agent (register with control plane, start heartbeats) without starting the HTTP server. Useful when embedding the agent in a larger application.

app, _ := agent.New(agent.Config{NodeID: "embedded", Version: "1.0.0"})
app.RegisterReasoner("process", processHandler)

// Register with control plane but don't start HTTP server
if err := app.Initialize(ctx); err != nil {
    log.Fatal(err)
}

// Use the agent programmatically
result, _ := app.CallLocal(ctx, "process", input)
MemoryBackend Interface

Pluggable Memory Backends

The Go SDK uses a MemoryBackend interface for all storage. Swap implementations for testing, local dev, or production.

type MemoryBackend interface {
    Get(scope MemoryScope, scopeID, key string) (any, bool, error)
    Set(scope MemoryScope, scopeID, key string, value any) error
    Delete(scope MemoryScope, scopeID, key string) error
    List(scope MemoryScope, scopeID string) ([]string, error)
    SetVector(scope MemoryScope, scopeID, key string, embedding []float64, metadata map[string]any) error
    GetVector(scope MemoryScope, scopeID, key string) ([]float64, map[string]any, bool, error)
    SearchVector(scope MemoryScope, scopeID string, embedding []float64, opts SearchOptions) ([]VectorSearchResult, error)
    DeleteVector(scope MemoryScope, scopeID, key string) error
}

Built-in Backends

BackendConstructorDescription
InMemoryBackendagent.NewInMemoryBackend()Thread-safe in-process map. Default. Ideal for tests.
ControlPlaneMemoryBackendagent.NewControlPlaneMemoryBackend(url, token, nodeID)Delegates to the control plane's distributed memory API. Use in production.
// Unit tests — fast, isolated, no external dependencies
app, _ := agent.New(agent.Config{
    NodeID:        "test-agent",
    MemoryBackend: agent.NewInMemoryBackend(),
})

// Production — persistent, distributed
app, _ := agent.New(agent.Config{
    NodeID:        "prod-agent",
    AgentFieldURL: "http://controlplane:8080",
    MemoryBackend: agent.NewControlPlaneMemoryBackend(
        "http://controlplane:8080",
        os.Getenv("AF_TOKEN"),
        "prod-agent",
    ),
})

Custom Backend

Implement the MemoryBackend interface to use Redis, DynamoDB, or any other store:

type RedisMemoryBackend struct {
    client *redis.Client
}

func (r *RedisMemoryBackend) Get(scope agent.MemoryScope, scopeID, key string) (any, bool, error) {
    fullKey := fmt.Sprintf("%s:%s:%s", scope, scopeID, key)
    val, err := r.client.Get(context.Background(), fullKey).Result()
    if err == redis.Nil {
        return nil, false, nil
    }
    if err != nil {
        return nil, false, err
    }
    var result any
    json.Unmarshal([]byte(val), &result)
    return result, true, nil
}

// ... implement remaining methods (Set, Delete, List, SetVector, GetVector, SearchVector, DeleteVector)

app, _ := agent.New(agent.Config{
    NodeID:        "redis-agent",
    Version:       "1.0.0",
    MemoryBackend: &RedisMemoryBackend{client: redisClient},
})
Complete Example

Full Example

package main

import (
    "context"
    "log"

    "github.com/Agent-Field/agentfield/sdk/go/agent"
    "github.com/Agent-Field/agentfield/sdk/go/ai"
)

func main() {
    app, err := agent.New(agent.Config{
        NodeID:        "support-agent",
        Version:       "1.0.0",
        AgentFieldURL: "http://localhost:8080",
        AIConfig:      ai.DefaultConfig(),
    })
    if err != nil {
        log.Fatal(err)
    }

    app.RegisterReasoner("answer", func(ctx context.Context, input map[string]any) (any, error) {
        question, _ := input["question"].(string)

        history, _ := app.Memory().SessionScope().Get(ctx, "history")

        resp, err := app.AI(ctx, question,
            ai.WithSystem("You are a helpful support agent."),
            ai.WithTemperature(0.3),
        )
        if err != nil {
            return nil, err
        }

        answer := resp.Text()

        app.Memory().SessionScope().Set(ctx, "history", map[string]any{
            "question": question,
            "answer":   answer,
            "previous": history,
        })

        return map[string]any{"answer": answer}, nil
    }, agent.WithDescription("Answer a support question"))

    if err := app.Serve(context.Background()); err != nil {
        log.Fatal(err)
    }
}