Documentation
¶
Overview ¶
Package backend defines the ProcessBackend interface for managing agent processes. It abstracts away the underlying process management mechanism so the daemon and CLI can work with any backend.
Interface methods:
- CreateSession/DestroySession manage logical session groupings
- StartAgent/StopAgent launch and terminate agent processes
- SendMessage/SendInterrupt deliver input to agents
- IsAgentAlive checks agent process health
- Attach connects a terminal to an agent
- GetOutputReader streams agent output
Index ¶
- Variables
- type AgentConfig
- type AgentHandle
- type BackendInfo
- type DirectBackend
- func (b *DirectBackend) Attach(ctx context.Context, session, agentName string, readOnly bool) error
- func (b *DirectBackend) Available() bool
- func (b *DirectBackend) CreateSession(ctx context.Context, name string) error
- func (b *DirectBackend) DestroySession(ctx context.Context, name string) error
- func (b *DirectBackend) GetOutputReader(ctx context.Context, session, agentName string) (io.ReadCloser, error)
- func (b *DirectBackend) HasSession(ctx context.Context, name string) (bool, error)
- func (b *DirectBackend) IsAdopted(ctx context.Context, session, agentName string) (bool, error)
- func (b *DirectBackend) IsAgentAlive(ctx context.Context, session, agentName string) (bool, error)
- func (b *DirectBackend) ListSessions(ctx context.Context) ([]string, error)
- func (b *DirectBackend) Name() string
- func (b *DirectBackend) SendEscape(ctx context.Context, session, agentName string) error
- func (b *DirectBackend) SendInterrupt(ctx context.Context, session, agentName string) error
- func (b *DirectBackend) SendMessage(ctx context.Context, session, agentName, message string) error
- func (b *DirectBackend) StartAgent(ctx context.Context, cfg AgentConfig) (*AgentHandle, error)
- func (b *DirectBackend) StopAgent(ctx context.Context, session, agentName string) error
- func (b *DirectBackend) SubscribeEvents(ctx context.Context, session, agentName string) (uint64, <-chan sidecar.Event, []sidecar.Event, func(), error)
- func (b *DirectBackend) SubscribeOutput(session, agentName string) (uint64, <-chan string, func(), error)
- func (b *DirectBackend) SubscribeOutputLive(session, agentName string) (uint64, <-chan string, func(), error)
- type LiteralSender
- type PaneInspector
- type PipePaneManager
- type ProcessBackend
- type SidecarSubscriber
- type TerminalAdapter
- func (a *TerminalAdapter) GetPanePID(ctx context.Context, session, window string) (int, error)
- func (a *TerminalAdapter) SendEnter(ctx context.Context, session, window string) error
- func (a *TerminalAdapter) SendKeys(ctx context.Context, session, window, text string) error
- func (a *TerminalAdapter) SendKeysLiteral(ctx context.Context, session, window, text string) error
- func (a *TerminalAdapter) SendKeysLiteralWithEnter(ctx context.Context, session, window, text string) error
- func (a *TerminalAdapter) StartPipePane(ctx context.Context, session, window, outputFile string) error
- func (a *TerminalAdapter) StopPipePane(ctx context.Context, session, window string) error
Constants ¶
This section is empty.
Variables ¶
var ErrAgentAdopted = fmt.Errorf("agent was re-adopted without PTY; restart required for input")
ErrAgentAdopted is returned when trying to send input to a re-adopted process whose PTY fd was lost during a daemon restart. The caller should restart the agent.
Functions ¶
This section is empty.
Types ¶
type AgentConfig ¶
type AgentConfig struct {
// SessionName is the session this agent belongs to.
SessionName string
// AgentName is the unique name for this agent within the session
// (e.g., "supervisor", "worker-0").
AgentName string
// WorkDir is the working directory for the agent process.
WorkDir string
// BinaryPath is the path to the agent binary (e.g., "oat-agent").
BinaryPath string
// Args are command-line arguments for the agent binary.
Args []string
// Env is additional environment variables for the agent process.
// Each entry is "KEY=VALUE".
Env []string
// EnvPrefix is a shell prefix run before the agent binary
// (e.g., "source ~/.zshrc; export GH_TOKEN=...;").
//
// SECURITY: This string is concatenated verbatim into the
// `$SHELL -l -c <cmd>` invocation in DirectBackend.Spawn. Anything
// placed here must already be shell-safe — values quoted via
// quoteShellValue (or equivalent), identifiers validated. Never
// populate this field from caller-supplied or repo-supplied data.
// The only sanctioned producer is daemon.buildAgentEnvPrefix.
EnvPrefix string
// InitialPrompt is the system prompt file path to write before launch.
// If non-empty, written to .oat/AGENTS.md in the WorkDir.
InitialPrompt string
// LogFile is the path to capture agent output. If empty, a default
// path is derived from the session/agent name.
LogFile string
// MOTD is an optional message of the day to display before the agent starts.
MOTD string
// SidecarPath, when non-empty, tells the backend to bind a Unix-socket
// sidecar.Server at that path before the agent process is spawned. The
// path is also threaded into the agent's env as OAT_SIDECAR_SOCKET so
// the Python sidecar_emitter can connect. Empty = sidecar disabled
// (current production default; behavior is unchanged).
SidecarPath string
}
AgentConfig contains all parameters needed to start an agent.
type AgentHandle ¶
type AgentHandle struct {
// PID is the OS process ID of the agent.
PID int
// Stdin provides write access to the agent's input.
// May be nil if the backend manages input internally.
Stdin io.WriteCloser
// Stdout provides read access to the agent's output.
// May be nil if the backend manages output internally.
Stdout io.ReadCloser
// LogFile is the path where agent output is being captured.
LogFile string
}
AgentHandle represents a running agent process.
type BackendInfo ¶
type BackendInfo interface {
// Name returns the backend identifier (e.g., "direct").
Name() string
// Available returns true if this backend can be used in the current
// environment (e.g., PTY support exists).
Available() bool
}
BackendInfo provides metadata about a backend implementation. This is separate from ProcessBackend to allow checking availability without instantiating a full backend.
type DirectBackend ¶
type DirectBackend struct {
// contains filtered or unexported fields
}
DirectBackend implements ProcessBackend using creack/pty. Sessions are logical groupings tracked in memory. Agents run as child processes with a real PTY so interactive CLI tools (like oat-agent) work correctly.
Session metadata is persisted to dataDir/backend-sessions.json so that still-running agent processes can be re-adopted after a daemon restart.
func NewDirectBackend ¶
func NewDirectBackend() *DirectBackend
NewDirectBackend creates a new DirectBackend without persistence.
func NewDirectBackendWithDataDir ¶
func NewDirectBackendWithDataDir(dataDir string) *DirectBackend
NewDirectBackendWithDataDir creates a DirectBackend that persists session metadata to dataDir/backend-sessions.json. On construction it re-adopts any agent processes that are still alive from a previous daemon instance.
func (*DirectBackend) Attach ¶
Attach connects the current terminal to the agent. If stdin is a terminal, provides interactive PTY proxy. Otherwise, tails the log file.
func (*DirectBackend) Available ¶
func (b *DirectBackend) Available() bool
Available returns true — direct PTY is always available on Unix.
func (*DirectBackend) CreateSession ¶
func (b *DirectBackend) CreateSession(ctx context.Context, name string) error
CreateSession creates a logical session (just a map entry).
func (*DirectBackend) DestroySession ¶
func (b *DirectBackend) DestroySession(ctx context.Context, name string) error
DestroySession stops all agents in the session and removes it.
func (*DirectBackend) GetOutputReader ¶
func (b *DirectBackend) GetOutputReader(ctx context.Context, session, agentName string) (io.ReadCloser, error)
GetOutputReader opens the agent's log file for reading.
func (*DirectBackend) HasSession ¶
HasSession checks if a session exists.
func (*DirectBackend) IsAdopted ¶
IsAdopted returns true if the given agent was re-adopted after a daemon restart and has no PTY — meaning it can be monitored but not sent input.
func (*DirectBackend) IsAgentAlive ¶
IsAgentAlive checks if the agent process is still running.
func (*DirectBackend) ListSessions ¶
func (b *DirectBackend) ListSessions(ctx context.Context) ([]string, error)
ListSessions returns all session names.
func (*DirectBackend) SendEscape ¶
func (b *DirectBackend) SendEscape(ctx context.Context, session, agentName string) error
SendEscape sends Escape (0x1b) to the agent's PTY. Cancels "Thinking..." state without killing the process.
func (*DirectBackend) SendInterrupt ¶
func (b *DirectBackend) SendInterrupt(ctx context.Context, session, agentName string) error
SendInterrupt sends Ctrl-C (0x03) to the agent's PTY. For adopted processes without a PTY, falls back to sending SIGINT.
func (*DirectBackend) SendMessage ¶
func (b *DirectBackend) SendMessage(ctx context.Context, session, agentName, message string) error
SendMessage sends text + Enter to the agent's PTY. Acquires per-agent lock to prevent concurrent writes from interleaving and to prevent writing to a PTY that is being closed by StopAgent. Returns ErrAgentAdopted if the agent was re-adopted without a PTY.
func (*DirectBackend) StartAgent ¶
func (b *DirectBackend) StartAgent(ctx context.Context, cfg AgentConfig) (*AgentHandle, error)
StartAgent launches an agent process with a real PTY.
func (*DirectBackend) StopAgent ¶
func (b *DirectBackend) StopAgent(ctx context.Context, session, agentName string) error
StopAgent terminates a running agent: SIGTERM → wait 5s → SIGKILL.
func (*DirectBackend) SubscribeEvents ¶
func (b *DirectBackend) SubscribeEvents( ctx context.Context, session, agentName string, ) (uint64, <-chan sidecar.Event, []sidecar.Event, func(), error)
SubscribeEvents returns a live channel of sidecar events for the given agent, along with any ring-buffered events the new subscriber missed. Implements the optional SidecarSubscriber interface so the daemon's stream_events command can deliver structured events to the TUI.
When the agent has no sidecar (OAT_USE_SIDECAR unset), the broadcaster still exists but Publish is never called, so the returned channel simply never fires and catchup is nil — the protocol is stable in both states.
func (*DirectBackend) SubscribeOutput ¶
func (b *DirectBackend) SubscribeOutput(session, agentName string) (uint64, <-chan string, func(), error)
SubscribeOutput subscribes to live ANSI-stripped output from the given agent. Returns the subscription ID, a channel of lines, and a cancel function. The channel includes catch-up lines from the ring buffer. Returns an error if the agent is not found or has no broadcaster (adopted).
func (*DirectBackend) SubscribeOutputLive ¶
func (b *DirectBackend) SubscribeOutputLive(session, agentName string) (uint64, <-chan string, func(), error)
SubscribeOutputLive is like SubscribeOutput but skips ring buffer catch-up. Use this when the caller already has prior content and only wants new lines.
type LiteralSender ¶
type LiteralSender interface {
// SendKeysLiteral sends text to the agent without appending Enter.
SendKeysLiteral(ctx context.Context, session, agent, text string) error
// SendEnter sends just the Enter key.
SendEnter(ctx context.Context, session, agent string) error
}
LiteralSender is an optional interface that backends can implement to support sending text without a trailing Enter key.
type PaneInspector ¶
type PaneInspector interface {
GetPanePID(ctx context.Context, session, agent string) (int, error)
}
PaneInspector is an optional interface for backends that support inspecting the pane PID.
type PipePaneManager ¶
type PipePaneManager interface {
StartPipePane(ctx context.Context, session, agent, outputFile string) error
StopPipePane(ctx context.Context, session, agent string) error
}
PipePaneManager is an optional interface for backends that support output capture via pipe-pane.
type ProcessBackend ¶
type ProcessBackend interface {
// CreateSession creates a new logical session (a grouping of agents).
CreateSession(ctx context.Context, name string) error
// DestroySession tears down a session and all agents within it.
DestroySession(ctx context.Context, name string) error
// HasSession checks whether a session exists.
HasSession(ctx context.Context, name string) (bool, error)
// StartAgent launches an agent process within a session.
// Returns a handle to the running agent.
StartAgent(ctx context.Context, cfg AgentConfig) (*AgentHandle, error)
// StopAgent terminates a running agent. Sends SIGTERM, waits, then SIGKILL.
StopAgent(ctx context.Context, session, agent string) error
// IsAgentAlive checks whether an agent process is still running.
IsAgentAlive(ctx context.Context, session, agent string) (bool, error)
// SendMessage sends text to an agent's stdin followed by Enter.
// Must be safe for concurrent use (implementations serialize per-agent).
SendMessage(ctx context.Context, session, agent, message string) error
// SendInterrupt sends Ctrl-C to an agent.
SendInterrupt(ctx context.Context, session, agent string) error
// SendEscape sends the Escape key (0x1b) to an agent.
// Used to cancel a "Thinking..." state without killing the process.
SendEscape(ctx context.Context, session, agent string) error
// GetOutputReader returns a reader for an agent's output stream.
GetOutputReader(ctx context.Context, session, agent string) (io.ReadCloser, error)
// Attach connects the current terminal to an agent for interactive use.
// If stdin is a terminal, provides interactive PTY proxy; otherwise tails log.
// If readOnly is true, attaches in read-only mode.
Attach(ctx context.Context, session, agent string, readOnly bool) error
// ListSessions returns all session names managed by this backend.
// Used by cleanup/repair to find orphaned sessions.
ListSessions(ctx context.Context) ([]string, error)
}
ProcessBackend abstracts agent process lifecycle management. The DirectBackend implementation uses creack/pty for process management.
func BackendFromEnv ¶
func BackendFromEnv() ProcessBackend
BackendFromEnv creates a ProcessBackend using the OAT_BACKEND environment variable. This is the standard entry point for CLI initialization (no persistence).
func NewBackend ¶
func NewBackend(preference string, dataDir string) ProcessBackend
NewBackend creates a ProcessBackend based on the preference string. Supported values:
- "direct": use DirectBackend (uses creack/pty)
- "" (empty): defaults to DirectBackend
dataDir is the directory for persisting session metadata (e.g., ~/.oat/). If empty, session persistence is disabled.
type SidecarSubscriber ¶
type SidecarSubscriber interface {
// SubscribeEvents returns a live channel of sidecar events for
// (session, agent). catchup holds the most-recent buffered events
// at subscription time in chronological order — deliver those to
// the consumer before draining the channel so a mid-session TUI
// sees prior context immediately. cancel is idempotent and must
// be called when the consumer is done to free the subscriber slot.
SubscribeEvents(ctx context.Context, session, agent string) (
subID uint64,
events <-chan sidecar.Event,
catchup []sidecar.Event,
cancel func(),
err error,
)
}
SidecarSubscriber is an optional interface for backends that expose a structured-event sidecar stream per agent. Consumers (e.g., the daemon's stream_events command) type-assert on this rather than adding the method to ProcessBackend, so non-sidecar backends (tmux, container) don't have to stub an unsupported method.
The subscription succeeds even when the sidecar is off for that agent — the channel just never fires. That keeps the protocol stable regardless of feature-flag state.
type TerminalAdapter ¶
type TerminalAdapter struct {
Backend ProcessBackend
}
TerminalAdapter wraps a ProcessBackend to implement the agent.TerminalRunner interface. This bridges the old terminal-centric API with the new backend abstraction, allowing the agent.Runner to work with any ProcessBackend.
In the TerminalRunner interface, "session" and "window" map to the backend's "session" and "agent" parameters respectively.
Optional capabilities (LiteralSender, PaneInspector, PipePaneManager) are detected via interface assertion — no backend-specific type assertions needed.
func NewTerminalAdapter ¶
func NewTerminalAdapter(b ProcessBackend) *TerminalAdapter
NewTerminalAdapter creates a TerminalAdapter for the given backend.
func (*TerminalAdapter) GetPanePID ¶
GetPanePID gets the process ID running in a pane. Uses the PaneInspector interface if available. For backends that don't support this (DirectBackend), callers should use AgentHandle.PID instead.
func (*TerminalAdapter) SendEnter ¶
func (a *TerminalAdapter) SendEnter(ctx context.Context, session, window string) error
SendEnter sends just the Enter key.
func (*TerminalAdapter) SendKeys ¶
func (a *TerminalAdapter) SendKeys(ctx context.Context, session, window, text string) error
SendKeys sends text followed by Enter. Maps to Backend.SendMessage.
func (*TerminalAdapter) SendKeysLiteral ¶
func (a *TerminalAdapter) SendKeysLiteral(ctx context.Context, session, window, text string) error
SendKeysLiteral sends text without pressing Enter. Uses the LiteralSender interface if the backend supports it, otherwise falls back to SendMessage (which includes Enter).
func (*TerminalAdapter) SendKeysLiteralWithEnter ¶
func (a *TerminalAdapter) SendKeysLiteralWithEnter(ctx context.Context, session, window, text string) error
SendKeysLiteralWithEnter sends text + Enter atomically. Maps to Backend.SendMessage.
func (*TerminalAdapter) StartPipePane ¶
func (a *TerminalAdapter) StartPipePane(ctx context.Context, session, window, outputFile string) error
StartPipePane starts capturing pane output to a file. Uses PipePaneManager if available. Other backends handle output capture internally (e.g., DirectBackend tees PTY output to log file).
func (*TerminalAdapter) StopPipePane ¶
func (a *TerminalAdapter) StopPipePane(ctx context.Context, session, window string) error
StopPipePane stops capturing pane output.