hermesacp

package module
v0.0.0-...-153c66b Latest Latest
Warning

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

Go to latest
Published: Sep 20, 2026 License: MIT Imports: 36 Imported by: 0

README

acp-go-hermes

acp-go-hermes exposes the Hermes as an Agent Client Protocol agent. It launches one hermes serve process per ACP session, connects to its authenticated loopback WebSocket, and streams ACP session updates back to the client.

hermes inherits the adapter's environment and keeps sessions in its own home. A session started over ACP can be continued natively:

acp-go-hermes              # host runs a session in /work
cd /work && hermes chat --cli --resume NATIVE_SESSION_ID

New, load, and resume responses and session-list entries expose the current native ID as _meta.hermes.nativeSessionId. Use it for native CLI continuation. ACP requests continue to use the stable ACP sessionId. The store's configuration record saves both IDs with the matching native history.

Install

go install github.com/savid/acp-go-hermes/cmd/acp-go-hermes@latest

Verified against hermes 0.21.3, found on PATH or named with -path.

Run

acp-go-hermes [-path hermes] [-home DIR] [-scratch-dir DIR] [-model provider/id] [-seed-file rel=host]... [-debug]
Flag Meaning
-path hermes executable; a bare name is searched on PATH
-home hermes config root, passed as HERMES_HOME; empty inherits hermes's own resolution
-scratch-dir accepted and ignored; this adapter allocates no ephemeral state
-model default model for new sessions as provider/id
-seed-file <relpath>=<hostpath> written into hermes's config root before launch; repeatable
-debug debug logs to stderr
-version print the adapter version

OpenTelemetry exporters are configured from the standard OTEL_* variables.

Embed

err := hermesacp.Serve(ctx, os.Stdin, os.Stdout,
    hermesacp.WithHome("/srv/hermes"),
    hermesacp.WithSessionStore(store),
)

Options: WithExecutablePath, WithHome, WithScratchDir, WithInputHandoffRoot, WithDefaultModel, WithConfiguredModels, WithEnv, WithSeedFiles, WithSessionStore, WithConcurrencyLimits, WithImageLimits, WithLogger, WithTracerProvider, WithMeterProvider, WithTextMapPropagator, WithAgentName, WithAgentTitle, WithAgentVersion.

Session options

_meta.hermes.options on session/new, session/load, and session/resume, or WithSessionHermesOptions from Go:

Field Meaning
model provider/id for the session
env environment overlay for the session's hermes process
extraPathDirs absolute directories prepended to PATH, in order
effort session reasoning effort: none, minimal, low, medium, high, xhigh, max, or ultra

The session environment reaches Hermes and its direct subprocesses. The native terminal starts a login shell, whose system and user startup files can reorder PATH.

Unknown option fields and nonempty mcpServers are invalid parameters. outputSchema is unsupported. Authentication uses Hermes's native configuration. Permissions are native approvals; an unavailable or cancelled host answer denies that request. Native clarify requests use ACP form elicitation. Sudo, secret, and terminal-buffer requests receive an empty value; desktop, vault, and setup bridges are unsupported.

_meta.hermes.rawEvent.enabled forwards native events on _hermes/rawEvent. Optional lifecycle negotiation enables ordered lifecycle updates.

Config options

session/set_config_option accepts model (provider/id) and effort. The model menu contains Hermes's catalog and any WithConfiguredModels entries.

Image input

Images are accepted as inline data or through WithInputHandoffRoot and passed to Hermes's image attachment API. Put all text, resource links, and text resources before the images; images alone and multiple images are accepted. Forwarded text after the first image fails with {"error":"unsupported","field":"prompt"} before any native image upload or prompt dispatch. User-only text excluded from native input does not affect ordering. An image blob's URI is provenance and is not sent as prompt text. Image output is not advertised.

Session store

WithSessionStore commits the native per-conversation JSON export under the main subpath and the session configuration under config, format hermes-session-json-v1. Native auth files are not part of the snapshot. The default store is in memory; provide a durable store to restore across adapter restarts.

