acpagent

package module
v0.1.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 6, 2026 License: MIT Imports: 25 Imported by: 0

README

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 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 but the coding agent you want to run is exposed as an ACP command.

What You Get

Capability Behavior
ADK agent Implements the ADK agent.Agent interface.
ACP lifecycle Starts one ACP subprocess per agent instance and closes it on shutdown.
Session binding Creates, stores, reuses, and resumes ACP sessions through ADK session state.
Event mapping Converts ACP updates into ADK events and state deltas.
Permissions Routes ACP permission requests through PermissionHandler.
Session config Applies ACP session-bound values such as model, mode, or thought level.
MCP forwarding Sends configured MCP servers to ACP session creation and resume calls.
Diagnostics Uses *slog.Logger for adapter logs and a separate writer for provider stderr.

Try It With OpenCode

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

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

	acpagent "github.com/normahq/go-adk-acpagent"
)

func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
		Level: slog.LevelInfo,
	}))

	agentRuntime, err := acpagent.New(acpagent.Config{
		Context:    context.Background(),
		Command:    []string{"opencode", "acp"},
		WorkingDir: "/workspace",
		Logger:     logger,
		Stderr:     io.Discard,
	})
	if err != nil {
		log.Fatal(err)
	}
	defer func() {
		if err := agentRuntime.Close(); err != nil {
			log.Printf("close ACP agent: %v", err)
		}
	}()

	// Pass agentRuntime to an ADK runner.
}

Provider Commands

Provider Command
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 ACP Any executable that speaks ACP on stdin/stdout.

Runnable examples are available for OpenCode and Codex.

Configuration Cheatsheet

Field Purpose
Command ACP subprocess argv.
WorkingDir Process directory and default ACP session cwd.
Logger Adapter diagnostics through *slog.Logger.
Stderr ACP subprocess stderr forwarding.
PermissionHandler Application decision point for ACP permission requests.
SessionConfig ACP session config values applied through session/set_config_option.
MCPServers MCP servers forwarded to ACP sessions.
Instruction / GlobalInstruction ADK-style instructions prepended to prompts.
ReasoningEffort Provider reasoning effort metadata when supported.
OutputKey ADK state key for the final visible model output.

ACP provider error metadata helpers are available from:

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

Documentation By Task

Task Start here
Understand lifecycle Concepts
Understand ACP-to-ADK event mapping Event mapping
Choose a provider command Provider recipes
Manage cwd, session metadata, config values, plans, and output state Session state
Debug startup, JSON-RPC streams, permissions, and provider errors Troubleshooting

Production Notes

  • Call Close during shutdown so the ACP subprocess exits cleanly.
  • Keep ACP protocol messages on stdout and provider logs on stderr.
  • Use SessionConfig for session-bound model, mode, thought-level, or provider-specific choices.
  • Treat SessionStateKey as adapter-owned except for documented _meta, config_values, and cwd overrides.

Tests

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

CI requires at least 95% total statement coverage for both modules.

Documentation

Overview

Package acpagent provides a Google ADK agent implementation backed by an Agent Client Protocol (ACP) coding agent.

The package starts an ACP-compatible coding-agent subprocess, talks to it as an ACP client over stdio, and maps each ADK session to a remote ACP session. By default, ACP session creation uses Config.WorkingDir as the ACP session cwd.

Per-session overrides via ADK state

Callers can override ACP session creation per ADK session by setting CWDStateKey before the first invocation in that ADK session:

map[string]any{
  CWDStateKey: "/absolute/path", // optional
}

ACP-specific session/new metadata may be provided under SessionStateKey:

map[string]any{
  acpagent.SessionStateKey: map[string]any{
    "meta": map[string]any{ // optional; forwarded to ACP session/new _meta
      "codex": map[string]any{"approvalMode": "manual"},
    },
  },
}

