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.

ts
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

kindEffect
commandRender a command with args, inject the text.
skillPre-expand a skill’s body into context (no skill_load round-trip).
mcpCall an MCP tool host-side and inject its result. tool is the prefixed <server>__<tool>.
promptA 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:

ts
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.

Type to search…

↑↓ navigate open esc close