Crux
API Reference@use-crux/core

Prompts

prompt(), createPrompts(), Prompt, and prompt resolution.

import { prompt, createPrompts } from "@use-crux/core";

prompt() creates a frozen, SDK-agnostic prompt definition. It is the contract adapters execute and the object Crux inspects, tests, caches, and instruments.

prompt(config)

const summarize = prompt({
  id: "summarize",
  input: z.object({ text: z.string() }),
  output: z.object({ summary: z.string() }),
  system: "Summarize the text in one sentence.",
  prompt: ({ input }) => input.text,
});

Config

FieldTypeDescription
idstring?Stable identifier for registry lookup, logs, devtools, evals, and cache keys.
descriptionstring?Human-readable description.
tagsreadonly string[]?Tags for filtering, discovery, and eval grouping.
usereadonly ContextEntry[]?Composition entries. Accepts context(), when(), match(), skills, memory, blackboards, retrievers, retrieval pipelines, grounding, custom contributor() entries, and falsy values.
inputZodType?Prompt-owned input schema. Merged with context and contributor input schemas.
outputZodType?Structured output schema. Omit for text generation.
systemstring | ({ input }) => string | Promise<string>Role, policy, and high-level instructions. Rendered first and never dropped by token budgeting. Mutually exclusive with messages.
promptstring | ({ input }) => stringSingle-turn user/task message. Mutually exclusive with messages.
messages({ input }) => Message[]Message-list mode for chat transcripts, few-shot examples, and multi-turn prompts. Mutually exclusive with system and prompt.
settingsGenerationSettings?SDK-agnostic defaults such as temperature, maxTokens, topP, topK, reasoning, toolChoice, stopWhen, and maxSteps. Provider-specific options belong in each adapter's typed extra option.
adaptProviderAdaptations?Provider/model-specific system, prompt, and settings overrides.
hooksPromptHooks?Per-prompt lifecycle hooks.
toolsToolSet?Prompt-local tools. Prompt-time tool names must be unique across skills, contexts, contributors, blackboards, and prompt config; collisions throw with both owners named. Call-site generate()/stream() tools intentionally override after resolution.
toolMiddlewareToolMiddleware | readonly ToolMiddleware[]?Server-side wrappers for prompt tools, including audit hooks and human approval.
testsEvalCase[]?Inline eval cases discovered by the eval runner.
constraintsConstraintDef[]?Semantic constraints evaluated around generation.
guardrailsGuardrailDef[]?Pre/post generation guardrails.
cachePromptCacheOptions?Prompt cache hints. semantic configures semantic response caching; provider: true requests a provider cache breakpoint for the prompt-owned system block.

Return Value

prompt() returns a frozen Prompt.

PropertyDescription
_tagRuntime discriminant: 'Prompt'.
idPrompt identifier.
descriptionDescription string.
tagsTag array.
contextsThe original use tuple.
inputSchemaRuntime merged input schema, if any.
outputSchemaOutput schema, if configured.
hasOutputLiteral true when structured output is configured, otherwise literal false.
configReadonly original config.
MethodDescription
.resolve(options)Validates input and returns the composed ResolvedPrompt without calling a model.
.inspect(options)Returns token/count/source breakdowns for debugging composition and token budgets.

Tool Model Output

Text-mode prompts can use tools. A tool has two outputs:

const lookupCustomer = tool({
  description: "Lookup a customer",
  parameters: z.object({ customerId: z.string() }),
  execute: async ({ customerId }) => {
    return await loadCustomerRecord(customerId);
  },
  toModelOutput: ({ output }) => ({
    type: "json",
    value: {
      id: output.id,
      plan: output.plan,
      supportTier: output.supportTier,
    },
  }),
});

execute() returns the raw value for your application. toModelOutput() returns the value sent back to the model for the next tool-use step.

type ToolModelOutput =
  | { type: "text"; value: string }
  | { type: "json"; value: JsonValue }
  | { type: "execution-denied"; reason?: string }
  | { type: "error-text"; value: string }
  | { type: "error-json"; value: JsonValue }
  | { type: "content"; value: readonly ContentPart[] };

The shape is provider-neutral and compatible with the AI SDK's model-message format. Recognizable AI SDK parts normalize to canonical content at the boundary. Native adapters use the same metadata: Google maps supported media to function-response parts, Anthropic maps text/images/PDFs to native tool_result content blocks, and OpenAI Chat Completions receives deterministic text references for non-text parts because that API only accepts text tool results.

Composition With use

use is intentionally broad. The prompt resolver asks each entry what it contributes.

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,
});

