Trace contracts
Typed TraceContract expectations (Beta) and experimental matchers.
Support level: Beta
Typed trajectory expectations over local AgentInspect traces via defineTraceContract / evaluateTraceContract (agent-inspect/checks).
What is shipped
Contracts compile to deterministic check rules for common cases:
- run status / completion / max duration
- tool required / forbidden / allowed / maxCalls / order (
requiredTools/forbiddenToolsaliases) - selectable
requiredOrderMode(first-occurrence|happens-before|all-occurrences) - additive
tools.orderRuleswith per-rule occurrence modes - typed cross-kind
steps.orderRelations(TOOL ↔ LLM endpoints; additive in 6.31) - bounded
tools.argumentsJSON Pointer checks (exists|type|equals|oneOf) controlsdeclared-versus-enforced invariantsretry/ side-effect safety using explicit attempt identity (additiveretry.operations[]recovery oracles in 6.27)alternatives.anyOffor one level of legitimate alternate paths- actor
scopeselectors (runId,subAgentId,groupId,workflowStep,rootEventId) - observation
requireProvenance(structural method / evidence / same-run event references) lintTraceContract/explainTraceContractfor brittle-contract diagnostics- LLM maxCalls / maxTotalTokens / allowedModels
- evidence-bearing findings on failures
- evaluation over logical TraceFacts (raw events remain available)
tools.requiredOrder semantics
requiredOrder / orderRules match TOOL events only (via canonical tool names). LLM / LOGIC / other kinds with the same display name do not satisfy the order and are not relabeled as tools. When a required name exists only under another kind, tool.usage reports that kind in the finding message.
requiredOrder is expanded into adjacent pair ordering rules with unique ids:
[A, B, C]
→ contract.tool.order.0: A before B
→ contract.tool.order.1: B before CrequiredOrderMode selects one ordering relation for every generated pair:
- unlisted intermediate tools are allowed;
- TraceContract
requiredOrderimplies presence — every listed name is added to the effective required-tool set; first-occurrence(default when omitted) compares first occurrences in start/encounter order; later repetitions do not invalidate an earlier valid order, and interval overlap emits a non-failingtool.order.overlapwarning;happens-beforerequires the firstbeforeoccurrence to finish before the firstafteroccurrence starts;all-occurrencesrequires everybeforeoccurrence to finish before everyafteroccurrence starts (max(before.end) <= min(after.start));- causal modes fail when a required interval boundary cannot be resolved instead of falling back to encounter order.
Examples for requiredOrder: ["retrieve_policy", "send_email"] (both TOOL-typed):
| Trajectory | Result |
|---|---|
retrieve_policy → send_email | PASS |
retrieve_policy → rerank_docs → send_email | PASS |
retrieve_policy → send_email → retrieve_policy | PASS under omitted / first-occurrence; FAIL under all-occurrences |
send_email → retrieve_policy | FAIL (order) |
cache_lookup → send_email | FAIL (missing retrieve_policy via implied presence) |
LLM named generate present, no TOOL generate | FAIL presence with non-TOOL kind diagnostic; not treated as a tool |
Low-level createToolOrderingRule({ before, after }) alone may still pass when an endpoint is missing (compositional). TraceContract requiredOrder does not.
For overlapping first calls, omitted / first-occurrence warns while happens-before fails.
Immediate or positional all-pairs matching is not implemented.
steps.orderRelations (shipped — experimental, 6.31)
Typed before/after relations with explicit kind: "TOOL" | "LLM" endpoints. Use this when a TOOL must precede an LLM (or vice versa). It does not change TOOL-only tools.requiredOrder / orderRules semantics.
defineTraceContract({
steps: {
orderRelations: [
{
before: { kind: "TOOL", name: "retrieve_policy" },
after: { kind: "LLM", name: "generate_answer" },
// mode?: "first-occurrence" | "happens-before" | "all-occurrences"
// requireEndpoints?: boolean // default true
},
],
},
});- Default
modeisfirst-occurrence(same three modes as tool order). - Default
requireEndpoints: true— missing kind+name fails; a same display name under the other kind does not satisfy the endpoint. - LLM names match finished LLM events after stripping common prefixes (
llm:,generation:, …). - Low-level
createStepOrderingRuleis compositional whenrequireEndpointsis omitted/false.
Experimental Vitest / Jest matchers (shipped)
| Package | Export | Matchers |
|---|---|---|
@agent-inspect/vitest | agentInspectVitestMatchers | toPassTraceContract, toHaveRequiredTool |
@agent-inspect/jest | agentInspectJestMatchers | toPassTraceContract, toHaveRequiredTool |
These are Experimental — API names may evolve. There is no expectTrace(...).toSatisfyTraceContract helper.
See API.md, TRACE-FACTS.md, and packages/core/src/checks/contract.ts.
Rule kinds
TraceContract rules fall into distinct categories. Mixing them incorrectly is a common source of false failures (see GitHub #308 and #309).
tools.required (shipped)
Unconditional path invariant: every named tool must appear at least once in the trace.
- Use when the tool is always part of a valid execution path.
- Do not use for steps that legitimate shortcuts may skip (for example cache hits that bypass
retrieve). - Prefer
alternatives.anyOforobservations.requiredwhen a shortcut is valid.
tools.requiredOrder (shipped — selectable ordering modes)
The evaluator expands each list into adjacent pairs and applies one requiredOrderMode to every pair.
- TraceContract
requiredOrderimplies presence of every listed tool (unioned intotools.required). requiredOrderMode: "first-occurrence"is the default first-occurrence start/encounter relation; overlapping intervals emit a non-failing warning.requiredOrderMode: "happens-before"requires the first before to end before the first after starts; overlap fails.requiredOrderMode: "all-occurrences"requires every before to end before every after starts; any cross-boundary overlap or later before fails.- Missing interval boundaries fail closed in the two causal modes.
alternatives.anyOf (shipped)
One level of named deterministic branches. Base rules always apply. At least one complete branch must pass.
defineTraceContract({
run: { requireCompleted: true },
tools: { required: ["generate"] },
alternatives: {
anyOf: [
{
id: "cache-hit",
contract: {
tools: { required: ["cache_lookup"], forbidden: ["retrieve"] },
observations: { required: ["cache-hit-valid"] },
},
},
{
id: "retrieve",
contract: {
tools: { required: ["retrieve_policy"], requiredOrder: ["retrieve_policy", "send_email"] },
observations: { required: ["retrieval-context-valid"] },
},
},
],
},
});Constraints:
- unique branch ids
- no nested
alternatives - no predicates / runtime DSL
- unused failed branches do not fail the contract when another branch passes
- if none pass →
contract.alternatives.none-satisfied
observations.required (shipped)
Requires externally observed or effect evidence (for example HTTP status, file write, cache key) rather than a specific tool call. Prefer this when the invariant is about outcome rather than which tool ran.
scope (shipped — experimental)
Select one actor before evaluation using explicit metadata only:
defineTraceContract({
scope: { subAgentId: "verifier-agent" },
tools: { required: ["run_tests"] },
});Supported selectors: runId, subAgentId, groupId, workflowStep, rootEventId (subtree projection).
- zero matches → error (no whole-session fallback)
- singular selector matching multiple runs → error
- no timestamp, prose, or display-name inference
- successful selection reports the actor and evidence event count
observations.requireProvenance (shipped — experimental)
Structural provenance for named outcomes. These checks prove method/evidence linkage was recorded; they do not prove the claim is semantically true, authorized, complete, or externally trusted.
defineTraceContract({
observations: {
required: ["refund-confirmed"],
requireProvenance: {
method: true,
evidence: true,
sameRunEventReference: true,
},
},
});Bounded evidence shapes: string event id, { eventId }, or { eventIds } (max 16). Method must be in the ObservedOutcomeMethod vocabulary. Omitting requireProvenance leaves prior observation behavior unchanged.
tools.arguments / tools.orderRules / controls / retry (shipped — experimental, 6.23; retry chronology corrected in 6.25.1; retry.operations in 6.27)
defineTraceContract({
tools: {
defaultOccurrenceMode: "first-occurrence",
orderRules: [
{ before: "authorize", after: "charge", occurrenceMode: "all-occurrences" },
],
arguments: [
{
tool: "charge",
occurrence: "all",
path: "/dryRun",
operator: "equals",
expected: true,
},
],
},
controls: {
declaredTools: ["search", "charge"],
enforcedTools: ["search", "charge"],
requireDeclaredMatchesEnforced: true,
requireObservedWithinEnforced: true,
requiredStages: [{ stage: "enforced" }],
},
retry: {
maxAttempts: 2,
nonIdempotentTools: ["charge"],
requireIdempotencyEvidenceForRetry: true,
requireRecoveredFailureVisible: true,
fallbackOnlyAfterFailure: true,
operations: [
{
tool: "retrieve_policy",
sideEffectClass: "read",
maxAttempts: 2,
retryableErrors: { codes: ["TRANSIENT"] },
requireFailureBeforeRetry: true,
requireSameArguments: "structured-or-digest",
requireTerminalSuccess: true,
requireRecoveredFailureVisible: true,
successfulResultDependency: {
consumerKind: "LLM",
requireExplicitReference: true,
},
},
],
},
});Retry classification (6.25.1): a genuine retry is detected from explicit identity preference — attemptNumber > 1, valid retryOf (target exists and precedes), distinct later attemptId under the same operationId, or a later finished attempt in an explicitly grouped operation — not only from a prior ok. error → success without idempotencyKey / noSideEffect evidence fails when requireIdempotencyEvidenceForRetry is set. fallbackOnlyAfterFailure and requireRecoveredFailureVisible require chronological earlier failure in the related chain. A client idempotencyKey is evidence of intent, not proof of exactly-once mutation. AgentInspect evaluates traces; it does not execute retries.
Bounded recovery operations (6.27 / 6.29.5): retry.operations[] adds per-tool oracles for safe read recovery first (retrieve_policy recipe). Same-arguments checks accept structured payloads or matching digests and fail closed when both are missing. Write sideEffectClass treats timeout/unknown/running completion as unevaluable → fail; a client idempotencyKey or producer noSideEffect/sideEffect:false flag does not clear that finding. Write-retry is not safe by default.
Missing structured argument evidence fails closed (AI_CHECK_TOOL_ARGUMENT_EVIDENCE_UNAVAILABLE). Findings never include full actual inputs.
Manual instrumentation stores caller metadata under attributes.metadata. Tool-argument checks therefore also accept structured object/array evidence at:
attributes.metadata.arguments
attributes.metadata.input
attributes.metadata.toolArgumentsPrecedence: top-level attributes.arguments|input|toolArguments, then nested metadata keys, then structured inputSummary. Preview strings are never parsed as JSON. This does not enable default raw argument capture.
Capture capability matrix (tool-argument evidence)
| Source | Structured input | Preview only | Digest only | Unavailable |
|---|---|---|---|---|
Manual attributes.arguments / attributes.input (object) | yes | — | — | — |
Manual attributes.metadata.arguments / input / toolArguments (object/array) | yes | — | — | — |
Manual inputSummary string | — | yes | — | for pointer checks |
| AI SDK / LangChain metadata-only default | — | sometimes | — | typical |
| OpenAI Agents metadata-only | — | sometimes | — | typical |
| MCP / OTLP / OpenInference import | varies | varies | optional digest | when unmapped |
| Custom TraceReader | reader-defined | reader-defined | reader-defined | fail closed |
Do not advertise a structured argument rule when the selected capture mode cannot supply object evidence.
Lint and explain (shipped)
import { lintTraceContract, explainTraceContract } from "agent-inspect/checks";
lintTraceContract(contract); // brittle / invalid shape diagnostics
explainTraceContract(contract); // human-readable intent linesCLI relationship
npx agent-inspect check <run-id> --dir .agent-inspectSuites and gates can consume check results; see SUITES-COHORTS-GATES.md.
Limitations
- Experimental/Beta API — may evolve in minors
- Contract tests are smoke-level; prefer check-engine tests for deep rule coverage
- Always review findings before treating a green check as product proof
- No nested alternatives, all-pairs matching, or general temporal DSL