session

import "github.com/jbcom/radioactive-ralph/internal/session"

Package session wraps a `claude -p` subprocess so the supervisor can drive it via stream-json over stdin/stdout.

The stream-json protocol pairs message objects, one per JSON line:

  • Messages FROM the supervisor to Claude carry `{“type”:”user”, …}` with a user-scoped content block.

  • Messages FROM Claude carry `{“type”:”assistant”, …}` or `{“type”:”result”, …}` when the agent finishes its turn.

Only the shape Ralph depends on is modeled here. Unknown fields are preserved as raw JSON so a future Claude Code version that adds a field doesn’t break replay.

Index

Constants

DefaultClaudeBin is the binary name the supervisor invokes when no override is provided. Tests override this to use a fake binary.

const DefaultClaudeBin = "claude"

func RenderSystemPrompt

func RenderSystemPrompt(opts PromptOptions) string

RenderSystemPrompt combines the variant’s bias snippets with operator preferences and installed inventory to produce the –append-system-prompt text.

Rules:

  1. For each bias category the variant declares a snippet for: a. If OperatorChoices has Disabled=true, skip. b. Else if OperatorChoices has Skill!=”” AND inventory has it, render the snippet with {skill} expanded. c. Else if variant snippet contains {skill} and inventory has ANY helper matching the category name, pick the first alphabetically for determinism. d. Else skip.

  2. Output is newline-joined with a fixed preamble announcing the variant and a safety note about the tool allowlist.

  3. Output is deterministic given the same inputs — categories are iterated in alphabetical order.

type BiasChoice

BiasChoice is the operator’s resolved preference for a single bias category. Typically sourced from config.toml’s [capabilities] section plus per-variant overrides.

Skill is the full helper name (e.g. “coderabbit:review”). Empty means “no preference, use variant default if inventory has it”.

Disabled means “skip this bias entirely regardless of inventory”.

type BiasChoice struct {
    Skill    string
    Disabled bool
}

type Event

Event is a parsed inbound frame plus any decoding error. Consumers handle Err before Inbound.

type Event struct {
    Inbound Inbound
    Err     error
}

type Inbound

Inbound is a JSON-line frame received from `claude -p`. Each frame is one of several shapes depending on type; the raw bytes are retained so downstream consumers (event log, supervisor) can re-emit or re-parse as needed.

type Inbound struct {
    // Type is the top-level message kind. Common values: "assistant",
    // "user", "result", "system". Unknown values are preserved as
    // strings and handled gracefully.
    Type string `json:"type"`

    // SessionID is Claude's view of its session UUID. The supervisor
    // uses this to correlate resume operations.
    SessionID string `json:"session_id,omitempty"`

    // Subtype narrows the meaning of Type. For type=result, the
    // subtypes Ralph cares about are "success" (agent done, clean exit)
    // and "error_max_turns" (turn cap hit).
    Subtype string `json:"subtype,omitempty"`

    // Message is the assistant/user payload for type in {assistant, user}.
    // Kept as a raw JSON so we don't flatten Claude's content-block shape.
    Message json.RawMessage `json:"message,omitempty"`

    // Result is the final result payload for type=result.
    Result json.RawMessage `json:"result,omitempty"`

    // Raw is the full JSON line as received, retained for event-log
    // archival.
    Raw []byte `json:"-"`
}

type Options

Options configures a Session spawn.

type Options struct {
    // ClaudeBin overrides DefaultClaudeBin. Tests use the fake-claude
    // binary path here.
    ClaudeBin string

    // WorkingDir is the cwd for the subprocess. Typically a worktree.
    WorkingDir string

    // SystemPrompt is the content for --append-system-prompt. Combined
    // variant + inventory biases go here.
    SystemPrompt string

    // Model pins the model tier — "haiku", "sonnet", or "opus". Empty
    // means "let claude choose" which in practice is sonnet.
    Model string

    // Effort pins the reasoning-effort level — "low", "medium",
    // "high", or "max" (as accepted by `claude --effort`). Empty
    // means "claude decides" which for sonnet is medium. Fixit's
    // advisor subprocess defaults to "high" so opus reasons deeply
    // during planning.
    Effort string

    // AllowedTools are passed to --allowed-tools. Supervisor builds
    // this from the variant profile's ToolAllowlist.
    AllowedTools []string

    // SessionID is the Claude session UUID to pin. Empty means
    // generate a new one (Spawn will populate this field).
    SessionID string

    // ResumeMode triggers `claude -p --resume <SessionID>` instead of a
    // fresh spawn. Requires a non-empty SessionID.
    ResumeMode bool

    // SentinelTaskID is the task identifier the supervisor prompts
    // about after a resume, to verify continuity. Empty in fresh spawns.
    SentinelTaskID string

    // ExtraArgs are additional `claude -p` flags the caller wants.
    ExtraArgs []string
}

