hermesacp

package module
v0.0.0-...-6b1e219 Latest Latest
Warning

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

Go to latest
Published: Sep 5, 2026 License: GPL-3.0 Imports: 45 Imported by: 0

README

acp-go-hermes

Go ACP agent that exposes the local Hermes CLI as an Agent Client Protocol agent.

Go Reference CI

Use it as either:

  • a standalone ACP subprocess: acp-go-hermes
  • an embedded Go adapter through hermesacp.Serve, or in-process through hermesacp.NewAgent with hermesacp.WithClient

Install

Library:

go get github.com/savid/acp-go-hermes

CLI:

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

For local development, run the command straight from a checkout:

go run ./cmd/acp-go-hermes -path "$(command -v hermes)"

The process speaks ACP over stdin/stdout and reserves stdout for ACP JSON-RPC; diagnostics go to stderr. In normal use an editor or ACP host launches it as a subprocess rather than a human-facing chat UI.

Quickstart

The example programs run from a checkout of this repo, so clone it first:

git clone https://github.com/savid/acp-go-hermes && cd acp-go-hermes

Run a tiny local client that launches the agent, sends one prompt, and prints the reply (the prompt argument is optional):

go run ./examples/minimal-client "Reply with hello from ACP"

Or drive the agent from an interactive client session:

go run ./examples/interactive-chat

Load and resume a stored session transcript:

go run ./examples/resume-from-file -file ./examples/resume-from-file/session.jsonl

Embedded Go

package main

import (
	"context"
	"log"
	"os"

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

func main() {
	err := hermesacp.Serve(context.Background(), os.Stdin, os.Stdout,
		hermesacp.WithExecutablePath("hermes"),
		hermesacp.WithScratchDir("/tmp/hermes-acp-scratch"),
		hermesacp.WithDefaultModel("openai/gpt-5.5"),
	)
	if err != nil {
		log.Fatal(err)
	}
}

See Go API docs for options such as the Hermes executable path, the scratch directory for ephemeral per-session state, default model, environment, session storage, concurrency limits, and OpenTelemetry providers. WithHome is unsupported and rejects a non-empty value at session start. Use WithScratchDir for ephemeral state. WithSharedHermesHome explicitly opts official Hermes into one durable native home shared by this adapter's otherwise independent per-session processes; that adapter claims the home root exclusively and a second adapter asking for the same root is refused. WithProviderAuthRoot names the durable directory that holds the values-free provider-auth ledger. Provider auth is advertised only when that ledger root and the shared Hermes home are configured; naming the ledger root without a shared home fails initialize instead. Configuring both also canonicalizes the shared home — the adapter resolves its symlinks and uses the resolved path as HERMES_HOME.

A host that embeds the Agent and calls its ACP methods in-process builds it with NewAgent and supplies the ACP client itself through WithClient; that client is what the agent streams session updates, permission requests, and elicitations to. Serve installs the connection it builds as that client and refuses an option set carrying one.

An embedded host can pass WithHostAuthority to supply the complete native environment, prepare and reclaim native trees, and launch every managed Hermes process. A supplied authority is mandatory for that agent instance: errors do not fall back to direct execution. Provider-auth extensions are not advertised in this mode; ordinary ACP sessions remain available.

What It Provides

  • ACP session lifecycle: create, prompt, cancel, close, list, load, resume, delete, and fork.
  • Provider OAuth brokered through seven session-scoped _hermes/auth/* extension methods over the hermes serve REST auth API. Native Hermes owns credential bytes in the explicit shared durable HERMES_HOME; the adapter keeps only values-free connection lineage. A login has one hard precondition: the authorize leg refuses before any native call unless the session's process runs behind the private browser-launcher shim, because Hermes accepts --no-browser and then ignores it. Hermes returns its authorization URL on this API path without executing a browser launcher; a required pinned Linux canary verifies that no-launch behavior through the production adapter.
  • One hermes serve process per session. By default each has a freshly generated HERMES_HOME and runs as the adapter's operating-system account. Embedded hosts can supply WithHostAuthority to route the version probe and session server through a host-owned process and filesystem boundary. The adapter materializes each native tree before preparing it, then reclaims it before snapshot reads or removal. The explicit shared-home mode remains an ordinary standalone residence with separate processes, ports, tokens, browser shims, event streams, environment, and wrapper control roots.
  • Gateway event mapping from the loopback Hermes WebSocket into ACP methods and notifications.
  • Prompt streaming for messages, tool calls, diffs, usage, and session metadata.
  • Static PNG, JPEG, GIF, and WebP prompt images through Hermes image.attach_bytes, including image-MIME embedded resource blobs, with the enforced byte and format bounds advertised at initialize under _meta["acp-go.dev/mediaEnvelope"].
  • Two inbound image transports: embedded base64, and — for a co-located host that sets WithInputHandoffRoot — digest-verified local files read under that read-only root, contained by the kernel and never named in the native request.
  • Input-only image transport: Hermes image artifacts are not projected as typed ACP image output.
  • Command, file, and generic permission prompts, plus MCP elicitation bridging.
  • MCP stdio and streamable HTTP server configuration through the session request builders.
  • Native message replay on session/load, replay-free session/resume, and store tombstones on session/delete.
  • Forking through _hermes/session/fork, and optional raw gateway events through _hermes/rawEvent after per-session opt-in.
  • Durable mirroring through a host-provided SessionStore in the hermes-state-db-v1 format with sequenced tar+zstd+base64 archive chunks.
  • OpenTelemetry telemetry through injected tracer, meter, and propagator providers, recording no prompt or tool secrets by default.

Docs

Development

make audit
make test-integration-smoke
make test-integration-live
make test-integration-cover

Live integration tests use disposable temporary Hermes homes. The official shared-home proof plants a fake xAI OAuth fixture, makes no provider request, and never reads or mutates the operator's Hermes home.

License

GNU General Public License v3.0.

Documentation

Overview

Package hermesacp exposes the local Hermes CLI as an Agent Client Protocol agent.

Most hosts run the agent over a pair of JSON-RPC streams using Serve. Serve launches one authenticated loopback `hermes serve` process per ACP session, maps ACP requests into native gateway WebSocket JSON-RPC calls, and streams gateway events back to the client as ACP session updates. Without a HostAuthority, native processes run under the adapter's ordinary same-identity launcher. WithHostAuthority routes every native launch and prepared tree through the supplied authority. A cancelled or timed-out session lazily resumes its exact native key from the last committed snapshot on the next prompt. Hosts must complete ACP initialization before issuing session or other agent methods.

Hosts should use Serve for the JSON-RPC transport; hosts that embed the agent directly construct one with NewAgent and the same Option values, plus WithClient to supply the ACP client the agent streams session updates, permission requests, and elicitations to. Serve installs the connection it builds as that client and refuses an option set carrying one. A direct prompt error caused by gateway loss matches ErrGatewayDisconnected and retains its exact native cause; Serve converts that chain to bounded, secret-safe ACP error data at the wire boundary. Hermes authentication and provider credentials remain owned by native Hermes. Each session runs under its own ephemeral `HERMES_HOME` materialized beneath the scratch parent from WithScratchDir. WithSharedHermesHome explicitly opts official Hermes into one durable native home shared by this adapter's otherwise independent per-session processes; the adapter claims that home root exclusively and a second adapter is refused it. Provider OAuth additionally requires WithProviderAuthRoot for values-free connection lineage, and enabling it canonicalizes the shared home: the adapter creates it 0700, resolves its symlinks, and uses the resolved path as HERMES_HOME. A login leg also requires the per-process browser-launcher shim and refuses before any native call without it. The adapter never reads or copies credentials. WithHome remains unsupported.

Prompt images arrive either as embedded base64 or, for a co-located host that sets WithInputHandoffRoot, as digest-verified files under that read-only root. The adapter enforces the same gates on both transports and advertises the bounds it enforces at initialize; the handoff root is read-only, so it materializes nothing and WithScratchDir remains the only source of ephemeral on-disk state.

Hosts that need durable remote resume can provide WithSessionStore. A session store receives `hermes-state-db-v1` snapshots keyed by the ACP-visible session ID and subpath, can back session/list, and can hydrate a snapshot into a fresh per-session Hermes home for session/load or session/resume when isolated native state is absent. Shared-home mode stores logical metadata only and resumes against the durable official database.

Hosts can call CallForkSession for the Hermes fork extension method _hermes/session/fork. Raw Hermes gateway events are emitted as RawEventMethod notifications only when a session request opts in with WithSessionRawEvents.

Hosts that need adapter telemetry can provide OpenTelemetry providers with WithTracerProvider and WithMeterProvider. The package never configures global OpenTelemetry providers; the acp-go-hermes binary handles env-based exporter setup for command-line use. Caller-supplied providers remain owned by the caller, including ForceFlush and Shutdown.

Index

Examples

Constants

View Source
const (
	AuthMethodsMethod    = "_hermes/auth/methods"
	AuthAuthorizeMethod  = "_hermes/auth/authorize"
	AuthCallbackMethod   = "_hermes/auth/callback"
	AuthStatusMethod     = "_hermes/auth/status"
	AuthCancelMethod     = "_hermes/auth/cancel"
	AuthInventoryMethod  = "_hermes/auth/inventory"
	AuthDisconnectMethod = "_hermes/auth/disconnect"
)

Session-scoped provider-auth extension methods. Hermes owns credential material in the configured native auth home; this surface coordinates only values-free flow and lineage state.

View Source
const (
	ForkSessionMethod = "_hermes/session/fork"
	RawEventMethod    = "_hermes/rawEvent"
)
View Source
const (
	SessionStoreMainSubpath = ""
	SessionStoreFormat      = "hermes-state-db-v1"
)

Variables

View Source
var (
	// ErrHostAuthorityUnavailable reports that the borrowed host boundary cannot continue.
	ErrHostAuthorityUnavailable = errors.New("host authority unavailable")
	// ErrContainmentIncomplete reports that native process containment is uncertain.
	ErrContainmentIncomplete = errors.New("native containment incomplete")
	// ErrNativeTreeBusy reports a non-mutating reclaim refusal while lease processes remain.
	ErrNativeTreeBusy = errors.New("native tree has live lease processes")
)
View Source
var (
	ErrSessionOperationAmbiguous        = errors.New("hermes session-operation recovery is ambiguous")
	ErrSessionOperationStoreUnavailable = errors.New("hermes session-operation store is unavailable")
)
View Source
var ErrGatewayDisconnected = nativehermes.ErrGatewayDisconnected

ErrGatewayDisconnected classifies loss of the Hermes gateway while retaining the exact native transport cause in the returned error chain.

View Source
var ErrSessionPumpOverflow = errors.New("hermes session actor mailbox overflow")

Functions

func CancelRequest

func CancelRequest(sessionID acp.SessionId, turnNonce string) acp.CancelNotification

CancelRequest builds an active-turn cancellation carrying the mandatory route nonce.

func DeleteSessionRequest

func DeleteSessionRequest(sessionID acp.SessionId) acp.UnstableDeleteSessionRequest

func ForkSessionRequest

func ForkSessionRequest(sessionID acp.SessionId, cwd string, opts ...SessionRequestOption) acp.UnstableForkSessionRequest

func HTTPMCPServer

func HTTPMCPServer(name string, url string, headers map[string]string) acp.McpServer

func LoadSessionRequest

func LoadSessionRequest(sessionID acp.SessionId, cwd string, opts ...SessionRequestOption) acp.LoadSessionRequest

func NewSessionRequest

func NewSessionRequest(cwd string, opts ...SessionRequestOption) acp.NewSessionRequest

func PromptRequest

func PromptRequest(sessionID acp.SessionId, turnNonce string, blocks ...acp.ContentBlock) acp.PromptRequest

func ResumeSessionRequest

func ResumeSessionRequest(sessionID acp.SessionId, cwd string, opts ...SessionRequestOption) acp.ResumeSessionRequest

func Serve

func Serve(ctx context.Context, input io.Reader, output io.Writer, opts ...Option) (returnErr error)
Example (Initialize)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

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

done := make(chan error, 1)
go func() {
	done <- 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"`
			McpCapabilities struct {
				Http bool `json:"http"`
			} `json:"mcpCapabilities"`
		} `json:"agentCapabilities"`
	} `json:"result"`
}
_ = json.Unmarshal([]byte(line), &response)

fmt.Println(len(response.Result.AuthMethods) == 0)
fmt.Println(response.Result.AgentCapabilities.LoadSession)
fmt.Println(response.Result.AgentCapabilities.McpCapabilities.Http)
Output:
true
true
true

func SetModelRequest

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

func StdioMCPServer

func StdioMCPServer(name string, command string, args []string, env map[string]string) acp.McpServer

func TextPromptRequest

func TextPromptRequest(sessionID acp.SessionId, turnNonce, text string) acp.PromptRequest

Types

type Agent

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

Agent exposes Hermes through ACP.

func NewAgent

func NewAgent(opts ...Option) *Agent

func (*Agent) Authenticate

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

Authenticate advertises no method, so every call is refused. The reserved lifecycle literal is inspected first: a request naming a key this surface never carries is malformed before it is unauthenticated.

func (*Agent) Cancel

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

func (*Agent) Close

func (a *Agent) Close() error

func (*Agent) CloseSession

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

func (*Agent) HandleExtensionMethod

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

func (*Agent) Initialize

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

func (*Agent) ListSessions

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

func (*Agent) LoadSession

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

func (*Agent) Logout

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

func (*Agent) NewSession

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

func (*Agent) Prompt

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

func (*Agent) ResumeSession

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

func (*Agent) SetSessionMode

SetSessionMode carries no native mode surface, so it answers method-not-found. The lifecycle refusal still precedes that answer, for the same reason Authenticate's does.

type ConcurrencyLimits

type ConcurrencyLimits struct {
	MaxActiveSessions        int
	MaxConcurrentClientCalls int
}

ConcurrencyLimits bounds work accepted by one Agent.

type ExtensionNotificationHandler

type ExtensionNotificationHandler interface {
	ExtensionNotification(ctx context.Context, method string, params any) error
}

ExtensionNotificationHandler is the optional client surface that receives the agent's namespaced extension notifications. A directly embedded client that implements it receives RawEventMethod exactly as a JSON-RPC peer does; one that does not is never sent them.

type HermesOption

type HermesOption func(*HermesOptions)

func WithHermesEnv

func WithHermesEnv(env map[string]string) HermesOption

func WithHermesExtraPathDirs

func WithHermesExtraPathDirs(dirs ...string) HermesOption

WithHermesExtraPathDirs configures absolute directories placed, in order, ahead of the native base PATH for this session's Hermes process and terminal commands. Session Env cannot carry PATH, BASH_ENV, or the adapter's private carrier namespace; this ordered option is the sole session PATH authority.

func WithHermesModel

func WithHermesModel(model string) HermesOption

func WithHermesOutputSchema

func WithHermesOutputSchema(schema map[string]any) HermesOption

type HermesOptions

type HermesOptions struct {
	Model         string            `json:"model,omitempty"`
	Env           map[string]string `json:"env,omitempty"`
	ExtraPathDirs []string          `json:"extraPathDirs,omitempty"`
	OutputSchema  map[string]any    `json:"outputSchema,omitempty"`
}

HermesOptions is the stable Hermes-specific subset accepted at _meta.hermes.options.

func NewHermesOptions

func NewHermesOptions(opts ...HermesOption) HermesOptions

func (HermesOptions) Meta

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

Meta returns an ACP _meta object for the supported Hermes-specific options.

type HostAuthority

type HostAuthority interface {
	NativeEnvironment() map[string]string
	PrepareNativeTree(context.Context, string) error
	ReadNativeAppendLog(context.Context, string, uint64) ([][]byte, error)
	WriteNativeAppendLog(context.Context, string, [][]byte) error
	ReclaimNativeTree(context.Context, string) error
	StartNative(context.Context, NativeRequest) (NativeProcess, error)
}

HostAuthority owns native process execution and prepared-tree transitions.

type ImageLimits

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

ImageLimits bounds decoded image bytes. Every field counts decoded bytes, never base64 characters or enclosing JSON, and defaults to 6 MiB decoded (6,291,456 bytes). A field explicitly set to zero in a supplied ImageLimits disables that adapter policy limit; it never bypasses native framing, provider, memory, or host request limits. Negative fields are rejected at agent construction. The two output fields are accepted for the uniform option surface; this adapter emits no typed image output, so no output limit is ever consulted.

type InMemorySessionStore

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

func NewInMemorySessionStore

func NewInMemorySessionStore() *InMemorySessionStore

func (*InMemorySessionStore) Append

func (s *InMemorySessionStore) Append(ctx context.Context, key SessionKey, entries []SessionStoreEntry) error

func (*InMemorySessionStore) Delete

func (s *InMemorySessionStore) Delete(ctx context.Context, key SessionKey) error

func (*InMemorySessionStore) ListSessions

func (s *InMemorySessionStore) ListSessions(ctx context.Context) ([]SessionSummary, error)

func (*InMemorySessionStore) ListSubkeys

func (s *InMemorySessionStore) ListSubkeys(ctx context.Context, key SessionKey) ([]string, error)

func (*InMemorySessionStore) Load

func (*InMemorySessionStore) Replace

func (s *InMemorySessionStore) Replace(ctx context.Context, main SessionKey, replacements []SessionStoreReplacement) error

type ListSessionsRequestOption

type ListSessionsRequestOption func(*acp.ListSessionsRequest)

func WithListSessionsCursor

func WithListSessionsCursor(cursor string) ListSessionsRequestOption

func WithListSessionsCwd

func WithListSessionsCwd(cwd string) ListSessionsRequestOption

func WithListSessionsMeta

func WithListSessionsMeta(meta map[string]any) ListSessionsRequestOption

WithListSessionsMeta merges host-supplied metadata into a `session/list` request's `_meta`, on the same reserved-namespace terms as WithSessionMeta.

type NativeProcess

type NativeProcess interface {
	Stdin() io.WriteCloser
	Stdout() io.ReadCloser
	Stderr() io.ReadCloser
	Wait(context.Context) (NativeResult, error)
	Revoke(context.Context) error
}

NativeProcess is a host-owned native process with revocation and terminal observation.

type NativeRequest

type NativeRequest struct {
	Executable       string
	Arguments        []string
	Environment      []string
	WorkingDirectory string
}

NativeRequest describes one host-authorized native process launch.

type NativeResult

type NativeResult struct {
	ExitCode int
	Signal   int
	Revoked  bool
}

NativeResult describes a terminal native process.

type Option

type Option func(*Options)

Option configures the Hermes ACP agent.

func WithAgentName

func WithAgentName(name string) Option

func WithAgentTitle

func WithAgentTitle(title string) Option

func WithAgentVersion

func WithAgentVersion(version string) Option

func WithClient

func WithClient(client acp.Client) Option

WithClient supplies the ACP client a directly constructed Agent streams to.

Serve builds a JSON-RPC connection and installs it as the agent's client, so a host that runs the agent over stdio never sets this. A host that embeds the Agent and calls its ACP methods in-process gets the same outbound surface only through this option: session updates, permission requests, and elicitations are agent-to-client traffic, and an Agent with no client cannot deliver them. The supplied client is called from the agent's own goroutines and must be safe for concurrent use.

Elicitation is delivered only when the client also implements UnstableCreateElicitation (the SDK's ClientExperimental surface); a client without it refuses elicitations instead of dropping them. Extension notifications, including RawEventMethod, are delivered only when the client implements ExtensionNotificationHandler.

func WithConcurrencyLimits

func WithConcurrencyLimits(limits ConcurrencyLimits) Option

func WithDefaultModel

func WithDefaultModel(model string) Option

func WithEnv

func WithEnv(env map[string]string) Option

WithEnv supplies the static Agent-scoped native environment overlay. BASH_ENV and ACP_GO_HERMES_PATH_DIR_* are reserved for the adapter's native terminal PATH carrier and fail Agent construction.

func WithExecutablePath

func WithExecutablePath(path string) Option

func WithHome

func WithHome(path string) Option

WithHome is unsupported because native residence selection is explicit. Use WithScratchDir for isolated ephemeral state or WithSharedHermesHome for the official-Hermes shared durable mode.

func WithHostAuthority

func WithHostAuthority(authority HostAuthority) Option

WithHostAuthority routes native processes and tree ownership through authority.

func WithImageLimits

func WithImageLimits(limits ImageLimits) Option

WithImageLimits replaces every decoded-byte image limit with the supplied values. A zero field disables that adapter policy limit; a negative field is rejected at agent construction. Omitting the option leaves all four fields at their default of 6 MiB decoded.

func WithInputHandoffRoot

func WithInputHandoffRoot(dir string) Option

WithInputHandoffRoot sets the read-only root under which the host has materialized handoff image files, enabling the local-handoff prompt form: an image block with empty data, a file URI under this root, and an acp-go.dev/handoff envelope carrying the file's sha256 digest and size. The path must be absolute; a relative path is rejected at agent construction. Omitting the option leaves the form rejected as invalid_handoff, and the acp-go.dev/handoff capability is then not advertised. Files under the root stay host-owned: the adapter resolves, reads, and verifies them, and never writes, moves, or removes anything there.

func WithLogger

func WithLogger(logger *slog.Logger) Option

func WithMeterProvider

func WithMeterProvider(provider metric.MeterProvider) Option

func WithProviderAuthRoot

func WithProviderAuthRoot(path string) Option

WithProviderAuthRoot sets the durable directory that houses the values-free provider-auth ledger. The directory is created 0700 when missing and ledger entries are written 0600. Provider auth is enabled only when WithSharedHermesHome is also set. The root carries no config or auth-resolution semantics and is never a scratch parent.

Three wrong configurations answer differently. Omitting this option leaves every provider-auth method unadvertised and answering method-not-found: a leg that cannot record what it did is never offered. A relative root, or this option without WithSharedHermesHome, is a construction failure that every Initialize reports as an internal error. An absolute root the agent cannot prepare — one it cannot create, restrict to 0700, or confirm as a writable directory — is logged at warn level and leaves the surface unadvertised while the rest of the agent works.

func WithScratchDir

func WithScratchDir(dir string) Option

WithScratchDir sets the sole parent directory for all ephemeral on-disk materialization: isolated per-session Hermes homes, sqlite temp directories, and process/server temp roots. An empty value (the default) means the system temp directory. The directory is created with 0700 permissions when missing.

func WithSeedFiles

func WithSeedFiles(files map[string]string) Option

WithSeedFiles maps relative paths to file contents that the adapter writes into each session's isolated Hermes config root before launching hermes, so hermes reads them as its own config (for example config.yaml). Paths are confined to that root: absolute paths, ".." segments, and empty keys are rejected at session start. Contents are written verbatim, so secrets belong in WithEnv and are referenced from seeded files by env-var indirection (for example hermes key_env), never written into a seeded file.

func WithSessionStore

func WithSessionStore(store SessionStore) Option

func WithSessionStoreLoadTimeout

func WithSessionStoreLoadTimeout(timeout time.Duration) Option

func WithSharedHermesHome

func WithSharedHermesHome(path string) Option

WithSharedHermesHome explicitly selects official shared-home mode: every native gateway uses path as its exact durable HERMES_HOME. This intentionally gives up per-session Hermes-home isolation so credentials and native sessions survive adapter restarts with the official runtime.

The path must be absolute and already clean, and requires ordinary same-identity execution. Provider-auth extension methods additionally require WithProviderAuthRoot, and configuring both canonicalizes this path: the agent creates the directory 0700 when absent, resolves its symlinks, and adopts the resolved path for the rest of its life. That resolved path — not the spelling passed here — is the HERMES_HOME and shared XDG root every native process receives, the root the exclusive home-root claim fences, and the value the ledger's per-home key hashes, so a caller that named the home through a symlink must compare against the resolved form. Without WithProviderAuthRoot the path is used verbatim. Each ACP session retains its own native process, environment, PATH additions, event stream, and adapter-owned control generation. Native database and auth state are intentionally shared through path. Managed MCP configuration must be identical for every session owned by the Agent.

func WithTextMapPropagator

func WithTextMapPropagator(propagator propagation.TextMapPropagator) Option

func WithTracerProvider

func WithTracerProvider(provider trace.TracerProvider) Option

func WithTurnTimeout

func WithTurnTimeout(timeout time.Duration) Option

WithTurnTimeout bounds how long a single native turn may run before the wrapper interrupts it, closes and proves the whole native process boundary, and fails the prompt with a hermes_turn_failed error whose cause is "timeout". The default of 0 disables the deadline. A timeout is a failure, not a user cancel, so it is never reported as StopReason cancelled.

type Options

type Options struct {
	AgentName    string
	AgentTitle   string
	AgentVersion string

	ExecutablePath string
	HostAuthority  HostAuthority
	// Home is unsupported because native residence selection is explicit. Use
	// ScratchDir for isolated ephemeral state or SharedHermesHome for the
	// official-Hermes shared durable mode.
	Home string
	// ScratchDir is the sole parent directory for all ephemeral on-disk
	// materialization: isolated per-session Hermes homes, sqlite temp
	// directories, and process/server temp roots. An empty value means the
	// system temp directory. It is created with 0700 permissions when missing.
	ScratchDir string
	// InputHandoffRoot is the read-only root under which the host materializes
	// handoff image files. An empty value (the default) leaves the local-handoff
	// prompt form rejected. It is not a materialization option: the adapter
	// never writes, moves, or removes anything under it.
	InputHandoffRoot string
	// ProviderAuthRoot is the absolute, host-owned, durable directory that
	// houses the values-free provider-auth ledger. It is not ephemeral
	// materialization: the ledger deliberately outlives every session and every
	// native generation, which is the one class of state a scratch parent must
	// not hold. Provider auth is enabled only when SharedHermesHome is also set.
	ProviderAuthRoot string
	// SharedHermesHome selects official shared-home mode. Every native
	// process uses this one durable home, so per-session Hermes-home isolation is
	// intentionally disabled. Per-session processes and adapter-owned control
	// generations remain independent. When ProviderAuthRoot is also set, NewAgent
	// replaces this field with the symlink-resolved residence it prepared, and
	// that resolved path is what every native process and every home-keyed claim
	// then uses.
	SharedHermesHome string
	DefaultModel     string
	Env              map[string]string

	Logger            *slog.Logger
	TracerProvider    trace.TracerProvider
	MeterProvider     metric.MeterProvider
	TextMapPropagator propagation.TextMapPropagator

	// Client is the ACP client an embedded agent streams to. It is set by
	// WithClient and is the embedding host's counterpart to the JSON-RPC
	// transport Serve builds: without it a directly constructed Agent has
	// nowhere to publish session updates, permission requests, or
	// elicitations. Serve refuses an Options carrying one, because Serve
	// installs the connection it owns.
	Client                  acp.Client
	SessionStore            SessionStore
	SessionStoreLoadTimeout time.Duration
	ConcurrencyLimits       ConcurrencyLimits
	ImageLimits             ImageLimits
	SeedFiles               map[string]string
	TurnTimeout             time.Duration
	// contains filtered or unexported fields
}

Options configures the ACP agent process and Hermes sessions it starts.

type SessionKey

type SessionKey struct {
	SessionID string
	Subpath   string
}

type SessionRequestOption

type SessionRequestOption func(*sessionRequestConfig)

func WithSessionAdditionalDirectories

func WithSessionAdditionalDirectories(paths ...string) SessionRequestOption

func WithSessionHermesOptions

func WithSessionHermesOptions(options HermesOptions) SessionRequestOption

func WithSessionMCPServers

func WithSessionMCPServers(servers ...acp.McpServer) SessionRequestOption

func WithSessionMeta

func WithSessionMeta(meta map[string]any) SessionRequestOption

WithSessionMeta merges host-supplied metadata into a session lifecycle request's `_meta`. The `acp-go.dev/*` namespace is family-global and reserved: its envelopes are stamped by this package and read by every sibling, so a caller key inside it is refused rather than merged or overwritten. A merged one would put a host's value where a reader expects a family envelope; an overwritten one would silently discard what the host asked for. Neither is a request this builder can honestly produce, and there is no return value to carry the refusal, so it panics — the caller is a program naming a namespace that is not its own.

func WithSessionOutputSchema

func WithSessionOutputSchema(schema map[string]any) SessionRequestOption

func WithSessionRawEvents

func WithSessionRawEvents(enabled bool) SessionRequestOption

type SessionStore

type SessionStore interface {
	Append(ctx context.Context, key SessionKey, entries []SessionStoreEntry) error
	Load(ctx context.Context, key SessionKey) ([]SessionStoreEntry, error)
	Replace(ctx context.Context, main SessionKey, replacements []SessionStoreReplacement) error
	Delete(ctx context.Context, key SessionKey) error
	ListSessions(ctx context.Context) ([]SessionSummary, error)
	ListSubkeys(ctx context.Context, key SessionKey) ([]string, error)
}

type SessionStoreEntry

type SessionStoreEntry = json.RawMessage

type SessionStoreReplacement

type SessionStoreReplacement struct {
	Key     SessionKey
	Entries []SessionStoreEntry
}

type SessionStoreTerminalState

type SessionStoreTerminalState struct {
	MessageID  string
	Outcome    string
	StopReason string
}

SessionStoreTerminalState is the durable foreground boundary of one committed hermes-state-db-v1 main snapshot: the terminal assistant identity the native archive holds, and the truthful outcome the wrapper recorded for the last accepted turn.

MessageID is empty until a turn completes a finished assistant row. Outcome and StopReason are empty until an accepted turn settles; StopReason stays empty when Outcome is `failed`, because no ACP v1 stop reason names a failure.

func InspectSessionStoreTerminalState

func InspectSessionStoreTerminalState(
	logicalSessionID string,
	entries []SessionStoreEntry,
) (SessionStoreTerminalState, error)

InspectSessionStoreTerminalState validates exactly one current-format main snapshot for logicalSessionID and returns its latest finished assistant identity. A valid session that has not completed an assistant turn returns the zero state. Missing terminal summaries and snapshots from any other format are rejected.

type SessionSummary

type SessionSummary struct {
	SessionID          string
	UpdatedAtUnixMilli int64
	Cwd                string
	Title              string
	Meta               map[string]any
}

Directories

Path Synopsis
cmd
acp-go-hermes command
examples
minimal-client command
internal
lifecycle
Package lifecycle implements the `acp-go.dev/lifecycle` extension: the closed event vocabulary, the strict wire decoder, and the reducer that validates one ordered session lifecycle stream.
Package lifecycle implements the `acp-go.dev/lifecycle` extension: the closed event vocabulary, the strict wire decoder, and the reducer that validates one ordered session lifecycle stream.

Jump to

Keyboard shortcuts

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