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_URL set → Server mode
  • AGENTFIELD_URL empty → Standalone mode

Standalone vs Control Plane

FeatureStandalone ModeControl 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.json

Hybrid 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 serve

Limitations 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 only

No Workflow Tracking

Execution DAG not recorded:

// Standalone: no workflow DAG
// No parent-child relationship tracking
// No execution history

No DIDs/VCs

No cryptographic identity or verifiable credentials:

// Standalone: no DID generation, no VCs
// No audit trail

Detecting 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 --help

Container 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=data

Package 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
end
brew 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
})

Next Steps