type Outbound

Outbound is a JSON-line frame sent TO `claude -p`. Only user messages and interrupts are supported — the CLI does not accept assistant or system messages over stdin.

type Outbound struct {
    Type    string        `json:"type"`
    Message OutboundInner `json:"message"`
}

func NewUserMessage

func NewUserMessage(text string) Outbound

NewUserMessage builds an Outbound frame wrapping the given text as a single user-role text block.

type OutboundContentPart

OutboundContentPart is a single content block. Ralph only emits text.

type OutboundContentPart struct {
    Type string `json:"type"` // "text"
    Text string `json:"text"`
}

type OutboundInner

OutboundInner mirrors the subset of the Anthropic message shape that `claude -p –input-format stream-json` accepts.

type OutboundInner struct {
    Role    string                `json:"role"`
    Content []OutboundContentPart `json:"content"`
}

type PromptOptions

PromptOptions feeds RenderSystemPrompt.

type PromptOptions struct {
    // Variant is the active profile. Required.
    Variant variant.Profile

    // Inventory is the capability inventory. Required; if empty, bias
    // injection silently skips slots.
    Inventory inventory.Inventory

    // OperatorChoices is the per-category operator preference map.
    // Keys are BiasCategory values; missing keys fall back to the
    // variant's declared snippet target.
    OperatorChoices map[variant.BiasCategory]BiasChoice
}

type Session

Session wraps a running `claude -p` subprocess speaking stream-json.

Usage:

s, err := session.Spawn(ctx, session.Options{...})
for ev := range s.Events() {
    // handle ev.Inbound
}
s.SendUserMessage(ctx, "do X")
s.WaitForIdle(ctx)
s.Close()

The session is single-goroutine-safe on Send* and Close; Events() may be consumed from any goroutine. Resume reuses the SessionID so Claude’s conversation history is preserved across subprocess restarts.

type Session struct {
    // contains filtered or unexported fields
}

func Spawn

func Spawn(ctx context.Context, opts Options) (*Session, error)

Spawn launches a new Claude subprocess and starts the reader goroutine.

For ResumeMode=false, a fresh SessionID is generated (UUID v4) and passed via –session-id so Ralph can later resume by that ID. For ResumeMode=true, the SessionID must be set and is passed via –resume; on first successful read, Spawn emits a sentinel user message naming SentinelTaskID so the caller can verify continuity.

func (*Session) Close

func (s *Session) Close() error

Close terminates the subprocess and waits for the reader to exit.

func (*Session) Events

func (s *Session) Events() <-chan Event

Events returns a channel of inbound frames. Closed when the reader goroutine exits (EOF or error).

func (*Session) Interrupt

func (s *Session) Interrupt() error

Interrupt sends SIGINT to the subprocess, which Claude Code treats as an in-flight cancellation. Safe to call on a closed session.

func (*Session) SendUserMessage

func (s *Session) SendUserMessage(_ context.Context, text string) error

SendUserMessage writes a user-role message as a JSON line on stdin. Resets the idle signal so a subsequent WaitForIdle waits for the next result frame.

func (*Session) SessionID

func (s *Session) SessionID() string

SessionID returns Claude’s session UUID for this process.

func (*Session) WaitForIdle

func (s *Session) WaitForIdle(ctx context.Context) error

WaitForIdle blocks until the subprocess emits a `type=result` frame or the context is cancelled.

Generated by gomarkdoc