Behavior:

  • If `state[CWDStateKey]` is set, it overrides Config.WorkingDir for ACP session creation.
  • The adapter persists the canonical ACP session ID under `state[SessionStateKey].session_id` and uses that value for ACP session/resume when the ACP agent advertises resume capability. ACP session configuration values are persisted under `state[SessionStateKey].config_values`.
  • As soon as the adapter binds a remote ACP session, it stores the canonical ACP session ID in the live ADK session state under `state[SessionStateKey]`.
  • If `state[SessionStateKey].session_id` is absent, the package creates a new ACP session.
  • For newly created ACP sessions, resolved startup instructions are passed through two channels: `session/new._meta.codex` receives `baseInstructions` and `developerInstructions` when those keys are not already set, and the first real user `session/prompt` is prepended with the combined instructions. The adapter does not send a separate instruction-only prompt.
  • The adapter does not use ACP `session/load`; ACP v1 load replays prior history, and this adapter does not yet map that replay into ADK-visible history.
  • If `state[SessionStateKey].meta` is set, it is passed through to ACP session/new._meta and session/resume._meta.
  • If `state[SessionStateKey].config_values` is set, it overrides matching Config.SessionConfig defaults for that ADK session.
  • Overrides are read when the ACP session is first created for the ADK session. Subsequent changes do not rebind that existing ACP session.

Invalid override values (for example, non-string `state[CWDStateKey]`, non-object `state[SessionStateKey]`, non-object `state[SessionStateKey].meta`, non-string `state[SessionStateKey].session_id`, invalid `state[SessionStateKey].config_values`, or a cwd that is not a valid existing directory) cause invocation failure before ACP session creation.

ACP plan updates

ACP `session/update.plan` notifications are projected into ADK event state under PlanStateKey. Each event carries the full replacement snapshot at `event.Actions.StateDelta[PlanStateKey]` with an `entries` field containing the current ACP plan entries. Plan updates do not appear as content parts.

Index

Examples

Constants

View Source
const (
	// SessionStateKey is the reserved ADK session-state key for ACP-specific
	// per-session settings.
	//
	// The value at this key must be an object with optional fields:
	//   - "meta" (object): forwarded to ACP session/new._meta
	//   - "session_id" (string): canonical ACP session id returned by the ACP
	//     agent and used for ACP session/resume
	//
	// This state is the source of truth for ACP session identity. If the ADK
	// session is deleted, this state is deleted with it and no in-memory ACP
	// session binding is reused.
	SessionStateKey = "acp_session"
	// PlanStateKey is the ADK session-state key used for ACP plan snapshots.
	//
	// Each ACP session/update.plan notification is projected into
	// event.Actions.StateDelta[PlanStateKey] as the authoritative full plan
	// replacement snapshot.
	PlanStateKey = "acp_plan"
	// CWDStateKey is the ADK session-state key used to override the ACP
	// session working directory for a single ADK session.
	CWDStateKey = "cwd"
)

Variables

View Source
var (
	// ErrPromptAlreadyActive is returned when a prompt is already in progress for
	// the same ACP session ID.
	ErrPromptAlreadyActive = errors.New("acp prompt already active")
)

Functions

This section is empty.

Types

type Agent

type Agent struct {
	adkagent.Agent
	// contains filtered or unexported fields
}

Agent is an ADK agent implementation backed by an Agent Client Protocol (ACP) coding-agent subprocess. It manages the subprocess lifecycle and maps ACP sessions to ADK sessions.

func New

func New(cfg Config) (*Agent, error)

New creates an ADK agent backed by an ACP client process.

It starts the ACP process, performs ACP initialization, and creates ACP sessions lazily per ADK session.

Per ADK session, callers may provide state overrides:

  • CWDStateKey (string): override ACP session/new cwd
  • SessionStateKey.meta (object): forwarded to ACP session/new._meta and session/resume._meta

ACP session IDs are agent-owned. The adapter persists the canonical ACP session id under SessionStateKey.session_id and uses that value for session/resume when the ACP agent advertises resume capability.

ACP session/load is not used by this adapter. Per ACP v1, load replays prior history; until that replay is intentionally mapped into ADK-visible history, the adapter restores only through session/resume or creates a new ACP session.

If no override is provided, Config.WorkingDir is used as ACP session cwd. The first ACP session created for an ADK session is reused for subsequent invocations in that same ADK session. For newly created ACP sessions, resolved instructions are sent in session/new._meta.codex and prepended to the first real user prompt. The adapter does not send a separate instruction-only prompt.

The caller is responsible for calling Close() to shut down the subprocess.

func (*Agent) Close

func (a *Agent) Close() error

Close shuts down the underlying ACP client process.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client manages a single Agent Client Protocol (ACP) subprocess and its communication over standard input/output. It implements the acp.Client interface to handle protocol-level callbacks and manages multiple concurrent prompt sessions.

func NewClient