Every entry below can contribute during prompt resolution. Dedicated primitives have their own API because they need richer lifecycle behavior than a plain context; custom cases use contributor().

Common entry behavior:

EntryWhat it contributes
context()System text, input fields, tools, constraints, and guardrails.
when()Includes a context only when its predicate passes.
match()Selects one branch of contexts by runtime discriminator.
memory()Memory context, memory tools, capture/flush lifecycle.
blackboard()Shared state context plus focused read/write tools.
retriever() / retrievalRecipe()Retrieval context, search/source tools, and trace metadata according to injection options.
grounding()Evidence injection, citation metadata, constraints, and optional tools.
skill()A skill index plus LoadSkill and LoadReference tools. Loaded skill instructions are re-injected at system-prompt level.
contributor()Custom context, tools, constraints, guardrails, metadata, when gating, nested use entries, and pipeline re-entry with any entry kind.
false, null, undefinedIgnored. Useful for static flags.

Manual .asContext() and .asTools() helpers remain useful when integrating with a framework that does not run Crux prompt resolution directly. For normal prompts, prefer putting the primitive in use.

Custom Contributor Entries

Use contributor() when you are building your own primitive that should compose 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() }),
  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(),
  }),
  system: "Answer using the current account plan.",
  prompt: ({ input }) => input.question,
});

Reach for dedicated primitives first: skills, memory, blackboards, retrieval and grounding. Use contributor() for project-specific composition that does not belong in one of those categories.

contributor() is a first-class use entry that can gate itself, bundle nested entries, and contribute to every prompt channel.

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

const supportTools = contributor({
  id: "support-tools",
  input: z.object({ plan: z.string() }),
  // Excluded with a recorded reason when false: visible in .inspect()
  // and devtools. contribute() is never called for excluded entries.
  when: (input) => input.plan !== "free",
  // Resolved BEFORE this contributor's own contribution.
  use: [docsRetriever.asContext({ topK: 4 })],
  contribute: async ({ input }) => ({
    tools: await loadSupportTools(input.plan),
    metadata: { supportTier: input.plan },
  }),
});
OptionTypeDescription
idstringRequired. Appears in observability artifacts (contributor:<id>), exclusion records, and tool-collision errors.
inputZodObject?Fields merged into the prompt's input schema as required keys; typed in contribute().
when(input) => booleanSynchronous gate. Excluded entries record { source: 'contributor:<id>', reason: 'when() predicate returned false' }.
usereadonly ContextEntry[]?Nested entries resolved before the contribution: any entry kind, including other contributors.
contribute(args) => ContributionAsync-capable. Returned contexts/use re-enter the pipeline; tools merge with collision detection; constraints, guardrails, and metadata land on the resolved prompt.

Entries created by contributor() resolve through the same compiled prompt pass as contexts, memory, skills, and blackboards.

Input Inference

The final input type is:

Prompt input = prompt.input & all active context input schemas

Plain contexts contribute required fields. Conditional contexts contribute optional fields because Crux cannot know at compile time which runtime branch will be active.

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

const  = ({
  : "locale",
  : .({ : .() }),
  : ({  }) => `Locale: ${.}`,
});

const  = ({
  : "brand",
  : .({ : .().() }),
  : ({  }) => `Brand voice: ${. ?? "default"}`,
});

const  = ({
  : "reply",
  : [, (({  }) => (), )],
  : .({ : .() }),
  : "Reply to the user.",
  : ({  }) => .,
});

In this example, question and locale are required. brandVoice is optional.

System Composition Order

When resolving system mode, Crux composes text in this order:

  1. The prompt's own system text.
  2. Active cached contexts (cache: true) in use array order.
  3. Active uncached contexts in use array order.
  4. Nested entries before the context that owns them within their cache tier.

priority does not reorder normal output. It controls token-budget dropping inside the uncached tail: low-priority uncached contexts are dropped first. The prompt's own system text and cached prefix are always kept. If the stable prefix alone exceeds tokenBudget, Crux warns and the prompt.budget artifact includes prefixOverflow: true.

const resolved = await assistant.resolve({
  input: { userId: "user_123", question: "How do I configure SSO?" },
  tokenBudget: 4000,
});

resolved.system;

When observability is enabled, prompt resolution emits composition artifacts for devtools. Resolved contexts use context.contribution artifacts (active, checked-not-included, or dropped-budget state plus source, tokens, priority, cache status, and reason/branch when relevant). Calls with tokenBudget also emit a prompt.budget artifact showing usedTokens, totalTokens, dropped contributions, and prefixOverflow: true when the stable provider-cache prefix is larger than the budget.

system + prompt Mode

Use this mode for most single-turn calls.

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

