Documentation
¶
Overview ¶
Package agenthost is the runtime's client/host-role primitive for driving another ACP agent, the way an editor like Zed spawns an agent binary over stdio and drives it as a client. It is the counterpart to this repo's existing ACP *agent* role (`contenox acp`, runtime/acpsvc): where acpsvc makes contenox speak Agent, agenthost makes the runtime speak Client against something else.
The harness — the libacp.Client callback surface an agent calls back into (session/request_permission, fs/*, terminal/*, session/update) — is always a parameter supplied by the caller, never assembled inside this package. That seam is deliberate: this package builds the low-level plumbing to spawn/connect an external ACP agent kind (runtimetypes.AgentKindExternalACP) plus minimal single-turn session driving on top of it (DriveTurn, drive.go); a real harness registry/service and a "chain" agent kind (runtimetypes.AgentKindChain, reserved in the schema) are later work.
Index ¶
- func McpServerForACP(row *runtimetypes.MCPServer) (libacp.McpServer, error)
- func ResolveForwardedMcpServers(ctx context.Context, resolver McpServerResolver, names []string) ([]libacp.McpServer, error)
- type Agent
- type ExternalACPAgent
- type Handle
- type McpServerResolver
- type RecordingHarness
- type TurnRequest
- type TurnResult
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func McpServerForACP ¶
func McpServerForACP(row *runtimetypes.MCPServer) (libacp.McpServer, error)
McpServerForACP maps a registered MCP server row to its ACP session/new wire shape — the inverse of acpsvc's mcpRowFromLibacp (the direction where contenox is the agent receiving servers from an editor).
Only reachability data is forwarded: argv for stdio, URL plus explicitly configured headers for http/sse. Contenox-side auth synthesis (authToken, authEnvKey, oauth client material) and injectParams are machinery of contenox's own MCP client connections and are deliberately NOT translated into the payload — if a hosted agent needs credentials for a server, they must be written into the row's headers explicitly, where forwarding them is a visible choice.
func ResolveForwardedMcpServers ¶
func ResolveForwardedMcpServers(ctx context.Context, resolver McpServerResolver, names []string) ([]libacp.McpServer, error)
ResolveForwardedMcpServers turns an agent's mcp_servers allowlist (runtimetypes.ExternalACPConfig.McpServers) into the libacp.McpServer entries to pass down in ACP session/new. A name that resolves to nothing is a loud error, not a silent skip: the allowlist is consent the user wrote down, and dropping an entry unnoticed would mean checking/driving the agent with less context than they declared.
Types ¶
type Agent ¶
type Agent interface {
// Connect spawns or attaches to the agent and returns a live Handle
// wired to harness. harness is supplied by the caller: a production
// harness assembling real permission/fs/terminal handling, a minimal
// test harness, or libacp.UnimplementedClient{} as a no-op, can all be
// passed here unchanged — Connect never builds one itself.
Connect(ctx context.Context, harness libacp.Client) (*Handle, error)
}
Agent is the runtime-side primitive for driving another ACP agent: given a harness, Connect establishes a live connection to that agent and returns a Handle wired to it. ExternalACPAgent is the only implementation in this slice (a spawned/attached external ACP peer); the interface is named generically, not "ExternalACPAgent-specific", so a future "chain" agent kind — an in-runtime task chain addressed the same way — is a drop-in second implementation without changing this seam.
type ExternalACPAgent ¶
type ExternalACPAgent struct {
Config runtimetypes.ExternalACPConfig
// Stderr, if set, receives the spawned subprocess's stderr as it is
// written (see acpexec.WithStderr). Defaults to io.Discard.
Stderr io.Writer
// KillGrace, if positive, overrides how long teardown waits for the
// spawned agent to exit after its stdin is closed before killing it
// (see acpexec.WithKillGrace; default 5s). Persistent agents — testy,
// most editor adapters — never exit on stdin-close, so a short grace
// here is what keeps their teardown from stalling for the full default.
KillGrace time.Duration
}
ExternalACPAgent is the runtimetypes.AgentKindExternalACP implementation of Agent: it connects to an external ACP agent described by an ExternalACPConfig, either by spawning it as a subprocess over stdio (the v1, implemented path — it wraps libacp/acpexec, the same subprocess plumbing the client-e2e tests use to drive testy) or, in the future, by dialing it as a network endpoint (not implemented yet; Connect returns a clear error for that transport instead of silently doing nothing).
func NewExternalACPAgent ¶
func NewExternalACPAgent(cfg runtimetypes.ExternalACPConfig) *ExternalACPAgent
NewExternalACPAgent returns an ExternalACPAgent for cfg.
type Handle ¶
type Handle struct {
// Conn is the live ACP client-side connection to the agent. Callers
// issue ACP calls against it directly: Initialize, NewSession, Prompt,
// and so on (see libacp/clientconn.go for the full outbound surface).
Conn *libacp.ClientSideConnection
// contains filtered or unexported fields
}
Handle is a live connection to an agent, returned by Agent.Connect. It owns that connection's lifecycle: Close tears down the underlying transport (e.g. the spawned subprocess) and waits for the connection's read loop to exit before returning, so a caller that has called Close knows the agent is fully torn down, not just "asked to stop".
type McpServerResolver ¶
type McpServerResolver interface {
GetByName(ctx context.Context, name string) (*runtimetypes.MCPServer, error)
}
McpServerResolver is the narrow lookup surface ResolveForwardedMcpServers needs; mcpserverservice.Service satisfies it. Declared here so this package stays free of a service dependency.
type RecordingHarness ¶
type RecordingHarness struct {
libacp.UnimplementedClient
// contains filtered or unexported fields
}
RecordingHarness is the minimal libacp.Client harness for driving an agent whose turn only needs to be observed, not interacted with: it records every session/update notification and, via the embedded UnimplementedClient, rejects the request-shaped callbacks (permission, fs/*, terminal/*). A turn that needs a permission answered needs a scripted harness instead — this one exists so a caller can read an agent's streamed reply after the fact.
Safe for concurrent use: updates arrive on the connection's read-loop goroutine while callers read snapshots from their own.
func (*RecordingHarness) AvailableCommands ¶
func (h *RecordingHarness) AvailableCommands() []libacp.AvailableCommand
AvailableCommands returns the agent's advertised slash-command set — the most recent available_commands_update recorded, since each update is a full replacement list per the spec. Nil means the agent never advertised any. This surfaces the hosted agent's command menu to callers (`agent check` prints it); merging it with contenox's own acpsvc command set is the re-exposure layer's concern, not this package's.
func (*RecordingHarness) MessageText ¶
func (h *RecordingHarness) MessageText() string
MessageText concatenates the text of every agent_message_chunk recorded so far: the agent's streamed reply as one string.
func (*RecordingHarness) SessionUpdate ¶
func (h *RecordingHarness) SessionUpdate(_ context.Context, n libacp.SessionNotification) error
SessionUpdate records n. It never returns an error — observing a turn must not be able to disturb it.
func (*RecordingHarness) Updates ¶
func (h *RecordingHarness) Updates() []libacp.SessionNotification
Updates returns a snapshot of every recorded session/update, in arrival order.
type TurnRequest ¶
type TurnRequest struct {
// Cwd is the session working directory. Required: ACP's session/new
// requires one, and spec-correct agents expect it to be absolute.
Cwd string
// Prompt is the user prompt for the driven turn. Required.
Prompt []libacp.ContentBlock
// ClientInfo identifies this host to the agent. Defaults to a
// "contenox-agenthost" identity when nil. The ACP spec requires both
// name and version, and real-world agents (the claude-code-acp adapter)
// hard-reject an initialize without a version — so DriveTurn fills an
// empty Version with the runtime's own before sending.
ClientInfo *libacp.Implementation
// ClientCapabilities advertises what the supplied harness can actually
// serve. The zero value (nothing advertised) is the honest match for
// RecordingHarness.
ClientCapabilities libacp.ClientCapabilities
// McpServers are MCP servers passed down to the agent in session/new —
// typically the resolved form of the agent's mcp_servers allowlist (see
// ResolveForwardedMcpServers). DriveTurn filters them against the
// agent's initialize-advertised mcpCapabilities before sending; what was
// kept and dropped is reported on TurnResult.
McpServers []libacp.McpServer
// Stderr, if set, receives the spawned agent's stderr as it is written —
// pass a buffer so a failing turn is diagnosable without rerunning.
Stderr io.Writer
// KillGrace, if positive, bounds how long teardown waits for the agent
// to exit on stdin-close before killing it (see
// ExternalACPAgent.KillGrace). Set it short for persistent agents that
// never exit on their own.
KillGrace time.Duration
}
TurnRequest describes the one prompt turn DriveTurn drives.
type TurnResult ¶
type TurnResult struct {
Initialize libacp.InitializeResponse
SessionID libacp.SessionID
StopReason libacp.StopReason
// ForwardedMcpServers and DroppedMcpServers name which of
// TurnRequest.McpServers actually reached the agent in session/new and
// which were withheld because the agent's mcpCapabilities can't consume
// their transport. Callers surface these so a user who allowlisted a
// server learns when the agent never saw it.
ForwardedMcpServers []string
DroppedMcpServers []string
}
TurnResult is what one driven turn produced on the request/response plane. The notification plane (streamed chunks, tool calls) lives on the harness — see RecordingHarness.
func DriveTurn ¶
func DriveTurn(ctx context.Context, agent *runtimetypes.Agent, harness libacp.Client, req TurnRequest) (*TurnResult, error)
DriveTurn composes a resolved agents row with the host: it connects to the external ACP agent the row describes and drives one full initialize → session/new → session/prompt turn against it with harness, tearing the connection down before returning. Resolving the row (by name, via the registry service) stays with the caller — this package remains registry-agnostic; it only consumes the resolved *runtimetypes.Agent.
A nil error means the whole loop closed: the agent answered the prompt with a terminal stopReason and the spawned process tore down cleanly (Close errors are returned, not swallowed). Everything the agent streamed during the turn is on the harness, which DriveTurn passes through untouched per this package's harness seam.