Lifecycle Phases
Where agent lifecycle hooks run inside the runtime pipeline.
Lifecycle hooks run inside the runner after the runtime has accepted work and before projected state is returned to clients.
Phase Boundaries
| Phase | Can change model input? | Can change persisted transcript? | Can call tools? | Notes |
|---|---|---|---|---|
runtimeContext | Indirectly | No | No | Builds trusted agent-wide state for one execution episode. |
prepareRun | Initial execution-local context only | Only through respond | No | Per-episode guardrails, quota, cache, and limits; runs again on resume. |
resolveModelContext | Chooses durable/base messages | No | No | Lazy middleware over provider-backed history capped at the step-start physical anchor. |
| Tool assembly | Yes, by selecting tools | No | No | Static agent tools plus any frontend-defined client-tool snapshots captured for the current request. |
prepareModelStep | Yes | Only through respond or block with response | No | Ephemeral per-step patch. |
aroundModelCall | Yes | Through returned model stream | No | Narrow model-call wrapper. |
beforeToolCall | Tool input only | Through tool part | No | Can continue, replace input, skip, or reject. |
afterToolResult | Tool result only | Through tool part | No | Normalizes any tool result before model sees it. |
transformStepStream | Future steps only | Yes | No | Canonical stream boundary. |
afterStep | Future steps only | No direct message rewrite | No | Runs after the canonical step commit and before the run transition; can append or replace execution-local next-step model context. |
| Commit | No | Yes | No | Runtime writes the owner-fenced run/thread transition and any required terminal tool finalization. |
Regeneration, retry, and stored message-visibility changes use two additional
phases. prepareHistoryRewrite receives a regenerate or visibility rewrite
and returns thread metadata patches that commit in the same transaction as the
history change. After commit, historyRewritten observes the rewrite as a
best-effort fact for idempotent external reconciliation.
The durable source of truth is still storage: threads, messages, queued items, runs, and active leases.
Hooks do not write raw provider records. They return structured decisions, and
the runtime applies those decisions while preserving ThreadVersion, the
tail-message invariant, run/admin lease ownership, scope isolation, and watch
invalidation semantics.
Internal Interceptors
Runtime correctness does not depend on user lifecycle hooks.
Queue admission, active-run leases, stop handling, wakeups, run lifecycle transitions, and realtime publishing are internal runtime interceptors. They run in fixed runtime order and are not exposed as app lifecycle hooks.
This avoids the old problem where queue behavior could depend on public onAfterStep ordering.
Canonical Vs Presentation
transformStepStream is canonical.
If it removes a secret, the currently connected client sees the redacted chunks, the refreshed thread still shows the redacted text, and the next model step sees the redacted message.
Live-only presentation transforms are not part of AgentLifecycle. If a transport or client smooths chunks, folds frames, or changes display-only framing, a refreshed client still gets the snapshot built from committed state.
Tool Approval
Lifecycle hooks can validate or reject tool calls, but they are not the suspend/resume mechanism.
When a tool needs human input, define a suspend step on the tool. The waiting state is persisted in the tail message's tool part and projected as a pending action. Any client or trusted backend actor can later submit the typed resume payload.
beforeToolCall runs on the initial model-created tool call and on backend resume before execution continues. afterToolResult runs only when there is a final model-visible result: completion, skipped output, rejection, submitted client result, manual result, failure, or cancellation.
See Suspend And Resume.
Ordering
Lifecycle helpers compose deterministically.
For composeLifecycle(a, b, c):
resolveModelContextis middleware:ais outermost,cis closest to the default durable-context leaf, and each layer chooses whether and how to call its memoizednext();prepareRun,prepareModelStep,transformStepStream,beforeToolCall,afterToolResult, andafterStepruna, thenb, thenc;prepareHistoryRewriteruns in that order and shallow-merges returned private and public thread metadata patches;historyRewrittenruns every observer in that order and reports aggregated failures after the rewrite has committed;- value-transforming phases pass the patched input, stream, tool input, or tool
result to later hooks;
prepareRunandafterStepinstead accumulate child decisions without mutating the shared input; aroundModelCallwraps in the same order, soais outermost andcis closest to the underlying call;- within one model step,
beforeToolCallruns for every tool call before any ready tool starts executing or routing; - ready tools from the same model step run in parallel, while the final transcript remains ordered by the model's tool-call order;
respondcommits a terminal assistant response and ends the run after canonical output policy;skiporrejectskips execution or routing for that call;- skipped outputs and submitted results still validate and run
afterToolResult; - thrown errors fail the run.
aroundModelCallmay catch setup failures from the call it wraps; failures delivered later while consuming the returned stream require stream-aware handling.
Observability
Lifecycle hooks are for behavior. Observability should use telemetry whenever it does not need to change execution.
Use telemetry for:
- run started, finished, failed, and cancelled events;
- model-call timing and token usage;
- tool-call timing and errors;
- queue and lease events;
- transport and realtime publishing;
- storage retries and provider failures.
Keeping telemetry separate prevents logging code from accidentally changing persisted agent behavior.