Documentation
¶
Overview ¶
Package session is the agent harness's session runner: it runs an agent's session for an assignment on a turn executor, in a git worktree of its own, with the session gates installed and every event logged under the run's trace.
The runner is a library. It owns no process: the binary that mounts it grants the process capability every git, gh and Claude Code child starts through, names the state directory and supplies the command Claude Code calls for each gate.
Index ¶
- Constants
- Variables
- func PushRemoteURL(ctx context.Context, launcher proc.ILauncher, worktree string) (string, error)
- func ReadRecipe(directory string) (*pb.AgentAssignmentRecipe, error)
- func RunDirectory(stateDirectory string, assignmentID string) string
- func RunTrace(state *RunState) *telemetryv1.TraceContext
- func WriteRunState(directory string, state *RunState) error
- type AgentSessionRunner
- func (runner *AgentSessionRunner) Open(ctx context.Context, recipe *pb.AgentAssignmentRecipe, environment ...string) (*OpenSession, error)
- func (runner *AgentSessionRunner) Reopen(ctx context.Context, recipe *pb.AgentAssignmentRecipe, environment ...string) (*OpenSession, error)
- func (runner *AgentSessionRunner) Resume(ctx context.Context, recipe *pb.AgentAssignmentRecipe, prompt string) (*pb.AgentAssignmentReceipt, error)
- func (runner *AgentSessionRunner) Run(ctx context.Context, recipe *pb.AgentAssignmentRecipe) (*pb.AgentAssignmentReceipt, error)
- func (runner *AgentSessionRunner) StateDirectory() string
- type AgentSessionRunnerOption
- func WithClaudeExecutable(executable string) AgentSessionRunnerOption
- func WithGateCommand(arguments ...string) AgentSessionRunnerOption
- func WithLauncher(launcher proc.ILauncher) AgentSessionRunnerOption
- func WithOpenTurnExecutors(factory OpenTurnExecutorFactory) AgentSessionRunnerOption
- func WithRouter(router IRouter) AgentSessionRunnerOption
- func WithStateDirectory(directory string) AgentSessionRunnerOption
- func WithTurnExecutors(factory TurnExecutorFactory) AgentSessionRunnerOption
- type EventLog
- func (log *EventLog) Close() error
- func (log *EventLog) Context(ctx context.Context) (context.Context, error)
- func (log *EventLog) Failure(ctx context.Context, session string, turn int, eventType string, ...)
- func (log *EventLog) Logger() *slog.Logger
- func (log *EventLog) Record(ctx context.Context, session string, turn int, eventType string, ...)
- type IOpenTurnExecutor
- type IRouter
- type ITurnExecutor
- type OpenSession
- func (open *OpenSession) Close(ctx context.Context) error
- func (open *OpenSession) Directory() string
- func (open *OpenSession) FirstPrompt() string
- func (open *OpenSession) Interrupt(ctx context.Context) error
- func (open *OpenSession) Plan() *pb.AgentAssignmentPlan
- func (open *OpenSession) State() RunState
- func (open *OpenSession) Turn(ctx context.Context, prompt string) (*pb.AgentAssignmentReceipt, error)
- type OpenTurnExecutorFactory
- type RouteRequest
- type RunState
- type TurnExecutorFactory
- type TurnExecutorSpec
Constants ¶
const ( KeyTraceID = "trace_id" KeySpanID = "span_id" KeyParentSpanID = "parent_span_id" KeySessionID = "session_id" KeyTurn = "turn" KeySequence = "sequence" KeyEventType = "event_type" KeyElapsed = "elapsed_ms" KeyError = "error" )
The keys every event log record carries. The turn executor's own records (ipc/model/claudecode) use the same spellings, so one log reads as one trace.
const ( EventTypeRunStarted = "harness_run_started" EventTypeWorktreeReady = "harness_worktree_ready" EventTypeTurnRequested = "harness_turn_requested" EventTypeRunFinished = "harness_run_finished" EventTypeGateDecision = "session_gate_decision" EventTypeReceiptWritten = "harness_receipt" )
Event types the harness itself writes; the turn executor writes the stream-json types of the events it forwards.
const ( HookPreToolUse = "PreToolUse" HookPostToolUse = "PostToolUse" // ToolBash is the tool both gates watch. ToolBash = "Bash" )
The Claude Code hook events the harness installs gates on, spelled as Claude Code's settings and hook input spell them.
const ( // RunStateFile records the run for its gates and for a resumed turn. RunStateFile = "run.json" // SettingsFile is the Claude Code settings the session starts with. SettingsFile = "settings.json" // EventsFile is the run's event log, one JSON object per line. EventsFile = "events.jsonl" // WorktreeDirectory is the git worktree the session works in. WorktreeDirectory = "worktree" )
The files of one run directory, <state directory>/<assignment id>.
const EventTypeRunResumed = "harness_run_resumed"
EventTypeRunResumed records that a run was reopened on its recorded conversation, after the harness restarted.
const EventTypeSessionClosed = "harness_session_closed"
EventTypeSessionClosed records that an open session's turn executor was closed.
const PushRemote = "origin"
PushRemote is the remote the harness pushes a work branch to and opens its pull request on.
const RecipeFile = "recipe.json"
RecipeFile holds the run's frozen recipe, so a restarted harness can reopen the run without being handed it again.
Variables ¶
var ( // ErrInvalidOption reports a nil option or an option value the runner // cannot use. ErrInvalidOption = errors.New("harness session: invalid option") // ErrNoLauncher reports a runner built without the process capability. ErrNoLauncher = errors.New("harness session: a process launcher is required") // ErrNoStateDirectory reports a runner built without a state directory. ErrNoStateDirectory = errors.New("harness session: an absolute state directory is required") // ErrNoGateCommand reports a runner built without the gate command. ErrNoGateCommand = errors.New("harness session: a gate command is required") // ErrInvalidRecipe reports a recipe the harness cannot run. ErrInvalidRecipe = errors.New("harness session: invalid recipe") // ErrSessionExists reports a run already started for the assignment; a // follow-up turn is a Resume. ErrSessionExists = errors.New("harness session: the assignment already has a session; resume it") // ErrWorktreeCollision reports a worktree path already occupied by // something the harness did not record. ErrWorktreeCollision = errors.New("harness session: the worktree path is already occupied") // ErrWorktree reports a worktree git could not create. ErrWorktree = errors.New("harness session: could not create the worktree") // ErrRunMismatch reports a recorded run that belongs to another recipe. ErrRunMismatch = errors.New("harness session: the recorded run belongs to a different recipe") // ErrNoPrompt reports a resumed turn with nothing to say. ErrNoPrompt = errors.New("harness session: a resumed turn needs a prompt") // ErrTurnFailed reports a turn the turn executor did not complete. ErrTurnFailed = errors.New("harness session: the turn failed") // ErrPushRemote reports a worktree whose push remote git could not name. ErrPushRemote = errors.New("harness session: could not read the push remote's URL") )
var ErrNoRunState = errors.New("harness session: no run is recorded in this directory")
ErrNoRunState reports a run directory with no recorded run.
var ErrSessionClosed = errors.New("harness session: the session is closed")
ErrSessionClosed reports a turn on an open session after Close.
Functions ¶
func PushRemoteURL ¶
PushRemoteURL is the URL of the worktree's push remote, as git records it: the repository gh is told to act on, so a clone whose gh default is another remote still gets its pull request where the branch was pushed.
func ReadRecipe ¶
func ReadRecipe(directory string) (*pb.AgentAssignmentRecipe, error)
ReadRecipe reads the recipe a run recorded when it opened.
func RunDirectory ¶
RunDirectory is the deterministic directory of an assignment's run.
func RunTrace ¶
func RunTrace(state *RunState) *telemetryv1.TraceContext
RunTrace is the run's root span recorded in state.
func WriteRunState ¶
WriteRunState replaces the run state atomically, so a gate never reads a half-written file. Gates and hooks use this to persist state changes like push times.
Types ¶
type AgentSessionRunner ¶
type AgentSessionRunner struct {
// contains filtered or unexported fields
}
AgentSessionRunner runs agent sessions for prepared assignments.
func NewAgentSessionRunner ¶
func NewAgentSessionRunner(options ...AgentSessionRunnerOption) (*AgentSessionRunner, error)
NewAgentSessionRunner validates the whole option set before building the runner.
func (*AgentSessionRunner) Open ¶
func (runner *AgentSessionRunner) Open(ctx context.Context, recipe *pb.AgentAssignmentRecipe, environment ...string) (*OpenSession, error)
Open prepares the assignment's session exactly as Run does, up to the first turn: the recipe is frozen, the worktree created, the gates installed and the turn executor opened on the session. The first turn is the caller's to run, with OpenSession.FirstPrompt. The worktree creation is bounded by ctx; the executor's process is bounded by the session, which Close ends.
func (*AgentSessionRunner) Reopen ¶
func (runner *AgentSessionRunner) Reopen(ctx context.Context, recipe *pb.AgentAssignmentRecipe, environment ...string) (*OpenSession, error)
Reopen reopens a run a previous harness process left open: the same assignment, worktree, branch and trace, and the turn executor opened on the recorded conversation, resumed when a turn has run on it. No worktree is created and nothing is replayed.
func (*AgentSessionRunner) Resume ¶
func (runner *AgentSessionRunner) Resume(ctx context.Context, recipe *pb.AgentAssignmentRecipe, prompt string) (*pb.AgentAssignmentReceipt, error)
Resume runs a follow-up turn on the assignment's existing session, in the same worktree and under the same trace.
func (*AgentSessionRunner) Run ¶
func (runner *AgentSessionRunner) Run(ctx context.Context, recipe *pb.AgentAssignmentRecipe) (*pb.AgentAssignmentReceipt, error)
Run starts the assignment's session: it prepares the recipe, creates the worktree, installs the gates and runs the first turn on the task. The receipt is returned whenever the session was started, also when the turn failed, so its links are never lost with the error.
func (*AgentSessionRunner) StateDirectory ¶
func (runner *AgentSessionRunner) StateDirectory() string
StateDirectory is the directory every run lives under, as <assignment id>/.
type AgentSessionRunnerOption ¶
type AgentSessionRunnerOption func(runner *AgentSessionRunner) error
AgentSessionRunnerOption configures an AgentSessionRunner.
func WithClaudeExecutable ¶
func WithClaudeExecutable(executable string) AgentSessionRunnerOption
WithClaudeExecutable runs Claude Code from executable instead of the name claude on PATH.
func WithGateCommand ¶
func WithGateCommand(arguments ...string) AgentSessionRunnerOption
WithGateCommand is the command Claude Code calls for every gated hook, with the hook event and the run directory appended. Required.
func WithLauncher ¶
func WithLauncher(launcher proc.ILauncher) AgentSessionRunnerOption
WithLauncher grants the process capability git, gh and the turn executor start through. Required.
func WithOpenTurnExecutors ¶
func WithOpenTurnExecutors(factory OpenTurnExecutorFactory) AgentSessionRunnerOption
WithOpenTurnExecutors replaces the open Claude Code turn executor.
func WithRouter ¶
func WithRouter(router IRouter) AgentSessionRunnerOption
WithRouter routes every opened session: without one, each session opens a new conversation.
func WithStateDirectory ¶
func WithStateDirectory(directory string) AgentSessionRunnerOption
WithStateDirectory keeps each run under directory/<assignment id>. Required; the directory must be absolute.
func WithTurnExecutors ¶
func WithTurnExecutors(factory TurnExecutorFactory) AgentSessionRunnerOption
WithTurnExecutors replaces the Claude Code turn executor.
type EventLog ¶
type EventLog struct {
// contains filtered or unexported fields
}
EventLog appends one run's records to its events.jsonl. The runner and every gate process open it for appending; each record is one write of one line, so records from the session and its hooks interleave whole.
func OpenEventLog ¶
func OpenEventLog(directory string, trace *telemetryv1.TraceContext) (*EventLog, error)
OpenEventLog opens the event log of the run in directory for appending, under trace, the run's root span.
func (*EventLog) Context ¶
Context is ctx carrying the run's trace, so spans derived from it, the turn executor's included, belong to the run.
func (*EventLog) Failure ¶
func (log *EventLog) Failure(ctx context.Context, session string, turn int, eventType string, message string, err error, attributes ...slog.Attr)
Failure writes one error record of eventType under a new child span.
type IOpenTurnExecutor ¶
type IOpenTurnExecutor interface {
ITurnExecutor
Interrupt(ctx context.Context) error
Close(ctx context.Context) error
}
IOpenTurnExecutor is a turn executor kept open across turns: the Claude Code process whose stream-json input stays open in production, a double in specs. Interrupt asks it to end the running turn at its next safepoint; Close ends it and joins what it started.
type IRouter ¶
type IRouter interface {
Route(ctx context.Context, request RouteRequest) (string, error)
}
IRouter decides which real session, a Claude Code conversation, a virtual session runs on: an existing one it fits well in, which it resumes, or request.Session, a new one.
type ITurnExecutor ¶
type ITurnExecutor interface {
Propose(ctx context.Context, turn *claudecode.Turn) (*model.Proposal[claudecode.Event], error)
}
ITurnExecutor carries out one turn of a session: the Claude Code turn executor in production, a double in specs.
type OpenSession ¶
type OpenSession struct {
// contains filtered or unexported fields
}
OpenSession is one assignment's session held open: its run directory, event log and worktree exist, its turn executor is open, and no turn has run until the caller asks for one. One goroutine drives it; turns never overlap.
func (*OpenSession) Close ¶
func (open *OpenSession) Close(ctx context.Context) error
Close ends the turn executor, bounded by ctx, and closes the event log. It is idempotent.
func (*OpenSession) Directory ¶
func (open *OpenSession) Directory() string
Directory is the run directory: events.jsonl, run.json, settings.json and the worktree live there.
func (*OpenSession) FirstPrompt ¶
func (open *OpenSession) FirstPrompt() string
FirstPrompt is the task as the agent first reads it.
func (*OpenSession) Interrupt ¶
func (open *OpenSession) Interrupt(ctx context.Context) error
Interrupt asks the running turn to end at the executor's next safepoint.
func (*OpenSession) Plan ¶
func (open *OpenSession) Plan() *pb.AgentAssignmentPlan
Plan is the frozen recipe the session runs.
func (*OpenSession) State ¶
func (open *OpenSession) State() RunState
State is a copy of the run's record as last written.
func (*OpenSession) Turn ¶
func (open *OpenSession) Turn(ctx context.Context, prompt string) (*pb.AgentAssignmentReceipt, error)
Turn runs prompt as the session's next turn on the open executor and returns the receipt. ctx bounds the wait for the turn's result; the executor's process outlives it.
type OpenTurnExecutorFactory ¶
type OpenTurnExecutorFactory func(ctx context.Context, spec TurnExecutorSpec) (IOpenTurnExecutor, error)
OpenTurnExecutorFactory opens the turn executor an OpenSession holds.
func ClaudeCodeOpenTurnExecutors ¶
func ClaudeCodeOpenTurnExecutors(launcher proc.ILauncher, executable string) OpenTurnExecutorFactory
ClaudeCodeOpenTurnExecutors opens one Claude Code process per session through launcher and keeps it across turns.
type RouteRequest ¶
type RouteRequest struct {
Virtual string
// Session is the conversation the virtual session opens when no
// existing one fits.
Session string
Model string
// Agent is the agent's identity: its id and a hash of its instructions.
// A resumed conversation keeps the system prompt it first recorded, so
// only a conversation with the same identity may be attached.
Agent string
// Key is the topic key: the assignment's ticket.
Key string
// Summary is the assignment in a line: its pull request title.
Summary string
}
RouteRequest is what a router knows about a virtual session (one assignment) before it opens.
type RunState ¶
type RunState struct {
AssignmentID string `json:"assignment_id"`
AgentID string `json:"agent_id"`
TicketURL string `json:"ticket_url"`
SessionID string `json:"session_id"`
TraceID string `json:"trace_id"`
SpanID string `json:"span_id"`
Repository string `json:"repository"`
Worktree string `json:"worktree"`
Branch string `json:"branch"`
BaseBranch string `json:"base_branch"`
PullRequestTitle string `json:"pull_request_title"`
// Turns counts the turns started on the session, so a gate attributes its
// decisions to the turn that is running.
Turns int `json:"turns"`
// LastPushTime records the Unix timestamp of the most recent push,
// initialized when the session starts and updated by the commit gate.
LastPushTime int64 `json:"last_push_time,omitempty"`
// Model is the LLM model the session runs on, needed by the commit hook.
Model string `json:"model,omitempty"`
}
RunState is what the harness records about a run so its session gates and a later resumed turn act on the same session, trace and branch. Gates read it; only the runner writes it.
func ReadRunState ¶
ReadRunState reads the run recorded in directory.
type TurnExecutorFactory ¶
type TurnExecutorFactory func(spec TurnExecutorSpec) (ITurnExecutor, error)
TurnExecutorFactory builds the turn executor for one turn.
func ClaudeCodeTurnExecutors ¶
func ClaudeCodeTurnExecutors(launcher proc.ILauncher, executable string) TurnExecutorFactory
ClaudeCodeTurnExecutors builds Claude Code turn executors that start executable through launcher.
type TurnExecutorSpec ¶
type TurnExecutorSpec struct {
Session uuid.UUID
Directory string
// Arguments are the turn executor's extra arguments: the settings with
// the gates, the model, the permission mode and the allowed tools.
Arguments []string
Logger *slog.Logger
// Resume is true when the session already exists.
Resume bool
// Environment is appended, as NAME=value, to the environment the turn
// executor's process inherits: the Bazel cache locations, for one.
Environment []string
}
TurnExecutorSpec is what the runner asks of a turn executor.