Crux
GuidesSafety

Constraints

Retryable semantic assertions for generated output.

Constraints enforce semantic requirements after the model has produced an output candidate. Use a constraint when the output is structurally valid but needs a retry because it fails a business, quality, grounding, or style rule.

constraints.ts
import { boundary, constraint } from "@use-crux/core/safety";

export const citeSources = constraint({
  id: "cite-sources",
  on: boundary.output.text(),
  run: async (text) =>
    text.includes("[1]")
      ? { pass: true }
      : { pass: false, feedback: "Include at least one citation." },
});

Attach constraints globally with createSafetyPlugin(), on prompts and contexts, or per call with constraints: [...].

Authoring

Every constraint has a stable id, one output boundary, and a run(subject, ctx) callback. The boundary drives the subject type:

type Answer = {
  answer: string;
  citations: { title: string; url: string }[];
};

const grounded = constraint({
  id: "grounded-answer",
  on: boundary.output.object<Answer>(),
  maxRetries: 2,
  run: async (object) =>
    object.citations.length > 0
      ? { pass: true }
      : { pass: false, feedback: "Return at least one source citation." },
});

Use boundary.output.path<T>()('field.path') when one structured field owns the rule:

const titleLength = constraint({
  id: "title-length",
  on: boundary.output.path<Answer>()("answer"),
  run: async (answer) =>
    answer.length <= 280
      ? { pass: true }
      : { pass: false, feedback: "Keep the answer under 280 characters." },
});

Result shape:

return { pass: true, metadata: { score: 0.96 } };
return { pass: false, feedback: "Explain what must change." };

Malformed results fail closed with SafetyResultError, including JavaScript callers that bypass the TypeScript discriminated union.

Execution

Constraints run through the same per-call Safety session as guardrails. The session applies output guardrails first, so constraints judge the protected candidate, not raw model text. If any assert constraint fails, Crux combines the feedback, asks the model to regenerate, guards the new candidate, and checks constraints again.

Attempt 1 -> schema validates -> output guardrails -> constraints fail
Retry feedback -> regenerate -> output guardrails -> constraints pass
Return guarded final output

All constraints in a round run concurrently. The model sees all failure feedback at once instead of one retry per constraint.

Severity And Retry Budget

OptionDefaultMeaning
severity: 'assert'yesFailure triggers retry and eventually ConstraintViolationError.
severity: 'suggest'noFailure is recorded, but the final output can still return.
maxRetries2Per-constraint retry budget.
constraintMaxRetriesunsetShared per-call cap across all constraints.
const formalTone = constraint({
  id: "formal-tone",
  on: boundary.output.text(),
  severity: "suggest",
  run: async (text) =>
    text.includes("gonna")
      ? { pass: false, feedback: "Use formal wording." }
      : { pass: true },
});

await adapter.generate(prompt, {
  input,
  constraints: [citeSources, formalTone],
  constraintMaxRetries: 3,
});

Built-in Strategies

Use constraint.judge(...) to turn a scoring judge into a retryable Safety constraint:

import { judge } from "@use-crux/core/scoring";
import { boundary, constraint } from "@use-crux/core/safety";

const brandJudge = judge({
  id: "brand-voice",
  criteria: "Does the answer match our direct, warm brand voice?",
  scale: { min: 1, max: 10 },
  generate: generateObjectFn,
  model,
});

export const brandVoice = constraint({
  id: "brand-voice",
  on: boundary.output.text(),
  run: constraint.judge({ judge: brandJudge, minScore: 7 }),
});

Use constraint.citations(...) when the output must cite retrieved or injected sources. Both helpers carry safe strategy metadata for observability, Devtools, and Project Index.

Completed operations

transcribe() accepts constraints only at boundary.output.text(). They run once against the guarded top-level transcript. An assert failure throws; a suggest failure is retained in result.safety. The provider is not re-called and constraintMaxRetries is intentionally absent from transcription options.

generateImage() and generateSpeech() do not expose constraints because their required outputs have no constraint boundary. A typed image prompt that resolves any constraints fails before provider I/O; remove those constraints or move the semantic requirement to an ordinary language generation.

Transcription constraints inspect only the guarded top-level transcript text, never timed details. An enforced rewrite clears segments and words in the returned canonical result so callers cannot mistake stale timing for guarded text. See the full content-primitive matrix.

Report Mode And Tuning

Constraints do not have a mode field on their definition. Use per-call safety.tune when you want to shadow a constraint:

await adapter.generate(prompt, {
  input,
  safety: {
    tune: {
      "brand-voice": { mode: "report" },
    },
  },
});

Report-mode constraints run and audit decisions but do not retry, block, or change output. Unknown tune ids throw.

Streaming

Live streams cannot regenerate earlier text, so constraints run in report mode at end-of-stream. If a constraint must enforce before text is visible, write a guardrail for that streaming boundary instead.

Object and path constraints on streams are final-output checks. Incremental path constraints over partially parsed object streams are tracked as a post-beta roadmap item.

Errors And Audit

ConstraintViolationError is policy-terminal. It carries the failing constraints, total attempts, last guarded output, and safe Safety decision metadata. Raw failed output is not exposed by default.

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

try {
  await adapter.generate(prompt, { input, constraints: [citeSources] });
} catch (error) {
  if (error instanceof ConstraintViolationError) {
    error.failedConstraints;
    error.decisions[0]?.captured.preview;
  }
}

Use evaluateConstraint() for focused unit tests. In Evals, prefer decision-report assertions when the contract you care about is the runtime Safety decision:

ctx.expect.decisionReport.safety.toHaveOutcome("cite-sources", "retry");

On this page