Crux
API Reference@use-crux/core

Prompt Resolution

use composition, input inference, message modes, output modes, and provider adaptation.

Prompt resolution turns one authored Prompt plus its use entries into the provider-neutral request an adapter executes.

Composition With use

const assistant = prompt({
  id: "assistant",
  use: [
    brandContext,
    userMemory,
    docsGrounding,
    threadBlackboard,
    process.env.NODE_ENV === "development" && debugContext,
  ],
  input: z.object({
    userId: z.string(),
    question: z.string(),
  }),
  system: "Answer carefully.",
  prompt: ({ input }) => input.question,
});
EntryContribution
context()System text, input fields, tools, constraints, and guardrails
when() / match()Conditional Context inclusion
thread()Exact conversation history for managed execution and atomic accepted-turn publication
memory()Memory Context, tools, and capture or flush lifecycle
blackboard()Shared-state Context and focused tools
retriever() / retrievalRecipe()Retrieval Context, search or source tools, and trace metadata
grounding()Evidence injection, citation metadata, constraints, and optional tools
skill()Skill index, loader tools, and loaded instructions
contributor()Custom Context, tools, constraints, guardrails, metadata, nested entries, and pipeline re-entry
false, null, undefinedIgnored, which is useful for static flags

Manual .asContext() and .asTools() helpers remain useful for frameworks that do not run Prompt resolution. Normal Prompts should place primitives in use.

A Prompt or Agent may resolve exactly one Thread. Managed execution reads its history once, prepends that exact snapshot to the rendered request, and publishes the rendered user turn plus the accepted assistant/tool exchange as one atomic causal group. The returned result exposes the publication receipt as threadCommit. If a call supplies explicit messages, those messages take precedence and Thread storage is neither read nor written for that invocation.

Custom Contributor Entries

Use contributor() to build a project-specific primitive that composes through use:

import { context, contributor, prompt } from "@use-crux/core";
import { z } from "zod";

const accountPlan = contributor({
  id: "account-plan",
  input: z.object({ accountId: z.string() }),
  when: (input) => input.accountId.length > 0,
  use: [docsContext],
  async contribute({ input }) {
    const plan = await loadAccountPlan(input.accountId);

    return {
      contexts: [
        context({
          id: "account-plan-context",
          system: `Current account plan: ${plan.name}`,
        }),
      ],
      metadata: {
        accountPlan: plan.name,
      },
    };
  },
});

const support = prompt({
  id: "support",
  use: [accountPlan],
  input: z.object({
    accountId: z.string(),
    question: z.string(),
  }),
  prompt: ({ input }) => input.question,
});
OptionTypeDescription
idstringRequired identity for observability, exclusions, and tool-collision errors
inputZodObject?Fields merged into the Prompt input and typed in contribute()
when(input) => booleanSynchronous gate; excluded entries are visible in inspection and never call contribute()
usereadonly ContextEntry[]?Nested entries resolved before this contribution
contribute(args) => ContributionAsync-capable contribution of Contexts, tools, constraints, guardrails, metadata, or nested use entries

Returned Contexts and use entries re-enter the same compiled pipeline. Tools use normal collision detection. Prefer a dedicated first-party primitive when one matches the lifecycle you need.

Input Inference

The final input combines the Prompt schema with every composed input schema. Plain Contexts contribute required fields. Conditional Contexts contribute optional fields because their runtime branch is not statically known.

merged-input.ts
import { context, prompt, when } from "@use-crux/core";
import { z } from "zod";

const locale = context({
  id: "locale",
  input: z.object({ locale: z.string() }),
  system: ({ input }) => `Locale: ${input.locale}`,
});

const brand = context({
  id: "brand",
  input: z.object({ brandVoice: z.string().optional() }),
  system: ({ input }) => `Brand voice: ${input.brandVoice ?? "default"}`,
});

const reply = prompt({
  id: "reply",
  use: [locale, when(({ brandVoice }) => Boolean(brandVoice), brand)],
  input: z.object({ question: z.string() }),
  prompt: ({ input }) => input.question,
});

Here, question and locale are required; brandVoice is optional.

System Composition Order

Resolution composes:

  1. The Prompt's own system text.
  2. Active cached Contexts in use order.
  3. Active uncached Contexts in use order.
  4. Nested entries before their owning Context within the same cache tier.

Priority orders explicitly authorized representation alternatives; it never authorizes dropping an exact Context. The Prompt-owned text and every plain Context remain exact and required. If no complete authorized representation fits inputBudget.max, planning fails before provider dispatch.

Excluded conditional Contexts contribute nothing. Representation changes retain the contributor's owned capabilities unless an explicitly droppable contributor is omitted.

Prompt and Message Modes

Use system plus prompt for normal single-turn calls:

const rewrite = prompt({
  input: z.object({ text: z.string() }),
  system: "Rewrite without changing meaning.",
  prompt: ({ input }) => input.text,
});

Use messages when a full message list is the source of truth:

const reply = prompt({
  input: z.object({ userMessage: z.string() }),
  messages: ({ input }) => [
    { role: "system", content: "You are concise." },
    { role: "user", content: "Answer in one sentence." },
    { role: "assistant", content: "Understood." },
    { role: "user", content: input.userMessage },
  ],
});

Typed callers cannot combine messages with system or prompt. Composed system text is still resolved and adapters fold it into their native message format.

Text and Structured Output

output selects the adapter execution path:

Prompt shapeAdapter behavior
No outputText generation; results expose adapter-specific text
With outputStructured generation; results expose validated object data

See the Structured Output guide for schema lowering and optional field behavior.

Provider Adaptation

Use adapt for small provider or model-specific changes:

const extract = prompt({
  output: z.object({ entities: z.array(z.string()) }),
  system: "Extract named entities.",
  prompt: ({ input }) => input.text,
  adapt: {
    openai: {
      settings: { temperature: 0 },
    },
    anthropic: {
      appendSystem: "\nReturn concise JSON only.",
    },
    "*": {
      settings: { maxTokens: 800 },
    },
  },
});

Resolution checks the exact provider, slash-prefixed model providers such as openai/gpt-4o, then "*". In Prompt mode, system adaptations become uncached system blocks. In messages mode, the final adapted system text is folded into the message list and resolved.system remains absent.

On this page