Standalone Mode
Run Go agents without the control plane for local execution and distribution
What is Standalone Mode?
Standalone mode allows your agent binary to run without connecting to the AgentField control plane. The agent executes reasoners locally with full functionality minus control plane features.
Enabling Standalone Mode
Simply omit or leave empty the AgentFieldURL configuration:
a, err := agent.New(agent.Config{
NodeID: "my-agent",
// AgentFieldURL not set = standalone mode
CLIConfig: &agent.CLIConfig{
AppName: "my-tool",
},
})Or use environment variable:
a, err := agent.New(agent.Config{
NodeID: "my-agent",
AgentFieldURL: os.Getenv("AGENTFIELD_URL"), // Empty = standalone
CLIConfig: &agent.CLIConfig{
AppName: "my-tool",
},
})Automatic mode selection:
AGENTFIELD_URLset → Server modeAGENTFIELD_URLempty → Standalone mode
Standalone vs Control Plane
| Feature | Standalone Mode | Control Plane Mode |
|---|---|---|
| CLI execution | ✅ Yes | ✅ Yes |
| Local reasoners | ✅ Direct execution | ✅ Via HTTP |
| Cross-agent calls | ❌ Not available | ✅ Routed by control plane |
| Workflow DAG | ❌ No tracking | ✅ Full tracking |
| DID/VC | ❌ No verification | ✅ Cryptographic audit trail |
| Auto-discovery | ❌ Not discoverable | ✅ Registered in control plane |
| Shared memory | ❌ Local only | ✅ Distributed across agents |
| Dependencies | ✅ Zero (single binary) | ⚠️ Requires control plane |
| Startup time | ✅ <100ms | ⚠️ Network handshake |
Use Cases
Local Development & Testing
Test reasoners quickly without infrastructure:
# Build once
go build -o my-agent
# Test locally (no control plane needed)
./my-agent --set input="test data"
# Iterate rapidly
vim main.go
go build -o my-agent
./my-agent --set input="new test"CLI Tool Distribution
Ship single-binary tools to users:
# Users download binary
curl -L https://releases.example.com/my-tool -o my-tool
chmod +x my-tool
# Run immediately (no setup)
./my-tool --set query="AI agents"No runtime dependencies, no containers, no configuration.
Edge Deployment
Run agents on edge devices without control plane connectivity:
// Embedded device or offline environment
a, err := agent.New(agent.Config{
NodeID: "edge-processor",
// No AgentFieldURL - runs standalone
})Continuous Integration
Run agents in CI pipelines:
# .github/workflows/test.yml
- name: Test agent
run: |
go build -o agent
./agent analyze --input-file test-data.jsonHybrid Deployment
Same binary for both modes:
# Development: standalone
./my-agent --set input=data
# Staging: with control plane
AGENTFIELD_URL=http://staging:8080 ./my-agent serve
# Production: with control plane
AGENTFIELD_URL=http://prod:8080 ./my-agent serveLimitations in Standalone Mode
No Cross-Agent Calls
Calling other agents requires control plane:
a.RegisterReasoner("orchestrator", func(ctx context.Context, input map[string]any) (any, error) {
// This fails in standalone mode
result, err := a.Call(ctx, "other-agent.reasoner", input)
if err != nil {
// Handle: control plane not available
return nil, err
}
return result, nil
}, agent.WithCLI())Workaround: Detect mode and provide fallback:
a.RegisterReasoner("processor", func(ctx context.Context, input map[string]any) (any, error) {
// Try control plane first
if hasControlPlane(a) {
result, err := a.Call(ctx, "other-agent.reasoner", input)
if err == nil {
return result, nil
}
}
// Fallback: local execution
return localProcess(input), nil
}, agent.WithCLI())No Shared Memory
Memory is local only in standalone mode:
// Standalone: memory not shared across agents
a.Memory.Set(ctx, "agent", "key", "value") // Local onlyNo Workflow Tracking
Execution DAG not recorded:
// Standalone: no workflow DAG
// No parent-child relationship tracking
// No execution historyNo DIDs/VCs
No cryptographic identity or verifiable credentials:
// Standalone: no DID generation, no VCs
// No audit trailDetecting Standalone Mode
Check if control plane is available:
func hasControlPlane(a *agent.Agent) bool {
return a.Config.AgentFieldURL != ""
}
a.RegisterReasoner("flexible", func(ctx context.Context, input map[string]any) (any, error) {
if hasControlPlane(a) {
// Control plane available: use advanced features
return processWithWorkflow(ctx, input)
}
// Standalone: simple execution
return processLocally(input), nil
}, agent.WithCLI())Graceful Degradation
Design agents to work in both modes:
a.RegisterReasoner("analyze", func(ctx context.Context, input map[string]any) (any, error) {
var result map[string]any
if hasControlPlane(a) {
// Try to call specialized agent
res, err := a.Call(ctx, "nlp-agent.analyze", input)
if err == nil {
result = res.(map[string]any)
} else {
// Fallback to local
result = localAnalyze(input)
}
} else {
// Standalone: always local
result = localAnalyze(input)
}
return result, nil
}, agent.WithCLI())Distribution Strategies
GitHub Releases
Use GoReleaser for multi-platform builds:
# .goreleaser.yml
builds:
- binary: my-agent
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64# Users download
curl -L https://github.com/org/my-agent/releases/latest/download/my-agent-linux-amd64 -o my-agent
chmod +x my-agent
./my-agent --helpContainer Images (Optional)
Even in standalone mode, can containerize:
FROM scratch
COPY my-agent /my-agent
ENTRYPOINT ["/my-agent"]docker run my-agent:latest --set input=dataPackage Managers
Distribute via Homebrew, apt, etc:
# Formula/my-agent.rb
class MyAgent < Formula
desc "AI-powered CLI tool"
homepage "https://example.com"
url "https://github.com/org/my-agent/releases/download/v1.0.0/my-agent-darwin-amd64"
sha256 "..."
def install
bin.install "my-agent-darwin-amd64" => "my-agent"
end
endbrew install my-agent
my-agent --set query="search"Configuration for Standalone
Standalone agents typically use:
Environment Variables
type Config struct {
APIKey string
Model string
LogLevel string
}
func loadConfig() Config {
return Config{
APIKey: os.Getenv("OPENAI_API_KEY"),
Model: getEnv("MODEL", "gpt-4"),
LogLevel: getEnv("LOG_LEVEL", "info"),
}
}
func getEnv(key, defaultVal string) string {
if val := os.Getenv(key); val != "" {
return val
}
return defaultVal
}Config Files (Optional)
import "github.com/spf13/viper"
func loadConfig() Config {
viper.SetConfigName(".my-agent")
viper.SetConfigType("yaml")
viper.AddConfigPath("$HOME")
viper.AddConfigPath(".")
viper.ReadInConfig()
return Config{
APIKey: viper.GetString("api_key"),
Model: viper.GetString("model"),
}
}Migration Path
Start Standalone
// v1.0: Standalone only
a, err := agent.New(agent.Config{
NodeID: "my-agent",
// No AgentFieldURL
})Add Optional Control Plane
// v2.0: Optional control plane
a, err := agent.New(agent.Config{
NodeID: "my-agent",
AgentFieldURL: os.Getenv("AGENTFIELD_URL"), // Optional
})
// Graceful degradation
a.RegisterReasoner("process", func(ctx context.Context, input map[string]any) (any, error) {
if hasControlPlane(a) {
// Use control plane features
}
// Fallback to local
}, agent.WithCLI())Full Control Plane Integration
// v3.0: Require control plane
a, err := agent.New(agent.Config{
NodeID: "my-agent",
AgentFieldURL: requireEnv("AGENTFIELD_URL"), // Required
})Best Practices
Design for Both Modes
Build agents that work standalone but leverage control plane when available:
// ✅ Good: Works in both modes
a.RegisterReasoner("analyze", func(ctx context.Context, input map[string]any) (any, error) {
result := localAnalyze(input)
if hasControlPlane(a) {
// Enhance with control plane features
a.Memory.Set(ctx, "agent", "last_analysis", result)
}
return result, nil
}, agent.WithCLI())
// ❌ Bad: Fails in standalone mode
a.RegisterReasoner("analyze", func(ctx context.Context, input map[string]any) (any, error) {
// This fails if control plane unavailable
return a.Call(ctx, "other-agent.process", input)
}, agent.WithCLI())Document Requirements
Clearly state control plane requirements:
CLIConfig: &agent.CLIConfig{
HelpPreamble: `This tool can run standalone or with AgentField control plane.
Standalone mode: All features except cross-agent calls
Control plane mode: Set AGENTFIELD_URL for full features`,
}Provide Meaningful Errors
result, err := a.Call(ctx, "other-agent.process", input)
if err != nil {
if !hasControlPlane(a) {
return nil, fmt.Errorf("cross-agent calls require control plane (set AGENTFIELD_URL)")
}
return nil, err
}Version Alongside Control Plane
Keep agent and control plane versions compatible:
a, err := agent.New(agent.Config{
NodeID: "my-agent",
Version: "1.2.0", // Match control plane compatibility
})