Help us shape the future of Go! Take the 2026 Go Developer Survey

console

package
v1.0.0-rc.12 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 30, 2026 License: Apache-2.0 Imports: 29 Imported by: 0

Documentation

Overview

Package console owns the record of a chat: pi's session files (console.md §5a: terminal = presentation, session files = record) read through one parser, and the transcript projected from them as chat bubbles, with the hand-back signal and the submit verification the runtime relies on. The containers, the steering and the HTTP doors stay in internal/apptown; the words are read here, the same way whether the session is live or long ended.

Index

Constants

View Source
const (
	WorkMount     = "/work"                 // the checkout (a change's worktree for a change console)
	Mount         = "/console"              // the session dir: pi-home, sessions, scratch, steering.md
	SessionsMount = Mount + "/sessions"     // pi --session-dir
	UploadsMount  = Mount + "/uploads"      // pasted pictures
	ScratchMount  = Mount + "/scratch"      // throwaway notes and experiments
	TmuxSession   = "console"               // tmux -s / -t
	AgentSession  = "console"               // the agent's session id, unless another agent's session is resumed
	MetaFile      = "session.json"          // present = alive
	EndedFile     = "ended.json"            // the record after End: skipped by recovery, read by the transcript
	SteeringFile  = "steering.md"           // the prompt the agent reads: authored text, the provision's notes, the notices
	AuthoredFile  = "steering.authored.md"  // the authored part, as handed to Open
	NoticesFile   = "notices.md"            // what the console told the agent since: resumes
	StagedFile    = "staged.json"           // the files the last provision placed, for replacement
	RehearsedFile = "rehearsed-fingerprint" // the change console's boot-rehearsal memory
	SavedFile     = "saved.json"            // the opener's record of the chat's last save to its branch
)

The container's view of a session, and the names pi and tmux are pinned to. Every path the steering promises and the runtime writes comes from here, so a mount cannot move without the prose and the code moving too.

Variables

View Source
var (
	ErrNoSession = fault.New(fault.NotFound, "no such session")
	ErrEnded     = errors.New("console session is stopped")
	// ErrAsleep: watching a terminal never starts the agent.
	ErrAsleep = fault.New(fault.Conflict, "this chat is not running; it starts again when someone who may work on it says something")
	// ErrWatching: a watcher's terminal takes no keystrokes.
	ErrWatching = fault.New(fault.Forbidden, "this terminal is open to watch, not to type")
)
View Source
var ErrNoLocalChats = errors.New("this chat ran on AppTown, which no longer runs chats")

ErrNoLocalChats refuses running a chat on an AppTown that runs none.

View Source
var ErrNoPicture = errors.New("no such picture")

ErrNoPicture: the chat has no picture by that name, or the name is not one the console gives.

View Source
var ToolchainPrelude = toolchainPrelude("/work", "/tmp")

ToolchainPrelude installs the workspace's declared toolchain (mise.toml) before a session starts, wherever it runs (AppTown's own Docker or a worker's), so `python`, `gofmt`, `node` are on PATH through the per-workspace shims without the agent having to know about mise (complaints #50, #59: the documented commands were "not found" until someone ran `mise install`). A failed install no longer passes silently: a session whose Ruby never installed went on to hand-write a Rails skeleton, and nobody saw why. The installer's output is kept in the container's /tmp, a failure is said on stderr (the step's log, a chat's terminal) with the output's last lines, and the session still starts with whatever toolchains the image has.

Functions

func HelperErrorText

func HelperErrorText(raw string) string

HelperErrorText turns a provider's error into a sentence a person can act on. Provider JSON ({"error":{"message":…}}) is unwrapped; anything else is shown trimmed.

func LocalTLSPrelude

func LocalTLSPrelude(t LocalTLS) string

