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
- Variables
- func AcquireOwnedLock(path string) (func(), error)
- func ApplyInstancePlan(p *InstancePlan, st InstanceStores) error
- func ApplyMarkerPlan(p *MarkerPlan, st MarkerStores) error
- func BridgeSource() []byte
- func Denied(format string, a ...any) error
- func IsPermissionDenied(err error) bool
- func JournalMatchesPlan(e MarkerJournalEntry, p *MarkerPlan) bool
- func Obj(props map[string]any, required ...string) map[string]any
- func Redact(s string) string
- func RollbackInstancePlan(p *InstancePlan, entries []JournalEntry, st InstanceStores) (reverted int, skipped []string, err error)
- func RollbackMarkers(entries []MarkerJournalEntry, st MarkerStores) (reverted int, skipped []string, err error)
- func ValidAgentID(id string) bool
- func ValidTaskID(id string) bool
- func ValidateInstanceJournal(p *InstancePlan, entries []JournalEntry) error
- func WriteBridge(root string) (string, error)
- type AdoptRequest
- type AdoptResult
- type AdoptedSession
- type Agent
- type AgentPlanItem
- type AgentView
- type Binding
- type Bridge
- type BridgeUpgradeError
- type CancelRequest
- type Config
- type ConflictError
- type CreatedSession
- type CreationEvent
- type EditResult
- type Event
- type InstancePlan
- func PlanInstances(sessions map[string]*session.Session, agents []*Agent, ...) *InstancePlan
- func PlanInstancesWithHistory(sessions map[string]*session.Session, agents []*Agent, ...) *InstancePlan
- func PlanInstancesWithOptions(sessions map[string]*session.Session, agents []*Agent, ...) *InstancePlan
- type InstanceStores
- type JournalEntry
- type Lease
- type MCPServer
- type Manager
- func (m *Manager) Adopt(req AdoptRequest) (*AdoptResult, error)
- func (m *Manager) AgentStatus(agentID string) (*AgentView, error)
- func (m *Manager) Cancel(taskID string) (*Task, error)
- func (m *Manager) CancelTask(req CancelRequest) (*Task, error)
- func (m *Manager) Events(agentID string, after int64, limit int, wait time.Duration) ([]Event, int64, error)
- func (m *Manager) Inspect(agentID string, lines int) (string, error)
- func (m *Manager) List() ([]*AgentView, error)
- func (m *Manager) PeekStatus(agentID string) (*AgentView, error)
- func (m *Manager) Queue(agentID string, recent int) (*QueueView, error)
- func (m *Manager) Relaunch(agentID string, force bool) error
- func (m *Manager) RelaunchIfUnchanged(agentID string, expectLaunchCount int) error
- func (m *Manager) Release(agentID, by string, dropQueued bool) (*Agent, error)
- func (m *Manager) Result(taskID string, offset, maxBytes int) (*ResultChunk, error)
- func (m *Manager) RetireForSession(agentID, session string) error
- func (m *Manager) Send(req SendRequest) (*SendResult, error)
- func (m *Manager) Start(req StartRequest) (*StartResult, error)
- func (m *Manager) SupersedePending(req SupersedePendingRequest) (*EditResult, error)
- func (m *Manager) Takeover(agentID, by string) (*Agent, error)
- func (m *Manager) TaskStatus(taskID string) (*TaskView, *AgentView, error)
- func (m *Manager) UpdatePending(req UpdatePendingRequest) (*EditResult, error)
- type MarkerBackup
- type MarkerBackupRef
- type MarkerJournalEntry
- type MarkerPlan
- type MarkerPlanItem
- type MarkerPlanOptions
- type MarkerStores
- type MarkerWitnesses
- type PaneInfo
- type PermissionError
- type PlanOptions
- type Progress
- type ProgressView
- type QueueItem
- type QueueView
- type RecordedBindingFunc
- type ResultChunk
- type SendRequest
- type SendResult
- type SessionAdopter
- type SessionCreator
- type SessionPlanItem
- type StartRequest
- type StartResult
- type Store
- func (s *Store) AgentDir(id string) string
- func (s *Store) AppendEvent(agentID string, ev Event) (Event, error)
- func (s *Store) CreationEventOf(agentID string) (CreationEvent, bool, error)
- func (s *Store) FindTask(taskID string) (*Task, error)
- func (s *Store) ListAgents() ([]*Agent, error)
- func (s *Store) ListAgentsLenient() ([]*Agent, map[string]error, error)
- func (s *Store) ListTasks(agentID string) ([]*Task, error)
- func (s *Store) LoadAgent(id string) (*Agent, error)
- func (s *Store) LoadBridge(agentID string) (*Bridge, error)
- func (s *Store) LoadTask(agentID, taskID string) (*Task, error)
- func (s *Store) ReadEvents(agentID string, after int64, limit int) ([]Event, int64, error)
- func (s *Store) ReadProgress(agentID, taskID string) *ProgressView
- func (s *Store) ReadResult(agentID, taskID string) (string, bool, error)
- func (s *Store) RecordedBinding(agentID string) (string, time.Time, error)
- func (s *Store) SaveAgent(a *Agent) error
- func (s *Store) SaveTask(t *Task) error
- func (s *Store) WithAgentLock(id string, fn func() error) error
- type SupersedePendingRequest
- type Task
- type TaskView
- type Tmux
- func (t Tmux) Capture(paneID string, lines int) (string, error)
- func (t Tmux) HasSession(name string) bool
- func (t Tmux) NewWindow(session, name, dir, command string) (windowID, paneID string, err error)
- func (t Tmux) Pane(paneID string) PaneInfo
- func (t Tmux) RespawnPane(paneID, dir, command string) error
- func (t Tmux) Run(a ...string) (string, error)
- type Tool
- type ToolProvider
- type UpdatePendingRequest
Constants ¶
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.
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.
const ( ReasonNoMarker = "" /* 158-byte string literal not displayed */ ReasonAdoptedMarker = "" /* 207-byte string literal not displayed */ )
Reasons explaining a candidate.
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" 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.
const ( // BridgeKindUpgrade: the live bridge holding the task speaks an older // protocol; only a human relaunch helps. BridgeKindUpgrade = "bridge_upgrade_required" // (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.
const ( MarkerActionRestore = "restore_local_only_marker" MarkerActionCandidate = "candidate_needs_owner_confirmation" MarkerActionSkip = "skip" MarkerActionPresent = "marker_present" )
Marker repair plan actions.
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" 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.
const ( LeaseManaged = "managed" LeaseHuman = "human" )
Lease holders.
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.
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.
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.
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 )
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.
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.
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.
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.
const BridgeProgressProtocol = 3
BridgeProgressProtocol is the oldest bridge protocol that reports mid-task progress. Older bridges work as before and simply report none.
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.
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).
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.
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.
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.
const MaxPromptPreview = 512
MaxPromptPreview bounds prompt previews in the queue view.
const MaxQueuePending = 50
MaxQueuePending bounds pending items returned by one queue view.
const MaxRecent = 10
MaxRecent bounds recent finished tasks in the queue view.
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 ¶
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.
var ErrConflict = errors.New("conflict")
ErrConflict reports a failed revision check.
var ErrEditLimit = errors.New("edit limit reached")
ErrEditLimit reports that a task reached MaxEditsPerTask.
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.
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.
var ErrNotFound = errors.New("not found")
ErrNotFound reports an unknown agent or task.
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.
ErrTmuxUnavailable means tmux could not be queried, so pane state is unknown. Callers fail closed and change nothing.
Functions ¶
func AcquireOwnedLock ¶
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 IsPermissionDenied ¶
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 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 ValidTaskID ¶
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 ¶
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.
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 ¶
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) 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) PeekStatus ¶
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) Relaunch ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 (*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 ¶
MarkerBackup is one reviewed sessions.json backup.
type MarkerBackupRef ¶
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.
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 ¶
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 ¶
SendRequest queues a follow-up prompt for an existing managed agent.
type SendResult ¶
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 (*Store) AppendEvent ¶
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) ListAgents ¶
func (*Store) ListAgentsLenient ¶
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) ReadEvents ¶
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 ¶
ReadResult returns the redacted final text for a task.
func (*Store) RecordedBinding ¶
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.
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 ¶
CancelOutcome describes what became of a cancellation request, or "" if none was made.
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) HasSession ¶
HasSession reports whether an exact tmux session name exists.
func (Tmux) RespawnPane ¶
RespawnPane restarts command in an existing pane, killing what runs there.
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.