Agent Progress¶
An agent can advertise long-running work (a build, a deploy, a test sweep) as tasks with progress. Leo's apps show them next to the agent and notify when one finishes. The daemon owns the data; the apps only render it. Tasks live in memory: a daemon restart clears them.
Reporting¶
From inside the agent's own session, either:
- the
leo_progressMCP tool:{id, title?, percent?, indeterminate?, status?, eta_seconds?, state?} - the CLI, for scripts in the agent's pane:
leo progress build --title "Xcode build" --percent 10 --eta 5m
leo progress build --percent 62 --status "Compiling 412/980"
leo progress deploy --title "Deploy" --indeterminate
leo progress build --done # or --fail / --cancel
Leo tells each agent about leo_progress itself: the MCP server's initialize carries a short instruction (Claude Code adds it to the system prompt), and Leo appends the same line to a Codex agent's developer instructions. Nothing needs adding to your own config.
Both authenticate with the agent's per-launch attention token (LEO_ATTENTION_TOKEN, with LEO_WEB_PORT and LEO_API_TOKEN), so an agent can only write its own tasks: there is no agent argument, and the name is never taken from the request or LEO_PROCESS_NAME. Only agents Leo launched with hooks have a token; other agents, and leo_dispatch subagents, get a readable error. leo progress exits 2 outside an agent.
Automatic tasks¶
Leo also creates tasks on its own, so progress shows up without the agent reporting anything. They carry "source": "auto" (agent-reported tasks are "source": "agent") and are always indeterminate: the apps show elapsed time, never an ETA.
- Long shell commands. A command an agent runs (the
Bashtool) becomes a task once it has run 20 seconds; a shorter one never shows. The title is the tool'sdescriptionwhen it has one, otherwise the program and its first argument (heavy xcodebuild): never the whole command line, which can hold secrets. It endssucceeded,failed(a failing exit) orcancelled(interrupted, or still open when the turn ended). A background shell stays running until its task-notification,TaskStopor the end of itsStoplist.
Auto tasks clear themselves 60 seconds after they end, whatever their end state (commands fail routinely, so a failure never persists), send no finish notification, are never stale and cannot be dismissed (409). They have their own cap of 8 running per agent, counted apart from agent-reported tasks; past it new ones are skipped. When an agent reports its own running task with leo_progress, its running automatic command tasks are removed: the agent's task is the richer view of the same work.
The ids auto.… are reserved for the daemon: an agent report whose id starts with auto. is rejected (400).
Commands are tracked from the hooks Leo already relays for a supervised agent, plus an async PreToolUse hook limited to Bash. Hooks are set when an agent launches, so an agent that was already running picks the PreToolUse hook up at its next relaunch; Leo does not restart it. Until then, commands the agent runs in the background still appear (they are armed by the PostToolUse that starts them). opencode has no hooks, so it gets no automatic tasks.
Dispatch runs are not tasks: the sidebar already lists them as dispatch rows. An older daemon published them as auto.dispatch.<id> tasks; the task store is in memory, so none survives the restart that installs this version.
Codex: its shell tool names are matched against a fixed list (shell, local_shell, exec_command, container.exec) that has not been verified against a live Codex session, and Codex reports no failure exit. A Codex command therefore only succeeds or is cancelled, and a Codex tool with another name is not tracked.
Task fields¶
| field | rules |
|---|---|
id | 1–64 characters of [a-z0-9._-]; the agent's own key |
title | required on the first report, at most 80 characters |
percent | 0–100 (clamped); implies a determinate bar. indeterminate: true is the alternative, not both |
status | at most 120 characters, e.g. Compiling 412/980 |
eta_seconds | seconds from now, stored as an absolute eta_at |
state | running (default), succeeded, failed or cancelled |
Reports are upserts: omitted fields keep their value. Strings are trimmed and stripped of control characters. A report to an ended task starts a fresh run that keeps only the title.
Lifecycle¶
- A running task with no update for 15 minutes is flagged
stale; any update clears it. succeededandcancelledtasks are removed 10 minutes after they end.failedtasks stay until dismissed.- Auto tasks are removed 60 seconds after they end (see above).
- All of an agent's tasks are removed when it stops (including restart and reset).
- Bounds per agent: 8 running tasks (a 9th is rejected with
409), 8 failed and 8 finished tasks (the oldest is dropped). Auto tasks have the same bounds, counted separately: 8 running (a 9th is skipped, silently) and 8 ended.
API¶
POST /api/agent/progress(agent bearer):{"token", "id", ...}→200 {"ok": true, "task": {...}}. Errors are readable:400validation (including a reservedauto.id),403unknown or expired token,409too many running tasks.DELETE /api/v1/agents/{name}/tasks/{id}(operator token) andDELETE /agents/{name}/tasks/{id}(socket) dismiss an ended (succeeded, failed or cancelled) task:204,404unknown,409if the task is still running or is an auto task. Agents cannot dismiss.GET /api/v1/stateagent rows carrytasks(omitted when empty); the/eventsstream carriesagent_task_changedwith the whole task, ortask: nullwhen it is gone. Publishes are coalesced to at most two per second per task, and a terminal state or removal goes out at once.hello.featuresadvertisesagent_progress.