Crux
GuidesContext planning

Adaptive preparation hooks

Amend provider calls and composition children at explicit execution boundaries.

const responder = agent({
  id: "support-responder",
  prompt: supportReply,
  model: standardModel,
  prepareStep: ({ reason }) =>
    reason === "validation-retry"
      ? { model: reviewModel }
      : undefined,
});

Preparation hooks return constrained deltas before provider or managed-child I/O. They observe immutable execution facts and cannot rewrite canonical messages, credentials, safety policy, or output contracts.

Use static configuration first

Put stable context on the Prompt and stable models or budgets on the Agent:

const configuredReply = prompt({
  id: "configured-support-reply",
  use: [supportPolicy],
  prompt: "Answer the support ticket.",
});

const responder = agent({
  id: "support-responder",
  prompt: configuredReply,
  model: standardModel,
  inputBudget: { max: 24_000 },
});

Add a hook only when the decision depends on facts that exist at an execution boundary, such as a Tool result, validation retry, committed Blackboard state, or Pipeline output.

Decision pointHook
Before each semantic provider callprepareStep
Before a composition invokes one managed AgentprepareInvocation

Prepare each provider call

prepareStep runs before the initial provider call, later Tool rounds, and validation retries:

const responder = agent({
  id: "tool-aware-support",
  prompt: supportReply,
  model: standardModel,
  prepareStep: ({ reason, toolHistory, previousReceipt }) => {
    const searched = toolHistory.some(
      (entry) => entry.name === "searchKnowledge" && entry.result !== undefined,
    );

    if (reason === "tool-result" && searched) {
      return {
        use: { add: [answerVerification] },
        activeTools: ["verifyAnswer"],
        inputBudget: { max: 22_000 },
      };
    }

    if (previousReceipt?.warnings.length) {
      return { model: reviewModel };
    }
  },
});

The context includes original input, canonical messages for observation, normalized Tool history, prior request evidence, honest statistics, declared resources, and a deadline signal. The callback receives no mutable provider request.

An invocation-level callback replaces the Agent default:

await generate(responder, {
  input: { ticketId: "ticket_4821" },
  prepareStep: ({ reason }) =>
    reason === "initial"
      ? { use: { add: [priorityCustomerContext] } }
      : undefined,
});

Transport retries do not run the callback again. They reuse the exact sealed request. A new semantic call receives a fresh non-accumulating amendment.

Return a constrained amendment

return {
  use: {
    add: [answerVerification],
    remove: [{ id: "optional-reply-examples" }],
  },
  tools: { verifyAnswer },
  activeTools: ["verifyAnswer"],
  model: reviewModel,
  inputBudget: { optimizeAt: 18_000, max: 22_000 },
};
FieldUse
use.addAdd an amendable top-level contributor for this boundary.
use.removeRemove a top-level contributor by original object or unique id.
toolsAdd exact Tool definitions with unique names.
activeToolsSelect available Tool names after contributor resolution.
modelSelect a compatible concrete model.
inputBudgetReplace whole-request pressure for this boundary.

History ownership cannot change inside a hook. A ladder root is one removal unit; you cannot remove a nested rung. Removing a contributor also removes its owned capabilities only when the contributor's policy allows complete omission.

Unknown Tool names, Tool collisions, add-and-remove conflicts, or protected capability removal throw RequestCompositionError with code INVALID_COMPOSITION before provider dispatch.

Read declared resources

Preparation can read supported structured state from the inherited graph:

const supportState = workingState({
  id: "support-control",
  schema: supportControlSchema,
});

const supportMemory = memory({
  id: "support-memory",
  records,
  namespace: ({ input }) => `ticket:${input.ticketId}`,
  blocks: [supportState],
});

const stateAwareReply = prompt({
  id: "state-aware-support-reply",
  use: [supportMemory],
  prompt: "Answer the support ticket.",
});

const responder = agent({
  id: "state-aware-support",
  prompt: stateAwareReply,
  async prepareStep({ resources }) {
    const state = await resources.read(supportState);
    return state?.escalated ? { model: reviewModel } : undefined;
  },
});

The first read pins the value and revision for that boundary. Repeated reads return the same immutable value. A declared resource with no value returns null.

Do not use resources as general Storage access. It supports declared workingState() and Blackboard handles, not transcript history, retrieval, assets, arbitrary loaders, or writes. A resource added by the current callback becomes readable at the next boundary.

Abnormal reads throw ResourceReadError:

ReasonMeaning
undeclaredThe handle is outside the inherited contributor graph.
unauthorizedThe current execution boundary cannot read it.
unresolvedThe reader produced neither a value nor null.
storage-unavailableBacking storage failed.

Catch a reason only when your application has an explicit safe fallback.

Use statistics honestly

prepareStep: ({ stats }) => {
  const usage = stats.run.usage;

  if (usage.coverage.tokens === "complete" &&
      usage.inputTokens !== undefined &&
      usage.inputTokens > 18_000) {
    return { inputBudget: { max: 22_000 } };
  }
}

stats.run describes the managed run, while stats.root describes the outer activity root. Usage coverage is complete, partial, or none. Do not treat an absent token value as zero. Model-call counters include exact transport retries reported by adapters.

These values are in-memory control facts, not a durable billing ledger.

Prepare composition children

prepareInvocation runs before one managed Agent child:

const result = await pipeline({
  id: "support-resolution",
  context: { ticketId: "ticket_4821" },
  model: standardModel,
  steps: [
    { name: "classify", agent: classifier },
    { name: "reply", agent: responder },
  ],
  prepareInvocation: ({ step, context }) => {
    if (step.name !== "reply") return;

    return {
      use: { add: [resolutionContext(context.classify)] },
      model: reviewModel,
    };
  },
});

Pipeline callbacks receive stage identity and accumulated context. Parallel callbacks receive branch identity and shared context. Consensus callbacks receive candidate identity and input. Swarm callbacks receive hop and handoff facts.

The accepted amendment becomes the child baseline. The child's prepareStep then starts from that baseline before each provider call. Function-only stages and nested composition wrappers do not trigger an outer callback because they do not identify one managed provider operation.

Understand failure and timing

A callback failure, automatic 30-second ceiling, or caller cancellation throws PreparationError with reason callback, timeout, or aborted. Unhandled resource failures remain ResourceReadError. Invalid amendments remain RequestCompositionError: INVALID_COMPOSITION.

No provider or child I/O starts after a boundary failure. Keep preparation small. Start or join long work earlier, persist a compact result, then read it through a declared resource.

Inspect accepted decisions

const inspection = await result.steps[0].request.inspect();
console.log(inspection.preparation);

Inspection records amendment counts, change flags, pinned resource identities and hashes, and the sealed request id. It never contains resource values, messages, Tool payloads, or raw callback errors.

Composition results expose a causal requestReceipts tree. Each managed leaf contains its ordered provider receipts, and nested compositions remain nested.

On this page