session/load restores and replays history; session/resume restores without replay. A missing conversation is imported through Hermes's native HTTP API. Existing native history must contain the stored history as a prefix; divergent or shorter native histories fail restore. Each completed prompt commits its snapshot before returning. Close preserves Hermes's native state, and delete removes the store entry without deleting the native conversation.

The native binding identifies the durable Hermes conversation. Transient gateway IDs stay internal. Native compression that changes the durable ID poisons the session instead of storing a different conversation under the original ID.

Account usage

_hermes/accountUsage with {"providerId": "<id>"} reads one provider's allowance through the gateways config.yaml routes it to: anthropic, openai-codex, opencode-go, or openrouter. Hermes exposes no provider credentials natively, so a provider no configured gateway reports answers {"available": false, "reason": "not_authenticated"}. Initialize advertises the read under _meta.hermes.accountUsage as {"method": "_hermes/accountUsage", "scope": "agent", "providers": [...]}.

Development

make test
make lint
make audit
make test-integration-smoke   # needs hermes installed, spends no tokens
make test-integration-live    # spends model tokens

Unit tests run the test binary as a scripted fake hermes and need no installed hermes, credentials, or network.

Documentation

Overview

Package hermesacp exposes Hermes as an Agent Client Protocol agent.

Serve starts one authenticated loopback Hermes gateway per ACP session. The harness inherits the adapter environment and keeps its native state in HERMES_HOME, so the conversation can also continue through Hermes's CLI.

WithSessionStore supplies durable per-conversation snapshots. Load restores and replays history; resume restores without replay. The adapter preserves native state when sessions close.

Hosts supply telemetry providers through WithTracerProvider and WithMeterProvider; the package never configures global providers.

Index

Examples

Constants

View Source
const (
	// RawEventMethod is the notification carrying one raw hermes event when a
	// session opted in through _meta.hermes.rawEvent.enabled.
	RawEventMethod = "_hermes/rawEvent"
	// AccountUsageMethod reads a provider's account usage through the gateways
	// hermes's config routes to.
	AccountUsageMethod = "_hermes/accountUsage"
	// SessionStoreFormat identifies the store layout this package writes: native
	// per-conversation JSON exports under main plus the adapter's session
	// record under the config subpath.
	SessionStoreFormat = "hermes-session-json-v1"
)

Variables

This section is empty.

Functions

func Serve

func Serve(ctx context.Context, input io.Reader, output io.Writer, opts ...Option) (returnErr error)

Serve runs an ACP agent over the provided streams. It blocks until the context is cancelled or the peer closes the connection, then closes the agent.

Example (Initialize)

ExampleServe_initialize embeds the agent over a pair of pipes, the same wiring a host uses for stdio, and reads the capabilities the handshake advertises.

package main

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

	hermesacp "github.com/savid/acp-go-hermes"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	clientToAgentReader, clientToAgentWriter := io.Pipe()
	agentToClientReader, agentToClientWriter := io.Pipe()

	done := make(chan error, 1)

	go func() {
		done <- hermesacp.Serve(ctx, clientToAgentReader, agentToClientWriter)
	}()

	_, _ = fmt.Fprintln(clientToAgentWriter,
		`{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1}}`)

	line, _ := bufio.NewReader(agentToClientReader).ReadString('\n')

	cancel()
	_ = clientToAgentWriter.Close()
	<-done

	var response struct {
		Result struct {
			AuthMethods       []any `json:"authMethods"`
			AgentCapabilities struct {
				LoadSession         bool           `json:"loadSession"`
				SessionCapabilities map[string]any `json:"sessionCapabilities"`
			} `json:"agentCapabilities"`
		} `json:"result"`
	}

	_ = json.Unmarshal([]byte(line), &response)

	fmt.Println(len(response.Result.AuthMethods))
	fmt.Println(response.Result.AgentCapabilities.LoadSession)
	fmt.Println(len(response.Result.AgentCapabilities.SessionCapabilities))
}
Output:
0
true
5

func SetModelRequest

func SetModelRequest(sessionID acp.SessionId, model string) acp.SetSessionConfigOptionRequest

