Crux
GuidesContext planning

Context planning

Fit complete provider requests without silently changing canonical context.

const result = await generate(supportReply, {
  model,
  input: { ticketId: "ticket_4821" },
  inputBudget: { optimizeAt: 24_000, max: 30_000 },
});

console.log(result.steps[0].request.adaptations);

Crux plans every language-model request after it resolves the concrete model and before it calls the provider. The planner measures messages, Tools and schemas, media, output reserve, and provider overhead as one request.

Plain context stays exact and required. Crux selects a shorter representation only when you authorize it on that contributor. If no legal complete request fits, execution fails before provider dispatch.

When to use context planning

Use the planning APIs when any of these conditions applies:

  • Conversations or retrieved documents grow over time.
  • Tool schemas and Tool results consume a meaningful part of the context window.
  • You need a product-specific operating target below the model limit.
  • You need evidence of what changed before a provider call.

You do not need representation wrappers for small, fixed requests. The planner still measures those requests and emits an exact receipt with adaptations: [].

Start with the canonical request

Define the full source first:

const productCatalog = context({
  id: "product-catalog",
  system: loadProductCatalog,
});

const supportReply = prompt({
  id: "support-reply",
  use: [productCatalog],
  prompt: ({ input }) => `Resolve ticket ${input.ticketId}.`,
});

If this request becomes too large, Crux does not infer which content may be lost. It throws RequestCompositionError with code REQUEST_TOO_LARGE.

Authorize only safe changes

Add the smallest ladder your product can accept:

import {
  droppable,
  offloadable,
  prefer,
  summarizable,
} from "@use-crux/core";

const catalogIndex = context({
  id: "product-catalog-index",
  system: loadProductCatalogIndex,
});

const plannedCatalog = droppable(
  offloadable(
    summarizable(
      prefer(productCatalog, catalogIndex),
    ),
  ),
);

The fixed order is:

full -> authored alternative -> generated summary -> exact reference -> omitted

Start with prefer() when you can maintain a reviewed compact source. Add summarizable() for descriptive content that may lose detail. Add offloadable() when exact recovery matters. Add droppable() only when the whole contributor and its capabilities are optional.

Pick a history policy

NeedUse
Complete transcript must stay exactCaller-owned messages with no projection
Recent exact turns are sufficienthistory.recent()
Older turns may become a derived summaryhistory()
const conversationalReply = prompt({
  id: "conversational-support",
  use: [history({ recent: { messages: 12 } })],
  prompt: "Continue the conversation.",
});

History projection never mutates the canonical transcript. See History planning for causal grouping, artifact misses, and source precedence.

Use a Thread when the application should persist that canonical transcript and keep concurrent or edited continuations as branches.

Set pressure independently

inputBudget does not authorize loss. It tells the planner when to prefer an authorized smaller candidate and when to reject:

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

optimizeAt is soft. max is strict. Both apply independently to each provider call, including later Tool rounds.

Inspect before and after execution

Use preview() for prospective fit:

const planned = await preview(responder, {
  input: { ticketId: "ticket_4821" },
});

if (planned.status !== "fits") {
  console.log(planned.status, planned.diagnostics);
}

Use the request receipt for the exact executed call:

const result = await generate(responder, {
  input: { ticketId: "ticket_4821" },
});

const receipt = result.steps[0].request;
const inspection = await receipt.inspect();

console.table(inspection.contributions);
console.table(inspection.breakdown.contributions);

Preview never prepares missing artifacts or executes a provider. Receipt inspection reports only redacted identities, sizes, decisions, and links. It does not retain authored prompt, message, Tool-result, or resource content.

Adapt at real execution boundaries

Use prepareStep when the next provider call depends on Tool history, usage, or declared structured state. Use prepareInvocation when a composition child needs a different baseline.

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

Static configuration is easier to test and should remain your default. Hooks cannot replace canonical messages or bypass capability, safety, and output validation.

Failure behavior

Planning failures happen before the provider is called:

ErrorMeaning
RequestCompositionError: REQUEST_TOO_LARGENo complete legal candidate fits.
RequestCompositionError: REPRESENTATION_UNAVAILABLEA required summary, reference, or support capability is unavailable.
RequestCompositionError: INVALID_COMPOSITIONA ladder, history policy, capability graph, or amendment is invalid.
PreparationErrorA preparation callback failed, timed out, or was cancelled.
ResourceReadErrorA preparation resource read was abnormal.

Errors and diagnostics are content-free. They include safe identities, counts, and remedies instead of source text.

On this page