Skip to content
Conduit

mcp server

go-adk-acpagent

ACP-backed Google ADK agent adapter for Go

normahq/go-adk-acpagentcommunitylistedunreviewed

Set up

Runs on your computer. Pick your agent and operating system.

This project does not publish an install method we can turn into a config. Follow the README instead.

README

synced Oct 26, 2026

go-adk-acpagent

test lint security release Go Reference License Version

Run ACP-compatible coding agents as Google ADK agent.Agent implementations.

go-adk-acpagent/v2 starts a coding-agent subprocess, talks to it with Agent Client Protocol (ACP) over stdio, binds ACP sessions to ADK sessions, and emits ADK events for streamed text, thoughts, tool calls, usage, plan updates, and provider errors.

Use it when your application is built on Google ADK v2 but the coding agent you want to run is exposed as an ACP command.

What You Get

CapabilityBehavior
ADK agentImplements the ADK agent.Agent interface.
ACP lifecycleStarts one ACP subprocess per agent instance and closes it on shutdown.
Session bindingCreates, stores, reuses, and resumes ACP sessions through ADK session state.
Event mappingConverts ACP updates into ADK events and state deltas.
Structured promptsPreserves ordered ADK content and negotiates optional image/audio/embedded-resource blocks from ACP initialize capabilities; file data falls back to baseline resource links.
PermissionsMaps protocol callbacks into ADK-facing PermissionRequest values handled by PermissionHandler.
Session configApplies ACP session-bound values such as model, mode, or thought level.
MCP forwardingSends configured MCP servers to ACP session creation and resume calls.
DiagnosticsUses *slog.Logger for adapter logs and a separate writer for provider stderr.

Try It With OpenCode

go get github.com/normahq/go-adk-acpagent/v2
package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"log/slog"
	"os"

	acpagent "github.com/normahq/go-adk-acpagent/v2"
	"google.golang.org/adk/v2/agent"
	"google.golang.org/adk/v2/runner"
	"google.golang.org/adk/v2/session"
	"google.golang.org/genai"
)

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

func run() error {
	ctx := context.Background()
	logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
		Level: slog.LevelInfo,
	}))

	// 1. Create the ACP-backed ADK agent.
	agentRuntime, err := acpagent.NewWithContext(ctx, acpagent.Config{
		Command:    []string{"opencode", "acp"},
		WorkingDir: "/workspace",
		Logger:     logger,
		Stderr:     io.Discard,
	})
	if err != nil {
		return fmt.Errorf("create ACP agent: %w", err)
	}
	defer func() {
		if err := agentRuntime.Close(); err != nil {
			log.Printf("close ACP agent: %v", err)
		}
	}()

	// 2. Wire the agent into an ADK runner.
	sessionService := session.InMemoryService()
	r, err := runner.New(runner.Config{
		AppName:        "opencode-app",
		Agent:          agentRuntime,
		SessionService: sessionService,
	})
	if err != nil {
		return fmt.Errorf("create runner: %w", err)
	}

	// 3. Create an ADK session.
	sess, err := sessionService.Create(ctx, &session.CreateRequest{
		AppName: "opencode-app",
		UserID:  "user-1",
	})
	if err != nil {
		return fmt.Errorf("create session: %w", err)
	}

	// 4. Run a turn and stream events.
	prompt := genai.NewContentFromText("Explain this codebase", genai.RoleUser)
	for ev, err := range r.Run(ctx, "user-1", sess.Session.ID(), prompt, agent.RunConfig{}) {
		if err != nil {
			return fmt.Errorf("run turn: %w", err)
		}
		if ev.Content != nil {
			for _, part := range ev.Content.Parts {
				if part.Text != "" && !part.Thought {
					fmt.Print(part.Text)
				}
			}
		}
	}
	fmt.Println()
	return nil
}

Provider Commands