LocalTLSPrelude runs in a container before its command (a chat's agent, a capture of a preview), when AppTown has a local authority. Every client resolves *.localhost to the container's own loopback itself, before any hosts file, so the prelude leads that loopback's public port to AppTown: the TLS stream passes through untouched and the preview's name arrives as SNI and Host, as from a browser. Trusting the authority is not its job: it is in the extra certificates every container AppTown starts here is given (internal/trust, trust.Run), which the container runs first. Everything here fails soft: a chat starts even if a tool is missing.

func ToolchainArgs

func ToolchainArgs(argv []string) []string

ToolchainArgs wraps an argv so the prep runs first and the command then replaces the shell (signals and exit status pass straight through). The argv's last word is the agent's prompt: when the install failed, the agent is told so after it (toolchainInstallNote).

func ValidPictureName

func ValidPictureName(name string) bool

ValidPictureName reports whether name is one Upload gives a pasted picture: never a path.

Types

type Agent

type Agent interface {
	// Start makes the agent run for the launch: create it if absent, start
	// it if stopped, leave it if running.
	Start(ctx context.Context, in Launch) error
	// Stale reports that what runs no longer matches the launch (the image,
	// the model or the Docker mode moved), so it should be rebuilt when safe.
	Stale(ctx context.Context, in Launch) (bool, error)
	// Status is the runtime's view: does it exist, is it running, is a
	// terminal held on it.
	Status(ctx context.Context, id string) (Status, error)
	// Deliver types text at the agent and presses enter, verifying against
	// the session record that the words entered the conversation.
	Deliver(ctx context.Context, in Launch, text string) error
	// Interrupt stops the agent's current turn; the conversation stays.
	Interrupt(ctx context.Context, id string) error
	// Attach opens a terminal on the agent's screen, recent output replayed.
	Attach(ctx context.Context, id string, size Size) (Terminal, error)
	// Stop stops the agent's process; its filesystem and the session dir stay.
	Stop(ctx context.Context, id string) error
	// Remove takes the agent's runtime away entirely: process, sidecar,
	// networks. The session stays; the next Start builds a new runtime.
	Remove(ctx context.Context, id string) error
	// End takes the runtime away for good: the session has ended and its
	// runtime is never started again. For a runtime on this machine it is
	// Remove; a runtime on a worker also fences the session there.
	End(ctx context.Context, id string) error
	// Reap removes runtime left behind by sessions that no longer exist.
	Reap(ctx context.Context, alive map[string]bool) error
}

Agent is the runtime a session's agent runs in: pi in a container under tmux for real, a recorder in tests. The console never shells out; every process, terminal and sidecar is behind this interface.

type Attachment

type Attachment struct {
	Name string // picture-N.ext
	Path string // where the agent sees it: UploadsMount/name
}

type CheckoutEdit

type CheckoutEdit struct {
	File         string `json:"file"`
	Before       string `json:"before"`
	After        string `json:"after"`
	BeforeDigest string `json:"beforeDigest"`
	AfterDigest  string `json:"afterDigest"`
}

CheckoutEdit holds only paths and content digests, never session credentials. It can be included in a relocation journal and retried after a crash.

func (CheckoutEdit) Apply

func (e CheckoutEdit) Apply() error

Apply changes only checkout, preserving ownership, credentials and unknown fields. Exact old/new digests make replay safe; a third state refuses.

type Config

type Config struct {
	DataDir string // session records live at <DataDir>/console/<id>
	NoDind  bool   // change consoles get no private Docker daemon
	// Mirror answers the Docker Hub mirror a private daemon and its
	// builder pull through ("" none), read at each launch. Nil is none.
	Mirror func(context.Context) (string, error)
	// Now is the console's clock: a session's last use and the idle and
	// busy rules judged against it, and the times its records carry. Nil
	// is the real clock.
	Now func() time.Time
}

Config is the console's own settings.

type Conversation

type Conversation struct {
	ID        string    `json:"id"`
	CreatedAt time.Time `json:"createdAt"`
	Actor     string    `json:"actor"`
	Messages  []Message `json:"messages"`
	// Ended: the session's record is its archive (EndedFile), not a live one.
	Ended bool `json:"-"`
	// Labels are the opener's, handed back unread, as on Meta.
	Labels map[string]string `json:"-"`
}

Conversation is one session's words, for a change's or a project's chat history.

type Dependencies

type Dependencies struct {
	Agent       Agent       // the runtime the agent runs in
	Environment Environment // the provision for a launch (model, env, mounts); nil runs sessions as opened
	// Settle is what an end does with the session's last record before the
	// session is archived or removed (the opener's accounting of it). It runs
	// after the agent's runtime has ended, so the runtime's last record is
	// in; an error keeps the session registered and its record in place. Nil
	// settles nothing.
	Settle func(ctx context.Context, s *Session) error
	Config Config
}

Dependencies is what Console needs and does not own.

type Ended

type Ended struct {
	Session *Session
}

Ended is what End leaves for the caller to finish: a change console's worktree is the change's and stays; a project console's snapshot is deleted here.

type Entry

type Entry struct {
	Type     string    // "session" | "model_change" | "message" | ...
	At       time.Time // the line's timestamp; zero when absent or unparsable
	Role     string    // message entries: user | assistant | toolResult
	Provider string    // assistant entries
	Model    string
	Usage    Usage
	Text     string // the text parts, joined by newlines (thinking excluded)
	Tools    []ToolCall
	// An assistant entry that ended in an error (the model call failed: no
	// credit, a bad key, a provider outage): stopReason "error" and the raw
	// provider message. Without surfacing this the chat looks busy forever.
	StopReason   string
	ErrorMessage string
}

Entry is one line of the record, reduced to what AppTown reads. One JSON object per line under pi's session directory: a "session" header, then "model_change" / "thinking_level_change" marks and "message" entries whose role is user, assistant or toolResult.

func ParseLine

func ParseLine(line string) (Entry, bool)

ParseLine decodes one record line. Lines that are not JSON objects (blank, corrupt) are skipped, not errors: the record is append-only and a bad line is pi's, not ours.

func (Entry) Aborted

func (e Entry) Aborted() bool

Aborted: the person stopped the turn.

func (Entry) Failed

func (e Entry) Failed() bool

Failed: the helper's turn ended without an answer, a model error or the person pressing stop (pi records that as stopReason "aborted").

func (Entry) HasText

func (e Entry) HasText() bool

HasText: the entry says something visible (not just thinking or tools).

type Environment

type Environment interface {
	Prepare(ctx context.Context, s *Session) (Provision, error)
}

Environment answers what a session's launch gets, at every wake, because the answer moves (a model follows a slot, a binding is renamed). The system that opened the console implements it.

type File

type File struct {
	Ext  string // png, jpg, webp, gif
	Body []byte
}

Upload stores a pasted picture under the session, server-named, and says where the agent will find it.

type Launch

type Launch struct {
	ID           string
	Dir          string // the session dir, mounted at Mount
	Actor        string
	Token        string
	Workspace    Workspace // mounted at WorkMount
	AgentSession string    // the agent's session id to resume
	Model        string
	Docker       bool // a private Docker daemon for the agent's own builds
	// Mirror is the Docker Hub mirror that daemon and its builder pull
	// through ("" none): data from AppTown, on its machine or a worker's.
	Mirror string
	Env    []string
	Mounts []string
	// Image is the agent image to create from; empty means the runtime's
	// own. A worker runs the image its AppTown names.
	Image string
}

Launch is everything the runtime needs to run a session's agent. The console fills the session facts; the environment (a dependency) supplies the provision: the model, the env, the mounts, and the staged home.

type LocalTLS

type LocalTLS struct {
	CACert []byte // the authority's certificate (PEM), never its key: trusted for deployments (internal/trust)
	Port   int    // the public port, the one in every https://….localhost:<port> address
	Target string // where that port really is from the container: host.docker.internal:<apptownd's port>
	// PreviewDomain is the preview tier's domain (preview.localhost). Tools
	// that ask the system resolver (Python, Node), unlike curl and
	// browsers, do not answer *.localhost names themselves, so a change
	// chat's own preview name is written into its hosts file.
	PreviewDomain string
}

LocalTLS is an AppTown serving itself over HTTPS from its own local certificate authority (apptownd -local-tls, the desktop trial). A chat on that AppTown trusts the authority and reaches AppTown's HTTPS addresses (its change's preview) at their public URLs.

