Type System
Input merging, conditional return types, and generation settings.
Input merging
When a prompt uses contexts with input schemas, all fields are merged into a single type:
import { , } from '@crux/core'
import { } from 'zod'
const = ({
: .({ : .().() }),
: ({ }) => '...',
})
const = ({
: [],
: .({ : .() }),
})
type Input = .<typeof .>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 '@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 providedThe 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 typeThe 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 contributionConditional 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 // stringGeneration settings
GenerationSettings provides SDK-agnostic fields (temperature, maxTokens, topP, topK, stopSequences, frequencyPenalty, presencePenalty) plus an index signature for pass-through. Each adapter maps these to its SDK's naming conventions.
Resolution & inspection
Every prompt has .resolve() and .inspect() methods that work without executing any model call.
.resolve() — returns the composed, SDK-agnostic prompt data:
const resolved = editDraft.resolve({
input: { ... },
provider: 'openai',
tokenBudget: 4000,
})
// → { system, prompt, schema, tools, settings }.inspect() — shows how the system message was assembled:
const debug = editDraft.inspect({ input: { ... }, tokenBudget: 2000 })
debug.system.total // the full assembled system text
debug.system.parts // InspectPart[] — source, text, tokens, skipped
debug.system.totalTokens // total system tokens
debug.prompt // { text, tokens } | undefined
debug.totalTokens // system + prompt tokens
debug.droppedContexts // DroppedContext[] — dropped by token budget
debug.excludedContexts // ExcludedContext[] — excluded by when/match conditions
debug.tools // string[] — tool names from active contexts + configIntrospection 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