Adapters receive a resolved system string and a user prompt string.

messages Mode

Use messages when you need a full message list.

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 },
  ],
});

Context and contributor system text is still resolved. Adapters map the resolved prompt into their SDK-specific message format. Typed callers cannot combine messages with system or prompt; put authored system text in a system message when using message-list mode.

Structured vs Text Output

output controls the adapter execution path.

const textPrompt = prompt({
  system: "Write a title.",
  prompt: "A guide to vector search",
});

const objectPrompt = prompt({
  output: z.object({ title: z.string() }),
  system: "Write a title.",
  prompt: "A guide to vector search",
});
Prompt shapeAdapter behavior
No outputText generation. Results expose text in adapter-specific shape.
With outputStructured generation. Results expose validated object data in adapter-specific shape.

Provider Adaptation

Use adapt when the same prompt needs small provider-specific changes.

const extract = prompt({
  id: "extract",
  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 exact provider, slash-prefixed model providers such as openai/gpt-4o, then '*'. In prompt mode, system adaptations are inserted as systemBlocks with source: "adaptation:<key>" and providerCache: false. In messages mode, the final post-adaptation system text is folded into the messages array and both resolved.system and resolved.systemBlocks stay absent.

.resolve(options)

.resolve() returns SDK-agnostic prepared data. It is useful for debugging, custom adapters, and framework bridges.

const resolved = await support.resolve({
  input: { userId: "user_123", question: "How do I configure SSO?" },
  provider: "openai",
  modelId: "gpt-4o",
});

resolved.system;
resolved.systemBlocks;
resolved.prompt;
resolved.messages;
resolved.tools;
resolved.constraints;
resolved.guardrails;
resolved.metadata;
resolved.settings;

.resolve() does not call a model.

.inspect(options)

.inspect() runs resolution and returns debugging metadata.

const inspection = await support.inspect({
  input: { userId: "user_123", question: "How do I configure SSO?" },
  tokenBudget: 4000,
});

inspection.system.parts;
inspection.droppedContexts;
inspection.excludedContexts;
inspection.totalTokens;

Use this when a prompt has many use entries and you need to understand what was included, excluded, or dropped.

compilePrompt(config, { ports? })

Compile a prompt config once: config validation, input-schema merge/conflict detection, and resolver port binding. Each resolve() call runs one pipeline pass and returns a PromptResolution with SDK-ready args plus an inspection view derived from the same pass.

import { compilePrompt, createResolverFakes } from "@use-crux/core";

const fakes = createResolverFakes();
const compiled = compilePrompt(config, { ports: fakes.ports });

const pass = await compiled.resolve({ input });
const resolved = pass.args;
const inspection = pass.inspect();
fakes.observability.contributionPreviews("checked-not-included"); // exclusions, no transport needed

Ports: observability, skills, cache, clock, tokenizer, policy, diagnostics, instrumentation. Omitted ports use the production runtime adapters, so compilePrompt(config) uses the same ambient pipeline as prompt(config). createResolverFakes() builds a complete, deterministic set in one call (with each port also exposed as a named handle for assertions); the individual fakes (recordingObservability, inMemorySkillSource, inMemoryContextCache, fixedClock, staticTokenizer, collectingDiagnostics, staticPolicy, recordingInstrumentation) also ship from @use-crux/core when you want to substitute just one.

The tokenizer port estimates every token count the pipeline reports (system parts, prompt text, dropped contexts), so a deterministic counter pins token-budget behavior without depending on the production chars/4 estimate. The skills port owns registry fetch, skill-index generation, and activation-session creation behind one seam.

compiled.inspect() runs the same pipeline in quiet inspection mode. pass.inspect() is free and never re-runs the pipeline, which means lifecycle hooks and debuggers observe facts from the exact resolution sent to the adapter.

compilePrompt() returns a PromptResolutionPipeline (the older CompiledPrompt name remains as a deprecated alias). PromptResolution is exported from @use-crux/core; the older Resolution type name remains as a deprecated compatibility alias.

For adapter and primitive authors, the lowered contributor contract types behind the pipeline are also exported: LoweredContributor, Contribution, GateResult, MergedResolution, and related types. The lowering, driver, and schema-collection functions are internal to the compiled prompt boundary. Application code never needs these.

createPrompts(tree)

Organizes prompts into a deeply frozen tree with type inference at every level.

const prompts = createPrompts({
  editor: {
    rewrite,
    classify,
  },
  support: {
    answerSupport,
  },
});

prompts.editor.rewrite;
prompts.support.answerSupport;
prompts._all;

_all is a non-enumerable flat list used by registries and eval runners.

On this page