Lifecycle (init / postSuccess)
Deterministic steps run before and after the agent loop.
An agent’s lifecycle is a deterministic, host-controlled wrapper around the
run loop. It is declarative data on the AgentSpec — JSON-serialisable and
covered by the spec hash — not callbacks.
defineAgent({
// …
commands: ["setup", "lint-and-commit"],
lifecycle: {
init: [
{ kind: "command", name: "setup", args: { repo: "drover" } },
{ kind: "skill", name: "house-style" },
{ kind: "mcp", tool: "github__list_open_issues" },
],
postSuccess: [{ kind: "command", name: "lint-and-commit" }],
},
});init — prime the opening turn
init steps run before the first model call. Each resolves to a text
block; the blocks are composed into the opening user turn, ahead of the run
input — so the agent starts already primed.
init is skipped on resume (the blocks are already in the checkpointed
history).
postSuccess — a closing turn
postSuccess steps run after the agent’s natural completion, and only
when the run’s terminal status is success — they are skipped on error,
quota, cancelled, and paused. It is not an unconditional
finally; the name says so deliberately.
The blocks are appended as one closing user turn and the loop continues — the agent executes them with its tools. This is how “lint and commit when done” works: it is a closing instruction the agent acts on, not a side effect.
The run output is captured before postSuccess runs, so a postSuccess
turn cannot change it — it is post-processing. Output-schema self-correction
is also disabled for the continuation, so a plain post-processing reply can’t
fail the original schema. If a postSuccess step is misconfigured or its
turns fail, the run’s terminal status becomes error.
postSuccess shares the run’s quota.maxDurationMs budget — the wall-clock timer
stays armed across init, the loop, and postSuccess.
Step kinds
kind | Effect |
|---|---|
command | Render a command with args, inject the text. |
skill | Pre-expand a skill’s body into context (no skill_load round-trip). |
mcp | Call an MCP tool host-side and inject its result. tool is the prefixed <server>__<tool>. |
prompt | A literal text block. |
command / skill / mcp steps are gated by the agent’s commands /
skills / mcpServers allowlists. A step referencing something outside the
allowlist, missing from a registry, or a failed MCP call fails the run with a
LifecycleError — a broken lifecycle fails loudly, never silently.
Lifecycle vs. plugin hooks
Use lifecycle for anything that shapes what the agent sees or does —
commands, skills, MCP priming. Use a plugin’s run-level hooks for pure side
effects that the model never sees:
const metricsPlugin: HarnessPlugin = {
id: "metrics",
onRunStart: (ctx) => Effect.sync(() => track("run.start", ctx.runId)),
// async effects are awaited — fire-and-forget a webhook here is fine
onRunEnd: (result, ctx) => Effect.promise(() => postWebhook(result)),
};onRunStart fires after init, before the loop; onRunEnd after
postSuccess and terminal resolution. Both are observation-only — async
effects are awaited, and any failure or defect is swallowed so an observer
hook can’t crash a run.