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 validated one-shot form: a positional `-p <prompt>` with stream-json output, which Claude runs to a terminal result and exits. The reader (event → status, result envelope, scrollback rendering) is the live v1 surface. The stdin *control protocol* here (interrupt, get_context_usage, can_use_tool approvals) additionally requires `--input-format stream-json`, which v1 does not pass — so those control paths are present but dormant until the deferred bidirectional-control phase wires them. 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.
Index ¶
- type Opts
- type PermissionDecision
- type PermissionRequest
- 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) Fd() uintptr
- 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) NotifyUserInput()
- func (s *Session) PeakRSSBytes() int64
- func (s *Session) Pgid() int
- func (s *Session) Poke()
- func (s *Session) ProcessPID() int
- func (s *Session) RecentlyAdopted(time.Duration) bool
- func (s *Session) Resize(uint16, uint16) error
- 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) WaitForUserIdle(time.Duration, time.Duration) bool
- 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
// OnPermission is invoked for each inbound can_use_tool request. If nil,
// every tool request is denied (fail-closed — a headless session must not
// block on a human that will never answer). The callback must not block
// indefinitely; it runs on the read loop's goroutine via a worker.
OnPermission func(PermissionRequest) PermissionDecision
}
Opts configures a headless session launch. It mirrors the fields pty.SessionOpts needs, plus the approval callback.
type PermissionDecision ¶
type PermissionDecision struct {
Allow bool
// Reason is surfaced to the agent on deny.
Reason string
}
PermissionDecision is graith's answer to a PermissionRequest.
type PermissionRequest ¶
type PermissionRequest struct {
RequestID string
ToolName string
// Input is the raw tool input JSON, passed through to the approval backend.
Input json.RawMessage
}
PermissionRequest is an inbound can_use_tool control request: the CLI asking graith to approve a tool call.
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 body.
func (*Session) DetachWriter ¶
func (*Session) ExitSignal ¶
func (*Session) Fd ¶
Fd has no meaning for a pipe-backed session (there is no ptmx). It returns 0; the daemon's upgrade FD-handoff skips sessions it can't hand off.
func (*Session) Interrupt ¶
Interrupt interrupts the running agent. In v1 one-shot mode the CLI is launched without `--input-format stream-json`, so there is no stdin control channel to carry an `interrupt` control request (it would just time out) — Interrupt therefore sends SIGINT to the process group. The count/delay arguments exist only for SessionDriver compatibility. (When the bidirectional control channel is enabled in a later phase, this becomes a control request — see sendControlInterrupt.)
func (*Session) LastOutputAt ¶
func (*Session) NotifyUserInput ¶
func (s *Session) NotifyUserInput()
NotifyUserInput is a no-op: headless sessions have no attached human typing.
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 vt10x screen, 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) WaitForUserIdle ¶
WaitForUserIdle returns immediately true: no interactive user to wait on.
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).
type Snapshot ¶
type Snapshot struct {
Status Status
ToolName string
Result *ResultEnvelope
Degraded bool // reader hit malformed control frames / decode failures
}
Snapshot is the structured status the daemon reads off a headless driver in place of PTY scraping.