ProviderCommand
OpenCode[]string{"opencode", "acp"}
Codex[]string{"npx", "-y", "@normahq/codex-acp-bridge@latest"}
Claude Code[]string{"npx", "-y", "@zed-industries/claude-code-acp@latest"}
Pi[]string{"npx", "-y", "pi-acp"}
Generic ACPAny executable that speaks ACP on stdin/stdout.

Runnable examples are available for OpenCode, Codex, Claude Code, and Pi.

Configuration Cheatsheet

FieldPurpose
CommandACP subprocess argv.
LazyInitializationDefer process launch and initialize until invocation.
InitializeTimeoutBound gate wait, process launch, and initialize independently. Zero inherits SetupTimeout; negative is invalid.
SetupTimeoutBound session attachment; default initialize budget when InitializeTimeout is zero.
WorkingDirProcess directory and default ACP session cwd.
LoggerAdapter diagnostics through *slog.Logger.
StderrACP subprocess stderr forwarding.
PermissionHandlerApplication decision point for generic agent permission requests.
SessionConfigACP session config values applied through session/set_config_option.
MCPServersMCP servers forwarded to ACP sessions.
Instruction / GlobalInstructionADK-style instructions prepended to prompts.
ReasoningEffortProvider reasoning effort metadata when supported.
OutputKeyADK state key for the final visible model output.

ACP provider error metadata helpers are available from:

import "github.com/normahq/go-adk-acpagent/v2/acperror"

Documentation By Task

TaskStart here
Understand lifecycleConcepts
Understand ACP-to-ADK event mappingEvent mapping
Choose a provider commandProvider recipes
Manage cwd, session metadata, config values, plans, and output stateSession state
Debug startup, JSON-RPC streams, permissions, and provider errorsTroubleshooting

Startup And Attachment Budgets

Set independent budgets when cold initialize needs more time than session attachment:

Config{
    Command:           []string{"opencode", "acp"},
    LazyInitialization: true,
    InitializeTimeout: 60 * time.Second,
    SetupTimeout:      10 * time.Second,
}

InitializeTimeout covers waiting for initialization already in progress, process launch, and the ACP initialize handshake. It applies to eager and lazy initialization. Zero inherits SetupTimeout for compatibility; if both are zero, only caller and lifecycle cancellation/deadlines limit initialization. Negative values are rejected before launch. Earlier caller deadlines and application cancellation still win.

SetupTimeout continues to bound session/new and session/resume attachment and related setup RPCs. Neither duration limits prompts or the lifetime of a healthy process. A successful client survives the initialization deadline and request cancellation, and later invocations reuse it. Lazy mode still performs no process launch until invocation.

Call Close during shutdown. Cleanup closes stdin, allows 200ms for graceful exit, kills the owned immediate child if needed, and waits up to 5s more. An exceptional stalled process wait returns an explicit cleanup-timeout error; the original reaper remains responsible until the wait finishes. Initialization errors retain their cancellation/deadline cause, including when cleanup adds an error. Command wrapper descendants remain outside the immediate-child ownership contract.

Production Notes

  • Call Close during shutdown so the ACP subprocess exits cleanly.
  • Pass the application lifecycle context to NewWithContext. The legacy Config.Context field remains available for v2 compatibility.
  • Debug and trace logs omit prompt and update payload content. They retain structural diagnostics such as content-block types, counts, and byte lengths. Provider stderr remains separately controlled by Stderr.
  • Keep ACP protocol messages on stdout and provider logs on stderr.
  • Use SessionConfig for session-bound model, mode, thought-level, or provider-specific choices. Use SelectSessionConfigValue for select options and BooleanSessionConfigValue for boolean options.
  • Treat SessionStateKey as adapter-owned except for documented _meta, config_values, and cwd overrides.

Tests

go test -race ./...
go test ./... -coverprofile=coverage.out
go tool cover -func=coverage.out

CI requires at least 95% total statement coverage.

Provider integration tests are opt-in because they require external ACP commands and credentials:

task test:integration:codex
task test:integration:opencode
↑↓ navigate openesc close