func NewClient(ctx context.Context, cfg ClientConfig) (*Client, error)

NewClient starts an ACP subprocess and returns a protocol client over stdio.

Example
previous := os.Getenv("GO_WANT_ACP_HELPER")
_ = os.Setenv("GO_WANT_ACP_HELPER", "1")
defer func() {
	if previous == "" {
		_ = os.Unsetenv("GO_WANT_ACP_HELPER")
		return
	}
	_ = os.Setenv("GO_WANT_ACP_HELPER", previous)
}()

workingDir, err := os.MkdirTemp("", "runtime-acpagent-example-*")
if err != nil {
	fmt.Println(err)
	return
}
defer func() { _ = os.RemoveAll(workingDir) }()

client, err := NewClient(context.Background(), ClientConfig{
	Command: []string{os.Args[0], "-test.run=TestACPHelperProcess", "--"},
})
if err != nil {
	fmt.Println(err)
	return
}
defer func() { _ = client.Close() }()

if _, err := client.Initialize(context.Background()); err != nil {
	fmt.Println(err)
	return
}
sessionResp, err := client.NewSession(context.Background(), workingDir, nil)
if err != nil {
	fmt.Println(err)
	return
}

fmt.Println(string(sessionResp.SessionId) != "")
Output:
true

func (*Client) Authenticate

func (c *Client) Authenticate(ctx context.Context, methodID string) error

Authenticate requests ACP authentication for a specific method.

func (*Client) Close

func (c *Client) Close() error

Close stops the ACP subprocess and waits for cleanup to finish.

func (*Client) CreateSession

func (c *Client) CreateSession(ctx context.Context, cwd string, configValues []SessionConfigValue, mcpServers []acp.McpServer) (acp.NewSessionResponse, error)

CreateSession creates a new ACP session and applies configured session config values when requested.

func (*Client) CreateSessionWithMeta

func (c *Client) CreateSessionWithMeta(
	ctx context.Context,
	cwd string,
	configValues []SessionConfigValue,
	mcpServers []acp.McpServer,
	meta map[string]any,
) (acp.NewSessionResponse, error)

CreateSessionWithMeta creates a new ACP session with optional session/new _meta and applies configured session config values when requested.

This helper is equivalent to:

  1. NewSessionWithMeta(ctx, cwd, mcpServers, meta)
  2. optionally SetSessionConfigOption(...) for ACP session config values

The ACP protocol requires cwd to be an absolute path.

func (*Client) CreateTerminal

CreateTerminal reports unsupported terminal creation for this ACP client.

func (*Client) Initialize

func (c *Client) Initialize(ctx context.Context) (acp.InitializeResponse, error)

Initialize performs ACP protocol initialization and validates protocol compatibility.

func (*Client) KillTerminal

KillTerminal reports unsupported terminal command control for this ACP client.

func (*Client) LoadSession

func (c *Client) LoadSession(ctx context.Context, sessionID, cwd string, mcpServers []acp.McpServer) (acp.LoadSessionResponse, error)

LoadSession loads an existing ACP session in the provided working directory.

The ACP protocol requires cwd to be an absolute path.

func (*Client) LoadSessionWithMeta

func (c *Client) LoadSessionWithMeta(
	ctx context.Context,
	sessionID,
	cwd string,
	mcpServers []acp.McpServer,
	meta map[string]any,
) (acp.LoadSessionResponse, error)

LoadSessionWithMeta loads an existing ACP session in the provided working directory and sends optional _meta extensions with the load request.

The ACP protocol requires cwd to be an absolute path.

func (*Client) NewSession

func (c *Client) NewSession(ctx context.Context, cwd string, mcpServers []acp.McpServer) (acp.NewSessionResponse, error)

NewSession creates a new ACP session in the provided working directory.

The ACP protocol requires cwd to be an absolute path.

func (*Client) NewSessionWithMeta

func (c *Client) NewSessionWithMeta(
	ctx context.Context,
	cwd string,
	mcpServers []acp.McpServer,
	meta map[string]any,
) (acp.NewSessionResponse, error)

NewSessionWithMeta creates a new ACP session in the provided working directory and sends optional _meta extensions with the session request.

The ACP protocol requires cwd to be an absolute path.

func (*Client) Prompt

func (c *Client) Prompt(ctx context.Context, sessionID, prompt string) (<-chan ExtendedSessionNotification, <-chan PromptResult, error)

