Go SDK

Use docker-agent as a Go library to embed AI agents in your applications.

Overview

docker-agent can be used as a Go library, allowing you to build AI agents directly into your Go applications. This gives you full programmatic control over agent creation, tool integration, and execution.

Import Path
import "github.com/docker/docker-agent/pkg/..."

Core Packages

Package Purpose
pkg/agent Agent creation and configuration
pkg/runtime Agent execution and event streaming
pkg/session Conversation state management
pkg/team Multi-agent team composition
pkg/tools Tool interface and utilities
pkg/tools/builtin Built-in tools (shell, filesystem, etc.)
pkg/model/provider/* Model provider clients
pkg/config/latest Configuration types
pkg/environment Environment and secrets
pkg/embeddedchat Headless chat session for embedding the agent runtime in a custom UI
pkg/tui/components/toolconfirm Tool-confirmation policy: Decision enum, BuildPermissionPattern, key bindings, and rejection-reason presets. Share this instead of copying the permission-pattern logic.
pkg/tui/service StaticSessionState — a SessionStateReader with conservative fixed values, for rendering message/tool views outside the full TUI app. Replaces hand-rolled nine-method stubs.
pkg/tui/animation Stopper / StopView — animation lifecycle contract. Call StopAnimation on views removed from the UI to prevent leaked tick subscriptions.
pkg/tui/components/transcript Embedded transcript view with read-only Messages() accessor for observing conversation structure in host tests and persistence layers.

Embedding TUI Components

When building custom UIs on top of docker-agent’s TUI primitives, four packages define the contracts that keep the runtime and the UI in sync:

Headless Embedded Chat (pkg/embeddedchat)

pkg/embeddedchat is a thin wrapper around the docker-agent runtime that lets you drive an agent from your own UI instead of running docker-agent’s Bubble Tea application. It handles runtime construction, event projection, and conversation state, exposing a simple Send / Confirm / Restart / Close API.

Creating a session

import (
    "context"
    "fmt"
    "strings"

    dagentcfg "github.com/docker/docker-agent/pkg/config"
    dagentruntime "github.com/docker/docker-agent/pkg/runtime"
    "github.com/docker/docker-agent/pkg/embeddedchat"
)

chat, err := embeddedchat.New(ctx, embeddedchat.Config{
    // AgentSource can be a file path, raw YAML bytes, or an OCI reference.
    AgentSource: dagentcfg.NewBytesSource("agent", []byte(agentYAML)),
})
if err != nil {
    return err
}
defer chat.Close()

Sending a message and reading events

Send appends the user message to the conversation and returns a channel of Event values. Drain the channel until it closes.

events, err := chat.Send(ctx, "Hello! What can you do?")
if err != nil {
    return err
}

var response strings.Builder
for ev := range events {
    switch {
    case ev.Text != "":
        response.WriteString(ev.Text)
    case ev.Tool != nil && ev.Tool.NeedsConfirmation:
        // Approve the pending tool call (use ResumeApproveSession to allow all).
        if err := chat.Confirm(ctx, dagentruntime.ResumeApprove()); err != nil {
            return err
        }
    case ev.Tool != nil && ev.Tool.Finished:
        fmt.Printf("[tool %s finished]\n", ev.Tool.Def.Name)
    case ev.Err != nil:
        fmt.Printf("error: %v\n", ev.Err)
    case ev.Done:
        fmt.Println("\n[turn complete]")
    }
}
fmt.Print(response.String())

Restarting the conversation

To start a fresh conversation without recreating the runtime:

if err := chat.Restart(); err != nil {
    return err
}

Event types

Field When set
Text Assistant text delta; accumulate into a string for the full reply.
Tool A tool call started, needs confirmation, or finished.
Tool.NeedsConfirmation Runtime is blocked until Confirm is called.
Tool.Finished Tool call completed; Tool.IsError is true if it errored.
Err A user-facing runtime error; no further content events follow.
Done Clean end of turn; no more events.
RuntimeEvent The original runtime.Event for callers that need the full stream.

For advanced use (custom elicitation, raw event inspection), call chat.Runtime() to access the underlying runtime.Runtime directly.

Optional Provider Build Tags

By default docker-agent includes all four cloud providers (OpenAI, Anthropic, Google, Amazon Bedrock). When embedding docker-agent in your own binary you can compile out unneeded providers — together with their transitive SDK dependencies — to reduce binary size.

Each provider is gated by a negative build tag prefixed docker_agent_ to avoid collisions with your own project’s tags:

Build tag Provider dropped Major dependency removed
docker_agent_no_openai OpenAI github.com/openai/openai-go
docker_agent_no_anthropic Anthropic github.com/anthropics/anthropic-sdk-go (partial — see note)
docker_agent_no_google Google / Vertex AI google.golang.org/genai, Vertex auth stack, and indirectly the Anthropic and OpenAI SDKs via Vertex Model Garden
docker_agent_no_bedrock Amazon Bedrock github.com/aws/aws-sdk-go-v2 stack (the largest provider dependency tree)

To build without Bedrock and OpenAI:

go build -tags 'docker_agent_no_bedrock docker_agent_no_openai' ./...

Requesting a model whose provider was compiled out fails at construction time with a clear "not compiled into this build" error. The dmr (Docker Model Runner) provider and the rule-based router are always compiled in.

Anthropic + Google dependency

The Google provider's Vertex Model Garden support also imports the Anthropic SDK, so the Anthropic dependency is only fully removed when both docker_agent_no_anthropic and docker_agent_no_google are set.

RAG Toolset (cgo-free builds)

The RAG toolset (type: rag) uses a tree-sitter code parser that requires cgo. When building without cgo — or when you want to drop the cgo dependency entirely — do not import the pkg/rag package in your binary.

By default the RAG toolset is opt-in: it is only linked when you blank-import its package:

import (
    _ "github.com/docker/docker-agent/pkg/tools/builtin/rag" // register RAG toolset
)

Without this import, a config that declares type: rag fails with a “toolset type not registered” error at startup. If your application does not use RAG, simply omit the blank import; the rest of docker-agent works without cgo.

Basic Example

Create a simple agent and run it:

package main

import (
    "context"
    "fmt"
    "log"
    "os/signal"
    "syscall"

    "github.com/docker/docker-agent/pkg/agent"
    "github.com/docker/docker-agent/pkg/config/latest"
    "github.com/docker/docker-agent/pkg/environment"
    "github.com/docker/docker-agent/pkg/model/provider/openai"
    "github.com/docker/docker-agent/pkg/runtime"
    "github.com/docker/docker-agent/pkg/session"
    "github.com/docker/docker-agent/pkg/team"
)

func main() {
    ctx, cancel := signal.NotifyContext(context.Background(),
        syscall.SIGINT, syscall.SIGTERM)
    defer cancel()

    if err := run(ctx); err != nil {
        log.Fatal(err)
    }
}

func run(ctx context.Context) error {
    // Create model provider
    llm, err := openai.NewClient(
        ctx,
        &latest.ModelConfig{
            Provider: "openai",
            Model:    "gpt-4o",
        },
        environment.NewDefaultProvider(),
    )
    if err != nil {
        return err
    }

    // Create agent
    assistant := agent.New(
        "root",
        "You are a helpful assistant.",
        agent.WithModel(llm),
        agent.WithDescription("A helpful assistant"),
    )

    // Create team and runtime
    t := team.New(team.WithAgents(assistant))
    rt, err := runtime.New(t)
    if err != nil {
        return err
    }

    // Run with a user message
    sess := session.New(
        session.WithUserMessage("What is 2 + 2?"),
    )

    messages, err := rt.Run(ctx, sess)
    if err != nil {
        return err
    }

    // Print the response
    fmt.Println(messages[len(messages)-1].Message.Content)
    return nil
}

Custom Tools

Define custom tools for your agent:

package main

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

    "github.com/docker/docker-agent/pkg/tools"
)

// Define the tool's input schema
type AddNumbersArgs struct {
    A int `json:"a"`
    B int `json:"b"`
}

// Implement the tool handler
func addNumbers(_ context.Context, toolCall tools.ToolCall) (*tools.ToolCallResult, error) {
    var args AddNumbersArgs
    if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil {
        return nil, err
    }

    result := args.A + args.B
    return tools.ResultSuccess(fmt.Sprintf("%d", result)), nil
}

func main() {
    // Create the tool definition
    addTool := tools.Tool{
        Name:        "add",
        Category:    "math",
        Description: "Add two numbers together",
        Parameters:  tools.MustSchemaFor[AddNumbersArgs](),
        Handler:     addNumbers,
    }

    // Use with an agent
    calculator := agent.New(
        "root",
        "You are a calculator. Use the add tool for arithmetic.",
        agent.WithModel(llm),
        agent.WithTools(addTool),
    )
    // ...
}

Streaming Responses

Process events as they happen:

func runStreaming(ctx context.Context, rt runtime.Runtime, sess *session.Session) error {
    events := rt.RunStream(ctx, sess)

    for event := range events {
        switch e := event.(type) {
        case *runtime.StreamStartedEvent:
            fmt.Println("Stream started")

        case *runtime.AgentChoiceEvent:
            // Print response chunks as they arrive
            fmt.Print(e.Content)

        case *runtime.ToolCallEvent:
            fmt.Printf("\n[Tool call: %s]\n", e.ToolCall.Function.Name)

        case *runtime.ToolCallConfirmationEvent:
            // Auto-approve tool calls
            rt.Resume(ctx, runtime.ResumeRequest{
                Type: runtime.ResumeTypeApproveSession,
            })

        case *runtime.ToolCallResponseEvent:
            fmt.Printf("[Tool response: %s]\n", e.Response)

        case *runtime.StreamStoppedEvent:
            fmt.Println("\nStream stopped")

        case *runtime.ErrorEvent:
            return fmt.Errorf("error: %s", e.Error)
        }
    }

    return nil
}

Multi-Agent Teams

Create agents that delegate to sub-agents:

package main

import (
    "github.com/docker/docker-agent/pkg/agent"
    "github.com/docker/docker-agent/pkg/team"
    "github.com/docker/docker-agent/pkg/tools/builtin"
)

func createTeam(llm provider.Provider) *team.Team {
    // Create a child agent
    researcher := agent.New(
        "researcher",
        "You research topics thoroughly.",
        agent.WithModel(llm),
        agent.WithDescription("Research specialist"),
    )

    // Create root agent with sub-agents
    coordinator := agent.New(
        "root",
        "You coordinate research tasks.",
        agent.WithModel(llm),
        agent.WithDescription("Team coordinator"),
        agent.WithSubAgents(researcher),
        agent.WithToolSets(builtin.NewTransferTaskTool()),
    )

    return team.New(team.WithAgents(coordinator, researcher))
}

Built-in Tools

Use docker-agent’s built-in tools:

import (
    "github.com/docker/docker-agent/pkg/config"
    "github.com/docker/docker-agent/pkg/tools/builtin"
)

func createAgentWithBuiltinTools(llm provider.Provider) *agent.Agent {
    // Runtime config for tools that need it
    rtConfig := &config.RuntimeConfig{
        Config: config.Config{
            WorkingDir: "/path/to/workdir",
        },
    }

    return agent.New(
        "root",
        "You are a developer assistant.",
        agent.WithModel(llm),
        agent.WithToolSets(
            // Shell tool for running commands
            builtin.NewShellTool(os.Environ(), rtConfig),
            // Filesystem tools
            builtin.NewFilesystemTool(rtConfig.Config.WorkingDir),
            // Think tool for reasoning
            builtin.NewThinkTool(),
            // Todo tool for task tracking
            builtin.NewTodoTool(),
        ),
    )
}

HTTP Middleware / Transport Wrappers

Use options.WithHTTPTransportWrapper to inject HTTP middleware into the transport chain of all provider clients built by docker-agent. This is useful for request tracing, injecting custom headers, collecting metrics, or any other cross-cutting concern at the HTTP layer.

import (
    "net/http"

    "github.com/docker/docker-agent/pkg/model/provider/options"
)

type headerTransport struct {
    base http.RoundTripper
}

func (t *headerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    req = req.Clone(req.Context())
    req.Header.Set("X-Request-Source", "my-app")
    return t.base.RoundTrip(req)
}

// Example: add a custom header to every outbound LLM request
wrapper := options.WithHTTPTransportWrapper(
    func(base http.RoundTripper) http.RoundTripper {
        return &headerTransport{base: base}
    },
)

client, err := openai.NewClient(ctx, &latest.ModelConfig{
    Provider: "openai",
    Model:    "gpt-4o",
}, env, wrapper)

The wrapper receives the already-instrumented transport (OpenTelemetry, SSE decompression, Desktop proxy support) as its base argument, so wrapping it preserves all built-in behaviour.

Supported providers: Anthropic, OpenAI, Gemini (GeminiAPI backend), Bedrock. Works in both direct and gateway/proxy mode.

Vertex AI not supported

Vertex AI uses an ADC-managed HTTP client that docker-agent cannot intercept. When a transport wrapper is set, docker-agent falls back to the GeminiAPI backend instead of Vertex AI — a debug message is logged.

In gateway mode the wrapper is called on every LLM request because gateway clients are rebuilt each call for short-lived auth tokens. In direct mode it is called once at client construction. Rate-limit responses (HTTP 429) are classified as non-retryable by the runtime and cause the model chain to skip to the next fallback, so wrappers that track per-request outcomes will observe these as failures rather than retried calls.

Returning nil from your wrapper function is not allowed; docker-agent logs a warning and keeps the original transport instead.

Using Different Providers

import (
    "github.com/docker/docker-agent/pkg/model/provider/anthropic"
    "github.com/docker/docker-agent/pkg/model/provider/gemini"
    "github.com/docker/docker-agent/pkg/model/provider/openai"
)

// OpenAI
openaiClient, _ := openai.NewClient(ctx, &latest.ModelConfig{
    Provider: "openai",
    Model:    "gpt-4o",
}, env)

// Anthropic
anthropicClient, _ := anthropic.NewClient(ctx, &latest.ModelConfig{
    Provider: "anthropic",
    Model:    "claude-sonnet-4-5",
}, env)

// Google Gemini
geminiClient, _ := gemini.NewClient(ctx, &latest.ModelConfig{
    Provider: "google",
    Model:    "gemini-3.5-flash",
}, env)

Session Options

import "github.com/docker/docker-agent/pkg/session"

sess := session.New(
    // Set a title for the session
    session.WithTitle("Code Review Task"),

    // Add user message
    session.WithUserMessage("Review this code for bugs"),

    // Limit iterations
    session.WithMaxIterations(20),
)

Error Handling

messages, err := rt.Run(ctx, sess)
if err != nil {
    if errors.Is(err, context.Canceled) {
        // User cancelled
        log.Println("Operation cancelled")
        return nil
    }
    if errors.Is(err, context.DeadlineExceeded) {
        // Timeout
        log.Println("Operation timed out")
        return nil
    }
    // Other error
    return fmt.Errorf("runtime error: %w", err)
}

// Check for errors in the event stream
for event := range rt.RunStream(ctx, sess) {
    if errEvent, ok := event.(*runtime.ErrorEvent); ok {
        return fmt.Errorf("stream error: %s", errEvent.Error)
    }
}

Complete Example

See the examples/golibrary directory for complete working examples: