Skip to content

docs(adr): Agent Lifecycle State Machine - #1

Merged
brettchien merged 5 commits into
mainfrom
adr/agent-lifecycle
Aug 8, 2026
Merged

docs(adr): Agent Lifecycle State Machine#1
brettchien merged 5 commits into
mainfrom
adr/agent-lifecycle

Conversation

@brettchien

Copy link
Copy Markdown
Contributor

What problem does this solve?

Studio needs a single, runtime-independent way to say "what state is this
agent in"
— the vocabulary the director's console displays and the control
plane observes. Today state is expressed differently per runtime (ECS task
states, k8s pod phases, compose status) with no glanceable, agreed set.

This is Studio's first ADR: a canonical 5-state agent lifecycle.

What's in this ADR

docs/adr/agent-lifecycle.md defines 5 states — Starting / Running /
Unhealthy / Stopping / Stopped
(mutually exclusive, exhaustive) — with:

  • a one-line definition and "the one thing that matters" per state;
  • configuration = identity + version + state as the through-line;
  • a native → 5-state projection table for ECS / k8s / GKE / docker-compose;
  • key invariants: only Running does work; only Stopped is terminal;
    identity is default-deny and control-plane-issued; reclaim (spot/preemption)
    can jump from any live state straight to Stopped.
Starting → Running ⇄ Unhealthy → Stopping → Stopped

Prior Art

  • Hermes Agent: 6 CLI-driven operational states; simple Start→Running→Stop
    machine; HMAC-signed lifecycle events. Identity = HERMES_HOME path +
    process-name matching — the weak self-report we reject.
  • Pi (pi-agent-core): idle ⇄ turn phases with flushPendingWrites
    between turns — validates "checkpoint while Running." One layer below instance
    lifecycle.
  • Pi-Desktop: Tauri shell over pi --mode rpc CLI; clean shell/runtime
    split — validates a thin director front-end over a core.

Alternatives Considered

  • Adopt Hermes' 6 operational states verbatim — mixes install/service concerns
    with runtime state; no health/identity distinction.
  • Make the conversation turn loop the primary machine — sub-layer of Running.
  • K8s-style granular phases — too many for one-glance; folded into attributes.
  • Drop Unhealthy — loses the "alive but fenced-off" distinction.

Validation

Docs-only ADR. Mermaid renders on GitHub. No code changes.


Review Contract

  • Goal: Agree the canonical, runtime-independent 5-state agent lifecycle
    as the vocabulary Studio displays and the control plane observes.
  • Non-goals: the RuntimeDriver verb contract (separate ADR); fleet-level
    lifecycle; any implementation.
  • Accepted Residual Risks: none (docs-only).
  • Acceptance Criteria: states named and defined; projection table present;
    prior art documented.
  • Follow-ups: RuntimeDriver contract ADR; fleet lifecycle ADR.

brettchien and others added 4 commits August 8, 2026 10:30
Studio's first ADR: a runtime-independent 5-state machine (Starting /
Running / Unhealthy / Stopping / Stopped) for classifying an agent
instance, with config (identity + version + state) as the through-line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fold the 8-axis refute-pass findings (Mira/Jellyfish/Falcon):
- Paused promoted to a 6th state, defined by (desiredStatus,accepting_work,
  health); restores single-field dispatch predicate state==Running.
- Unhealthy is fault-only; version-skew becomes a Running 'superseded'
  attribute (drives drain->replace), not Unhealthy.
- reclaim split into planned (compressed Stopping) vs hard-loss (->Stopped);
  diagram gains Starting->Stopped, Unhealthy->Stopped, Paused edges.
- identity: name trust root (runtime injection), per-instance credential at
  Starting, CP-signed instance-bound lease + monotonic fencing epoch,
  revocation + re-prove on recovery.
- projection: desiredStatus discriminator (ECS STOPPED / k8s deletionTimestamp
  / compose stop-requested), ACTIVATING->Starting, DEACTIVATING conditional,
  compose requires healthcheck + restart:no, k8s Unknown->Unhealthy.
- config model: Instance = Desired Spec (identity+version) + Observed State
  (state is observed, not desired config).
- prior art: add k8s/Nomad/Temporal/OTP/EC2/systemd/Ray rows; death cause enum.
- restructure to MADR + Y-statement; add docs/review-runbook.md (8-axis rubric).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Final 8-axis re-review fixes (Jellyfish F1/F2 + Falcon MCP + Mira):
- discriminator 3->4 axes: add latching identity_verified (separates Starting
  from Unhealthy, whose 3-axis tuples collided). CP-observable per runtime.
- accepting_work authority pinned to CP/director, never agent self-report.
- superseded => cordon to Paused (accepting_work=false) -> Stopping/replace,
  so a superseded agent is never left dispatchable in Running.