Prompt sends a prompt to an ACP session and streams session updates.

func (*Client) PromptWithContent

func (c *Client) PromptWithContent(ctx context.Context, sessionID string, prompt []acp.ContentBlock) (<-chan ExtendedSessionNotification, <-chan PromptResult, error)

PromptWithContent sends a prompt composed of ACP content blocks and streams session updates.

func (*Client) ReadTextFile

ReadTextFile reports unsupported file read for this ACP client.

func (*Client) ReleaseTerminal

ReleaseTerminal reports unsupported terminal release for this ACP client.

func (*Client) RequestPermission

RequestPermission handles ACP permission callbacks.

func (*Client) ResumeSession

func (c *Client) ResumeSession(ctx context.Context, sessionID, cwd string, mcpServers []acp.McpServer) (acp.ResumeSessionResponse, error)

ResumeSession resumes an existing ACP session in the provided working directory.

The ACP protocol requires cwd to be an absolute path.

func (*Client) ResumeSessionWithMeta

func (c *Client) ResumeSessionWithMeta(
	ctx context.Context,
	sessionID,
	cwd string,
	mcpServers []acp.McpServer,
	meta map[string]any,
) (acp.ResumeSessionResponse, error)

ResumeSessionWithMeta resumes an existing ACP session in the provided working directory and sends optional _meta extensions with the resume request.

The ACP protocol requires cwd to be an absolute path.

func (*Client) SessionUpdate

func (c *Client) SessionUpdate(ctx context.Context, params acp.SessionNotification) error

SessionUpdate is part of the ACP client callback contract.

func (*Client) SetSessionConfigOption added in v0.1.4

SetSessionConfigOption sets an ACP session configuration option.

func (*Client) SetSessionMode

func (c *Client) SetSessionMode(ctx context.Context, sessionID, mode string) error

SetSessionMode selects the active mode for an ACP session.

func (*Client) SupportsSessionLoad

func (c *Client) SupportsSessionLoad() bool

SupportsSessionLoad reports whether the initialized ACP server advertises session/load support.

func (*Client) SupportsSessionResume

func (c *Client) SupportsSessionResume() bool

SupportsSessionResume reports whether the initialized ACP server advertises session/resume support.

func (*Client) TerminalOutput

TerminalOutput reports unsupported terminal output streaming for this ACP client.

func (*Client) WaitForTerminalExit

WaitForTerminalExit reports unsupported terminal wait operations for this ACP client.

func (*Client) WriteTextFile

WriteTextFile reports unsupported file write for this ACP client.

type ClientConfig

type ClientConfig struct {
	// Command is the argv array used to start the ACP subprocess.
	Command []string
	// WorkingDir is the directory where the ACP subprocess is executed
	// (cmd.Dir). It is independent from ACP session/new.cwd, which is provided
	// per session creation call.
	WorkingDir string
	// ClientName is the name reported to the ACP server. Defaults to "runtime-acpagent".
	ClientName string
	// ClientVersion is the version reported to the ACP server. Defaults to "dev".
	ClientVersion string
	// Stderr is an optional writer for the ACP subprocess's standard error.
	Stderr io.Writer
	// PermissionHandler decides how to respond to ACP permission requests.
	PermissionHandler PermissionHandler
	// Logger is the slog logger to use for this client.
	Logger *slog.Logger
}

ClientConfig configures an ACP subprocess client.

type Config