type Match

type Match struct {
	Scope  string
	Labels map[string]string
}

Match selects sessions: by scope, and by labels that must all match.

type Message

type Message struct {
	I     int    `json:"i"`
	Role  string `json:"role"` // "you" | "helper"
	Text  string `json:"text,omitempty"`
	Tools []struct {
		Name   string `json:"name"`
		Detail string `json:"detail,omitempty"`
	} `json:"tools,omitempty"`
	At string `json:"at,omitempty"`
	// Error: the helper's turn ended in a model error; Text says what and
	// what to do. The chat must never look busy over a helper that has
	// stopped.
	Error bool `json:"error,omitempty"`
	// Stopped: the person pressed stop and the helper aborted its turn.
	Stopped bool `json:"stopped,omitempty"`
}

Message is one chat bubble: a projection of the record at story zoom. The terminal remains one toggle away as the forensic layer; there is no second rendering that could drift.

type MessagePage

type MessagePage struct {
	Messages []Message
	Turn     Turn
}

MessagePage is the conversation from a bubble index on, with the turn state.

type Meta

type Meta struct {
	ID string `json:"id"`
	// Scope is what the opener groups sessions by ("project:56",
	// "change:305"); List, Transcript and Identity answer in its terms.
	Scope string `json:"scope"`
	// Labels are the opener's facts about the session, kept on the record
	// and returned with its identity. The console reads none of them.
	Labels map[string]string `json:"labels,omitempty"`
	Model  string            `json:"model"`
	// ModelPinned: the person named this model when opening the chat. An
	// unpinned session follows whatever the environment answers at wake.
	ModelPinned bool      `json:"modelPinned,omitempty"`
	Actor       string    `json:"actor"`
	CreatedAt   time.Time `json:"createdAt"`
	// AgentSession is the agent's own session id the runtime resumes
	// ("console" by default). Resuming another agent's session sets it to
	// that agent's, so the chat continues the agent that did the work.
	AgentSession string `json:"agentSession,omitempty"`
	// ResumedFrom: plain words for the header when another agent's session
	// was resumed.
	ResumedFrom string `json:"resumedFrom,omitempty"`
	// Token is the one credential the agent inside holds: it scopes what
	// the agent may do to its own session. It lives in the container env
	// and this record, never in a UI.
	Token string `json:"token"`
	// Checkout is the host path mounted at WorkMount. OwnsCheckout says the
	// console made it and deletes it with the session; otherwise it is
	// borrowed (a worktree) and stays. Writable says how it is mounted.
	Checkout     string `json:"checkout,omitempty"`
	OwnsCheckout bool   `json:"ownsCheckout,omitempty"`
	Writable     bool   `json:"writable,omitempty"`
	// Docker: the agent gets a private Docker daemon for its own builds.
	Docker bool `json:"docker,omitempty"`
	// LastUsed is the activity the server last saw when the record was
	// written; persisted so a restart does not age an active conversation
	// back to its birth.
	LastUsed time.Time `json:"lastUsed,omitempty"`
}

Meta is the session's record on disk: the durable identity, enough to rebuild the session after a restart. It is the ONLY shape written to disk and read back; runtime state lives on Session and in the agent runtime. The console knows nothing of what a session is for: Scope and Labels are the opener's, kept and handed back.

func (Meta) Label

func (m Meta) Label(key string) string

Label is one of the opener's facts, "" when absent.

type NoLocal

type NoLocal struct{}

NoLocal is the local runtime of an AppTown that runs no chats (-no-local-docker). That AppTown's start was refused while a chat placed on it had not ended, so nothing of one is here: its status is absent, and stopping, removing, ending and reaping have nothing to do. Waking one is refused (ErrNoLocalChats); its record and transcript stay.

func (NoLocal) Attach

func (NoLocal) Deliver

func (NoLocal) End

func (NoLocal) Interrupt

func (NoLocal) Interrupt(context.Context, string) error

func (NoLocal) Reap

func (NoLocal) Reap(context.Context, map[string]bool) error

func (NoLocal) Remove

func (NoLocal) Remove(context.Context, string) error

func (NoLocal) Stale

func (NoLocal) Stale(context.Context, Launch) (bool, error)

func (NoLocal) Start

func (NoLocal) Status

func (NoLocal) Status(context.Context, string) (Status, error)

func (NoLocal) Stop

type OpenRequest

type OpenRequest struct {
	// ID is the session's id, from NewSessionID, when the opener must name
	// the session before it exists (to take a change for it); empty picks
	// one.
	ID        string
	Scope     string            // what the opener groups sessions by
	Labels    map[string]string // the opener's facts, kept and handed back
	Workspace Workspace         // the checkout to mount; the caller made it
	Docker    bool              // a private Docker daemon for the agent's own builds
	Steering  string            // the authored prompt
	// Files are placed in the session dir once, at open, and kept for the
	// session's life beside the authored prompt, which may name them (the
	// guidance's skills): a restart keeps them as it keeps the prompt.
	Files  map[string]Staged
	Actor  string
	Model  string // the model the chat opens on
	Pinned bool   // the person named it; otherwise the environment's answer wins
	Resume *Resumption
}

OpenRequest is everything a session needs; it looks nothing up.

type Placed

type Placed struct {
	Local, Remote Agent
	// OnRemote reports whether a session's record places it on the remote
	// runtime; an error is unknown, and the call fails rather than asking
	// the wrong runtime.
	OnRemote func(id string) (bool, error)
}

Placed runs each session's agent on one of two runtimes by its recorded placement: a chat on a worker has its runtime there (Remote), every other on this machine (Local). The placement is read from the session's record, live or ended, so an ended chat's runtime is still asked where it ran: only the runtime a chat ran on can say it is gone.

func (Placed) Attach

func (p Placed) Attach(ctx context.Context, id string, size Size) (Terminal, error)

func (Placed) Deliver

func (p Placed) Deliver(ctx context.Context, in Launch, text string) error

func (Placed) End

func (p Placed) End(ctx context.Context, id string) error

func (Placed) Interrupt

func (p Placed) Interrupt(ctx context.Context, id string) error

func (Placed) Reap

func (p Placed) Reap(ctx context.Context, alive map[string]bool) error

Reap asks both runtimes to remove what sessions not alive left behind. The remote runtime is told only of the sessions placed on it (or whose placement cannot be read), since it resumes watching each one it is told of.

func (Placed) Remove

func (p Placed) Remove(ctx context.Context, id string) error

func (Placed) Stale

func (p Placed) Stale(ctx context.Context, in Launch) (bool, error)

func (Placed) Start

func (p Placed) Start(ctx context.Context, in Launch) error

func (Placed) Status

func (p Placed) Status(ctx context.Context, id string) (Status, error)

func (Placed) Stop

func (p Placed) Stop(ctx context.Context, id string) error

type Provision

type Provision struct {
	Model  string
	Env    []string
	Mounts []string
	Files  map[string]Staged // path under the session dir → what to put there
	Notes  string            // prose about the provision, refreshed each wake
}

