Go SDK
Build production agents in Go with the AgentField SDK
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
| Field | Type | Default | Description |
|---|---|---|---|
NodeID | string | required | Unique identifier for this agent node |
Version | string | required | Agent version (e.g. "1.0.0") |
TeamID | string | "default" | Groups related agents together |
AgentFieldURL | string | "" | Control plane URL |
ListenAddress | string | ":8001" | Address the HTTP server binds to |
PublicURL | string | "http://localhost" + ListenAddress | URL reported to the control plane |
Token | string | "" | Bearer token for control plane authentication |
DeploymentType | string | "long_running" | "long_running" or "serverless" |
LeaseRefreshInterval | time.Duration | 2m | Heartbeat interval with control plane |
DisableLeaseLoop | bool | false | Disable automatic heartbeats |
Logger | *log.Logger | stdout logger | Custom logger |
AIConfig | *ai.Config | nil | LLM configuration |
MemoryBackend | MemoryBackend | in-memory | Pluggable storage backend |
HarnessConfig | *HarnessConfig | nil | Default config for Harness() calls. nil or a zero value runs the aforge provider; AGENTFIELD_HARNESS_PROVIDER shifts that default. |
Tags | []string | nil | Metadata labels for policy-based authorization |
RequireOriginAuth | bool | false | Validate incoming requests |
InternalToken | string | "" | Token for origin auth |
EnableDID | bool | false | Auto-register a DID identity |
DID | string | "" | Pre-configured DID |
PrivateKeyJWK | string | "" | Ed25519 private key for DID signing |
VCEnabled | bool | false | Generate Verifiable Credentials |
LocalVerification | bool | false | Verify incoming DID signatures locally |
VerificationRefreshInterval | time.Duration | 5m | Cache refresh interval |
CLIConfig | *CLIConfig | nil | CLI 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
| Option | Description |
|---|---|
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
| Option | Description |
|---|---|
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
| Option | Description |
|---|---|
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() *MemoryReturns 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
| Method | Scope |
|---|---|
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)| Option | Description |
|---|---|
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) errorRegisters with the control plane, starts HTTP server, blocks until context cancelled or SIGTERM.
Run
func (a *Agent) Run(ctx context.Context) errorIntelligent mode router: CLI mode if CLI-enabled reasoners exist and arguments are present, otherwise falls back to Serve().
Handler
func (a *Agent) Handler() http.HandlerReturns 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| Field | Type | Description |
|---|---|---|
RunID | string | Unique run identifier |
ExecutionID | string | Unique execution identifier |
ParentExecutionID | string | Parent execution (for nested calls) |
SessionID | string | Session identifier |
ActorID | string | User/actor identifier |
WorkflowID | string | Current workflow |
Depth | int | Nesting depth |
CallerDID | string | DID of the calling agent |
ParentWorkflowID | string | Parent workflow identifier |
RootWorkflowID | string | Root workflow identifier |
AgentNodeID | string | Agent node identifier |
ReasonerName | string | Name of the reasoner being executed |
StartedAt | time.Time | Execution start time |
TargetDID | string | Target DID for this execution |
AgentNodeDID | string | DID of the agent node |
HTTP Endpoints
| Endpoint | Method | Description |
|---|---|---|
/health | GET | Health check |
/discover | GET | Capability discovery |
/execute | POST | Execute a reasoner (control plane routing) |
/execute/{name} | POST | Execute a specific reasoner |
/reasoners/{name} | POST | Direct 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
| Field | Type | Description |
|---|---|---|
AppName | string | CLI program name (shown in usage) |
AppDescription | string | Short description for help text |
DisableColors | bool | Disable colored output |
DefaultOutputFormat | string | Default output format |
HelpPreamble | string | Text shown before help content |
HelpEpilog | string | Text shown after help content |
EnvironmentVars | []string | Environment 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:
| Method | Description |
|---|---|
Name() string | Returns "openrouter" |
SupportedModalities() []string | Returns ["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:
| Field | Type | Description |
|---|---|---|
Prompt | string | Text prompt (required) |
Model | string | Model name |
Size | string | Image dimensions |
Quality | string | Quality level |
ImageConfig | *ImageConfig | Provider-specific config |
AudioRequest:
| Field | Type | Description |
|---|---|---|
Text | string | Text to synthesize (required) |
Model | string | TTS model |
Voice | string | Voice name |
Format | string | Audio format |
VideoRequest:
| Field | Type | Description |
|---|---|---|
Prompt | string | Text prompt (required) |
Model | string | Video model |
Duration | float64 | Duration in seconds |
Resolution | string | Output resolution |
AspectRatio | string | Aspect ratio |
GenerateAudio | *bool | Include audio track |
Seed | *int | Reproducibility seed |
PollInterval | time.Duration | Poll interval (default 30s) |
Timeout | time.Duration | Total timeout (default 10m) |
Extra | map[string]any | Additional parameters |
MediaResponse
| Field | Type | Description |
|---|---|---|
Text | string | Text content |
Images | []ImageData | Generated images |
Audio | *AudioData | Generated audio |
Files | []FileData | Generated files |
Videos | []VideoData | Generated videos |
RawResponse | any | Raw provider response |
VideoData
| Field | Type | Description |
|---|---|---|
URL | string | Video file URL |
Data | string | Base64-encoded video data |
MimeType | string | MIME type (e.g., "video/mp4") |
Filename | string | Suggested filename |
Duration | float64 | Duration in seconds |
Resolution | string | Output resolution |
AspectRatio | string | Aspect ratio |
HasAudio | bool | Whether video has audio track |
CostUSD | float64 | Generation 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) errorManually 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
| Backend | Constructor | Description |
|---|---|---|
| InMemoryBackend | agent.NewInMemoryBackend() | Thread-safe in-process map. Default. Ideal for tests. |
| ControlPlaneMemoryBackend | agent.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)
}
}