type Config struct {
	// Context is the base context for the agent's lifecycle.
	Context context.Context
	// Name is the display name of the agent. Defaults to "ACPAgent".
	Name string
	// Description describes the agent's purpose.
	Description string
	// BeforeAgentCallbacks are standard ADK lifecycle callbacks invoked before
	// the ACP-backed run starts.
	BeforeAgentCallbacks []adkagent.BeforeAgentCallback
	// AfterAgentCallbacks are standard ADK lifecycle callbacks invoked after
	// the ACP-backed run completes.
	AfterAgentCallbacks []adkagent.AfterAgentCallback
	// SessionConfig contains ACP session configuration values applied after
	// session/new or session/resume.
	SessionConfig []SessionConfigValue
	// Instruction is the optional instruction applied to each invocation.
	Instruction string
	// GlobalInstruction is the optional global instruction applied before
	// Instruction.
	GlobalInstruction string
	// ReasoningEffort selects the provider reasoning effort when supported.
	ReasoningEffort string
	// InstructionProvider dynamically provides [Config.Instruction] content.
	// When set, this takes precedence over [Config.Instruction].
	InstructionProvider InstructionProvider
	// GlobalInstructionProvider dynamically provides
	// [Config.GlobalInstruction] content. When set, this takes precedence over
	// [Config.GlobalInstruction].
	GlobalInstructionProvider InstructionProvider
	// SystemInstructions is deprecated and kept for backward compatibility.
	// Use Instruction instead.
	SystemInstructions string
	// ClientName is the name reported to the ACP server during initialization.
	ClientName string
	// ClientVersion is the version reported to the ACP server during initialization.
	ClientVersion string
	// Command is the argv array used to start the ACP subprocess.
	Command []string
	// WorkingDir is the default directory for ACP execution:
	//   - the ACP subprocess is started with this directory as cmd.Dir.
	//   - ACP session/new uses this as cwd unless overridden per ADK session
	//     via session state key [CWDStateKey].
	//
	// When session state override is present, the override takes precedence for
	// ACP session cwd selection.
	WorkingDir string
	// Stderr is an optional writer for the ACP subprocess's standard error.
	Stderr io.Writer
	// PermissionHandler decides how to respond to ACP permission requests.
	PermissionHandler PermissionHandler
	// Logger is the slog logger to use for this agent.
	Logger *slog.Logger
	// MCPServers is the map of MCP server configurations.
	MCPServers map[string]MCPServerConfig
	// SessionService is ignored. ACP session bindings are recorded through the
	// ADK session state exposed by the invocation context.
	SessionService session.Service
	// OutputKey stores the final visible model output in session state delta for this invocation.
	// When set, the final non-partial turn-complete event includes
	// event.Actions.StateDelta[OutputKey] = final visible output text.
	OutputKey string
}

Config configures an ACP-backed ADK agent.

type ExtendedSessionNotification

type ExtendedSessionNotification struct {
	acp.SessionNotification
	Raw    json.RawMessage
	Method string
}

ExtendedSessionNotification wraps an ACP notification with its raw JSON representation to allow access to fields not yet supported by the SDK.

type InstructionProvider

type InstructionProvider func(ctx adkagent.ReadonlyContext) (string, error)

InstructionProvider allows ACP instructions to be created dynamically using invocation context, mirroring llmagent semantics.

type MCPServerConfig

type MCPServerConfig struct {
	// Type selects the MCP transport implementation.
	Type MCPServerType
	// Cmd is the stdio server executable path or argv prefix.
	Cmd []string
	// Args appends additional stdio server arguments after Cmd.
	Args []string
	// Env defines environment variables for stdio server execution.
	Env map[string]string
	// WorkingDir sets the stdio server process working directory.
	WorkingDir string
	// URL is the base endpoint for HTTP and SSE MCP transports.
	URL string
	// Headers provides additional request headers for HTTP and SSE transports.
	Headers map[string]string
}

MCPServerConfig describes how to connect to an MCP server.

type MCPServerType

type MCPServerType string

MCPServerType represents the transport type for an MCP server.

const (
	// MCPServerTypeStdio is the stdio transport type.
	MCPServerTypeStdio MCPServerType = "stdio"
	// MCPServerTypeHTTP is the HTTP transport type.
	MCPServerTypeHTTP MCPServerType = "http"
	// MCPServerTypeSSE is the SSE (Server-Sent Events) transport type.
	MCPServerTypeSSE MCPServerType = "sse"
)

type PermissionHandler

PermissionHandler decides how ACP permission requests should be handled. It returns a response with the selected outcome or an error if the request could not be processed.

type PromptResult

type PromptResult struct {
	Response acp.PromptResponse
	Usage    *acp.Usage
	Raw      json.RawMessage
	Err      error
}

PromptResult contains the terminal Prompt RPC response, usage metadata, or an error.

type SessionConfigValue added in v0.1.4

type SessionConfigValue struct {
	// ID is the ACP session config option ID.
	ID string `json:"id"`
	// Value is the ACP session config value ID.
	Value string `json:"value"`
}

SessionConfigValue is an ACP session configuration value to apply to each bound ACP session.

Directories

Path Synopsis
Package acperror defines ACP provider failure metadata carried through ACP and ADK runtime layers.
Package acperror defines ACP provider failure metadata carried through ACP and ADK runtime layers.
examples
codex command
opencode command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL