acpexec

package
v0.36.0 Latest Latest
Warning

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

Go to latest
Published: Jul 19, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package acpexec spawns a subprocess and wires its stdin/stdout together as a single io.ReadWriteCloser, the transport shape libacp.NewAgentSideConnection and libacp.NewClientSideConnection both expect. It exists so an ACP peer (an editor driving a real agent binary, or a test driving the Rust reference agent/client binaries) can be reached over stdio without any caller having to hand-roll pipe plumbing and shutdown bookkeeping.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type LockedBuffer

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

LockedBuffer is a concurrency-safe io.Writer around a bytes.Buffer, meant for WithStderr: a subprocess writes to it from its own reader goroutine (installed by Spawn) while the caller reads String() later, typically only once something has already gone wrong and the process's stderr is wanted for a failure message.

func (*LockedBuffer) String

func (b *LockedBuffer) String() string

String returns everything written so far.

func (*LockedBuffer) Write

func (b *LockedBuffer) Write(p []byte) (int, error)

type Option

type Option func(*config)

Option configures Spawn. See WithStderr and WithKillGrace.

func WithKillGrace

func WithKillGrace(d time.Duration) Option

WithKillGrace overrides how long Close waits for the subprocess to exit on its own (after closing its stdin) before it escalates to Process.Kill. The default is 5 seconds.

func WithStderr

func WithStderr(w io.Writer) Option

WithStderr forwards the subprocess's stderr to w as it's written, instead of the default (io.Discard). Passing a *LockedBuffer lets a caller recover the subprocess's stderr for a failure message even though it is written from a different goroutine than the one that later reads it.

type Process

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

Process is a spawned subprocess wired up as an io.ReadWriteCloser: Read pulls from its stdout, Write pushes to its stdin, and Close begins its shutdown sequence (see Close). It is the concrete type Spawn returns, rather than a bare io.ReadWriteCloser, so callers that need it (tests, mainly) can still reach Wait's exit error without a type assertion.

func Spawn

func Spawn(ctx context.Context, cmd *exec.Cmd, opts ...Option) (*Process, error)

Spawn starts cmd and returns it as a Process. cmd's Stdin/Stdout are claimed via exec.Cmd.StdinPipe/StdoutPipe — callers must not have set them already. Stderr is discarded unless WithStderr overrides it.

If ctx is cancelled before the subprocess exits on its own, Spawn closes it down exactly as Close would (grace period, then kill) rather than leaking a running process past the caller's context.

Spawn's own error (a pipe-setup or Start failure) always means no process was left running; the caller has nothing to clean up in that case.

func (*Process) Close

func (p *Process) Close() error

func (*Process) Read

func (p *Process) Read(b []byte) (int, error)

Read reads from the subprocess's stdout. Once the subprocess exits (on its own, or via Close/ctx cancellation), Read returns io.EOF like any closed pipe.

func (*Process) Write

func (p *Process) Write(b []byte) (int, error)

Write writes to the subprocess's stdin.

type Supervisor

type Supervisor struct {
	// Command builds a fresh *exec.Cmd for each (re)start. Required. A new Cmd
	// is needed per attempt because an exec.Cmd cannot be reused once started.
	Command func(ctx context.Context) *exec.Cmd

	// MaxRestarts caps how many times Serve respawns after the first attempt.
	// Zero means one attempt with no restarts.
	MaxRestarts int

	// Backoff returns the delay before restart attempt n (1-based); nil means
	// no delay. Serve honors ctx cancellation while waiting.
	Backoff func(attempt int) time.Duration

	// OnRestart, if set, is called just before each restart with the 1-based
	// restart number and the error that triggered it. It fires only for genuine
	// restarts — a startup or non-retryable error ends Serve without invoking
	// it, so its absence together with a returned error is itself a signal that
	// the failure was fatal (check libacp.IsStartupError on Serve's result).
	OnRestart func(attempt int, cause error)

	// SpawnOptions are forwarded to Spawn on every attempt (e.g. WithStderr).
	SpawnOptions []Option
}

Supervisor keeps an agent subprocess alive across transient crashes by respawning it with backoff and re-running a caller-supplied session. It is the opt-in restart policy acpexec deliberately leaves out of Spawn (which is pure transport): hash wraps exactly this loop around its agent subprocess (reset-and-retry with a resume candidate, tmp/hash acp.go:1206,1288). A driver that wants that resilience constructs a Supervisor; one that wants a single process keeps calling Spawn directly.

The hard-won lesson it encodes: a startup error (IsStartupError) — a missing binary or an agent that cannot initialize — is never retried, because looping on it only hides the misconfiguration. Serve surfaces such an error to the caller instead of restarting forever.

func (*Supervisor) Serve

func (s *Supervisor) Serve(ctx context.Context, session func(ctx context.Context, proc *Process, attempt int) error) error

Serve spawns the agent and runs session against each live Process until the session succeeds (returns nil), ctx is cancelled, or a failure that is not worth retrying occurs. session receives a zero-based attempt counter (0 on the first spawn), so a restart (attempt > 0) can try session/resume before falling back to session/new — the resume-candidate memory hash keeps across reconnects (acp.go:1212), kept here as the caller's own closure state rather than baked into transport-only acpexec.

A spawn/start failure is wrapped as libacp.ErrAgentStartFailed and returned immediately (never retried). A session error is retried, up to MaxRestarts, only when libacp.IsRetryableError says so and it is not a startup error; otherwise it is returned as-is.

Jump to

Keyboard shortcuts

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