piagent

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Oct 22, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Package piagent manages Pi coding-agent sessions that DevX launches on behalf of a local MCP client while keeping them visible to, and controllable by, the human.

Design (see docs/pi-mcp.md for the full write-up):

  • Each managed agent is one interactive Pi TUI running in a dedicated tmux window of a normal DevX session. There is no hidden headless process: the pane a human attaches to is the process that receives remote prompts.
  • A bridge extension (bridge/devx-bridge.ts) loaded into that Pi process delivers queued dispatches with pi.sendUserMessage and reports results. It never goes through tmux keystrokes, so prompts are never typed into a shell or a human's half-written input.
  • A lease in agent.json decides who drives the agent. Human input in the TUI, or an explicit takeover, moves the lease to the human; queued remote dispatches then wait until a human releases control.
  • All shared state changes happen under one per-agent lock that the Go side and the Node bridge both honor.

Index

Constants

View Source
const (
	ActionAssignSessionID = "assign_session_instance_id"
	ActionBindAgent       = "bind_agent"
	ActionSkip            = "skip"
	ActionBound           = "already_bound"
	// ActionRestoreBinding re-writes a binding an older writer dropped from
	// agent.json, from the agent's own event log (never a new binding).
	ActionRestoreBinding = "restore_agent_binding"
	// ActionCandidate is an agent whose link to its session rests on
	// ambiguous evidence, so it is NEVER bound automatically: it is bound
	// only if the owner lists it with --confirm (recorded with basis
	// owner_confirmed). Two cases:
	//   - no marker names it: only name, path and creation-time ordering
	//     link them;
	//   - an adoption (managed_agent) marker names it: before instance ids,
	//     relaunching an adopted agent re-marked whatever session had the
	//     name, so the marker may be an artifact of a stale agent and only
	//     timestamp ordering would remain.
	// Only local-only markers (written into the record by the agent's own
	// start, never re-written onto another record) bind automatically.
	ActionCandidate = "candidate_needs_owner_confirmation"
)

Plan item actions.

View Source
const (
	BasisMarker         = "marker"          // local-only marker written at the agent's own start, plus ordering
	BasisOwnerConfirmed = "owner_confirmed" // no marker; owner confirmed the candidate explicitly
	BasisEventLog       = "event_log"       // restores the agent's own recorded binding
)

Binding basis recorded on a plan item, in the journal and in the agent's session_instance_bound event.

View Source
const (
	ReasonNoMarker      = "" /* 158-byte string literal not displayed */
	ReasonAdoptedMarker = "" /* 207-byte string literal not displayed */
)

Reasons explaining a candidate.

View Source
const (
	SkipRetired          = "retired agent: never rebound"
	SkipNoSession        = "agent's session no longer exists"
	SkipPathMismatch     = "session path differs from the agent's worktree"
	SkipProjectMismatch  = "session project differs from the agent's"
	SkipForeignMarker    = "session marker names a different agent"
	SkipBothMarkers      = "session carries both local-only and adoption markers"
	SkipDuplicateClaim   = "another live agent claims the same session"
	SkipSharedWorktree   = "another live agent or session uses the same worktree"
	SkipSessionNewer     = "session record is newer than the agent (likely recreated after the agent was created)"
	SkipNoTimes          = "session or agent has no creation time to compare"
	SkipContainer        = "container session"
	SkipBoundElsewhere   = "agent is bound to a different session instance (session was recreated)"
	SkipBoundIDMissing   = "agent is bound but the session lost its instance id and created_at does not match"
	SkipInvalidSessionID = "session has a malformed instance id"
	SkipSessionHasID     = "session has an instance id this migration did not assign, so it was created after this legacy agent (recreated)"
)

Reasons an agent is not bound automatically.

View Source
const (
	// BridgeKindUpgrade: the live bridge holding the task speaks an older
	// protocol; only a human relaunch helps.
	BridgeKindUpgrade = "bridge_upgrade_required"
	// BridgeKindUnavailable: the bridge holding the task is not reporting
	// (no/stale heartbeat, a different instance, fenced or stopped). It may
	// be transient: retry, inspect the session, do not relaunch blindly.
	BridgeKindUnavailable = "bridge_unavailable"
)

Kinds of BridgeUpgradeError.

View Source
const (
	MarkerActionRestore   = "restore_local_only_marker"
	MarkerActionCandidate = "candidate_needs_owner_confirmation"
	MarkerActionSkip      = "skip"
	MarkerActionPresent   = "marker_present"
)

Marker repair plan actions.

View Source
const (
	MarkerSkipNotMCP           = "agent was not created by MCP start"
	MarkerSkipAdopted          = "adopted agent: its session is a normal session, never local-only"
	MarkerSkipRetired          = "retired agent"
	MarkerSkipNoSession        = "agent's session no longer exists"
	MarkerSkipOtherMarker      = "session carries a marker that does not name this agent"
	MarkerSkipPath             = "session path differs from the agent's worktree"
	MarkerSkipProject          = "session project differs from the agent's"
	MarkerSkipContainer        = "container session"
	MarkerSkipExposed          = "" /* 129-byte string literal not displayed */
	MarkerSkipClaimants        = "another live agent claims the same session"
	MarkerSkipSharedWorktree   = "another live agent or session uses the same worktree"
	MarkerSkipNoTimes          = "session or agent has no creation time"
	MarkerSkipOrdering         = "session record is newer than the agent (recreated)"
	MarkerSkipGap              = "session record is too much older than the agent to be its start"
	MarkerSkipInstance         = "session is not the instance the agent is bound to"
	MarkerSkipForeignID        = "session has an instance id the unbound agent never recorded (recreated)"
	MarkerSkipNoEvent          = "agent's own agent_created event is missing or names a different session/instance"
	MarkerSkipTmuxMissing      = "no live tmux session of this name, so its creation tag cannot be checked"
	MarkerSkipTmuxMismatch     = "live tmux session is not tagged DEVX_LOCAL_ONLY_AGENT=<this agent>"
	MarkerSkipNoBackupWitness  = "no reviewed backup holds a local-only marker for this exact record"
	MarkerSkipBackupConflict   = "reviewed backups disagree about this record's marker"
	MarkerSkipBackupInstanceID = "backup record carries a different instance id than the current record"
	MarkerSkipWitnessError     = "a witness could not be read"
)

Reasons a marker is not restored.

View Source
const (
	LeaseManaged = "managed"
	LeaseHuman   = "human"
)

Lease holders.

View Source
const (
	TaskWaiting   = "waiting"   // accepted, not yet delivered to Pi
	TaskRunning   = "running"   // delivered to Pi; turn in progress
	TaskCompleted = "completed" // Pi settled with a normal stop
	TaskFailed    = "failed"    // Pi settled with an error
	TaskCancelled = "cancelled" // cancelled before delivery, or aborted
	TaskUnknown   = "unknown"   // delivered, but the Pi process that held it is gone
)

Task states exposed to MCP clients.

View Source
const (
	WaitHumanControl = "human_control"
	WaitBusy         = "busy"
	WaitQueued       = "queued_behind_other_task"
	WaitBridgeOnline = "bridge_starting_or_offline"
	WaitBindingStale = "pane_binding_stale"
	WaitDelivering   = "pending_delivery" // eligible; the bridge picks it up on its next tick
)

Waiting reasons explain why a waiting task has not been delivered.

View Source
const (
	AgentIdle        = "idle"
	AgentRunning     = "running"
	AgentHuman       = "human_control"
	AgentUnknown     = "unknown"
	AgentStaleBind   = "binding_stale"
	AgentPaneExited  = "pane_exited"
	AgentNotLaunched = "not_launched"
	AgentRetired     = "retired"
	// AgentAdoptedPending: an existing human Pi was adopted but is still the
	// original process without the DevX bridge; a relaunch (same Pi session)
	// is needed before tasks can be delivered.
	AgentAdoptedPending = "adopted_pending_relaunch"
)

Agent states.

View Source
const (
	// MaxPromptBytes bounds a dispatched prompt.
	MaxPromptBytes = 64 << 10
	// MaxExcerptBytes bounds result excerpts in status/send responses.
	MaxExcerptBytes = 4 << 10
	// MaxProgressExcerptBytes bounds the mid-task progress excerpt (latest
	// assistant message) in status responses and progress events.
	MaxProgressExcerptBytes = 500
	// MaxProgressToolBytes bounds the reported last tool name.
	MaxProgressToolBytes = 128
	// MaxResultChunk bounds one cursor read of a full result.
	MaxResultChunk = 16 << 10
	// MaxEventsPage bounds one page of events.
	MaxEventsPage = 200
	// BridgeStaleAfter is how old a bridge heartbeat may be before the
	// bridge is considered offline.
	BridgeStaleAfter = 5 * time.Second
)
View Source
const (
	PhaseQueued    = "queued"    // in the DevX queue; Pi has not seen it
	PhaseDelivered = "delivered" // handed to Pi; not yet accepted into a turn
	PhaseAccepted  = "accepted"  // Pi accepted it; the turn is in progress
	PhaseFinished  = "finished"  // terminal; see State
)

Delivery phases: where a task is in the hand-off to Pi. They separate what DevX did (queued, handed to Pi) from what Pi did (accepted the prompt into a turn) and from the outcome.

View Source
const (
	CancelBeforeDelivery   = "cancelled_before_delivery"
	CancelBeforeAcceptance = "cancelled_before_acceptance"
	CancelAbortPending     = "abort_pending"     // requested; the bridge has not acted yet
	CancelAbortSent        = "abort_sent"        // ctx.abort() was called; waiting for Pi to settle
	CancelAbortUnconfirmed = "abort_unconfirmed" // abort sent, but Pi has not settled within AbortConfirmAfter
	CancelAborted          = "aborted"           // Pi settled the turn as aborted
	CancelWithheld         = "abort_withheld"    // the bridge refused: see abort_withheld reason
	CancelFinishedFirst    = "finished_before_abort"
	CancelOutcomeUnknown   = "outcome_unknown" // the Pi process holding the turn went away
	// CancelLegacyRequested: a running cancel recorded before protocol
	// gating, held by a bridge that does not report whether it dispatched an
	// abort. Only the eventual settle is observable.
	CancelLegacyRequested = "abort_requested_legacy_bridge"
	CancelSuperseded      = "superseded"
)

Cancel outcomes reported for a task whose cancellation was requested. They distinguish requested (abort_pending), dispatched (abort_sent, abort_unconfirmed) and settled outcomes. "Settled" describes Pi's agent loop only: DevX does not track subprocesses spawned by tools or extensions and makes no claim that their process trees exited.

View Source
const AbortConfirmAfter = 30 * time.Second

AbortConfirmAfter bounds how long an abort may stay unsettled before it is reported as unconfirmed. Nothing is escalated: no keystrokes, no kill.

View Source
const BridgeCancelProtocol = 2

BridgeCancelProtocol is the oldest bridge protocol whose running-cancel handling this build relies on. Bridges at or above it may be asked to abort a running turn.

View Source
const BridgeProgressProtocol = 3

BridgeProgressProtocol is the oldest bridge protocol that reports mid-task progress. Older bridges work as before and simply report none.

View Source
const BridgeProtocol = 3

BridgeProtocol is the control protocol of the bridge embedded in this build. Version 2 adds: dropping a cancelled injection before acceptance, skipping cancel-flagged waiting tasks, aborting only accepted turns once under the same managed lease generation with no Pi-queued messages, and recording abort_sent/abort_withheld. Version 3 adds mid-task progress records (results/<task>.progress.json and "progress" events) and leaves the cancel semantics of 2 unchanged. A running Pi keeps the bridge it was launched with: restarting the MCP server does not refresh it; only a relaunch of that agent (a human action) does.

View Source
const EventProgress = "progress"

EventProgress is the event type the bridge writes after a completed assistant turn of a running task (bridge protocol 3+; rate limited).

View Source
const MarkerRepairMaxCreationGap = 2 * time.Minute

MarkerRepairMaxCreationGap bounds how long after its session record an MCP-created agent may have been created. Start writes the record and then the agent within milliseconds; a resumed start can take longer.

View Source
const MaxEditsPerTask = 32

MaxEditsPerTask bounds in-place edits of one task. Every applied edit's idempotency mark is kept for the task's whole life (never evicted), in the same atomic write as the edit, so a retry with an old key is always recognised. Instead of evicting marks, a further edit is refused: supersede the task (a new task with its own record) to keep changing it.

View Source
const MaxEventsResponseBytes = 60000

MaxEventsResponseBytes bounds the complete encoded MCP CallToolResult, including compatibility text, structuredContent and JSON escaping. Reserve over 5 KiB for normal JSON-RPC framing below a 64 KiB gateway limit; arbitrarily long caller-supplied request IDs are outside this result budget.

View Source
const MaxPromptPreview = 512

MaxPromptPreview bounds prompt previews in the queue view.

View Source
const MaxQueuePending = 50

MaxQueuePending bounds pending items returned by one queue view.

View Source
const MaxRecent = 10

MaxRecent bounds recent finished tasks in the queue view.

View Source
const MaxSessionListBytes = 60000

MaxSessionListBytes bounds the complete UTF-8 encoded MCP CallToolResult, including both compatibility text and structuredContent. This leaves over 5 KiB for JSON-RPC framing below a 64 KiB gateway result limit.

Variables

View Source
var ErrBridgeUpgradeRequired = errors.New("bridge upgrade required")

ErrBridgeUpgradeRequired reports that the Pi bridge holding a task does not support an operation; the agent must be relaunched by a human.

View Source
var ErrConflict = errors.New("conflict")

ErrConflict reports a failed revision check.

View Source
var ErrEditLimit = errors.New("edit limit reached")

ErrEditLimit reports that a task reached MaxEditsPerTask.

View Source
var ErrJournalNotInPlan = errors.New("journal entry is not a write in this run's plan")

ErrJournalNotInPlan means a journal entry does not record a write the run's plan contains; rollback refuses the whole run.

View Source
var ErrLaunchedSinceCheck = errors.New("agent was launched since it was checked")

ErrLaunchedSinceCheck means the agent was (re)launched after the caller last looked at it, so the caller's decision to relaunch is out of date.

View Source
var ErrNotFound = errors.New("not found")

ErrNotFound reports an unknown agent or task.

View Source
var ErrPlanStale = errors.New("records changed since the plan was made; re-run the dry run and review the new plan")

ErrPlanStale means the records changed since the plan was made.

View Source
var ErrTmuxUnavailable = errors.New("tmux could not be queried")

ErrTmuxUnavailable means tmux could not be queried, so pane state is unknown. Callers fail closed and change nothing.

Functions

func AcquireOwnedLock

func AcquireOwnedLock(path string) (func(), error)

AcquireOwnedLock takes a Go-only, cross-process mkdir lock at path (with crashed-owner recovery). Used by optional tool providers such as the artifact bridge for state that the Node bridge never touches.

func ApplyInstancePlan

func ApplyInstancePlan(p *InstancePlan, st InstanceStores) error

ApplyInstancePlan writes the plan. Every step re-checks its precondition against the LATEST records under the relevant lock and is idempotent: a step already done (same value present) is skipped; a step whose precondition no longer holds aborts with ErrPlanStale before writing. Session ids are written first (one atomic sessions write), then each agent binding under that agent's lock.

func ApplyMarkerPlan

func ApplyMarkerPlan(p *MarkerPlan, st MarkerStores) error

ApplyMarkerPlan writes the confirmed restorations of a reviewed plan.

func BridgeSource

func BridgeSource() []byte

BridgeSource returns the embedded Pi bridge extension.

func Denied

func Denied(format string, a ...any) error

Denied builds a PermissionError for SessionCreator implementations.

func IsPermissionDenied

func IsPermissionDenied(err error) bool

IsPermissionDenied reports whether err is a PermissionError.

func JournalMatchesPlan

func JournalMatchesPlan(e MarkerJournalEntry, p *MarkerPlan) bool

JournalMatchesPlan reports whether journal entry e records exactly one of the plan's confirmed restorations (same agent, session record identity and marker). Rollback acts only on such entries.

func Obj

func Obj(props map[string]any, required ...string) map[string]any

Obj builds a closed JSON object schema (exported for tool providers).

func Redact

func Redact(s string) string

Redact masks recognizable credentials in s.

func RollbackInstancePlan

func RollbackInstancePlan(p *InstancePlan, entries []JournalEntry, st InstanceStores) (reverted int, skipped []string, err error)

RollbackInstancePlan undoes the writes a run journaled. It refuses the whole run (nothing written) unless every entry is a write in the run's verified plan. Each value is removed only from the exact record the plan wrote it to, and only while that record still holds it: a session must keep the planned created_at and path, an agent the planned session, instance id and session_created_at. Anything else (recreated, rebound, already undone, never written because apply stopped first) is left alone and reported, so rollback is idempotent and safe to retry or run concurrently. Agent bindings are removed first, under each agent's lock; session ids then in one locked sessions write.

func RollbackMarkers

func RollbackMarkers(entries []MarkerJournalEntry, st MarkerStores) (reverted int, skipped []string, err error)

RollbackMarkers removes exactly the markers recorded in the journal, and only where the record is still the same instance holding that same marker.

func ValidAgentID

func ValidAgentID(id string) bool

func ValidTaskID

func ValidTaskID(id string) bool

func ValidateInstanceJournal

func ValidateInstanceJournal(p *InstancePlan, entries []JournalEntry) error

ValidateInstanceJournal checks that the plan is intact and that every journal entry records exactly a write the plan contains (same kind, record, id, restored flag, basis and, when present, plan hash).

func WriteBridge

func WriteBridge(root string) (string, error)