- Principle 6 in-flight set = Running U Paused U Stopping(within deadline).
- compose: docker pause (SIGSTOP) -> healthcheck stall -> Unhealthy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Jellyfish R1 + Mira (ECS): the v3 'superseded => Paused -> Stopping/replace'
sentence prescribed fleet-level rollout ordering (make-before-break, cordon
sequencing = deployment controller's job), which is this instance-level ADR's
declared non-goal. Keep the instance-level fact (superseded => accepting_work
=false => Paused, never dispatchable) and defer drain/replace ordering to a
future rollout / RuntimeDriver ADR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@brettchien
brettchien merged commit c534edf into main Aug 8, 2026
brettchien added a commit that referenced this pull request Aug 14, 2026
…relay, clearer errors (review #1–4)

Addresses the four findings from the Part B(1) backend review:

1〔med〕 In-flight guard for `session/prompt`. A second prompt while a turn
   was pending unconditionally overwrote `pending_prompt`, orphaning the first
   turn's result id so its `turn_end` never fired and the panel spinner hung.
   The single-shot loop now rejects (and logs) a prompt while one is in flight
   instead of clobbering the pending id. Part C's turn management still gates
   this, but the backend no longer depends on the UI for correctness.

2〔low〕 Non-blocking sidecar relay. `handle_inner` (`tools/list`/`tools/call`)
   awaited inline in the select! read arm, stalling queued prompts/cancels and
   chat chunks behind a slow round-trip. The relay now runs in a spawned task
   and returns its reply through a new `reply_tx` channel drained by the loop's
   single writer — responsiveness restored, wire frames still serialized. Each
   reply carries its own id, so out-of-order completion is fine for MCP.

3〔low〕 Clearer error when the outbound channel's receiver is gone. A send
   into a torn-down connection reported a bare "connection closed"; it now
   matches the not-connected message so the teardown window reads correctly.

4〔info〕 out_rx/reply_rx arms use Some(..) patterns, disabling the arm on a
   closed channel so a None can never busy-spin the loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
brettchien added a commit that referenced this pull request Aug 14, 2026
…p socket (#48)

* feat(chat): Part B(1) backend — session/prompt pipe over the live /acp socket

First implementation slice of ADR agent-chat-panel (Part C). Adds the
bidirectional chat pipe on the existing reverse-MCP session; no UI yet
(Part C) and no cross-reconnect session/resume yet (Part B2).

acp-tunnel:
- Session::prompt(text) — builds session/prompt {sessionId, prompt:[text]}
- Session::cancel() — the one-way session/cancel notification
- Inbound::AgentChunk classified from session/update/agent_message_chunk
  (other update kinds — thoughts, tool_call — openab doesn't emit yet)
- unit tests for all three (23 pass)

remote.rs:
- RemoteState grows an mpsc write-handle (prompt_tx), published only once
  the session is active, retracted by run_reconnecting/disconnect
- run_once's read loop becomes a tokio::select! over inbound frames AND
  outbound OutMsg (Prompt/Cancel); write never leaves the task
- prompt results are id-correlated (pending_prompt), not phase-driven —
  a method-less frame in SessionActive ends the turn only if its id
  matches the in-flight prompt; emits agent-update {kind:"turn_end"}
- agent_message_chunk → emit agent-update {kind:"chunk", text}

lib.rs:
- agent_prompt / agent_cancel commands (delegate to Remote), registered

Verified: acp-tunnel unit tests. studio-desktop compilation left to CI
(separate GUI workspace). Follow-ups: B2 session/resume + persistence,
then Part C the panel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(remote): backend chat robustness — in-flight guard, non-blocking relay, clearer errors (review #1–4)

Addresses the four findings from the Part B(1) backend review:

1〔med〕 In-flight guard for `session/prompt`. A second prompt while a turn
   was pending unconditionally overwrote `pending_prompt`, orphaning the first
   turn's result id so its `turn_end` never fired and the panel spinner hung.
   The single-shot loop now rejects (and logs) a prompt while one is in flight
   instead of clobbering the pending id. Part C's turn management still gates
   this, but the backend no longer depends on the UI for correctness.

2〔low〕 Non-blocking sidecar relay. `handle_inner` (`tools/list`/`tools/call`)
   awaited inline in the select! read arm, stalling queued prompts/cancels and
   chat chunks behind a slow round-trip. The relay now runs in a spawned task
   and returns its reply through a new `reply_tx` channel drained by the loop's
   single writer — responsiveness restored, wire frames still serialized. Each
   reply carries its own id, so out-of-order completion is fine for MCP.

3〔low〕 Clearer error when the outbound channel's receiver is gone. A send
   into a torn-down connection reported a bare "connection closed"; it now
   matches the not-connected message so the teardown window reads correctly.

4〔info〕 out_rx/reply_rx arms use Some(..) patterns, disabling the arm on a
   closed channel so a None can never busy-spin the loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant