Documentation
¶
Overview ¶
Package shellsession manages one persistent PTY-backed shell per chat session, rooted at the session's workspace root and outliving individual commands. Output lives in a bounded scrollback ring with monotonically increasing offsets; the agent never receives it streamed and must read scrollback explicitly. Run submits exactly one line, gated by HITL.
Index ¶
Constants ¶
const ( ToolsProviderName = "shell_session" ToolRun = "shell_session_run" ToolRead = "shell_session_read" )
Tool names: shell_session is one ToolsRepo exposing two function tools — Run gated by HITL (default policy approve), Read ungated (reference-only).
Variables ¶
var ErrNoSession = errors.New("shellsession: no live shell for this session")
ErrNoSession is returned by Write for a session with no live shell, distinct from a spawn failure: the caller addressed something that is not there.
Functions ¶
func NewTools ¶
func NewTools(mgr Manager) taskengine.ToolsRepo
NewTools returns the shell_session ToolsRepo, registered in the engine's LocalTools map under ToolsProviderName like local_shell/local_fs so it is HITL-wrapped and reachable only when shell tooling is on.
Types ¶
type Chunk ¶
Chunk is one batch of terminal output delivered to a subscriber: Offset is where Data begins in scrollback, and Reset marks a snapshot (initial or after PTY restart) the consumer should replace rather than append.
type Config ¶
type Config struct {
// CwdResolver returns the workspace root a new shell should be rooted at, given the tool/request context (which carries the session id); required.
CwdResolver func(ctx context.Context) string
// Workspace is the operator's workspace-root allowlist enforced against CwdResolver's output and the only source of the default root; nil means no allowlist, so an absolute cwd is taken as given.
Workspace *vfs.Factory
// Shell overrides the shell executable; empty picks a platform default.
Shell string
// ScrollbackBytes bounds retained output per shell (default 64 KiB).
ScrollbackBytes int
// IdleTimeout kills inactive shells (default 15m; <=0 disables reaping).
IdleTimeout time.Duration
// ScrubEnv, when set, maps the parent environment to the one a spawned shell inherits, so serve's own secrets never reach an agent-reachable PTY; nil inherits the full environment.
ScrubEnv func([]string) []string
// Interactive spawns shells for a HUMAN at a real terminal (ECHO on, shell draws its own prompt); the default (false) is the agent-facing posture — echo off, prompt suppressed — since output is scrollback for a model to read.
Interactive bool
// OnExit, when set, is invoked once per shell when it terminates, from a dedicated goroutine, for every cause (process exit, Kill, idle reap, Shutdown); total, never called twice for the same shell.
OnExit func(sessionID string)
}
Config configures a Manager; zero values fall back to sane defaults.
type Manager ¶
type Manager interface {
// Run ensures a shell exists for sessionID (rooted via the cwd resolver against ctx, used only at creation time) and submits one line to it.
Run(ctx context.Context, sessionID, line string) (RunResult, error)
// Open ensures a shell exists for sessionID without submitting anything, so an interactive client can attach before the first keystroke; idempotent, an already-live shell is returned as-is.
Open(ctx context.Context, sessionID string) error
// Write feeds raw bytes to sessionID's shell stdin VERBATIM (unlike Run, no newline or line discipline, since the bytes are a human's keystrokes); never creates a shell — an unknown session is ErrNoSession.
Write(sessionID string, data []byte) error
// Read returns scrollback for sessionID (bytes since `since` when since >= 0, otherwise the last `tailBytes`); never creates a shell.
Read(sessionID string, since int64, tailBytes int) ReadResult
// Resize records the terminal geometry for sessionID and applies it to the live shell if any; total (unknown session, reaped shell, or non-positive dimension are no-ops), and remembered even with no live shell so the next one is born at it.
Resize(sessionID string, rows, cols int)
// Subscribe registers fn for live output of sessionID, invoked from a dedicated goroutine so a slow consumer cannot stall the PTY; the current scrollback is delivered immediately as a Reset chunk.
Subscribe(sessionID string, fn func(Chunk)) (cancel func())
// Kill terminates and forgets sessionID's shell (session close/delete).
Kill(sessionID string)
// Shutdown kills every shell and stops the reaper.
Shutdown()
}
Manager owns the process-global set of per-session shells; all methods are safe for concurrent use and key on the internal chat-session id.
func NewManager ¶
NewManager builds a Manager and starts its idle reaper.
type ReadResult ¶
ReadResult is a scrollback slice: the content, the offset it starts at, and the current end marker to hand to the next read.
type ReadResultJSON ¶
type ReadResultJSON struct {
Content string `json:"content"`
FromOffset int64 `json:"from_offset"`
NextOffset int64 `json:"next_offset"`
Exists bool `json:"exists"`
Note string `json:"note,omitempty"`
}
ReadResultJSON is the structured result of a scrollback read.
type RunResult ¶
type RunResult struct {
Offset int64
Snapshot string
Started bool // a new shell was created for this run
}
RunResult is what Run returns after submitting a line: the scrollback end marker and a best-effort snapshot of the output captured within the initial window (empty when the command is still running silently).
type RunResultJSON ¶
type RunResultJSON struct {
Offset int64 `json:"offset"`
Output string `json:"output"`
Started bool `json:"started_new_shell,omitempty"`
Note string `json:"note,omitempty"`
}
RunResultJSON is the structured result the agent receives from a run: a marker and the initial output snapshot, followed up via shell_session_read with the returned offset.