Skip to content

Agent Runtime Status (Idle / Busy / Question)

Status: Open — design proposal (Phase 2, Agent Orchestrator Research Program)

jackin’s CLI is intentionally opaque to agent runtime: the operator launches an agent, the agent prints things, the operator interacts with it. There’s no structured observability — jackin has no way to answer “is this agent currently busy, idle, or waiting for input?” That makes every downstream observability feature impossible:

  • The operator console can’t show a status column.
  • The autonomous queue (Phase 4) can’t dispatch a queued task to an idle agent.
  • Idle-runtime cleanup (Phase 4) has no signal to act on.
  • Cost telemetry (Phase 3) can’t correlate token bursts with periods of active work.

Closing this gap is the load-bearing Phase 2 item: it’s the substrate that everything observable depends on.

  • Without runtime status, the program’s autonomous-queue thesis can’t work: a queue can’t dispatch to a slot if it doesn’t know whether the slot is free.
  • The console looks fixed-shape today (one row per workspace + agent picker). multicode’s per-row colored status indicator (orange=idle, green=busy, yellow=waiting) is the single biggest UX upgrade their TUI has over jackin’s, and it’s the same data this gap exposes.
  • Multi-runtime support already needs an adapter seam for runtime-specific behavior (entrypoint, auth, state). Status surfacing is one more responsibility per adapter, which means one more reason to land the adapter abstraction cleanly.

Sources:

multicode subscribes to the OpenCode SSE event stream and derives a three-state enum:

  • Idle — no in-flight tool call, no pending message
  • Busy — agent is actively generating or running a tool
  • Question — agent is waiting for user input

The state lives on WorkspaceSnapshot.root_session_status. The TUI renders it as a colored cell next to the workspace name. The transition stream is also consumed by the resource-usage poller (so CPU samples are tagged by status — not implemented today but visible in their schema).

The implementation cost is small: a parser per-provider that reads SSE/ event-stream output and emits status events. No agent-runtime changes; just a sidecar that watches the agent’s emitted protocol.

A small runtime-status adapter per supported runtime, bolted onto the existing multi-runtime adapter seam (the one that owns derived_image.rs / entrypoint.sh / instance/auth.rs / etc. per runtime). Each adapter emits status events to a single in-process channel; consumers (console, queue, cleanup, telemetry) subscribe to the channel.

pub enum AgentStatus {
Initializing,
Idle,
Busy { since: Instant },
Question { prompt: Option<String>, since: Instant },
Crashed { exit_code: i32 },
OomKilled,
}

Initializing covers the gap between container start and the first runtime event. Crashed and OomKilled are surfaced from Docker container state, not from the runtime adapter — they’re handled by the same mechanism so consumers see one stream.

  • Claude (today). Claude Code emits a streaming JSONL transcript when invoked with --output-format stream-json (or similar). The adapter attaches to that stream when available; if not (e.g. the operator is using Claude in a regular interactive session), the adapter falls back to a heartbeat-and-stdin-idle heuristic.
  • Codex (when multi-runtime ships). Codex’s app-server emits structured events; map those.
  • Amp (when multi-runtime ships). Amp’s thread API has an explicit status field; surface it.

The adapter is a separate process or task, not part of the runtime’s own startup. It reads the runtime’s structured output from a side channel (stream-json file, named pipe, server-sent events) and emits status events to jackin’s status bus. It’s allowed to know runtime specifics; the consumers are not.

A simple broadcast channel in src/runtime/launch.rs (or a new src/runtime/status.rs) that emits (instance_name, status_event) tuples. Console subscribes for live rendering; queue subscribes for dispatch; persistent storage layer subscribes to record state transitions.

Consumers should be tolerant of dropped events (broadcast channel with bounded capacity); status is always re-derivable from the most recent event plus container liveness.

  • AgentStatus enum + per-instance status state.
  • One status adapter for Claude — the only runtime today. Adapters for Codex and Amp ship alongside their multi-runtime support.
  • A status bus that consumers subscribe to.
  • A fallback heartbeat-and-idle heuristic for Claude when stream-json isn’t available, gated behind a --observable flag on jackin load so operators opt in.
  • Console subscribes to render a status cell next to each workspace. (The console rendering integration may slip a release behind the status-bus plumbing.)
  • No persistence in V1 — status is in-memory only. Persistent storage layer adds a status_log table later.
  • Heuristic fallbacks for runtimes without structured output. If a runtime doesn’t expose status, jackin’ shows Initializing → Unknown and the operator gets the existing opaque experience.
  • Per-tool granularity (which tool the agent is running right now). V1 is three-state; finer-grained surfacing is a follow-up.
  • Cross-instance status aggregation (a single “fleet view”). Wait until the queue exists.
  • --observable opt-in default. Should running Claude with stream-json parsing be the default in jackin’ (so status surfaces automatically) or opt-in (so the operator’s existing interactive flow is unchanged)? Recommended default: opt-in for V1, then revisit after operator feedback. stream-json output may interact with how the operator currently uses Claude’s TUI.
  • Heuristic-only fallback. Is a “no event for 30s and stdin is idle” signal good enough to call an agent Idle? Recommended: yes for queue dispatch (false negatives are safe — the queue just doesn’t fill the slot); no for cost telemetry (false negatives skew samples).
  • Adapter ownership. Is the status adapter part of the multi-runtime adapter trait, or a separate runtime-coupled module? Recommended: separate module — runtime adapters own launch, auth, entrypoint; status adapters own observability. Different reviewers, different cadence.
  • New module (e.g. src/runtime/status.rs) — status bus + adapter trait
  • src/runtime/launch.rs — wires the adapter into the launch path
  • src/console/manager/state.rs — subscribes for rendering
  • Future per-runtime files (Codex / Amp adapters) — bolt into this seam