Crux
GuidesAdvanced

Type System

Input merging, conditional return types, and generation settings.

Crux infers your prompt's input and output types from its schemas and contexts: input fields merge across use entries, conditional contexts become optional, and adapter return types change when a prompt declares output. This page explains the merging and inference rules you will hit in practice, plus the resolution and inspection APIs for checking what a prompt will actually send.

Input merging

When a prompt uses contexts with input schemas, all fields are merged into a single type:

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

const brand = context({
  input: z.object({ brandContext: z.string().optional() }),
  system: ({ input }) => "...",
});

const myPrompt = prompt({
  use: [brand],
  input: z.object({ instruction: z.string() }),
});

type Input = z.infer<typeof myPrompt.inputSchema>;
//   ^?

If two contexts declare the same input key, prompt throws at definition time. The prompt's own fields take precedence over context fields.

If two contexts legitimately need the same field, extract it into a shared context and have both depend on it, or namespace the fields (e.g., brandTone vs userTone).

Conditional contexts and input types

Conditional contexts (those wrapped with when(), placed inside match(), or using context-level when) have their input keys automatically wrapped as Partial<> in the merged type:

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

const brand = context({
  input: z.object({ brandVoice: z.string() }),
  system: ({ input }) => `## Brand\n${input.brandVoice}`,
});

const p = prompt({
  use: [when((i) => !!i.brandVoice, brand)],
  input: z.object({ instruction: z.string() }),
});

// brandVoice is optional because the context is conditional
p.resolve({ input: { instruction: "Edit this" } }); // OK: brandVoice omitted
p.resolve({ input: { instruction: "Edit", brandVoice: "Pro" } }); // OK: brandVoice provided

The same applies to context-level when:

const lang = context({
  input: z.object({ lang: z.string() }),
  when: ({ input }) => input.lang !== "English",
  system: ({ input }) => `Respond in ${input.lang}.`,
});
// lang's input key becomes optional in the merged type

The ContextEntry type accepted by use is:

type ContextEntry =
  | Context<any> // required input keys
  | ConditionalContext<any> // Partial input keys (from when())
  | MatchSpec // Partial input keys (from match())
  | false
  | null
  | undefined; // filtered out, no type contribution

Conditional return types

Adapters return different types based on whether output is defined:

// With output → result.object is typed
const result = await generate(structured, { model, input: { ... } })
result.object // z.infer<typeof OutputSchema>

// Without output → result.text is a string
const result = await generate(textOnly, { model, input: { ... } })
result.text // string

Generation settings

GenerationSettings provides SDK-agnostic fields (temperature, maxTokens, topP, topK, stopSequences, frequencyPenalty, presencePenalty, reasoning) plus portable tool-loop controls (toolChoice, stopWhen, maxSteps). Each adapter maps these to its SDK's naming conventions. Provider-specific options belong in that adapter's typed extra object, not in prompt settings.

Resolution & preview

Every prompt has a .resolve() method. The standalone preview() function observes whole-request planning without executing any model call.

.resolve(): returns the composed, SDK-agnostic prompt data:

const resolved = editDraft.resolve({
  input: { ... },
  provider: 'openai',
})
// → { system, prompt, schema, tools, settings }

preview(): measures the complete prospective request and reports whether it fits:

import { preview } from "@use-crux/core";

const result = await preview(editDraft, {
  input: { ... },
  model,
  inputBudget: { max: 2000 },
});

result.status       // "fits" | "over-limit" | "unknown"
result.inputTokens  // complete-request estimate, when available
result.adaptations  // redacted selected or prospective representations
result.diagnostics  // content-free reasons and remedies

Introspection properties

editDraft.id; // 'draft-edit'
editDraft.description; // string | undefined
editDraft.tags; // readonly string[]
editDraft.contexts; // the contexts tuple
editDraft.inputSchema; // merged Zod schema
editDraft.outputSchema; // Zod output schema | undefined
editDraft.hasOutput; // boolean
editDraft.config; // raw config object

Next steps

On this page