Agent States and Lifecycle
The agent state model and how to control it: states, transitions, loop modes, turn tools, and the birth-to-death lifecycle
On this page
Every ADF agent exists in one of five primary states. Understanding these states is key to controlling agent behavior.
States
| State | Description | Responds To |
|---|---|---|
| Active | LLM loop is running | N/A (already processing) |
| Idle | Default idle, responsive | Document edits, messages, direct chats, timers |
| Hibernate | Deep idle | Timers only |
| Suspended | Blocked by runtime | Owner approval only |
| Error | Failed, waiting for recovery | User messages only |
| Off | Fully stopped | Nothing (manual restart required) |
Active
The agent’s LLM is running. It’s processing input, calling tools, and generating responses. This is the only state where the LLM loop executes.
Idle
The default idle state. The agent is not running but will wake for most events — when you send it a message, when another agent sends it a message, when the document is edited, or when a timer fires.
Hibernate
A deeper idle state. The agent ignores most events and only wakes for timer triggers. Use hibernate for agents that should work on a schedule without being disturbed by messages or edits.
Entering hibernate (or off) drops the queued agent-scope trigger backlog — any pending agent-scope dispatches waiting to run are discarded rather than delivered on the next wake, and a trigger.dropped event is emitted for the discarded batch. A hibernating agent starts clean when a timer next wakes it, rather than replaying a pile of stale wakes.
Suspended
A safety state set by the runtime when an agent hits its limits.max_active_turns limit. The agent cannot resume on its own — it requires explicit owner approval through a human-in-the-loop dialog. If the owner doesn’t respond within the limits.suspend_timeout_ms window (default: 20 minutes), or denies, the agent transitions to off.
Error
The agent encountered a structural failure — something wrong with the executor itself, such as a corrupt session, a bad code path, or a tool registry fault. The error state persists rather than silently recovering. Agent-scope triggers are held back while in error — only a direct user message wakes the LLM loop and recovers the agent back to active. System-scope triggers, however, keep firing: the scope gate blocks only agent scope, and system scope fires in every state except off. So lambdas, log handlers, and system timers continue to run in error state; only the expensive LLM loop is quiesced. This keeps genuine failures visible without silently looping retries.
What does not trigger error state. Transient external failures — provider rate limits (429), provider outages (5xx), network timeouts, connection resets — are treated as operational, not structural. The executor classifies these inside its turn-loop catch block and returns the agent to idle instead of error. The agent’s timers and triggers keep firing, and the next attempt may succeed. A provider being unreachable for an hour leaves the agent idle, not destroyed.
This separation matters because landing in error takes the agent loop out of circulation — agent-scope wakes stop until a human intervenes. It is softer than off, though: unlike off, system-scope triggers keep firing (see the gating table in Triggers), so the agent stays reachable to its lambdas and infrastructure. Reserving error for real breakage means a flaky provider doesn’t quiesce the loop or unregister the agent from the mesh.
Provider error logging. Every transient failure writes to adf_logs with:
level = "warn"origin = "executor"event = "provider_error"
Structural failures write with level = "error", event = "turn_error". Agents can distinguish the two via db_* tools — a lambda that queries recent provider_error rows can drive its own retry, fallback-model, or escalation logic.
Off
Completely stopped. No triggers fire. The agent must be manually restarted. Use this to fully disable an agent.
Hard-off guarantee. Transitioning to off is a full teardown, not a soft pause. The runtime unregisters the agent from the mesh, disconnects all MCP servers, stops all channel adapters, closes WS connections, and destroys the code sandbox. After off, the agent is unreachable from the network — messages addressed to it fail to route. Restart re-establishes these connections from scratch.
This is the guarantee that makes lambda-triggered remote shutdown useful: when a parent sends an OFF command to a child and the child’s system-scope lambda calls sys_set_state('off'), the child immediately stops processing, stops responding on the mesh, and cannot continue whatever it was doing during its current turn. See Triggers for a worked example.
off is the only state that is never deferred to end-of-turn. When sys_set_state('off') is called from a lambda, HIL approval, or any code path, it aborts the in-flight LLM call immediately and clears all pending triggers. Other states (idle, hibernate) wait for the current turn to complete.
Setting state from the shell
adf_shell exposes the same tool as state [idle|hibernate|off], so a yield can be chained with the work that precedes it in one tool call:
meta set status "report shipped" && state idle
Bare state prints the current state and uses no tool — it keeps working when sys_set_state is disabled (setting then exits 126). The transition is a side effect of the whole invocation, not of the command’s position: later commands in the chain still run, so put state last. A state inside $(...) is a subshell side effect and is discarded. adf sys_set_state '{"state":"idle"}' works identically — state is just the ergonomic spelling.
State Transitions
Who can set each state:
LLM (via sys_set_state): idle, hibernate, off
LLM (via adf_shell): idle, hibernate, off — `state idle`
Lambda (via adf.sys_set_state): idle, hibernate, off
Runtime (automatic): suspended (on max_active_turns or denied HIL)
off (on suspend timeout)
Owner (human): active (from suspended, via approval dialog)
Daemon HTTP: any display state, via POST /agents/:id/state
Setting Display State Over HTTP
The daemon exposes POST /agents/:id/state with a body of { "state": "idle" | "hibernate" | "off" | ... } as the dedicated surface for moving a running agent’s live (fleet-map) state. This is the supported external way to set state.
Note that editing the config’s state field through PUT /agents/:id/config does not move a running agent — config edits carry fleet-map semantics but do not drive the live state machine. Use POST /agents/:id/state for that.
Wake and Return
When a trigger wakes an agent from idle or hibernate:
- The runtime records the previous idle state
- The agent transitions to active
- The LLM loop runs
- When the loop ends, the agent returns to its previous idle state
This means if an agent is idle and receives a message, it wakes to active, processes the message, and returns to idle. Unless the agent explicitly calls sys_set_state to change to a different state.
Crash recovery notice. If a turn was interrupted by a crash, reload, or hard shutdown, the next load injects a one-time system notice describing the interruption. That notice now carries absolute timestamps — when the interrupted turn started, when it last made progress, and the current time — plus the elapsed offline time, so the agent can account for how long it was down before acting on anything time-sensitive. This clock appears only in the recovery notice; a normal wake carries no injected timestamp.
Suspension Flow
When an agent reaches its max_active_turns limit:
- Runtime sets state to suspended
- A human-in-the-loop dialog appears: “Resume or shut down?”
- If the owner approves → back to active
- If the owner denies or the
suspend_timeout_mswindow elapses (default: 20 minutes) → transitions to off
This prevents runaway agents from consuming unlimited resources. If a legitimate workload keeps hitting the cap, raising limits.max_active_turns via sys_update_config is the natural remedy — HIL-gated (your principal approves — or denies).
Loop Modes
The loop_mode setting controls how the LLM loop behaves while the agent is in the active state. There are two modes.
Interactive Mode (Default)
Designed for conversational agents that work with humans.
| Behavior | Effect |
|---|---|
respond tool | Ends the turn — agent returns to idle |
say tool | Turn continues — used for status updates |
ask tool | Pauses the loop — waits for human input, then resumes |
sys_set_state | Ends loop and changes state |
Hit max_active_turns | Agent is suspended |
Example flow:
Human sends "summarize the data"
→ Agent wakes to active
→ Agent calls say("checking inbox...")
→ Agent calls db_query to fetch results
→ Agent calls respond("Here's your summary: ...") ← ENDS turn
→ Agent returns to idle
Autonomous Mode
Designed for agents that work independently without human interaction.
| Behavior | Effect |
|---|---|
respond tool | Logs output, turn continues |
say tool | Turn continues |
ask tool | Not available |
sys_set_state | Ends loop and changes state |
Hit max_active_turns | Agent is suspended |
In autonomous mode, the runtime appends to the system prompt: “You are in autonomous mode. You will not receive human input during this session. Use the say tool to report progress. Use respond to communicate results. Call sys_set_state when your work is complete.”
User Interrupt Restart
If you send a message while an agent is active (in any mode), the runtime aborts the current turn and restarts with your message. This means:
- The agent’s in-progress work is cancelled
- Any pending tool calls are filled with placeholder results
- Your message becomes the new input for a fresh turn
This is useful for redirecting an agent that’s going down the wrong path or providing urgent input without waiting for the current turn to finish.
Example flow:
Agent wakes from idle (timer trigger, autonomous mode)
→ Agent calls say("checking inbox, processing requests...")
→ Agent calls msg_read → gets 5 messages
→ Agent calls fs_write → updates report
→ Agent calls respond("Processed 5 messages, report updated") ← does NOT end turn
→ Agent calls sys_set_state("idle") ← ends loop, back to idle
Choosing a Loop Mode
| Use Case | Recommended Mode |
|---|---|
| Chat assistant | Interactive |
| Background worker | Autonomous |
| Scheduled reporter | Autonomous |
| Human-supervised agent | Interactive |
| Data processor | Autonomous |
Turn Tools
Three tools exist specifically for emitting text during the LLM loop. These replace raw text-only responses.
respond(message)
Emit text to the conversation. In interactive mode, this ends the turn. In autonomous mode, this logs the message and the turn continues.
say(message)
Emit text to the conversation without ending the turn. Use for status updates, intermediate observations, or progress reports.
ask(question)
Pose a question and block until the human responds. Only available in interactive mode. The loop pauses, the question appears in the chat, and when the human replies the loop resumes with their answer.
Raw Text (No Tool Call)
If the LLM emits text without calling any tool, it’s treated as an implicit respond(). The same mode-dependent rules apply.
Agent Lifecycle
Birth
An agent is created either by a human (through the UI or CLI) or by another agent (via sys_create_adf). A parent can give the child credentials through a template, which copies them into the child’s adf_identity rows. When using template-based creation, the child receives a fresh cryptographic identity (new DID and keypair) while inheriting the template’s config, files, and non-signing credentials. The parent’s identity (DID or nanoid) is always recorded in the child’s adf_parent_did metadata for lineage tracking.
If autostart: true is set in the agent’s config, the agent is started as a background agent immediately on creation (when created by a parent) and on every subsequent runtime boot. Password-protected agents are skipped during autostart — they require human unlock first.
Life
The agent processes triggers, communicates with other agents, and maintains its document and memory. It alternates between active and idle states based on events.
Identity Provisioning
The runtime generates the agent’s Ed25519 signing keys when the agent is created. The agent’s DID is derived from the public key. The config id stays a 12-character nanoid and is not the agent’s identity.
Independent resources
For each model call the runtime reads the provider key from the agent’s own adf_identity row (provider:<id>:apiKey). If the file has no key for that provider, the runtime uses its own key for the same provider id. A child created from a template holds copies of the template’s credentials, not references to the parent’s keys. An agent whose keys were issued to it, or set by its owner, does not depend on its parent to run.
Losing model access
If the provider key an agent uses is revoked at the provider, its model turns fail. Lambdas, timers and other code-path work keep running, and all data in the file remains readable.