Documentation
¶
Overview ¶
Package headless drives a Claude Code agent in headless stream-json mode (`claude -p --output-format stream-json`) instead of an interactive PTY. It parses the typed event stream for status and the terminal result cost/usage envelope. See docs/design/2026-07-13-headless-stream-json-design.md (#1075).
v1 runs the one-shot control-channel form: `claude -p --output-format stream-json --input-format stream-json --verbose`. The prompt is delivered as an initial stdin user message (not a positional arg), and stdin stays open so graith can issue an `interrupt` control request. Graith does not request or service native approvals; an unexpected inbound `can_use_tool` request is diagnosed and denied immediately because headless mode has no agent TUI. Stdin closes on the terminal `result` so the process exits, preserving one-shot semantics. See issue #1136 (Phase 4).
The control protocol is an SDK-internal contract, not a documented CLI API, so everything is written defensively (unknown message types ignored, malformed data lines skipped) and the whole feature is gated behind an experimental flag by the daemon. The wire shapes here are pinned to the forms empirically verified against claude 2.1.211 — notably the *asymmetric* request-id placement (see below).
Index ¶
- type Opts
- type ResultEnvelope
- type Session
- func (s *Session) Attach(w io.Writer)
- func (s *Session) BytesRead() int64
- func (s *Session) Close()
- func (s *Session) ContextUsage() (json.RawMessage, error)
- func (s *Session) CreatedAt() time.Time
- func (s *Session) Detach()
- func (s *Session) DetachWriter(w io.Writer)
- func (s *Session) Done() <-chan struct{}
- func (s *Session) ExitCode() int
- func (s *Session) ExitSignal() syscall.Signal
- func (s *Session) Exited() bool
- func (s *Session) ForceKill() error
- func (s *Session) Interrupt(_ int, _ time.Duration) error
- func (s *Session) Kill() error
- func (s *Session) LastOutputAt() time.Time
- func (s *Session) PeakRSSBytes() int64
- func (s *Session) Pgid() int
- func (s *Session) ProcessPID() int
- func (s *Session) RecentlyAdopted(time.Duration) bool
- func (s *Session) ScreenPreview() string
- func (s *Session) ScreenSnapshot() grpty.ScreenCapture
- func (s *Session) ScrollbackFile() *grpty.Scrollback
- func (s *Session) Snapshot() Snapshot
- func (s *Session) WasAdopted() bool
- func (s *Session) WriteInput(data []byte) error
- func (s *Session) WriteInputAndSubmit(data []byte) error
- type Snapshot
- type Status
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Opts ¶
type Opts struct {
ID string
Command string
Args []string
Dir string
Env map[string]string
LogPath string
MaxLogSize int64
// Processing limits (issue #1250). Each is optional: a zero/non-positive
// value falls back to the matching package default, so direct callers and
// tests that leave them unset keep the historical behaviour.
//
// MaxLineBytes bounds a single stream-json line read from stdout.
// ControlTimeout bounds a synchronous control request round-trip.
// InterruptTimeout bounds the interrupt control round-trip.
// PreviewBytes bounds the scrollback tail rendered by the preview/snapshot.
MaxLineBytes int
ControlTimeout time.Duration
InterruptTimeout time.Duration
PreviewBytes int
// Prompt is the initial turn, sent as a stream-json user message on stdin
// right after launch (the control-channel launch takes no positional
// prompt). It should be non-empty: with the control channel and no
// positional prompt, an empty prompt gives the CLI no turn to run, so it
// blocks on stdin and never reaches a result (the daemon guards against this
// at session creation).
Prompt string
// Control reports whether the process was launched with the stdin control
// channel (`--input-format stream-json`). When true, Interrupt issues an
// `interrupt` control request and stdin is closed on the terminal result so
// the one-shot process exits; when false, Interrupt falls back to SIGINT.
Control bool
}
Opts configures a headless session launch. It mirrors the fields pty.SessionOpts needs, plus the initial prompt.
type ResultEnvelope ¶
type ResultEnvelope struct {
IsError bool `json:"is_error"`
TotalCost float64 `json:"total_cost_usd"`
NumTurns int `json:"num_turns"`
DurationMS int64 `json:"duration_ms"`
DurationAPI int64 `json:"duration_api_ms"`
Usage json.RawMessage `json:"usage"`
Text string `json:"result"`
At time.Time `json:"-"`
}
ResultEnvelope is the terminal `result` message: the structured cost/usage summary the design feeds into token accounting (#644). A one-shot headless session emits exactly one of these.
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session is a headless stream-json agent process. It satisfies the daemon's SessionDriver surface so the session manager can hold it interchangeably with a *pty.Session.
func New ¶
New launches a headless session: starts the process with piped stdin/stdout/stderr and starts the read/stderr/wait goroutines. It returns an error if the process fails to start. (v1 one-shot mode does not perform an initialize handshake — that belongs to the deferred bidirectional control phase.)
func (*Session) ContextUsage ¶
func (s *Session) ContextUsage() (json.RawMessage, error)
ContextUsage issues a get_context_usage control request and returns the raw response payload.
func (*Session) DetachWriter ¶
func (*Session) ExitSignal ¶
func (*Session) Interrupt ¶
Interrupt interrupts the running agent. With the control channel enabled it issues an `interrupt` control request (clean, acknowledged), falling back to a SIGINT to the process group if the control round-trip fails (channel wedged, timed out, or process already exited). Without the control channel it sends SIGINT directly. The count/delay arguments exist only for SessionDriver compatibility (the control interrupt is a single acknowledged request).
func (*Session) LastOutputAt ¶
func (*Session) PeakRSSBytes ¶
PeakRSSBytes is not tracked for headless sessions.
func (*Session) Pgid ¶
Pgid returns the process-group id graith signals on Kill/ForceKill. Like *pty.Session, a headless session is started with Setsid, so the child is a group leader and its PGID equals its PID. Returns 0 when the pid is unknown. Part of the SessionDriver interface (issue #1104).
func (*Session) ProcessPID ¶
func (*Session) RecentlyAdopted ¶
RecentlyAdopted is always false: headless sessions are not adopted across daemon restart (their stdout pipe can't be re-read); the daemon resumes them.
func (*Session) ScreenPreview ¶
ScreenPreview returns a plain-text tail of the rendered scrollback. A headless session has no terminal screen emulator, so the overlay preview and the screen_preview control message degrade to the recent rendered output.
func (*Session) ScreenSnapshot ¶
func (s *Session) ScreenSnapshot() grpty.ScreenCapture
ScreenSnapshot returns a ScreenCapture whose Frame is the scrollback tail, so callers expecting a PTY snapshot get something sensible for a headless session.
func (*Session) ScrollbackFile ¶
func (s *Session) ScrollbackFile() *grpty.Scrollback
func (*Session) WasAdopted ¶
WasAdopted is always false: headless sessions are not adopted across daemon restart (their stdout pipe can't be re-read).
func (*Session) WriteInput ¶
WriteInput sends the bytes as a stream-json user message (a new turn).
func (*Session) WriteInputAndSubmit ¶
WriteInputAndSubmit is identical to WriteInput for headless: a user message is a complete, submitted turn (there is no separate submit key).