Provision is what the world outside supplies for a launch: the model, the container's environment and mounts, files staged into the session dir (the agent's settings, keys, a binary), and words about what was provisioned, which the console puts into the prompt. At the bottom an agent gets a model name, environment variables, mounted files and one block of text; this is that, and nothing more.

type Reader

type Reader struct {
	Path string
	Off  int64
}

Reader reads a record incrementally. Off is the byte offset of the first unread complete line; a trailing partial line (pi mid-write) is left for the next call. Zero value plus a path reads from the start.

func (*Reader) Next

func (r *Reader) Next() ([]Entry, error)

Next returns the entries in the complete lines appended since the last call (all of them on the first). A record shorter than the offset (a rewrite, not an append) is read again from the start.

The agent writes its record, so the reader trusts nothing about it: it reads only a regular file, opened through a root on the record's directory so a link cannot lead it elsewhere, up to the size it had when opened, and it skips a line longer than maxLine without holding it.

type Recording

type Recording struct {
	Meta
	Dir   string
	Live  bool
	Ended bool
}

Recording is a session's record on disk, live or ended, with its directory, whether it is live in this process, and whether its record is its archive (it ended).

func (Recording) ReadMessages

func (r Recording) ReadMessages() ([]Message, error)

ReadMessages are the recorded session's words, read from its record; a record that cannot be read is an error, never no words.

type RelocationRecord

type RelocationRecord struct {
	Meta Meta
	File string
	// contains filtered or unexported fields
}

RelocationRecord is a strictly read live session record. Ended records are historical evidence and remain untouched. This never resumes or reaps agents.

func RelocationRecords

func RelocationRecords(dataDir string) ([]RelocationRecord, error)

RelocationRecords returns the complete readable inventory or an error. An unreadable record is not an absent session and cannot be silently omitted.

func (RelocationRecord) CheckoutEdit

func (r RelocationRecord) CheckoutEdit(to string) (CheckoutEdit, error)

func (RelocationRecord) RefuseWriter

func (r RelocationRecord) RefuseWriter(ctx context.Context, agent Agent) error

RefuseWriter observes the full runtime status. An absent primary container does not prove a private daemon or workspace script stopped writing.

type Resumption

type Resumption struct {
	// Record is the other agent's record itself, as its owner kept it, for
	// the session Session; it is written as that session's file. Empty
	// starts a fresh agent.
	Record  []byte
	Session string
	Label   string // plain words for the header
}

Resumption continues another agent's session: its record file is copied in and resumed, and the header says so.

type RuntimeIdentity

type RuntimeIdentity struct {
	SessionID string
	Scope     string
	Labels    map[string]string
	Actor     string
}

RuntimeIdentity is who the token's holder is: the agent inside a session, with the opener's scope and facts.

type Service

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

Service owns the sessions: their records on disk, the registry in memory, and every operation on one. Built once.

func New

func New(deps Dependencies) *Service

func (*Service) Annotate

func (s *Service) Annotate(ctx context.Context, id, key, value string) error

Annotate keeps one of the opener's facts on the session's record.

func (*Service) Attach

func (s *Service) Attach(ctx context.Context, id string, size Size) (Terminal, error)

Attach opens a terminal on the session, waking the agent first. The terminal counts as a viewer until it is closed.

func (*Service) Block

func (s *Service) Block(key, value string) (func(), error)

Block admits the removal of everything labelled key=value (a project being deleted): refused while such a chat is being opened, is mid-conversation, or is ending. Until the release, no such session is opened, admitted or identified.

func (*Service) Busy

func (s *Service) Busy(ctx context.Context, m Match, within time.Duration) bool

Busy: a session the match selects is watched or was used within the window.

func (*Service) End

func (s *Service) End(ctx context.Context, id string) (Ended, error)

End removes the session: the agent's runtime and the record. What remains under the session dir (the session files, the steering) is the archived conversation. Failed checkout removal retains that archive for retry; ending an archive retries cleanup without waking or settling it.

func (*Service) Ended

func (s *Service) Ended(ctx context.Context, id string) (bool, error)

Ended is Gone without removing anything: it reports whether the session is over and its runtime already absent, for a check that must change nothing.

func (*Service) Evict

func (s *Service) Evict(ctx context.Context, id string) error

Evict takes the agent's runtime away and keeps the session registered: the reaper's move for a long-idle chat. The next message rebuilds it.

func (*Service) Get

func (s *Service) Get(ctx context.Context, id string) (*Session, error)

Get is the session by id.

func (*Service) Gone

func (s *Service) Gone(ctx context.Context, id string) (bool, error)

Gone reports whether a session is over and nothing of it can still write its workspace: it is neither registered nor recorded as live (it ended, or never opened), and neither its agent nor its private Docker daemon exists. A runtime left behind by an end whose removal failed is removed again first; the answer is never assumed.

func (*Service) Hold

func (s *Service) Hold(id string) func()

Hold keeps id from counting as ended until release, whatever its record and runtime show: an opener that has claimed something for a session not yet opened (a change), or an ending that still has work to settle for it (publishing its commits). After release, Gone judges the session by its record and runtime as usual.

func (*Service) Identity

func (s *Service) Identity(token string) (RuntimeIdentity, bool)

Identity resolves a session token: the credential the agent inside holds. A session on a project being deleted identifies nobody; so does an ended one.

func (*Service) Interrupt

func (s *Service) Interrupt(ctx context.Context, id string) error

Interrupt stops the agent's current turn; the conversation and the workspace are untouched.

func (*Service) List

func (s *Service) List(ctx context.Context, m Match) []*Session

List is every session, or those a Match selects.

func (*Service) Messages

func (s *Service) Messages(ctx context.Context, id string, from int) (MessagePage, error)

Messages is the conversation from bubble `from` on. Reading keeps the session warm: an open chat page is a viewer in spirit.

func (*Service) NewSessionID

func (s *Service) NewSessionID() string

NewSessionID makes a fresh session id: a UUIDv7 in its canonical text, minted from the console's clock.

func (*Service) Open

func (s *Service) Open(ctx context.Context, in OpenRequest) (*Session, error)

Open creates a session: its directory and record, then its agent (created, not yet started; the first message or terminal starts it).

func (*Service) Recorded

func (s *Service) Recorded(id string) (Meta, error)

Recorded reads a session's record from disk, live or ended, whether or not this process has restored it: what it was opened with, including the opener's labels.

func (*Service) RecordedPicture

func (s *Service) RecordedPicture(scope, id, name string) (string, error)

RecordedPicture is where a pasted picture of a scope's chat lives on disk, live or ended, read from the chat's record without registering or waking it: the chat's words stay readable after it ends, and so do its pictures. The chat must belong to the scope and the name must be one Upload gives; nothing is listed.

func (*Service) Records

func (s *Service) Records(scope string) ([]Recording, []string, error)

Records is every session a scope has had, live or ended, oldest first, read from the records on disk, and the folders whose record could not be read (RecordsMatching).

func (*Service) RecordsMatching

func (s *Service) RecordsMatching(m Match) (records []Recording, unreadable []string, err error)

RecordsMatching is every session a Match selects, live or ended, oldest first, read from the records on disk: an ended session is selected by its archived record, without registering or waking it. A folder whose record cannot be read or parsed may belong to the Match, so it is named in unreadable, never skipped silently; the caller decides what that means for it.

func (*Service) Remove

func (s *Service) Remove(ctx context.Context, id string) error

Remove ends the session and deletes everything it left on disk: purge.

func (*Service) RemoveEnded

func (s *Service) RemoveEnded(m Match) error

RemoveEnded deletes the folders of the ended sessions a Match selects, read from their records without registering or waking them: a deleted project's ended chats, words and pictures. A live session is left to Remove, which ends it first.

func (*Service) Reserve

func (s *Service) Reserve(labels map[string]string) (func(), error)

Reserve is the admission an Open holds while it provisions: a block on any of its labels admitted meanwhile is refused. Open takes it itself.

func (*Service) Restore

func (s *Service) Restore(meta Meta) *Session

Restore registers a session and writes its live record without touching its agent: what Resume finds per record, offered to tests that build sessions by hand. A registered chat always has its record (End archives it); a test whose folder cannot hold one panics here.

func (*Service) Resume

func (s *Service) Resume(ctx context.Context) error

Resume rebuilds the registry from the records on disk after a restart (agents survive restarts; terminals do not), then has the runtime reap what belongs to no record. The records are the only inventory of what may run, so the reap runs only when every one was read: a chat folder that cannot be listed, or a live record that cannot be read or is incomplete (kept for repair), is an error and nothing is reaped, since its agent may be one of those still running. The readable chats are registered either way. A folder with no live record is an ended chat.

func (*Service) Send

func (s *Service) Send(ctx context.Context, id, text string) error

Send types words at the agent, waking it first. Newlines fold to spaces: a raw newline would submit mid-thought.

func (*Service) Shutdown

func (s *Service) Shutdown(ctx context.Context) error

Shutdown ends the service's hold on every session for a process stop: terminals close, agents and records stay for the next Resume.

func (*Service) State

func (s *Service) State(ctx context.Context, sess *Session) State

State is where the session stands on the ladder.

func (*Service) Stop

func (s *Service) Stop(ctx context.Context, id string) error

Stop stops the agent and keeps everything else: the record, the files, the conversation. The next message or terminal wakes it.

func (*Service) Transcript

func (s *Service) Transcript(ctx context.Context, scope string) (convs []Conversation, unreadable []string, err error)

Transcript is every conversation a scope has had, oldest first, read from the records on disk: sessions end and their runtimes go, the words stay. A chat folder whose record or words cannot be read is named in unreadable, never left out silently: its scope may be this one. One bad folder does not stop the others being read.