WriteBridge installs the bridge extension under root (not in Pi's global extension directory, so it never loads into unrelated Pi sessions) and returns its path. It is rewritten only when the content changed.

Types

type AdoptRequest

type AdoptRequest struct {
	Session     string // DevX session name
	PaneID      string // exact tmux pane (%id) running the session's Pi
	PiSessionID string // Pi session ID of that conversation
	By          string
}

AdoptRequest registers an existing DevX session's running Pi as a managed agent. It is a human (CLI) action and is never exposed over MCP.

type AdoptResult

type AdoptResult struct {
	AgentID  string `json:"agent_id"`
	Session  string `json:"session"`
	Pane     string `json:"pane"`
	Replayed bool   `json:"replayed"`
}

AdoptResult identifies the adopted agent.

type AdoptedSession

type AdoptedSession struct {
	Name       string
	Path       string
	Project    string
	TmuxName   string
	InstanceID string
	CreatedAt  time.Time
}

AdoptedSession describes the existing session an agent was adopted into.

type Agent

type Agent struct {
	ID          string    `json:"id"`
	DevxSession string    `json:"devx_session"`
	Project     string    `json:"project,omitempty"`
	Worktree    string    `json:"worktree"`
	PiSessionID string    `json:"pi_session_id"`
	Binding     Binding   `json:"binding"`
	Lease       Lease     `json:"lease"`
	CreatedAt   time.Time `json:"created_at"`
	UpdatedAt   time.Time `json:"updated_at"`
	LaunchCount int       `json:"launch_count"`
	LaunchNonce string    `json:"launch_nonce,omitempty"`
	// LastLaunchAt is when the latest launch was recorded (before Pi starts).
	LastLaunchAt time.Time `json:"last_launch_at,omitempty"`
	// PaneLaunched is set once DevX has started Pi in the bound pane (a
	// launch or respawn completed). Until then an adopted agent's pane still
	// runs the human's own command.
	PaneLaunched  bool   `json:"pane_launched,omitempty"`
	CreatedBy     string `json:"created_by,omitempty"`
	StartTaskID   string `json:"start_task_id,omitempty"`
	StartIdemHash string `json:"start_idempotency_hash,omitempty"`
	// RetiredAt is set when the agent's DevX session was removed. A retired
	// agent keeps its records for inspection but accepts no new work.
	RetiredAt *time.Time `json:"retired_at,omitempty"`
	// Adopted marks an agent registered for an existing human-created DevX
	// session and Pi (devx agent adopt) rather than created by Start. Its
	// session is the owner's normal session: DevX never creates windows in
	// it and only ever respawns the exact adopted pane.
	Adopted bool `json:"adopted,omitempty"`
	// SessionInstanceID is the instance id of the exact DevX session record
	// this agent was bound to (at start or adoption, or by the reviewed
	// `devx session instances` migration). A session recreated under the same
	// name has a different id, so the agent is never bound to it implicitly.
	// Empty for agents from before instance ids that were not migrated.
	SessionInstanceID string `json:"session_instance_id,omitempty"`
	// SessionCreatedAt is the bound session record's created_at. It is the
	// identity witness when an older DevX writer dropped the record's
	// instance_id (see session.MatchesBoundInstance): authorization-relevant.
	SessionCreatedAt time.Time `json:"session_created_at,omitempty"`
}

Agent is the durable record for one managed Pi session.

type AgentPlanItem

type AgentPlanItem struct {
	AgentID           string `json:"agent_id"`
	Session           string `json:"session"`
	Action            string `json:"action"`
	Reason            string `json:"reason,omitempty"`
	SessionInstanceID string `json:"session_instance_id,omitempty"`
	// SessionCreatedAt is recorded on the agent as the drop-witness.
	SessionCreatedAt time.Time `json:"session_created_at,omitempty"`
	// Evidence for the owner (no secrets, no file contents).
	Marker          string `json:"marker"` // local_only | managed_agent | none
	SessionVsAgent  string `json:"session_created_vs_agent_created,omitempty"`
	Adopted         bool   `json:"adopted,omitempty"`
	Retired         bool   `json:"retired,omitempty"`
	CurrentBindings string `json:"current_binding,omitempty"`
	// Basis is what the binding rests on (see Basis* constants).
	Basis string `json:"basis,omitempty"`
}

AgentPlanItem is one agent record.

type AgentView

type AgentView struct {
	Agent        *Agent  `json:"agent"`
	State        string  `json:"state"`
	BridgeOnline bool    `json:"bridge_online"`
	Bridge       *Bridge `json:"bridge,omitempty"`
	PaneAlive    bool    `json:"pane_alive"`
	// PaneForeign means the bound pane id exists but provably belongs to
	// something else (its start command is not this agent's launch script),
	// typically because a restarted tmux server reused the id.
	PaneForeign bool `json:"pane_foreign,omitempty"`
	// BridgeDead means the Pi that last reported is confirmed gone: it shut
	// down, or its heartbeat is stale and its process no longer exists.
	BridgeDead  bool   `json:"bridge_dead,omitempty"`
	BindingOK   bool   `json:"binding_ok"`
	Detail      string `json:"detail,omitempty"`
	CurrentTask string `json:"current_task,omitempty"`
	Waiting     int    `json:"waiting_tasks"`
	LastSeq     int64  `json:"last_event_seq"`
	AttachHint  string `json:"attach_hint"`
	// BridgeProtocol is what the agent's live bridge reports (0 = a bridge
	// from before protocol versioning). BridgeUpgradeNeeded means the
	// running Pi was launched with an older bridge than this build's: it
	// lacks some features until a human relaunches it (protocol < 2: running
	// cancel is refused; protocol < 3: no mid-task progress is reported).
	BridgeProtocol      int  `json:"bridge_protocol"`
	BridgeUpgradeNeeded bool `json:"bridge_upgrade_needed,omitempty"`
	// LatestProgress is the newest progress the bridge recorded for
	// CurrentTask (redacted, bounded); nil when there is no running task or
	// its bridge has not reported any (bridges before protocol 3 never do).
	LatestProgress *ProgressView `json:"latest_progress,omitempty"`
}

AgentView is the reconciled status of an agent.

type Binding

type Binding struct {
	TmuxSession string `json:"tmux_session"`
	WindowID    string `json:"window_id"`
	PaneID      string `json:"pane_id"`
}

Binding is the exact tmux location of the agent's Pi process.

type Bridge

type Bridge struct {
	Instance      string    `json:"instance"`
	Nonce         string    `json:"nonce"`
	HumanTyping   bool      `json:"human_typing,omitempty"`
	PID           int       `json:"pid"`
	Pane          string    `json:"pane"`
	PiSessionID   string    `json:"pi_session_id"`
	PiSessionFile string    `json:"pi_session_file,omitempty"`
	Idle          bool      `json:"idle"`
	CurrentTask   string    `json:"current_task,omitempty"`
	Heartbeat     time.Time `json:"heartbeat"`
	Mode          string    `json:"mode,omitempty"`
	// Protocol is the bridge's control protocol version (see
	// BridgeProtocol). Bridges from before versioning write none (0).
	Protocol int `json:"protocol,omitempty"`
}

Bridge is the heartbeat the extension writes from inside Pi.

type BridgeUpgradeError

type BridgeUpgradeError struct {
	Kind     string // BridgeKindUpgrade or BridgeKindUnavailable
	AgentID  string
	TaskID   string
	Protocol int // protocol of the bridge holding the task; -1 if not known
	Reason   string
}

BridgeUpgradeError explains why an operation was refused for the bridge holding a task. Nothing is recorded when it is returned.

func (*BridgeUpgradeError) Error

func (e *BridgeUpgradeError) Error() string

func (*BridgeUpgradeError) Unwrap

func (e *BridgeUpgradeError) Unwrap() error

type CancelRequest

type CancelRequest struct {
	AgentID          string // optional; when set, the task must belong to it
	TaskID           string
	ExpectedRevision *int64
}

CancelRequest cancels a task. ExpectedRevision (optional) makes the cancellation of a pending task compare-and-swap, so a caller does not cancel a prompt that someone else just edited.

type Config

type Config struct {
	// PiCommand is the Pi executable. Defaults to "pi".
	PiCommand string
	// PiCommandArgs are placed right after PiCommand (e.g. when PiCommand is
	// a wrapper such as env). Human-owned configuration only.
	PiCommandArgs []string
	// PiArgs are extra arguments appended to the Pi command line. They come
	// from human-owned configuration, never from MCP callers.
	PiArgs []string
	// PassEnv names environment variables copied into the launch script.
	PassEnv []string
	// AllowedProjects is the allowlist for start. Empty denies all starts.
	AllowedProjects []string
	// LaunchPolicyErr, when set, means the owner's launch policy could not
	// be loaded. Start, launch and relaunch then fail closed with this error;
	// read-only and control operations keep working.
	LaunchPolicyErr error
	// DevxExecutable lets the bridge raise DevX attention flags. Optional.
	DevxExecutable string
	// WindowName is the tmux window name for agents.
	WindowName string
}

Config controls how agents are launched.

type ConflictError

type ConflictError struct {
	TaskID   string
	Expected int64
	Current  int64
	State    string
	Phase    string
	Reason   string
}

ConflictError explains a failed compare-and-swap.

func (*ConflictError) Error

func (e *ConflictError) Error() string

func (*ConflictError) Unwrap

func (e *ConflictError) Unwrap() error

type CreatedSession

type CreatedSession struct {
	Name       string
	Path       string
	Project    string
	TmuxName   string
	InstanceID string
	CreatedAt  time.Time
}

CreatedSession is what a SessionCreator returns for a new DevX session. InstanceID and CreatedAt identify the exact session record; the agent records them as its binding (Agent.SessionInstanceID). Empty means the creator cannot identify instances (test doubles, older builds).

type CreationEvent

type CreationEvent struct {
	Seq               int64  `json:"seq"`
	Session           string `json:"session"`
	SessionInstanceID string `json:"session_instance_id,omitempty"`
}

CreationEvent is what an agent's own agent_created event recorded.

type EditResult

type EditResult struct {
	TaskID   string `json:"task_id"`
	Revision int64  `json:"revision"`
	Replayed bool   `json:"replayed"`
	// ReplacementTaskID is set by supersede.
	ReplacementTaskID string `json:"replacement_task_id,omitempty"`
}

EditResult is the outcome of an edit.

type Event

type Event struct {
	Seq    int64          `json:"seq"`
	Time   time.Time      `json:"time"`
	Type   string         `json:"type"`
	TaskID string         `json:"task_id,omitempty"`
	Source string         `json:"source"`
	Data   map[string]any `json:"data,omitempty"`
}

Event is one entry in an agent's durable, cursor-addressable event log.

type InstancePlan

type InstancePlan struct {
	Version  int               `json:"version"`
	Sessions []SessionPlanItem `json:"sessions"`
	Agents   []AgentPlanItem   `json:"agents"`
	Summary  map[string]int    `json:"summary"`
	// OwnerConfirmed lists the candidate agent ids the owner confirmed
	// (part of the hash: a different confirmation set is a different plan).
	OwnerConfirmed []string `json:"owner_confirmed,omitempty"`
	// UnknownConfirmations are --confirm ids that are not candidates in
	// this plan; the command refuses to apply such a plan.
	UnknownConfirmations []string `json:"unknown_confirmations,omitempty"`
	Hash                 string   `json:"plan_hash"`
	Notes                []string `json:"notes,omitempty"`
}

InstancePlan is a reviewed, hashable set of changes.

func PlanInstances

func PlanInstances(sessions map[string]*session.Session, agents []*Agent, newID func(name string) string) *InstancePlan

PlanInstances computes the migration plan. sessions and agents are the current records; newID proposes the id for a session that has none (deterministic in production so a reviewed plan hash stays valid).

func PlanInstancesWithHistory

func PlanInstancesWithHistory(sessions map[string]*session.Session, agents []*Agent, newID func(name string) string, recorded RecordedBindingFunc) *InstancePlan

PlanInstancesWithHistory is PlanInstances that also detects agents whose agent.json lost its binding (an older DevX binary rewrote the record) and restores the binding recorded in their event log, instead of treating them as unbound legacy agents.

func PlanInstancesWithOptions

func PlanInstancesWithOptions(sessions map[string]*session.Session, agents []*Agent, newID func(name string) string, opts PlanOptions) *InstancePlan

PlanInstancesWithOptions computes the migration plan; see PlanInstances.

func (*InstancePlan) HasWork

func (p *InstancePlan) HasWork() bool

HasWork reports whether applying the plan would change anything.

func (*InstancePlan) VerifyHash

func (p *InstancePlan) VerifyHash() bool

VerifyHash reports whether the plan content still matches its hash.

type InstanceStores

type InstanceStores struct {
	// MutateSessions applies fn to the latest session records under the
	// sessions lock and persists them atomically.
	MutateSessions func(fn func(map[string]*session.Session) error) error
	// LoadSessions reads the latest session records (no lock needed: the
	// sessions file is replaced atomically).
	LoadSessions func() (map[string]*session.Session, error)
	Agents       *Store
	// Journal appends one entry durably (fsync) before returning.
	Journal func(JournalEntry) error
	Now     func() time.Time
}

InstanceStores is the narrow persistence surface the migration needs.

type JournalEntry

type JournalEntry struct {
	Time       time.Time `json:"time"`
	Kind       string    `json:"kind"` // session | agent
	Name       string    `json:"name"`
	InstanceID string    `json:"instance_id"`
	Restored   bool      `json:"restored_existing_id,omitempty"`
	Basis      string    `json:"basis,omitempty"`
	// PlanHash names the plan this step belongs to. Journals written
	// before it existed omit it; membership is still checked exactly.
	PlanHash string `json:"plan_hash,omitempty"`
}

JournalEntry is one applied step (newline-delimited JSON).

type Lease

type Lease struct {
	Holder     string    `json:"holder"`
	Generation int64     `json:"generation"`
	Since      time.Time `json:"since"`
	Reason     string    `json:"reason,omitempty"`
	By         string    `json:"by,omitempty"`
}

Lease records who currently drives an agent. Generation increments on every change so clients can detect that control moved underneath them.

type MCPServer

type MCPServer struct {
	M       *Manager
	Name    string
	Version string
	Client  string // identity recorded on agents created through this server
	// Extra optionally contributes additional tools (nil = none).
	Extra ToolProvider
}

MCPServer serves the tools over one stdio connection.

func (*MCPServer) Serve

func (s *MCPServer) Serve(in io.Reader, out io.Writer) error

Serve reads requests until EOF. Responses are written as they complete; tool calls run concurrently so a blocking pi_events does not stall others.

type Manager

type Manager struct {
	Store   *Store
	Tmux    Tmux
	Creator SessionCreator
	Config  Config
	// contains filtered or unexported fields
}

Manager implements the managed-agent operations shared by MCP and CLI.

func NewManager

func NewManager(store *Store, tmux Tmux, creator SessionCreator, cfg Config) *Manager

func (*Manager) Adopt

func (m *Manager) Adopt(req AdoptRequest) (*AdoptResult, error)

Adopt registers an existing session's Pi without touching it: no tmux command other than reading the pane, no relaunch, no prompt. The agent starts under human control, so nothing is delivered until the owner relaunches (to load the bridge) and releases. Adopting the same session, pane and Pi session again returns the existing agent.

func (*Manager) AgentStatus

func (m *Manager) AgentStatus(agentID string) (*AgentView, error)

AgentStatus reconciles durable state with tmux and the bridge heartbeat. It is safe to call after an MCP server restart: nothing lives in memory.

func (*Manager) Cancel

func (m *Manager) Cancel(taskID string) (*Task, error)

Cancel cancels a task by id (see CancelTask).

func (*Manager) CancelTask

func (m *Manager) CancelTask(req CancelRequest) (*Task, error)

CancelTask cancels a waiting task immediately, or asks the bridge to abort a running one. While a human holds control, running (accepted) work belongs to the human and remote cancellation is denied; a task handed to Pi but not yet accepted can still be cancelled, which only drops the injection. The bridge re-checks control at the moment it would abort and withholds the abort if control changed. Repeating a cancellation is a no-op that reports the current outcome.

func (*Manager) Events

func (m *Manager) Events(agentID string, after int64, limit int, wait time.Duration) ([]Event, int64, error)

Events returns events after the cursor. With wait > 0 it blocks (by watching the local log, not by polling any model) until a new event is written or wait elapses.

func (*Manager) Inspect

func (m *Manager) Inspect(agentID string, lines int) (string, error)

Inspect returns a read-only snapshot of the agent's pane.

func (*Manager) List

func (m *Manager) List() ([]*AgentView, error)

List returns reconciled views for all managed agents.

func (*Manager) PeekStatus

func (m *Manager) PeekStatus(agentID string) (*AgentView, error)

PeekStatus is AgentStatus without reconciliation: it never moves orphaned running tasks to unknown, so a dry run or a plan can look without writing task state. The view is the same except that such tasks still show as running.

func (*Manager) Queue

func (m *Manager) Queue(agentID string, recent int) (*QueueView, error)

Queue returns the reconciled queue for an agent.

func (*Manager) Relaunch

func (m *Manager) Relaunch(agentID string, force bool) error

Relaunch restarts Pi for an agent whose process exited, resuming the same Pi session ID so conversation history and identity are preserved.

func (*Manager) RelaunchIfUnchanged

func (m *Manager) RelaunchIfUnchanged(agentID string, expectLaunchCount int) error

RelaunchIfUnchanged relaunches like Relaunch(id, false), but only if the agent's launch count is still expectLaunchCount: nobody launched it since the caller decided it needed recovery. The check and the launch run under the agent's launch lock, so concurrent recoveries start at most one Pi.

func (*Manager) Release

func (m *Manager) Release(agentID, by string, dropQueued bool) (*Agent, error)

Release returns control to managed dispatch. Only humans (CLI or the Pi TUI command) can release; it is deliberately not exposed over MCP.

func (*Manager) Result

func (m *Manager) Result(taskID string, offset, maxBytes int) (*ResultChunk, error)

Result returns up to maxBytes of the (redacted) final text from offset.

func (*Manager) RetireForSession

func (m *Manager) RetireForSession(agentID, session string) error

RetireForSession retires agentID after its DevX session was removed: it marks the agent retired, cancels its waiting tasks, and records an event. Records are kept. It is a no-op if the agent does not exist, is already retired, or belongs to a different session.

func (*Manager) Send

func (m *Manager) Send(req SendRequest) (*SendResult, error)

func (*Manager) Start

func (m *Manager) Start(req StartRequest) (*StartResult, error)

Start is idempotent on IdempotencyKey: a retry after a dropped response or a crash mid-start returns (and finishes) the original start instead of creating a second session or task.

func (*Manager) SupersedePending

func (m *Manager) SupersedePending(req SupersedePendingRequest) (*EditResult, error)

SupersedePending atomically (under the agent lock) cancels a pending task and queues a replacement with the same queue order. The replacement id is fixed in an idempotency record written under the same lock before any task changes, and the old task is cancelled before the replacement is created, so a crash leaves at most one of them waiting and a retry completes the same replacement.

func (*Manager) Takeover

func (m *Manager) Takeover(agentID, by string) (*Agent, error)

Takeover gives a human control. Queued remote work stops being delivered until Release. A task already delivered keeps running in Pi, where the human can see and interrupt it.

func (*Manager) TaskStatus

func (m *Manager) TaskStatus(taskID string) (*TaskView, *AgentView, error)

func (*Manager) UpdatePending

func (m *Manager) UpdatePending(req UpdatePendingRequest) (*EditResult, error)

UpdatePending edits a pending task's prompt in place (same task id and queue position). The applied key is stored in the same atomic write as the edit, so a crash cannot apply an edit without recording it.

type MarkerBackup

type MarkerBackup struct {
	Path     string
	SHA256   string
	Sessions map[string]*session.Session
}

MarkerBackup is one reviewed sessions.json backup.

type MarkerBackupRef

type MarkerBackupRef struct {
	Path   string `json:"path"`
	SHA256 string `json:"sha256"`
}

MarkerBackupRef identifies a reviewed backup in the plan.

type MarkerJournalEntry

type MarkerJournalEntry struct {
	Time              time.Time             `json:"time"`
	Kind              string                `json:"kind"` // "local_only_marker"
	Session           string                `json:"session"`
	SessionCreatedAt  time.Time             `json:"session_created_at"`
	SessionPath       string                `json:"session_path"`
	SessionInstanceID string                `json:"session_instance_id,omitempty"`
	AgentID           string                `json:"agent_id"`
	Marker            session.LocalOnlyMeta `json:"marker"`
	PlanHash          string                `json:"plan_hash"`
}

MarkerJournalEntry is one applied marker write (newline-delimited JSON).

type MarkerPlan

type MarkerPlan struct {
	Version              int               `json:"version"`
	Backups              []MarkerBackupRef `json:"backups"`
	Items                []MarkerPlanItem  `json:"items"`
	OwnerConfirmed       []string          `json:"owner_confirmed,omitempty"`
	UnknownConfirmations []string          `json:"unknown_confirmations,omitempty"`
	Summary              map[string]int    `json:"summary"`
	Notes                []string          `json:"notes,omitempty"`
	Hash                 string            `json:"plan_hash"`
}

MarkerPlan is a reviewed, hashable set of marker restorations.

func PlanMarkerRepair

func PlanMarkerRepair(sessions map[string]*session.Session, agents []*Agent, opts MarkerPlanOptions) *MarkerPlan

PlanMarkerRepair computes the repair plan. It reads only.

func (*MarkerPlan) HasWork

func (p *MarkerPlan) HasWork() bool

HasWork reports whether applying the plan would write anything.

func (*MarkerPlan) VerifyHash

func (p *MarkerPlan) VerifyHash() bool

VerifyHash reports whether the plan content still matches its hash.

type MarkerPlanItem

type MarkerPlanItem struct {
	AgentID string `json:"agent_id"`
	Session string `json:"session"`
	Action  string `json:"action"`
	Reason  string `json:"reason,omitempty"`
	// Identity of the exact record (apply refuses any other).
	SessionCreatedAt  time.Time `json:"session_created_at,omitempty"`
	SessionPath       string    `json:"session_path,omitempty"`
	SessionInstanceID string    `json:"session_instance_id,omitempty"`
	// Identity of the exact agent.
	AgentCreatedAt time.Time `json:"agent_created_at,omitempty"`
	AgentBoundID   string    `json:"agent_session_instance_id,omitempty"`
	// Evidence.
	SessionVsAgent string   `json:"session_created_vs_agent_created,omitempty"`
	CreationSeq    int64    `json:"agent_created_event_seq,omitempty"`
	TmuxTag        string   `json:"tmux_tag,omitempty"`
	Witnesses      []string `json:"backup_witnesses,omitempty"`
	// Marker is the exact marker that will be written (from the backups).
	Marker *session.LocalOnlyMeta `json:"marker,omitempty"`
}

MarkerPlanItem is one MCP-created agent and its session.

type MarkerPlanOptions

type MarkerPlanOptions struct {
	Backups   []MarkerBackup
	Witnesses MarkerWitnesses
	Confirmed []string
}

MarkerPlanOptions configures PlanMarkerRepair.

type MarkerStores

type MarkerStores struct {
	// MutateSessions applies fn to the latest records under the sessions
	// lock and persists them atomically (no write if fn returns an error).
	MutateSessions func(fn func(map[string]*session.Session) error) error
	Agents         *Store
	// VerifyBackup re-reads a reviewed backup and returns its sha256.
	VerifyBackup func(path string) (string, error)
	// TmuxTag re-checks the live tmux tag at apply time.
	TmuxTag func(sessionName string) (string, bool, error)
	// Journal appends one entry durably (fsync) before returning.
	Journal func(MarkerJournalEntry) error
	Now     func() time.Time
}

MarkerStores is the narrow persistence surface the repair needs.

type MarkerWitnesses

type MarkerWitnesses struct {
	// Creation returns the agent's agent_created event (ok=false if none).
	Creation func(agentID string) (CreationEvent, bool, error)
	// TmuxTag returns the DEVX_LOCAL_ONLY_AGENT value of the live tmux
	// session with exactly this name; present=false if no such session.
	TmuxTag func(sessionName string) (value string, present bool, err error)
}

MarkerWitnesses are the read-only lookups the planner needs.

type PaneInfo

type PaneInfo struct {
	Exists bool
	// QueryFailed means tmux could not be asked (server busy or restarting,
	// timeout, tmux missing). It is NOT evidence that the pane is gone.
	QueryFailed bool
	SessionName string
	WindowID    string
	Dead        bool
	PID         string
	// LinkedSessions lists every session the pane's window is linked into.
	// Grouped sessions (e.g. DevX's "<name>-web" viewer) share windows, and
	// tmux reports whichever of them was used most recently as SessionName.
	LinkedSessions []string
	// Command is the pane's current foreground command (pane_current_command).
	Command string
	// StartCommand is the command the pane was created or last respawned
	// with (pane_start_command). HasStartCommand is false when tmux did not
	// report the field at all, so callers can tell "unknown" from "empty".
	StartCommand    string
	HasStartCommand bool
}

PaneInfo describes a pane as tmux currently sees it.

func (PaneInfo) InSession

func (p PaneInfo) InSession(session string) bool

InSession reports whether the pane's window belongs to session, either as the session tmux reports or through a linked/grouped session.

type PermissionError

type PermissionError struct{ Msg string }

PermissionError marks a request the caller is not allowed to make. MCP surfaces these distinctly from operational failures.

func (*PermissionError) Error

func (e *PermissionError) Error() string

type PlanOptions

type PlanOptions struct {
	// Recorded looks up an agent's binding in its event log (optional).
	Recorded RecordedBindingFunc
	// Confirmed is the set of candidate agent ids (no marker) the owner
	// explicitly confirmed. Nothing else turns a candidate into a binding.
	Confirmed []string
}

PlanOptions configures PlanInstancesWithOptions.

type Progress

type Progress struct {
	TaskID           string    `json:"task_id"`
	Turn             int       `json:"turn"`
	At               time.Time `json:"at"`
	Excerpt          string    `json:"excerpt"`
	ExcerptTruncated bool      `json:"excerpt_truncated,omitempty"`
	LastTool         string    `json:"last_tool,omitempty"`
}

Progress is the bridge's record of the newest completed assistant turn of a running task (results/<task>.progress.json). Excerpt is raw (bounded by the bridge); it is redacted and cut on read.

type ProgressView

type ProgressView struct {
	At               time.Time `json:"at"`
	Turn             int       `json:"turn"`
	Excerpt          string    `json:"excerpt"`
	ExcerptTruncated bool      `json:"excerpt_truncated,omitempty"`
	LastTool         string    `json:"last_tool,omitempty"`
}

ProgressView is the client-facing latest_progress: redacted and bounded.

type QueueItem

type QueueItem struct {
	TaskID        string     `json:"task_id"`
	Order         int64      `json:"order"`
	State         string     `json:"state"`
	Phase         string     `json:"phase"`
	Revision      int64      `json:"revision"`
	WaitingReason string     `json:"waiting_reason,omitempty"`
	CancelOutcome string     `json:"cancel_outcome,omitempty"`
	AgeSeconds    int64      `json:"age_seconds"`
	CreatedAt     time.Time  `json:"created_at"`
	DeliveredAt   *time.Time `json:"delivered_at,omitempty"`
	AcceptedAt    *time.Time `json:"accepted_at,omitempty"`
	AcceptedAs    string     `json:"accepted_as,omitempty"`
	FinishedAt    *time.Time `json:"finished_at,omitempty"`
	PromptBytes   int        `json:"prompt_bytes"`
	PromptPreview string     `json:"prompt_preview"`
	PreviewTrunc  bool       `json:"prompt_preview_truncated,omitempty"`
	Supersedes    string     `json:"supersedes,omitempty"`
	SupersededBy  string     `json:"superseded_by,omitempty"`
	Note          string     `json:"note,omitempty"`
	Editable      bool       `json:"editable"`
}

QueueItem is one entry of the queue view.

type QueueView

type QueueView struct {
	AgentID  string      `json:"agent_id"`
	State    string      `json:"agent_state"`
	Control  string      `json:"control"`
	LeaseGen int64       `json:"lease_generation"`
	InFlight []QueueItem `json:"in_flight"`
	Pending  []QueueItem `json:"pending"`
	// PendingTotal counts all queued tasks; Pending holds at most
	// MaxQueuePending of them, in delivery order.
	PendingTotal     int         `json:"pending_total"`
	PendingTruncated bool        `json:"pending_truncated,omitempty"`
	Recent           []QueueItem `json:"recent"`
	LastSeq          int64       `json:"last_event_seq"`
}

QueueView is the queue of one agent: work in flight, pending work in delivery order, and a few recent outcomes.

type RecordedBindingFunc

type RecordedBindingFunc func(agentID string) (string, time.Time, error)

RecordedBindingFunc returns an agent's binding as recorded in its event log ("" if none); see Store.RecordedBinding.

type ResultChunk

type ResultChunk struct {
	TaskID     string `json:"task_id"`
	State      string `json:"state"`
	Available  bool   `json:"available"`
	Offset     int    `json:"offset"`
	NextOffset int    `json:"next_offset"`
	TotalBytes int    `json:"total_bytes"`
	EOF        bool   `json:"eof"`
	Text       string `json:"text"`
}

ResultChunk is a cursor-addressed slice of a task's final text.

type SendRequest

type SendRequest struct {
	AgentID        string
	Prompt         string
	IdempotencyKey string
}

SendRequest queues a follow-up prompt for an existing managed agent.

type SendResult

type SendResult struct {
	TaskID   string `json:"task_id"`
	Replayed bool   `json:"replayed"`
}

SendResult describes the queued (or previously queued) task.

type SessionAdopter

type SessionAdopter interface {
	Adopt(name, agentID string) (AdoptedSession, error)
	ReleaseAdoption(name, agentID string) error
	VerifyAdopted(name string, a *Agent) (AdoptedSession, error)
}

SessionAdopter is implemented by SessionCreators that can register an existing, human-created DevX session for an adopted agent. Adopt must verify the session exists, is not local-only, and is not already managed, then record agentID on it; ReleaseAdoption undoes that record (used when adoption fails after the marker was written). VerifyAdopted checks, without writing anything, that the session still carries agentID's adoption marker and returns it (used to re-verify before relaunch, so a recreated session is never re-adopted implicitly).

type SessionCreator

type SessionCreator interface {
	// Create makes a new session owned by agentID. If the session already
	// exists it may be adopted only when it was created for this same agent
	// by an earlier interrupted attempt of this start. Anything else belongs
	// to someone else: fail.
	Create(name, project, agentID string) (CreatedSession, error)
	// EnsureTmux makes sure the session's tmux session exists and belongs
	// to agent a. For an agent bound to a session instance the session
	// record must be that instance (session.MatchesBoundInstance); a
	// recreated session is refused even if its marker names the agent.
	EnsureTmux(name string, a *Agent) error
	// Exists reports whether a DevX session with this name exists.
	Exists(name string) (bool, error)
}

SessionCreator creates and prepares new DevX sessions. The production implementation creates a local-only session (no ports, routes, templates or project services) bound to the agent id; tests use a temp dir.

type SessionPlanItem

type SessionPlanItem struct {
	Session    string `json:"session"`
	Action     string `json:"action"`
	InstanceID string `json:"instance_id,omitempty"`
	Existing   bool   `json:"existing_id,omitempty"`
	Reason     string `json:"reason,omitempty"`
	// Identity of the exact record the id is written to: apply refuses if
	// the record at this name no longer has this created_at and path.
	CreatedAt time.Time `json:"created_at"`
	Path      string    `json:"path"`
}

SessionPlanItem is one session record.

type StartRequest

type StartRequest struct {
	Project        string
	SessionName    string
	Prompt         string
	IdempotencyKey string
	By             string
}

StartRequest starts a new Pi agent in a NEW DevX session.

type StartResult

type StartResult struct {
	AgentID     string `json:"agent_id"`
	TaskID      string `json:"task_id"`
	Session     string `json:"session"`
	PiSessionID string `json:"pi_session_id"`
	Replayed    bool   `json:"replayed"`
}

StartResult identifies what was (or had already been) started.

type Store

type Store struct {
	Root string
	// contains filtered or unexported fields
}

Store is the on-disk state shared with the bridge extension. Layout:

<root>/agents/<agent-id>/agent.json        Agent record (lease, binding)
<root>/agents/<agent-id>/tasks/<id>.json   Task records
<root>/agents/<agent-id>/results/<id>.txt  Final assistant text per task
<root>/agents/<agent-id>/results/<id>.progress.json
                                           Latest mid-task progress (bridge-owned)
<root>/agents/<agent-id>/events.jsonl      Durable event log (seq ordered)
<root>/agents/<agent-id>/seq               Last event seq
<root>/agents/<agent-id>/bridge.json       Bridge heartbeat (bridge-owned)
<root>/agents/<agent-id>/agent.lock/       Shared mkdir lock
<root>/idempotency/<key-hash>.json         Start/send idempotency records

func NewStore

func NewStore(root string) *Store

func (*Store) AgentDir

func (s *Store) AgentDir(id string) string

func (*Store) AppendEvent

func (s *Store) AppendEvent(agentID string, ev Event) (Event, error)

AppendEvent appends to the durable event log. Callers must hold the agent lock; the bridge appends under the same lock so seq values never collide.

func (*Store) CreationEventOf

func (s *Store) CreationEventOf(agentID string) (CreationEvent, bool, error)

CreationEventOf returns the agent's first agent_created event.

func (*Store) FindTask

func (s *Store) FindTask(taskID string) (*Task, error)

FindTask locates a task by ID across managed agents.

func (*Store) ListAgents

func (s *Store) ListAgents() ([]*Agent, error)

func (*Store) ListAgentsLenient

func (s *Store) ListAgentsLenient() ([]*Agent, map[string]error, error)

ListAgentsLenient is ListAgents that keeps going past unreadable records: it returns every agent it could load plus the load error of each one it could not, so one corrupt agent.json does not hide the rest.

func (*Store) ListTasks

func (s *Store) ListTasks(agentID string) ([]*Task, error)

func (*Store) LoadAgent

func (s *Store) LoadAgent(id string) (*Agent, error)

func (*Store) LoadBridge

func (s *Store) LoadBridge(agentID string) (*Bridge, error)

func (*Store) LoadTask

func (s *Store) LoadTask(agentID, taskID string) (*Task, error)

func (*Store) ReadEvents

func (s *Store) ReadEvents(agentID string, after int64, limit int) ([]Event, int64, error)

ReadEvents returns up to limit events with Seq > after, plus the last seq in the log. Events are returned in log order; a missed or replayed event is recovered by re-reading from the client's last acknowledged seq.

func (*Store) ReadProgress

func (s *Store) ReadProgress(agentID, taskID string) *ProgressView

ReadProgress returns the latest progress the bridge recorded for a task, redacted and bounded for clients, or nil if none (older bridges never write it). An unreadable or mismatched record reads as none: progress is advisory and never fails a status call.

func (*Store) ReadResult

func (s *Store) ReadResult(agentID, taskID string) (string, bool, error)

ReadResult returns the redacted final text for a task.

func (*Store) RecordedBinding

func (s *Store) RecordedBinding(agentID string) (string, time.Time, error)

RecordedBinding returns the most recent session-instance binding recorded in the agent's event log (agent_created, agent_adopted or session_instance_bound; cleared by session_instance_unbound), or "" if none. A session_instance_bound event counts only if it records an accepted basis (marker, owner_confirmed or event_log): pre-release migration builds wrote basis-less events for bindings inferred from timestamps alone, and those must never be restored as trusted bindings.

func (*Store) SaveAgent

func (s *Store) SaveAgent(a *Agent) error

func (*Store) SaveTask

func (s *Store) SaveTask(t *Task) error

SaveTask writes a task. Callers hold the agent lock. Every write bumps the task's revision, which pending-work edits compare-and-swap on.

func (*Store) WithAgentLock

func (s *Store) WithAgentLock(id string, fn func() error) error

WithAgentLock runs fn while holding the lock shared with the bridge.

type SupersedePendingRequest

type SupersedePendingRequest struct {
	AgentID          string
	TaskID           string
	Prompt           string
	ExpectedRevision int64
	IdempotencyKey   string
}

SupersedePendingRequest cancels a pending task and queues a replacement in its queue position.

type Task

type Task struct {
	ID              string     `json:"id"`
	AgentID         string     `json:"agent_id"`
	Order           int64      `json:"order"`
	Prompt          string     `json:"prompt"`
	IdempotencyHash string     `json:"idempotency_hash"`
	State           string     `json:"state"`
	CreatedAt       time.Time  `json:"created_at"`
	UpdatedAt       time.Time  `json:"updated_at"`
	DeliveredAt     *time.Time `json:"delivered_at,omitempty"`
	ConfirmedAt     *time.Time `json:"confirmed_at,omitempty"`
	// AcceptedAs is how Pi took the prompt: "turn" (Pi was idle and started
	// a turn for it) or "followUp" (Pi was already busy and queued it
	// internally; the bridge never delivers as steer).
	AcceptedAs      string     `json:"accepted_as,omitempty"`
	FinishedAt      *time.Time `json:"finished_at,omitempty"`
	BridgeInstance  string     `json:"bridge_instance,omitempty"`
	LeaseGeneration int64      `json:"lease_generation_at_delivery,omitempty"`
	CancelRequested bool       `json:"cancel_requested,omitempty"`
	// CancelRequestedAt and CancelLeaseGeneration record when, and under
	// which lease generation, a running task's cancellation was requested.
	// The bridge aborts only while the lease is still managed at that same
	// generation; any control change in between withholds the abort.
	CancelRequestedAt     *time.Time `json:"cancel_requested_at,omitempty"`
	CancelLeaseGeneration int64      `json:"cancel_lease_generation,omitempty"`
	// CancelProtocol is the protocol of the bridge holding the task when a
	// running cancellation was requested. 0 means the request predates
	// protocol gating (made by an older DevX against an older bridge).
	CancelProtocol int `json:"cancel_protocol,omitempty"`
	// AbortSentAt is written by the bridge under the lock immediately
	// before it calls ctx.abort() (at most once per task).
	AbortSentAt *time.Time `json:"abort_sent_at,omitempty"`
	// AbortWithheld explains why the bridge refused to abort a requested
	// cancellation (human control, human input, Pi-queued messages).
	AbortWithheld   string `json:"abort_withheld,omitempty"`
	HumanIntervened bool   `json:"human_intervened,omitempty"`
	// Revision increments on every write (Go and bridge). Edits of pending
	// work are compare-and-swap on it.
	Revision int64 `json:"revision,omitempty"`
	// EditKeys holds the idempotency hashes of edits applied to this task
	// (bounded), so a retried edit is recognised as already applied.
	EditKeys []string `json:"edit_keys,omitempty"`
	// Supersedes/SupersededBy link a pending task to its replacement.
	Supersedes   string `json:"supersedes,omitempty"`
	SupersededBy string `json:"superseded_by,omitempty"`
	StopReason   string `json:"stop_reason,omitempty"`
	Error        string `json:"error,omitempty"`
	ResultBytes  int64  `json:"result_bytes,omitempty"`
	Note         string `json:"note,omitempty"`
}

Task is one dispatched prompt.

func (*Task) CancelOutcome

func (t *Task) CancelOutcome(now time.Time) string

CancelOutcome describes what became of a cancellation request, or "" if none was made.

func (*Task) Phase

func (t *Task) Phase() string

Phase reports the task's delivery phase.

func (*Task) Terminal

func (t *Task) Terminal() bool

Terminal reports whether the task will not change state again.

type TaskView

type TaskView struct {
	TaskID          string     `json:"task_id"`
	AgentID         string     `json:"agent_id"`
	State           string     `json:"state"`
	Phase           string     `json:"phase"`
	Revision        int64      `json:"revision"`
	WaitingReason   string     `json:"waiting_reason,omitempty"`
	CancelRequested bool       `json:"cancel_requested,omitempty"`
	CancelOutcome   string     `json:"cancel_outcome,omitempty"`
	AbortSentAt     *time.Time `json:"abort_sent_at,omitempty"`
	AbortWithheld   string     `json:"abort_withheld,omitempty"`
	AcceptedAt      *time.Time `json:"accepted_at,omitempty"`
	AcceptedAs      string     `json:"accepted_as,omitempty"`
	Supersedes      string     `json:"supersedes,omitempty"`
	SupersededBy    string     `json:"superseded_by,omitempty"`
	HumanIntervened bool       `json:"human_intervened,omitempty"`
	StopReason      string     `json:"stop_reason,omitempty"`
	Error           string     `json:"error,omitempty"`
	Note            string     `json:"note,omitempty"`
	CreatedAt       time.Time  `json:"created_at"`
	DeliveredAt     *time.Time `json:"delivered_at,omitempty"`
	FinishedAt      *time.Time `json:"finished_at,omitempty"`
	ResultBytes     int        `json:"result_bytes,omitempty"`
	Excerpt         string     `json:"result_excerpt,omitempty"`
	ExcerptTrunc    bool       `json:"result_excerpt_truncated,omitempty"`
	// LatestProgress is the newest mid-task progress the bridge recorded
	// for this task (redacted, bounded). Absent until the first completed
	// assistant turn is recorded, and always absent with bridges older than
	// protocol 3. After the task finishes it is the last progress recorded
	// before the result; read the result itself with pi_result.
	LatestProgress *ProgressView `json:"latest_progress,omitempty"`
}

TaskView is the client-facing status of one task.

type Tmux

type Tmux struct {
	// Exec, when set, receives the argv (without "tmux") instead of running
	// tmux directly.
	Exec func(args ...string) (string, error)
}

Tmux runs tmux commands. Production uses the zero value (the user's tmux server, addressed only by exact =session / %pane targets; DevX never kills a tmux server). Tests set Exec to a tmuxfixture guard, which pins every command to a fixture-owned socket and rejects anything else before it runs.

func (Tmux) Capture

func (t Tmux) Capture(paneID string, lines int) (string, error)

Capture returns the visible text of a pane plus some scrollback.

func (Tmux) HasSession

func (t Tmux) HasSession(name string) bool

HasSession reports whether an exact tmux session name exists.

func (Tmux) NewWindow

func (t Tmux) NewWindow(session, name, dir, command string) (windowID, paneID string, err error)

NewWindow creates a detached window running command and returns its ids.

func (Tmux) Pane

func (t Tmux) Pane(paneID string) PaneInfo

Pane looks up a pane by its stable %id.

func (Tmux) RespawnPane

func (t Tmux) RespawnPane(paneID, dir, command string) error

RespawnPane restarts command in an existing pane, killing what runs there.

func (Tmux) Run

func (t Tmux) Run(a ...string) (string, error)

type Tool

type Tool = tool

Tool is an MCP tool definition contributed by a ToolProvider.

type ToolProvider

type ToolProvider interface {
	// Tools returns the provider's currently enabled tools. Disabled
	// capabilities must not be listed.
	Tools() []Tool
	// CallTool returns a complete MCP tool result, or handled=false when
	// name is not one of the provider's enabled tools.
	CallTool(name string, args json.RawMessage) (result map[string]any, handled bool)
	// TaskArtifacts lists artifacts produced while a task ran, for
	// inclusion in pi_status. It returns nil when not applicable or not
	// authorized; it never fails the status call.
	TaskArtifacts(agent *Agent, task *TaskView) []map[string]any
	// Instructions is appended to the initialize instructions when non-empty.
	Instructions() string
}

ToolProvider contributes optional tools (for example the artifact bridge) to the MCP server. Providers own their argument validation, authorization and result shape; the server only routes calls to them.

type UpdatePendingRequest

type UpdatePendingRequest struct {
	AgentID          string
	TaskID           string
	Prompt           string
	ExpectedRevision int64
	IdempotencyKey   string
}

UpdatePendingRequest replaces the prompt of a task Pi has not received.

Jump to

Keyboard shortcuts

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