On this page

Every ADF agent exists in one of five primary states. Understanding these states is key to controlling agent behavior.

States

StateDescriptionResponds To
ActiveLLM loop is runningN/A (already processing)
IdleDefault idle, responsiveDocument edits, messages, direct chats, timers
HibernateDeep idleTimers only
SuspendedBlocked by runtimeOwner approval only
ErrorFailed, waiting for recoveryUser messages only
OffFully stoppedNothing (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:

  1. The runtime records the previous idle state
  2. The agent transitions to active
  3. The LLM loop runs
  4. 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:

  1. Runtime sets state to suspended
  2. A human-in-the-loop dialog appears: “Resume or shut down?”
  3. If the owner approves → back to active
  4. If the owner denies or the suspend_timeout_ms window 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.

BehaviorEffect
respond toolEnds the turn — agent returns to idle
say toolTurn continues — used for status updates
ask toolPauses the loop — waits for human input, then resumes
sys_set_stateEnds loop and changes state
Hit max_active_turnsAgent 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.

BehaviorEffect
respond toolLogs output, turn continues
say toolTurn continues
ask toolNot available
sys_set_stateEnds loop and changes state
Hit max_active_turnsAgent 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 CaseRecommended Mode
Chat assistantInteractive
Background workerAutonomous
Scheduled reporterAutonomous
Human-supervised agentInteractive
Data processorAutonomous

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.