func (*Service) Upload

func (s *Service) Upload(ctx context.Context, id string, f File) (Attachment, error)

func (*Service) UploadPath

func (s *Service) UploadPath(ctx context.Context, id, name string) (string, error)

UploadPath is where a named upload lives on disk, for serving it back.

func (*Service) UsesWorkspace

func (s *Service) UsesWorkspace(ctx context.Context, workspace string) bool

UsesWorkspace: a live session's checkout is this workspace or lies under it.

func (*Service) Warm

func (s *Service) Warm(ctx context.Context, id string) error

Warm has the agent running for the session without saying anything: an open chat page keeps its helper ready to answer.

func (*Service) Watch

func (s *Service) Watch(ctx context.Context, id string) (Terminal, error)

Watch opens a terminal on the session to watch its output: the screen replayed, then live. It does not wake the agent, touch the idle clock or count as a viewer, and it takes no keystrokes or resizes. Watching a chat whose agent is not running is refused (ErrAsleep).

type Session

type Session struct {
	Meta
	Dir string // <data>/console/<id>
	// contains filtered or unexported fields
}

Session is one conversation: its record, its directory on disk, and the runtime facts the service keeps (activity, viewers). The agent's process and terminal belong to the Agent runtime.

func (*Session) Active

func (s *Session) Active(now time.Time, within time.Duration) bool

Active: watched, or used within the window. Purge and remediation ask this.

func (*Session) Activity

func (s *Session) Activity() Turn

Activity is the turn as another page reads it: empty before the first message, so a chat nobody has spoken in is not said to be working.

func (*Session) Done

func (s *Session) Done() <-chan struct{}

Done is closed when the session ends; loops that watch a session select on it.

func (*Session) HandedBack

func (s *Session) HandedBack() bool

HandedBack: the agent has finished its turn.

func (*Session) Idle

func (s *Session) Idle(now time.Time, limit time.Duration) bool

Idle: nobody is watching and nothing has happened for limit. A fact about the session; who acts on it, and when, is the caller's business.

func (*Session) LastUsed

func (s *Session) LastUsed() time.Time

LastUsed is the last activity the service saw.

func (*Session) LatestRecord

func (s *Session) LatestRecord() (string, int64)

LatestRecord is the newest session file and its size: the marker delivery verification measures growth past.

func (*Session) Messages

func (s *Session) Messages() []Message

Messages is the conversation as chat bubbles, oldest first.

func (*Session) Persist

func (s *Session) Persist() error

Persist writes the session's record. Callers persist when the moment is worth surviving a restart (a message delivered, a viewer gone).

func (*Session) ReadMessages

func (s *Session) ReadMessages() ([]Message, error)

ReadMessages is Messages, with the failure to read the record when there was one.

func (*Session) RecordOffset

func (s *Session) RecordOffset() int64

RecordOffset is the transcript reader's position in the newest record, for tests of the resume.

func (*Session) Records

func (s *Session) Records() ([]string, error)

Records is every session file the conversation owns, oldest first: what usage accounting reads. A folder that cannot be read is an error.

func (*Session) SetLastUsed

func (s *Session) SetLastUsed(t time.Time)

SetLastUsed sets the idle clock; recovery and tests use it.

func (*Session) Steering

func (s *Session) Steering() string

Steering is the prompt as the agent reads it now.

func (*Session) Touch

func (s *Session) Touch()

Touch records activity for the idle clock.

func (*Session) Turn

func (s *Session) Turn() Turn

Turn is the agent's turn state for the chat page.

func (*Session) UserMessageAppended

func (s *Session) UserMessageAppended(file string, off int64) bool

UserMessageAppended: has a user message entered the record since the marker?

func (*Session) UsesWorkspace

func (s *Session) UsesWorkspace(workspace string) bool

UsesWorkspace: the session's checkout is this workspace or lies under it.

func (*Session) Viewers

func (s *Session) Viewers() int

Viewers is how many terminals are open on the session.

func (*Session) Workspace

func (s *Session) Workspace() Workspace

Workspace is the checkout the agent works in.

type Size

type Size struct{ Cols, Rows uint16 }

Size is a terminal's dimensions.

type Staged

type Staged struct {
	Data []byte
	Link string      // a symbolic link's target, when set
	Mode os.FileMode // 0 means 0644
}

Staged is one file the provision places: bytes, or a symbolic link.

type State

type State string

State is where a session stands on the ladder attached → detached → stopped → dead.

const (
	Attached State = "attached" // a terminal is open on it
	Detached State = "detached" // the agent runs, nobody is watching
	Stopped  State = "stopped"  // the agent is stopped; files intact; a message wakes it
	Dead     State = "dead"     // the agent runtime is gone; the next message rebuilds it
	// Unavailable: the runtime could not be asked (a worker that cannot be
	// reached); what runs is unknown, never assumed gone.
	Unavailable State = "unavailable"
)

