PAIPAI

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

PhaseCan change model input?Can change persisted transcript?Can call tools?Notes
runtimeContextIndirectlyNoNoBuilds trusted agent-wide state for one execution episode.
prepareRunInitial execution-local context onlyOnly through respondNoPer-episode guardrails, quota, cache, and limits; runs again on resume.
resolveModelContextChooses durable/base messagesNoNoLazy middleware over provider-backed history capped at the step-start physical anchor.
Tool assemblyYes, by selecting toolsNoNoStatic agent tools plus any frontend-defined client-tool snapshots captured for the current request.
prepareModelStepYesOnly through respond or block with responseNoEphemeral per-step patch.
aroundModelCallYesThrough returned model streamNoNarrow model-call wrapper.
beforeToolCallTool input onlyThrough tool partNoCan continue, replace input, skip, or reject.
afterToolResultTool result onlyThrough tool partNoNormalizes any tool result before model sees it.
transformStepStreamFuture steps onlyYesNoCanonical stream boundary.
afterStepFuture steps onlyNo direct message rewriteNoRuns after the canonical step commit and before the run transition; can append or replace execution-local next-step model context.
CommitNoYesNoRuntime 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):

  • resolveModelContext is middleware: a is outermost, c is closest to the default durable-context leaf, and each layer chooses whether and how to call its memoized next();
  • prepareRun, prepareModelStep, transformStepStream, beforeToolCall, afterToolResult, and afterStep run a, then b, then c;
  • prepareHistoryRewrite runs in that order and shallow-merges returned private and public thread metadata patches; historyRewritten runs 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; prepareRun and afterStep instead accumulate child decisions without mutating the shared input;
  • aroundModelCall wraps in the same order, so a is outermost and c is closest to the underlying call;
  • within one model step, beforeToolCall runs 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;
  • respond commits a terminal assistant response and ends the run after canonical output policy;
  • skip or reject skips execution or routing for that call;
  • skipped outputs and submitted results still validate and run afterToolResult;
  • thrown errors fail the run. aroundModelCall may 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.

On this page