SetModelRequest constructs a model selector update as "provider/id".

func ValidateHermesSessionMeta

func ValidateHermesSessionMeta(meta map[string]any) error

ValidateHermesSessionMeta runs the owned-namespace parsing of a session lifecycle request's _meta without an Agent and returns the same refusal.

func WithSessionHermesOptions

func WithSessionHermesOptions(options HermesOptions) wire.SessionRequestOption

WithSessionHermesOptions merges hermes-specific options into _meta.hermes.options of a session lifecycle request.

func WithSessionRawEvents

func WithSessionRawEvents(enabled bool) wire.SessionRequestOption

WithSessionRawEvents toggles raw hermes event emission for the session.

Types

type Agent

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

Agent exposes the hermes coding agent through ACP.

func NewAgent

func NewAgent(opts ...Option) *Agent

NewAgent creates an ACP agent for the hermes coding agent CLI. Construction never fails; a refused option is reported by Initialize and every session-establishing method as hermes_invalid_options.

func (*Agent) Authenticate

func (a *Agent) Authenticate(_ context.Context, params acp.AuthenticateRequest) (acp.AuthenticateResponse, error)

Authenticate exists because the SDK interface requires it. The harness authenticates itself in its own home, outside ACP.

func (*Agent) Cancel

func (a *Agent) Cancel(ctx context.Context, params acp.CancelNotification) (err error)

Cancel interrupts the session's in-flight turn. It is wire-silent on an unknown session or with no turn in flight.

func (*Agent) Close

func (a *Agent) Close() error

Close runs the shutdown ladder for every session and refuses every later request.

func (*Agent) CloseSession

func (a *Agent) CloseSession(ctx context.Context, params acp.CloseSessionRequest) (resp acp.CloseSessionResponse, err error)

CloseSession runs the shutdown ladder for one session.

func (*Agent) HandleExtensionMethod

func (a *Agent) HandleExtensionMethod(ctx context.Context, method string, params json.RawMessage) (any, error)

HandleExtensionMethod serves the account-usage read; every other extension method is method-not-found.

func (*Agent) Initialize

func (a *Agent) Initialize(ctx context.Context, params acp.InitializeRequest) (resp acp.InitializeResponse, err error)

Initialize implements ACP initialize.

func (*Agent) ListSessions

func (a *Agent) ListSessions(ctx context.Context, params acp.ListSessionsRequest) (resp acp.ListSessionsResponse, err error)

ListSessions lists live sessions and stored sessions, newest first.

func (*Agent) LoadSession

func (a *Agent) LoadSession(ctx context.Context, params acp.LoadSessionRequest) (resp acp.LoadSessionResponse, err error)

LoadSession restores a session and replays its history.

func (*Agent) Logout

func (a *Agent) Logout(_ context.Context, params acp.LogoutRequest) (acp.LogoutResponse, error)

Logout exists because the SDK interface requires it.

func (*Agent) NewSession

func (a *Agent) NewSession(ctx context.Context, params acp.NewSessionRequest) (resp acp.NewSessionResponse, err error)

NewSession creates and starts a hermes session.

func (*Agent) Prompt

func (a *Agent) Prompt(ctx context.Context, params acp.PromptRequest) (resp acp.PromptResponse, err error)

Prompt sends one turn to hermes and streams updates until it settles.

func (*Agent) ResumeSession

func (a *Agent) ResumeSession(ctx context.Context, params acp.ResumeSessionRequest) (resp acp.ResumeSessionResponse, err error)

ResumeSession restores a session without replaying its history.

func (*Agent) SetSessionConfigOption

func (a *Agent) SetSessionConfigOption(ctx context.Context, params acp.SetSessionConfigOptionRequest) (resp acp.SetSessionConfigOptionResponse, err error)

SetSessionConfigOption applies one select value.

func (*Agent) SetSessionMode

SetSessionMode exists because the SDK interface requires it. Native modes are config options, never ACP session modes.

func (*Agent) UnstableDeleteSession