type Status

type Status struct {
	Exists bool
	// SidecarExists: the agent's private Docker daemon exists, which also
	// mounts the workspace.
	SidecarExists bool
	// SidecarRunning: the private daemon runs, so work in it may be writing.
	SidecarRunning bool
	// ScriptExists: a script runs on the session's work tree beside the
	// agent (a worker chat's save), and may be writing it.
	ScriptExists bool
	Running      bool
	EverStarted  bool // it has run before: starting it again is a resume
	Attached     bool // the runtime holds a terminal on it
}

Status is the runtime's view of a session's agent.

type Terminal

type Terminal interface {
	io.ReadWriteCloser
	Resize(Size) error
}

Terminal is one viewer's window onto the agent's screen: reads deliver the recent output then live bytes, writes are keystrokes.

type ToolCall

type ToolCall struct {
	Name string
	Args map[string]any
}

ToolCall is one tool the helper called in an entry.

func (ToolCall) Detail

func (t ToolCall) Detail() string

Detail picks the one argument worth showing beside a tool's name in a chat bubble: the command, the path, the pattern, the URL.

type Transcript

type Transcript struct {
	Dir   string
	Since time.Time
	// contains filtered or unexported fields
}

Transcript is one chat's record on disk, parsed and kept between polls: the reader's offset, the bubbles so far, and the last message entry (the hand-back signal). A new record file (a fresh runtime's first entry) starts it over. Dir is the session's directory; Since hides the turns from before this chat existed (a chat that continues a lane agent's own session shows only its own turns; the lane's transcript lives on the change page). Zero Since shows everything.

func (*Transcript) LatestSessionFile

func (t *Transcript) LatestSessionFile() (string, int64)

LatestSessionFile returns the newest session record and its size: the before/after marker the submit path uses to verify a message landed.

func (*Transcript) Messages

func (t *Transcript) Messages() []Message

Messages is the conversation as chat bubbles, oldest first: shared by the live chat poll, the change transcript and the narrative (session files outlive their containers; the words are part of the change's story).

func (*Transcript) Offset

func (t *Transcript) Offset() int64

Offset is the reader's position in the newest record, for tests of the resume.

func (*Transcript) ReadMessages

func (t *Transcript) ReadMessages() ([]Message, error)

ReadMessages is Messages, with the failure to read the record when there was one: a record that cannot be read is not a chat with no words.

func (*Transcript) SessionFiles

func (t *Transcript) SessionFiles() ([]string, error)

SessionFiles is every record file this chat owns, oldest first: the pinned "console" session or the continued lane agent's own; names are timestamp-prefixed, so sorted order is chronological. Only regular files count: the agent writes this directory, and a link or a device there is not a record. A folder that does not exist holds no records; one that cannot be read is an error, never "no records".

func (*Transcript) TurnHandedBack

func (t *Transcript) TurnHandedBack() bool

TurnHandedBack reports whether the helper has finished its turn: the latest record entry is an assistant message with text and no tool calls, and the transcript has been quiet for a few seconds. This is the same structural signal the UI's green bubble uses; the moment the helper claims "done" is exactly the moment worth verifying.

func (*Transcript) UserMessageAppended

func (t *Transcript) UserMessageAppended(file string, off int64) bool

UserMessageAppended: has a user message entered the record since the (file, size) marker? A new file (a fresh runtime's first entry) counts from its start.

type Turn

type Turn string

Turn is whether the agent is mid-turn or has handed back.

const (
	Working    Turn = "working"
	HandedBack Turn = "handed-back"
)

type Usage

type Usage struct {
	Input      float64 `json:"input"`
	Output     float64 `json:"output"`
	CacheRead  float64 `json:"cacheRead"`
	CacheWrite float64 `json:"cacheWrite"`
	Total      float64 `json:"totalTokens"`
	Cost       struct {
		Total float64 `json:"total"`
	} `json:"cost"`
}

Usage is what an assistant entry says it cost: the accounting ledger's source.

type Workspace

type Workspace struct {
	Path     string
	Writable bool
	Owned    bool // the console made it and deletes it with the session
}

Workspace is the checkout the agent works in.

Directories

Path Synopsis
Package consoletest is the fake agent runtime: it stands in for pi in a container, remembers what it was started with, and writes the session record the way pi does so the console reads replies out of it.
Package consoletest is the fake agent runtime: it stands in for pi in a container, remembers what it was started with, and writes the session record the way pi does so the console reads replies out of it.
Package pi runs a console's agent for real: pi's interactive TUI inside a Docker container, under tmux, with apptownd holding a disposable tmux client over a host PTY.
Package pi runs a console's agent for real: pi's interactive TUI inside a Docker container, under tmux, with apptownd holding a disposable tmux client over a host PTY.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL