Documentation
¶
Overview ¶
Package board is the per-project roster of agents the build agent shares with the teammates it spawns.
A row is one agent: its handle, the session it runs in, whether it is a session or a teammate, the handle that spawned it, and its status. The spawned_by edges make the rows a tree — the project's running agents and who spawned whom — which is the whole of what the board is for. There is no post log: agents coordinate by editing files, and a child reports to its parent by finishing.
The package deliberately depends on nothing but database/sql and the project database: the agent loop, the server and the tools all import it, so an import in the other direction would be a cycle. The server wires the behaviour that needs those packages in through function fields.
Index ¶
- Constants
- Variables
- func NewHandle(role string) string
- func Slug(name string) string
- func ValidateHandle(name string) error
- type Handle
- type Store
- func (s *Store) BySession(ctx context.Context, sessionID string) (Handle, error)
- func (s *Store) Children(ctx context.Context, parent string) ([]Handle, error)
- func (s *Store) Dir() string
- func (s *Store) Get(ctx context.Context, handle string) (Handle, error)
- func (s *Store) MarkReported(ctx context.Context, handles []string) error
- func (s *Store) Reconcile(ctx context.Context, live map[string]bool) error
- func (s *Store) Register(ctx context.Context, h Handle) (Handle, error)
- func (s *Store) RegisterFresh(ctx context.Context, h Handle) (Handle, error)
- func (s *Store) Roster(ctx context.Context) ([]Handle, error)
- func (s *Store) SetStatus(ctx context.Context, handle, status string) error
- func (s *Store) SetStatusUnlessTerminal(ctx context.Context, handle, status string) (bool, error)
- func (s *Store) WaitStatus(ctx context.Context, handles []string, timeout time.Duration) ([]Handle, error)
Constants ¶
const ( // UserHandle is the project owner's handle. The user posts nothing to the // board, but the reserved name keeps an agent from taking it. UserHandle = "user" // SystemHandle names board-generated rows. Reserved for the same reason. SystemHandle = "system" )
const ( KindSession = "session" KindTeammate = "teammate" KindTask = "task" )
Handle kinds: a handle is either a session (the build agent, a resumed session), a teammate spawned by one, or a background task child a session's task tool delegated to.
const ( StatusIdle = "idle" StatusRunning = "running" StatusDone = "done" StatusStopped = "stopped" StatusStale = "stale" )
Handle statuses. "stale" is set at startup for a handle whose session is no longer alive; "stopped" is a teammate that was stopped deliberately. done, stopped and stale are terminal: nothing will move the row again, and the spawner is owed one report about it.
Variables ¶
var ErrHandleTaken = errors.New("handle already registered")
ErrHandleTaken reports that a handle is already on the board: RegisterFresh never reuses or overwrites a row, so a fresh registration of a taken name comes back with this instead of an upsert.
var ReservedHandles = map[string]bool{ UserHandle: true, SystemHandle: true, "board": true, }
ReservedHandles may not be registered or spawned: they name the project owner, the board itself, and board-generated entries.
Functions ¶
func NewHandle ¶
NewHandle names a fresh agent on the board: the role plus four random hex characters, so two sessions of one agent in a directory stay distinct.
func Slug ¶
Slug reduces a free-form name — a teammate's requested name, typically a phrase — to something ValidateHandle accepts.
func ValidateHandle ¶
ValidateHandle reports whether name is a handle the board accepts: lowercase letters, digits, dots, underscores and dashes, starting with a letter or digit, at most 32 characters. The reserved handles the user and the board answer to are refused, because accepting one would let an agent impersonate them.
Types ¶
type Handle ¶
type Handle struct {
ID string `json:"id"`
Directory string `json:"directory,omitempty"`
Handle string `json:"handle"`
SessionID string `json:"sessionId,omitempty"`
Role string `json:"role,omitempty"`
Kind string `json:"kind,omitempty"`
// SpawnedBy is the parent's handle; empty for a session that joined on its
// own. It is the tree edge.
SpawnedBy string `json:"spawnedBy,omitempty"`
Status string `json:"status"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
// contains filtered or unexported fields
}
Handle is one agent on a project's board.
func (Handle) Unreported ¶
Unreported reports whether this handle's status is news to its spawner: it is terminal, and it changed after the spawner was last told.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is one project directory's board. Every query is scoped to that directory, so a server in one project never sees another's agents.
func Open ¶
Open returns the board for dir. The schema is created by migration 065 and reshaped by 067, which run when the project database opens.
func (*Store) BySession ¶
BySession returns the handle a session is registered as, or the zero Handle.
func (*Store) MarkReported ¶
MarkReported records that the spawner has been told these handles' current status, by moving each row's notified_at up to its updated_at. Stamping the status time rather than the wall clock keeps "reported" and "changed since" comparable even when both land in the same second.
func (*Store) Reconcile ¶
Reconcile marks handles whose session no longer exists as stale, so the tree does not advertise agents from a previous run as running. live is the set of session ids that still exist in the project.
func (*Store) Register ¶
Register joins a handle to the board, or refreshes the row of one that is already there. Re-registering keeps the row's id, creation time and reported stamp: the tree edge and the fact that a completion was already reported both outlive a restart.
func (*Store) RegisterFresh ¶
RegisterFresh joins a handle to the board only if nobody holds its name. It is an insert, not the upsert Register is: a fresh registration must never overwrite an existing row, whose session keeps running and whose eventual report would land on the newcomer's row instead. The check and the insert share the store mutex, so a second registration of one name in this process waits and then sees the first one's row rather than racing it; the UNIQUE keys on board_agent still stand behind it, and a UNIQUE violation the driver reports anyway — another process got there first — is mapped to ErrHandleTaken.
func (*Store) SetStatus ¶
SetStatus records what an agent is doing, which is what the tree shows. A status change is the board's only event, so it wakes any waiter.
func (*Store) SetStatusUnlessTerminal ¶
SetStatusUnlessTerminal records a status the way SetStatus does, but only while the row is not terminal: done, stopped and stale are final, and the spawner is owed exactly one report about them. It reports whether the row moved, which is also whether the caller still owns that report.
The condition lives in the UPDATE rather than in a read-then-write above it: stopping a teammate and the teammate's loop winding up are two goroutines that race by design, and whichever writes second must not overwrite the other. A stop that lands first turns the loop's own completion into a no-op; a completion that lands first leaves the stop nothing to do.
func (*Store) WaitStatus ¶
func (s *Store) WaitStatus(ctx context.Context, handles []string, timeout time.Duration) ([]Handle, error)
WaitStatus blocks until every named handle is terminal (or gone from the roster) or the timeout passes, and returns the rows it watched. It re-reads on every wake, so a handle that finishes between two checks is never missed.