Help us shape the future of Go! Take the 2026 Go Developer Survey

acpcheck

package
v0.0.8 Latest Latest
Warning

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

Go to latest
Published: Sep 18, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package acpcheck is a real ACP client, used to grade a real ACP agent.

The distinction from `internal/acp`'s own tests is the whole reason this exists. Those stand an agent up in this process and speak to it through a pipe made in the same test: they prove the two halves of one package agree with each other, which is worth proving and is not the question anyone asks about a protocol. The question is whether the binary we ship says what somebody else's client will understand — and the only way to answer that is to be somebody else's client: launch the executable, write JSON-RPC on its standard input, read its standard output, and believe nothing that is not on the wire.

So nothing here imports the implementation. The message shapes are written out again by hand, from the protocol, because a client that shares the agent's types cannot catch the agent misnaming a field — both sides would be wrong together and the test would be green. That duplication is the point of the file and not an oversight in it.

Index

Constants

View Source
const (
	AllowOnce    = "allow-once"
	AllowAlways  = "allow-always"
	RejectOnce   = "reject-once"
	RejectAlways = "reject-always"
)

The option ids this shell offers. They are the agent's to name and a client must echo one back exactly, which is itself one of the things graded: an id we invent must be refused rather than guessed at.

Variables

This section is empty.

Functions

func RunAgent

func RunAgent(script string) int

RunAgent is the -as-agent mode: speak ACP as an agent until the client goes.

func Transcript

func Transcript(ctx context.Context, bin, dir, dialect string) (string, error)

Transcript drives one session and returns it, annotated.

dialect is which shell the binary should be; empty is its default.

func TrueCommand

func TrueCommand() string

TrueCommand is the absolute path of a program that does nothing and succeeds, or "" if there is none to be found.

Resolved rather than written down, because it was written down and it was wrong. Both the cost rows below and the benchmarks in bench_test.go named `/bin/true`, which does not exist on macOS — it lives in /usr/bin. Nothing said so: the spawn rows discarded the error with `_ =`, so a shell that started, failed to find the program, and exited 127 was timed as "the same command as a fresh sh -c", and the gated turn was timing a permission round trip for an exec that then failed. The numbers were plausible, which is why it survived — a process start dominates either way — but they were not measuring what the table said they were.

An absolute path rather than a bare name because a bare name would be the shell's own builtin on one side of the comparison and a process on the other, which is the same mistake wearing a different hat.

Types

type AgentReport

type AgentReport struct {
	Script  string `json:"script"`
	Asked   string `json:"asked"`
	Got     string `json:"got"`
	Refused bool   `json:"refused"`
	Error   string `json:"error,omitempty"`
}

AgentReport is what a scripted agent writes down about how it was treated.

Written to a file rather than to a stream, for a reason the protocol imposes: the agent's standard output is the connection and its standard error belongs to the shell's diagnostics. A file is the only channel out of an agent that is not also part of what is being measured.

type Ask

type Ask struct {
	ID       int
	Title    string   // the tool call's title, when the update carried one
	Kind     string   // execute, edit, read, …
	Options  []string // option ids, in the order offered
	Answered string   // the option id we replied with
}

Ask is a `session/request_permission` the agent sent us, and what we said.

type Client

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

Client is a connection to an agent running as a subprocess.

One goroutine reads, everybody else writes under a lock, and requests are matched to their answers by id — the ordinary shape of a JSON-RPC client, written out rather than taken from a library so that the framing this grades is the framing the protocol describes.

func Dial

func Dial(bin string, opts Options) (*Client, error)

Dial launches the agent and starts reading from it.

func (*Client) Asks

func (c *Client) Asks() []Ask

Asks returns the permission requests received so far, with our answers.

func (*Client) Call

func (c *Client) Call(ctx context.Context, method string, params any) (json.RawMessage, error)

Call sends a request and waits for its answer.

An agent that never answers is a failure of the kind this instrument exists to catch, so the wait is bounded by the context and a timeout is returned rather than hung on.

func (*Client) Cancel

func (c *Client) Cancel(session string)

Cancel is the protocol's cancellation, which is a notification: the answer to a canceled turn is the turn's own response, with a stop reason.

func (*Client) Close

func (c *Client) Close() error

Close shuts the connection down the way a client does: close the agent's input, which is the protocol's end-of-connection, and wait for it to go.

func (*Client) Impure

func (c *Client) Impure() []string

Impure returns the lines on standard output that were not protocol.

func (*Client) Initialize

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

Initialize performs the handshake and returns what the agent said about itself.

func (*Client) NewSession

func (c *Client) NewSession(ctx context.Context, dir string) (string, error)

NewSession opens a session rooted at dir and returns its id.

func (*Client) Notifications

func (c *Client) Notifications() []Notification

Notifications returns the session updates received so far.

func (*Client) Notify

func (c *Client) Notify(method string, params any)

Notify sends a notification, which by definition has no answer.

func (*Client) Output

func (c *Client) Output(stream string) string

Output is everything the shell printed, by stream.

The streams are separated because merging them is how an instrument stops being able to tell a diagnostic from a result — the mistake the conformance harness made with CombinedOutput and had to be rescued from.

func (*Client) Prompt

func (c *Client) Prompt(ctx context.Context, session, text string) (string, error)

Prompt sends one prompt and returns why the turn stopped.

func (*Client) Status

func (c *Client) Status(id string) string

Status returns the last status reported for a tool call.

func (*Client) Stderr

func (c *Client) Stderr() string

Stderr returns what the agent logged.

func (*Client) ToolCalls

func (c *Client) ToolCalls() []Update

ToolCalls returns the titles of the tool calls announced, in order.

This is the count that matters to the argument in compare.go: one tool call is one action the shell was about to take that the client both saw and could have refused.

func (*Client) Updates

func (c *Client) Updates() []Update

Updates returns every session update so far, decoded.

type Comparison

type Comparison struct {
	Script string

	// What each observer could see of the same run.
	PipeLines   []string // what came back on the pipe: the lines the script printed
	PipeActions int      // actions the pipe observer could see: none, by construction
	ACPActions  []string // the actions the protocol put to the client, in order
	Refusable   int      // how many of them the client could have refused

	// What it costs, as medians over a run of each.
	TurnPlain   time.Duration // a builtin command, in an established session
	TurnGated   time.Duration // a command that asks permission and is allowed
	SpawnOurs   time.Duration // the same command as a fresh `sh -c` child
	SpawnBash   time.Duration // and as a fresh `bash -c` child
	Handshake   time.Duration // initialize, over the pipe, once
	SessionOpen time.Duration // session/new
	Samples     int
}

Comparison is the whole of what Compare measured.

func Compare

func Compare(ctx context.Context, bin, dir, dialect, acpFlag string) (Comparison, error)

Compare runs the script both ways and times both arrangements.

dialect is which shell the binary should be, and it goes to both sides: timing one dialect against another dialect's pipe would make the ratio a measurement of the two dialects rather than of the two arrangements.

func (Comparison) Report

func (c Comparison) Report() string

Report renders the comparison.

type Config

type Config struct {
	Bin     string
	Root    string
	Self    string
	Only    string
	Dialect string
	// ACPFlag is how this binary is told to serve the protocol, and it is a
	// field because there are two answers rather than one. The multi-call
	// `sh` takes `-acp`; a dialect binary takes `--acp`, because real bash
	// accepts `-acp` as the `-a -c -p` bundle and a single-dash spelling
	// there would shadow working behavior. Empty means `-acp`.
	//
	// It is what makes the second table possible at all: the same rows, run
	// against the binary a shebang names rather than against the substrate's
	// own driver. See #2585, and #1826 for the same split in make sandbox.
	ACPFlag string
}

Config is what a run is given beside its context.

Root is where each row's scratch directory is made; the caller owns it, so a run whose files are worth looking at can be pointed at somewhere that survives. Dialect is which shell the binary should be — empty for its default, which for the multi-call binary is the core.

type Init

type Init struct {
	ProtocolVersion int `json:"protocolVersion"`
	AgentInfo       struct {
		Name    string `json:"name"`
		Version string `json:"version"`
	} `json:"agentInfo"`
	AuthMethods []struct {
		ID string `json:"id"`
	} `json:"authMethods"`
	AgentCapabilities json.RawMessage `json:"agentCapabilities"`
}

Init is the initialize result, in the fields a client acts on.

type Notification

type Notification struct {
	Method string
	Params json.RawMessage
}

Notification is a `session/update` as it arrived, kept whole.

Whole, because the interesting assertions are about fields a typed struct would drop: the `_meta` that says which stream a chunk came from, a `rawOutput` nobody has a type for yet. A grader that decodes into a struct can only ever check what the struct's author thought of.

type Options

type Options struct {
	// Args are the agent's arguments. `-acp` is not added for you: which
	// flags precede it is part of what some rows are testing.
	Args []string
	// Dir is the working directory to start the agent in.
	Dir string
	// Env, when non-nil, replaces the environment rather than adding to it.
	Env []string
	// Answer decides permission requests. A nil Answer allows once, which is
	// the answer that lets a row get far enough to be about something else.
	Answer func(Ask) string
	// Trace, when set, sees every line in both directions.
	Trace func(dir, line string)
	// Serve answers an agent's requests other than permission.
	Serve func(method string, params json.RawMessage) (any, *RPCError)
}

Options configure a connection.

type RPCError

type RPCError struct {
	Code    int             `json:"code"`
	Message string          `json:"message"`
	Data    json.RawMessage `json:"data,omitempty"`
}

RPCError is the error object of a JSON-RPC response.

func (*RPCError) Error

func (e *RPCError) Error() string

type Result

type Result struct {
	Rows  []Row
	Agent string // the version the agent named itself
}

Result is a whole run.

func Run

func Run(ctx context.Context, cfg Config) Result

Run grades the binary and returns the table.

func (Result) Failed

func (r Result) Failed() bool

Failed reports whether anything failed that no issue owns.

type Row

type Row struct {
	Name   string
	Claim  string // why anyone should care, in one line
	Pass   bool
	Detail string
	Known  string // the issue that owns a known gap, if one does
}

Row is one graded property.

type Update

type Update struct {
	Kind       string // agent_message_chunk, tool_call, tool_call_update, …
	ToolCallID string
	Title      string
	ToolKind   string // execute, edit, read, other
	Status     string // pending, in_progress, completed, failed
	Text       string // an agent message chunk's text
	Stream     string // stdout or stderr, from the chunk's _meta
	Raw        json.RawMessage
}

Update is a session update decoded far enough to grade.

Jump to

Keyboard shortcuts

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