The agent state model

Herdr tracks a lifecycle state for every recognized coding agent. Plugins read this state (via pane.agent_status_changed events and the CLI) and can report their own state for custom panes. Getting the model right is the difference between a plugin that reacts to real work and one that reacts to noise.

The five states

StateMeaningReady for input?
idleAt its prompt, and herdr has marked the pane seenYes
workingTurn in progressNo
blockedHerdr recognized an approval or question UINeeds you first
doneidle plus unseen: the turn finished but nobody lookedYes
unknownAgent present but lifecycle not confidently classifiableDo not assume
  • done vs idle is server seen-state: pane focus and agent focus mark a pane seen; reads do not. Each TUI client tracks viewed completions independently, so a client’s Done badge can differ from the CLI’s.
  • unknown is not success. If the distinction matters, accept exact states explicitly (--until idle --until done).

Status authority

One status authority per pane. When multiple sources could report state, lifecycle integrations beat screen manifests: if the OpenCode integration is installed and reporting, its word is the pane’s status, not what the detector reads off the terminal’s bottom buffer. Screen-manifest detection is the fallback that reads the live bottom-buffer (never the user-scrollable viewport).

For plugin authors this means:

  • Trust pane.agent_status_changed; it reflects the authority.
  • When a status looks wrong, diagnose with herdr agent explain <target> (see below) before working around it.
  • If your plugin hosts its own agent-like pane, report state with pane report-agent to become (or feed) the authority, and release with pane release-agent when done. Presentation-only labels go through pane report-metadata, which never affects status.

Rollups

Status rolls up pane to tab to workspace: a workspace shows the worst-case or aggregate signal of its agents. Notification-style plugins subscribe to pane.agent_status_changed and filter by status (typically deliver on blocked and done) rather than re-deriving hierarchy themselves.

agent explain

herdr agent explain <target> shows why a pane is in its state: the matched detection rule (or lifecycle integration) and any skip reason. When your plugin’s users report a stuck status, this is the first command to run, and the right thing to teach them to run.

Automation semantics you will depend on

Plugins that orchestrate agents (prompting, waiting, reading output) inherit these rules:

agent prompt --wait rejects an already blocked agent with agent_blocked without sending anything. Otherwise it submits the prompt, then waits up to five seconds to observe working or blocked. No activity observed: agent_prompt_stalled. This stops unrelated idle/done transitions from satisfying the wait.

Gotcha: A timeout or agent_prompt_stalled does not prove the prompt was not delivered. Read the agent before retrying, or you risk submitting the same prompt twice.

agent wait returns when the agent’s status matches. Default settled states: idle, done, or blocked. Repeat --until to accept several exact states. Waits have no default timeout.

completion_seq identifies the current idle transition as genuinely completed work. It matches that transition’s state_change_seq; startup readiness and session switches do not set it. Use it to distinguish “a turn finished” from “the session merely changed”. Older servers may omit the field.

Alternate-screen reads have an idle gate. For full-screen agents (Claude Code, OpenCode), an explicit agent read --lines N that needs more history than the visible screen pages through the agent’s own scroll UI, and that is only safe when the agent is idle. While working, blocked, or unknown it returns agent_not_idle. Fallback for long output while busy: ask the agent to write it to a file and read the file.

Pane moves alias ids. After pane move, continue with the new workspace-qualified id from the response (previous_pane_id keeps the old value); a wait already in progress ends with agent_not_running.