func (a *Agent) UnstableDeleteSession(ctx context.Context, params acp.UnstableDeleteSessionRequest) (resp acp.UnstableDeleteSessionResponse, err error)

UnstableDeleteSession tombstones the session first, then closes any live session with the same id. Native state stays in hermes's home.

type ConcurrencyLimits

type ConcurrencyLimits struct {
	MaxActiveSessions        int
	MaxConcurrentClientCalls int
}

ConcurrencyLimits controls per-agent backpressure. Zero fields use defaults.

type HermesOption

type HermesOption func(*HermesOptions)

HermesOption configures HermesOptions values.

func WithHermesEffort

func WithHermesEffort(level string) HermesOption

WithHermesEffort configures the reasoning level passed to hermes.

func WithHermesEnv

func WithHermesEnv(env map[string]string) HermesOption

WithHermesEnv configures the session environment overlay.

func WithHermesExtraPathDirs

func WithHermesExtraPathDirs(dirs ...string) HermesOption

WithHermesExtraPathDirs configures the directories prepended to the session PATH.

func WithHermesModel

func WithHermesModel(model string) HermesOption

WithHermesModel configures the session model as "provider/id".

type HermesOptions

type HermesOptions struct {
	// Model selects the hermes model for this session as "provider/id".
	Model string `json:"model,omitempty"`
	// Env overlays the session's hermes process environment.
	Env map[string]string `json:"env,omitempty"`
	// ExtraPathDirs are absolute directories prepended, in order, to the PATH
	// of this session's hermes process.
	ExtraPathDirs []string `json:"extraPathDirs,omitempty"`
	// Effort is a reasoning-level value passed unchanged to hermes.
	Effort string `json:"effort,omitempty"`
}

HermesOptions is the per-session options struct carried at _meta.hermes.options.

func NewHermesOptions

func NewHermesOptions(opts ...HermesOption) HermesOptions

NewHermesOptions constructs HermesOptions from functional options.

Example
package main

import (
	"fmt"

	hermesacp "github.com/savid/acp-go-hermes"
)

func main() {
	options := hermesacp.NewHermesOptions(
		hermesacp.WithHermesModel("provider/model"),
		hermesacp.WithHermesEffort("high"),
	)

	fmt.Println(options.Model)
	fmt.Println(options.Effort)
}
Output:
provider/model
high

func (HermesOptions) Meta

func (options HermesOptions) Meta() map[string]any

Meta returns exactly {"hermes": {"options": {...}}} with the selected fields.

type ImageLimits

type ImageLimits struct {
	MaxInputBytesPerImage     int64
	MaxInputBytesPerPrompt    int64
	MaxOutputBytesPerImage    int64
	MaxOutputBytesPerToolCall int64
}

ImageLimits bounds decoded image bytes. A zero field disables that policy limit; the frame clamp still applies.

type Option

type Option func(*Options)

Option configures the hermes ACP agent.

func WithAgentName

func WithAgentName(name string) Option

WithAgentName sets the protocol identifier advertised during ACP initialize.

func WithAgentTitle

func WithAgentTitle(title string) Option

WithAgentTitle sets the human-readable agent name advertised during ACP initialize.

func WithAgentVersion

func WithAgentVersion(version string) Option

WithAgentVersion sets the agent version advertised during ACP initialize.

func WithConcurrencyLimits

func WithConcurrencyLimits(limits ConcurrencyLimits) Option

WithConcurrencyLimits sets process-local backpressure limits.

func WithConfiguredModels

func WithConfiguredModels(ids []string) Option

WithConfiguredModels names the models the host lists explicitly.

func WithDefaultModel

func WithDefaultModel(model string) Option

WithDefaultModel selects the model for new sessions as "provider/id".

func WithEnv

func WithEnv(env map[string]string) Option

WithEnv sets the static agent-scoped environment overlay applied to every hermes process after the inherited environment and before the session env.

func WithExecutablePath

func WithExecutablePath(path string) Option

WithExecutablePath selects the hermes executable.

func WithHome

func WithHome(path string) Option

WithHome sets hermes's native config root, passed to every session as HERMES_HOME.

func WithImageLimits

func WithImageLimits(limits ImageLimits) Option

WithImageLimits bounds decoded image bytes. A zero field disables that policy limit; a negative field fails construction.

func WithInputHandoffRoot

func WithInputHandoffRoot(dir string) Option

WithInputHandoffRoot sets the absolute directory under which handoff-form prompt images are read. The adapter never writes there.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger configures structured diagnostic logging.

func WithMeterProvider

func WithMeterProvider(provider metric.MeterProvider) Option

WithMeterProvider configures the OpenTelemetry meter provider.

func WithScratchDir

func WithScratchDir(dir string) Option

WithScratchDir accepts the configured parent for ephemeral adapter state. This adapter allocates none.

func WithSeedFiles

func WithSeedFiles(files map[string]string) Option

WithSeedFiles registers files written into hermes's config root before each launch. Keys are paths relative to that root; values are the contents.

func WithSessionStore

func WithSessionStore(store acpcore.SessionStore) Option

WithSessionStore configures the session store.

func WithTextMapPropagator

func WithTextMapPropagator(propagator propagation.TextMapPropagator) Option

WithTextMapPropagator configures trace-context extraction from ACP _meta.

func WithTracerProvider

func WithTracerProvider(provider trace.TracerProvider) Option

WithTracerProvider configures the OpenTelemetry tracer provider.

type Options

type Options struct {
	// AgentName is the protocol identifier advertised during ACP initialize.
	AgentName string
	// AgentTitle is the human-readable agent name advertised during ACP initialize.
	AgentTitle string
	// AgentVersion is the agent version advertised during ACP initialize.
	AgentVersion string

	// ExecutablePath selects the hermes executable. A bare name is searched on the
	// base PATH; a path containing a separator is used as given. Empty means
	// "hermes".
	ExecutablePath string
	// Home is hermes's native config, auth, and runtime root, passed to every
	// session as HERMES_HOME. Empty leaves hermes to resolve its home from
	// the inherited environment exactly as it would from a shell.
	Home string
	// ScratchDir is accepted and ignored. This adapter allocates no ephemeral
	// state, so nothing is written under it.
	ScratchDir string
	// InputHandoffRoot is the absolute directory under which handoff-form
	// prompt images are read. Empty rejects the handoff form.
	InputHandoffRoot string
	// DefaultModel selects the model for new sessions as "provider/id".
	DefaultModel string
	// ConfiguredModels are the model ids the host lists explicitly, each as
	// "provider/id".
	ConfiguredModels []string
	// Env is the static agent-scoped overlay on the inherited process
	// environment every hermes process runs with.
	Env map[string]string

	// Logger receives structured diagnostic logs. If nil, the default logger is used.
	Logger *slog.Logger
	// TracerProvider records adapter spans. If nil, tracing is a no-op.
	TracerProvider trace.TracerProvider
	// MeterProvider records adapter metrics. If nil, metrics are no-ops.
	MeterProvider metric.MeterProvider
	// TextMapPropagator extracts trace context from ACP _meta. If nil, W3C
	// trace context plus baggage propagation is used.
	TextMapPropagator propagation.TextMapPropagator

	// SessionStore is the durability boundary for session rows. Nil installs a
	// fresh in-memory store.
	SessionStore acpcore.SessionStore
	// ConcurrencyLimits controls process-local backpressure.
	ConcurrencyLimits ConcurrencyLimits
	// SeedFiles maps paths relative to hermes's config root to file contents
	// written there before each launch.
	SeedFiles map[string]string
	// ImageLimits bounds decoded image bytes on prompt input and emitted
	// output. Every field defaults to 6 MiB when the option is omitted.
	ImageLimits ImageLimits
	// contains filtered or unexported fields
}

Options configures the ACP agent process and the hermes serve gateway sessions it starts.

Directories

Path Synopsis
cmd
acp-go-hermes command
Package integration holds the tests that run against an installed Hermes.
Package integration holds the tests that run against an installed Hermes.
internal

Jump to

Keyboard shortcuts

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