openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
239 lines (175 loc) • 28.8 kB
Markdown
---
summary: "Agent loop lifecycle, streams, and wait semantics"
read_when:
- You need an exact walkthrough of the agent loop or lifecycle events
- You are changing session queueing, writer claims, or transcript write fencing
title: "Agent loop"
---
The agent loop is the serialized, per-session run that turns a message into
actions and a reply: intake, context assembly, model inference, tool
execution, streaming, persistence.
## Entry points
- Gateway RPC: `agent` and `agent.wait`.
- CLI: `openclaw agent`.
## Run sequence
1. `agent` RPC validates params, resolves the session (`sessionKey`/`sessionId`), persists session metadata, and returns `{ runId, acceptedAt }` immediately.
2. `agentCommand` runs the turn: resolves model + thinking/verbose/trace defaults, loads the skills snapshot, calls `runEmbeddedAgent`, and emits a fallback **lifecycle end/error** if the embedded loop did not already emit one.
3. `runEmbeddedAgent`: serializes runs via per-session and global queues, resolves model + auth profile, builds the OpenClaw session, subscribes to runtime events, streams assistant/tool deltas, enforces the run timeout (aborting on expiry), and returns payloads plus usage metadata. For Codex app-server turns, native Codex owns provider liveness and the exact `turn/completed` outcome; quiet periods and assistant output do not end the turn.
4. `subscribeEmbeddedAgentSession` bridges runtime events to the `agent` stream: tool events to `stream: "tool"`, assistant deltas to `stream: "assistant"`, lifecycle events to `stream: "lifecycle"` (`phase: "start" | "finishing" | "end" | "error"`).
5. `agent.wait` (`waitForAgentRun`) waits for **lifecycle end/error** on a `runId` and returns `{ status: ok|error|timeout, startedAt, endedAt, error? }`.
The wait result also carries the run's `terminalReply` and, when available,
`terminalReceipt`. A receipt with `sourceReplyDelivered: true` confirms a final
reply reached the external source conversation. A2A announcements consume that
fact instead of using display-history mirrors as delivery evidence.
## Queueing and concurrency
Runs are serialized per session key (session lane) and optionally through a global lane, preventing tool/session races. Messaging channels choose a queue mode (steer/followup/collect/interrupt) that feeds this lane system; see [Command Queue](/concepts/queue).
Before streaming, an admitted run records its durable `activeWriterRunId` claim. Every transcript append or rewrite supplies `expectedWriterRunId`, and the synchronous commit transaction verifies that it still matches the active claim. A superseded run therefore cannot commit stale transcript data. The SQLite writer queue orders per-agent mutations, while the Gateway state-directory lock prevents another Gateway or `openclaw agent --local` process from owning the same state directory concurrently.
## Session and workspace preparation
- Workspace is resolved and created; sandboxed runs may redirect to a sandbox workspace root.
- Skills are loaded (or reused from a snapshot) and injected into env and prompt.
- Bootstrap/context files are resolved and injected into the system prompt.
- The session transcript target and writer claim are prepared before streaming starts. Later rewrites, compaction, and truncation use the same in-transaction writer-claim fence.
## Prompt assembly
System prompt is built from OpenClaw's base prompt, skills prompt, bootstrap context, and per-run overrides. Model-specific limits and compaction reserve tokens are enforced. See [System prompt](/concepts/system-prompt) for what the model sees.
## Hooks
OpenClaw has two in-process hook systems:
- **Internal hooks**: `HOOK.md` scripts for command and lifecycle events such as `command:new`.
- **Plugin hooks**: typed `api.on(...)` handlers inside the agent/tool lifecycle and Gateway pipeline, such as `before_tool_call`.
[HTTP webhooks](/automation/cron-jobs#webhooks) are separate: they accept external requests that trigger work, rather than subscribing to agent-loop events.
### Internal hooks (Gateway hooks)
- **`agent:bootstrap`**: runs while building bootstrap files before the system prompt is finalized. Use it to add or remove bootstrap context files.
- **Command hooks**: core emits `command:new`, `command:reset`, and `command:stop`. Other command names do not automatically become hook events.
See [Hooks](/automation/hooks) for setup and examples.
### Plugin hooks
These run inside the agent loop or gateway pipeline:
| Hook | Runs |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `before_model_resolve` | Pre-session (no `messages`), to deterministically override provider/model before resolution. |
| `before_prompt_build` | After session load (with `messages`), to inject `prependContext`, `systemPrompt`, `prependSystemContext`, or `appendSystemContext`, or, on supported runtimes with a turn-scoped submitted tool surface, narrow it with `toolsAllow`. An empty `toolsAllow` submits no optional tools; omitted leaves the host-resolved surface unchanged. Unsupported runtimes reject restrictive values instead of ignoring them. |
| `before_agent_reply` | After inline actions, before the LLM call. Lets a plugin claim the turn and return a synthetic reply or silence it entirely. |
| `agent_end` | After completion, with the final message list and run metadata. |
| `before_compaction` / `after_compaction` | Observe compaction cycles; these hooks do not rewrite or veto compaction. |
| `before_tool_call` / `after_tool_call` | Intercept tool params/results. |
| `before_install` | After operator install policy runs, on staged skill/plugin install material, when plugin hooks are loaded in the current process. |
| `tool_result_persist` | Synchronously transforms tool results before they are written to an OpenClaw-owned session transcript. |
| `message_received` / `message_sending` / `message_sent` | Inbound and outbound message hooks. |
| `session_start` / `session_end` | Session lifecycle boundaries. |
| `gateway_start` / `gateway_stop` | Gateway lifecycle events. |
Hook decision rules for outbound/tool guards:
- `before_tool_call`: `{ block: true }` is terminal and stops lower-priority handlers. `{ block: false }` is a no-op and does not clear a prior block.
- `before_install`: same terminal/no-op semantics as above. Use `security.installPolicy`, not `before_install`, for operator-owned install allow/warn/block decisions that must cover CLI install and update paths.
- `message_sending`: `{ cancel: true }` is terminal and stops lower-priority handlers. `{ cancel: false }` is a no-op and does not clear a prior cancel.
See [Plugin hooks](/plugins/hooks) for the hook API and registration details.
Harnesses can adapt these hooks. The Codex app-server harness keeps OpenClaw plugin hooks as the compatibility contract for documented mirrored surfaces; Codex native hooks are a separate, lower-level Codex mechanism.
## Streaming
- Assistant deltas stream from the agent runtime as `assistant` events.
- Block streaming can emit partial replies on `text_end` or `message_end`.
- Reasoning streaming can be a separate stream or block replies.
- See [Streaming](/concepts/streaming) for chunking and block reply behavior.
## Tool execution
- Tool start/update/end events emit on the `tool` stream.
- Tool results are sanitized for size and image payloads before logging/emitting.
- Messaging tool sends are tracked to suppress duplicate assistant confirmations.
## Reply shaping
Final payloads are assembled from assistant text (plus optional reasoning), inline tool summaries (when verbose and allowed), and assistant error text when the model errors.
- The exact silent token `NO_REPLY` is filtered from outgoing payloads.
- Messaging tool duplicates are removed from the final payload list.
- A fallback tool error warning appears only when a run ends with a tool failure and would otherwise leave the user with no reply. This guard is not configurable; a user-facing reply, including one already delivered by a messaging tool, prevents the warning.
Prompt-segment diagnostics attribute attachment/context blocks and generated inbound metadata separately from user text. A prompt containing only those blocks does not need trailing user text for reply processing to complete.
## Compaction and retries
Auto-compaction emits `compaction` stream events and can trigger a retry. On retry, in-memory buffers and tool summaries reset to avoid duplicate output. See [Compaction](/concepts/compaction).
## Event streams
- `lifecycle`: emitted by `subscribeEmbeddedAgentSession` (and as a fallback by `agentCommand`).
- `assistant`: streamed deltas from the agent runtime.
- `tool`: streamed tool events from the agent runtime.
The Gateway projects lifecycle and tool start/terminal events into the bounded,
metadata-only [audit ledger](/cli/audit). This projection records provenance and
result codes without copying prompts, messages, tool arguments, tool results,
or raw errors out of the transcript/runtime path.
## Chat channel handling
Assistant deltas buffer into chat `delta` messages. Terminal lifecycle events
produce chat `final`, `error`, or `aborted` messages. Definitive cancellation and
timeout events finalize immediately, including when the runtime reports them as
`phase: "error"`. Retryable errors keep a 15-second grace window for a fallback
or restart of the same run. Once the outer execution owner has finished its
attempts, it publishes `executionSettled: true`. The Gateway and `agent.wait`
consume that fact immediately, including preparation failures that never reached
a model or emitted a fallback step. Unmarked timeout and bare-abort observations
retain their existing wait-layer retry handling.
Cron attempt completions remain `finishing` across model fallbacks and
interim-acknowledgment retries; worker `finishing` events do not claim execution
settlement. Completed execution facts are captured before cron bookkeeping, but
finality is published only after execution settles. A new attempt clears the
prior outcome before preparation. Later workflow errors or aborts cannot
reclassify completed execution; cron persistence, delivery, and yielded-parent
continuation retain their separate outcomes.
History keeps a run active while its terminal session write is pending. Once
that write succeeds, history and session activity show the recorded end time
and duration without waiting for retry grace.
Live snapshots are scoped to their assistant message. A correction can shorten or clear the current preview without erasing earlier messages. Pending text is flushed before the terminal event; pacing live updates does not delay tool execution or transcript writes.
Run-duration metadata belongs to the current run, including when preparation fails before the model starts. In Control UI completed-work rollups, independent sends have separate elapsed-time boundaries: a failed turn and the idle time before the next send are not part of that next turn's work. Steering remains associated with its target run rather than being treated as an independent retry.
## Timeouts
When no result is available before an `agent.wait` deadline, the response contains
only `runId` and `status: "timeout"`.
It does not cancel the run or identify its execution phase; wait on the same
`runId` again to observe completion. A wait interrupted by Gateway lifecycle
shutdown includes `timeoutPhase: "gateway_draining"` without terminal metadata.
Known queued chat turns report `status: "pending"`, `timeoutPhase: "queue"`, and
`providerStarted: false`.
| Timeout | Default | Notes |
| ------------------------------------------------ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent.wait` | 30s | Wait-only; `timeoutMs` param overrides. Does not stop the underlying run. |
| Agent runtime (`agents.defaults.timeoutSeconds`) | 172800s (48h) | Elapsed execution budget, aborting on expiry. Progress does not reset it. Set `0` for unlimited execution; runtime-owned provider liveness still applies. Codex enforces this budget per attempt, independently of its native stream recovery. |
| CLI backend no-output watchdog | computed per fresh/resumed CLI run | Separate from the agent runtime and owned by the registered backend plugin. A CLI-internal background task shares the parent subprocess and does not outlive an overall agent timeout. |
| Cron isolated agent turn | owned by cron | The scheduler starts its own timer when execution begins, aborts the run at the configured deadline, then runs bounded cleanup before recording the timeout so a stale child session cannot keep the lane stuck. |
| Model idle timeout | Cloud 120s; self-hosted 300s | OpenClaw aborts a model request when no response chunks arrive before the idle window. `models.providers.<id>.timeoutSeconds` extends this idle watchdog for slow local/self-hosted providers, but stays bounded by any lower finite `agents.defaults.timeoutSeconds` or run-specific timeout, since those govern the whole agent run. Unlimited run budgets still keep the provider-class idle watchdog. Cron-triggered cloud model runs with no explicit model/agent timeout use the same default; with an explicit cron run timeout, cloud model stream stalls cap at 60s so configured model fallbacks can still run before the outer cron deadline. Cron-triggered runs on genuinely local endpoints (loopback/private baseUrl) keep the local idle opt-out; self-hosted providers on network baseUrls get the 300s implicit watchdog. With an explicit cron run timeout, local/self-hosted stalls cap at that timeout. Set `models.providers.<id>.timeoutSeconds` for slow local providers. |
| Provider HTTP request timeout | `models.providers.<id>.timeoutSeconds` | Covers connect, headers, body, SDK request timeout, guarded-fetch abort handling, and the model stream idle watchdog for that provider. Use for slow local/self-hosted providers (for example Ollama) before raising the whole agent runtime timeout; keep the agent/runtime timeout at least as high when the model request needs to run longer. |
The built-in OpenClaw harness publishes its execution deadline to the queue.
Approval waits pause the unused budget; resolving all pending approvals resumes
that same budget. Compaction can receive one bounded grace period. A question
from `ask_user` does not pause the overall execution budget. With `0`, no
execution timer is armed, but provider liveness, Stop, and bounded abort cleanup
still apply. Isolated post-tool finalization gets its own deadline and cancellation
controls rather than inheriting callbacks from the completed attempt.
A terminal timeout is a failed turn, not a successful completion. Chat and
command results retain its timeout explanation; earlier tool errors do not
replace that explanation or restart an already-final timed-out turn.
The model idle and provider HTTP rows describe the built-in OpenClaw model
path. Codex owns its native stream deadlines and network retries. After exact
native terminal receipt, OpenClaw allows two minutes for local settlement.
After separately bounded abort cleanup, queued projection gets a five-second
drain grace. Neither window resets on progress. These cleanup limits still
apply when the execution budget is unlimited. See
[Codex timeouts](/plugins/codex-harness-reference#timeouts).
When a runtime reports a definitive timeout, the Gateway records its terminal
status and error for the session sidebar immediately, without waiting for
provider retry grace. Opening the failed session dismisses its sidebar attention
as usual. A later successful turn clears the previous error and is not replaced
by an older delayed failure.
### Stuck session diagnostics
With diagnostics enabled, a built-in two-minute threshold classifies long `processing` sessions with no observed reply, tool, status, block, or ACP progress:
- Active embedded runs, model calls, and tool calls report as `session.long_running`. Owned silent model calls stay `session.long_running` until the abort threshold so slow or non-streaming providers are not flagged as stalled too early.
- Active work with no recent progress reports as `session.stalled`. Owned model calls switch to `session.stalled` at or after the abort threshold; ownerless stale model/tool activity is not hidden as long-running.
- `session.stuck` is reserved for recoverable stale session bookkeeping, including idle queued sessions with stale ownerless model/tool activity.
The abort threshold is at least 5 minutes and 3x the warning threshold. Stale session bookkeeping releases the affected session lane immediately after recovery gates pass; stalled embedded runs are abort-drained only after the abort threshold, so queued work resumes without cutting off merely slow runs. Recovery emits structured requested/completed outcomes; diagnostic state is marked idle only if the same processing generation is still current, and repeated `session.stuck` diagnostics back off while the session stays unchanged.
Pending human-input questions protect their exact active owner from stale-work
recovery. If checking a question expires it, or diagnostic reporting resumes or
replaces the run, that observation cannot authorize an abort of the resumed work.
Recovery revalidates the session generation captured with the observation.
A current Codex attempt waiting on native work is the exception to these idle
thresholds: it remains `session.long_running` with reason `runtime_owned_wait`
and is not aborted or taken over merely because it is quiet. Recovery and
steering revalidate that exact active owner and its execution budget. This does
not protect expired OpenClaw-owned requests or tools, cancellation, terminal
settlement, or ownerless state.
## Where things can end early
- Agent timeout (abort)
- AbortSignal (cancel)
- Gateway disconnect or RPC timeout
- `agent.wait` timeout (wait-only, does not stop the agent)
## Related
- [Tools](/tools) - available agent tools
- [Hooks](/automation/hooks) - event-driven scripts triggered by agent lifecycle events
- [Compaction](/concepts/compaction) - how long conversations are summarized
- [Exec Approvals](/tools/exec-approvals) - approval gates for shell commands
- [Thinking](/tools/thinking) - thinking/reasoning level configuration