ChanlChanl
Agent Architecture

Agent Behavioral Contracts: Stop Drift Before It Hits Callers

Behavioral drift happens with no prompt change and no failing test. Agent behavioral contracts enforce rules at runtime and flag drift before callers do.

DGDean GroverCo-founderFollow
July 7, 2026
13 min read
A Contract Document Overlaid on an AI Agent Decision Loop Diagram

Your agent passed all the evals last week. By Friday it was calling regular customers "buddy" and agreeing to refunds that aren't in policy. Nothing changed. Not the system prompt, not the model, not the test suite. Everything still passes.

This is behavioral drift, and it's the failure mode that existing evaluation pipelines are worst at catching. Drift doesn't produce errors. It produces behavior that's subtly, gradually wrong, and only becomes visible in production, when callers experience it directly.

Agent behavioral contracts are a way to catch it earlier. Or prevent it from happening at all.

What Is Behavioral Drift, and Why Do Standard Evals Miss It?

Behavioral drift happens when an agent's actions gradually diverge from its intended specification during a multi-turn conversation. A customer service agent starts formal and professional. Three turns in, the caller uses casual language, and the agent, wired to be personable, matches that register. By turn six, the agent is using shorthand it wouldn't use at turn one. By turn twelve, it's agreeing to things it should have declined, because the caller asked four times.

Standard evals miss this because they test individual turns in isolation. You pass in a context and check the output. Each individual turn might look fine. The pattern across turns is not.

A February 2026 paper on arXiv, "Agent Behavioral Contracts: Formal Specification and Runtime Enforcement for Reliable Autonomous AI Agents," evaluated 200 scenarios across 7 models from 6 different vendors. Behavioral drift appeared in all of them, including top-tier models. The failure patterns were consistent across vendors: accommodation under conversational pressure, gradual tone shift, and progressive compliance with requests the agent initially declined.

The paper's core insight is that you need a layer that's external to the agent and enforces constraints independently of the agent's own judgment. That's the behavioral contract.

This is the broader pattern behind agent drift and silent degradation: agents that change in production without anyone noticing.

The Design-by-Contract Approach Applied to Agents

Agent behavioral contracts give you four enforcement mechanisms that sit outside the agent itself: preconditions, invariants, governance policies, and recovery behaviors. Each covers a different failure mode. Together they form a runtime layer that the agent can't reason its way around.

The pattern comes from Design-by-Contract (DbC), a software engineering concept from Bertrand Meyer's work in the 1980s. A software component specifies what it requires before executing (preconditions), what it guarantees after executing (postconditions), and what stays true throughout its lifetime (invariants). Violations are caught at runtime, not in logs after the fact.

Applied to AI agents, the contract has four parts.

Preconditions define what must be true before the agent takes a specific action. "Identity must be verified before accessing account data." "The caller must have confirmed the order number before a return is initiated." Preconditions gate actions. If they fail, the action doesn't execute.

Invariants define what must stay true throughout every conversation turn. "The agent's formality score must stay above 0.65." "The agent must never state a refund is guaranteed without checking eligibility first." "The agent may not name competitors directly." Invariants are checked on every response.

Governance policies define what the agent is structurally allowed to do, independent of what the LLM decides to do. Which tools it can call. Which escalation paths exist. How many tool calls it can make per turn. The governance layer enforces these independently of the agent's reasoning.

Recovery behaviors define what happens when a violation is detected. A high-severity failure blocks the action and triggers escalation. A low-severity invariant violation logs the event and continues with a correction. Recovery is deterministic. You don't ask the LLM what to do about its own violation.

Writing a Contract in Code

The pattern is small enough to build yourself. A contract is plain data: the four parts above, each an entry the enforcement layer reads. You can load them from YAML, a database, or a plain config file. The paper that introduced this ships a Python reference engine if you'd rather adopt one than hand-roll it, but the mechanism is the point, so the version below is self-contained TypeScript you can read top to bottom. Here's a customer service contract as a plain object:

contracts/support-agent.ts·typescript
type Severity = "high" | "medium" | "low";
 
export const supportAgentContract = {
  name: "support-agent-v2",
  version: "1.0.0",
 
  preconditions: [
    {
      id: "identity-verified",
      check: (ctx: ConversationContext) =>
        ctx.session.get("identityVerified") === true,
      appliesTo: ["get_account_details", "initiate_refund", "update_contact"],
      failAction: "request_verification",
      severity: "high" as Severity,
    },
  ],
 
  invariants: [
    {
      id: "no-unverified-refund-promise",
      check: (turn: Turn) => {
        const mentionsRefund = /refund|reimburse|credit back/i.test(
          turn.agentText
        );
        const refundEligible = turn.ctx.session.get("refundEligible") === true;
        return !mentionsRefund || refundEligible;
      },
      correction:
        "I can definitely look into that. To confirm eligibility I'll need to check your order status first.",
      severity: "high" as Severity,
    },
    {
      id: "formality-maintained",
      check: (turn: Turn) => turn.metrics.formalityScore > 0.6,
      severity: "low" as Severity,
    },
    {
      id: "no-competitor-mention",
      check: (turn: Turn) =>
        !/(competitor-brand-a|competitor-brand-b)/i.test(turn.agentText),
      correction:
        "I'm not able to comment on other services, but I'd be happy to find the right option for you here.",
      severity: "medium" as Severity,
    },
  ],
 
  governance: {
    allowedTools: [
      "get_account",
      "create_ticket",
      "check_refund_eligibility",
      "initiate_refund",
      "book_callback",
    ],
    maxToolCallsPerTurn: 3,
    escalationTriggers: [
      "legal_threat",
      "third_escalation_request",
      "anger_sustained",
    ],
  },
 
  recovery: {
    onHighSeverityViolation: "escalate_to_human",
    onMediumSeverityViolation: "block_and_correct",
    onLowSeverityViolation: "log_and_continue",
  },
};

The contract is defined outside the agent. It never touches the system prompt, and the enforcement layer below is the only thing that reads it.

How Does Contract Enforcement Actually Work?

The enforcement layer sits in the request/response cycle. Before each agent response goes to the caller, it runs the invariant checks. Before each tool call executes, it validates the preconditions for that tool.

enforcement.ts·typescript
import { supportAgentContract } from "./contracts/support-agent";
 
// Run every invariant against the turn. A high- or medium-severity failure
// blocks the response and substitutes that rule's correction; low-severity
// failures are collected so they can be logged without stopping delivery.
function checkInvariants(turn: Turn) {
  const violations = supportAgentContract.invariants.filter(
    (inv) => !inv.check(turn)
  );
  const blocking = violations.find((v) => v.severity !== "low");
  return {
    violations,
    blocked: Boolean(blocking),
    correction: blocking?.correction,
  };
}
 
async function generateAgentResponse(
  userMessage: string,
  ctx: ConversationContext
): Promise<string> {
  const rawResponse = await llm.generate(userMessage, ctx);
 
  const result = checkInvariants({
    agentText: rawResponse,
    ctx,
    metrics: await computeMetrics(rawResponse),
  });
 
  if (result.blocked && result.correction) {
    // Return the contract's correction instead of the raw response
    await logger.logViolation(result.violations[0]);
    return result.correction;
  }
 
  if (result.violations.length > 0) {
    // Log low-severity violations and continue
    await logger.logViolations(result.violations);
  }
 
  return rawResponse;
}

Tool call enforcement works the same way, intercepting before execution:

tool-enforcement.ts·typescript
import { supportAgentContract } from "./contracts/support-agent";
 
// A tool runs only if every precondition that applies to it passes.
function checkPreconditions(toolName: string, ctx: ConversationContext) {
  const applicable = supportAgentContract.preconditions.filter((pre) =>
    pre.appliesTo.includes(toolName)
  );
  for (const pre of applicable) {
    if (!pre.check(ctx)) {
      return { pass: false, failAction: pre.failAction, ruleId: pre.id };
    }
  }
  return { pass: true as const };
}
 
async function executeToolCall(
  toolName: string,
  args: Record<string, unknown>,
  ctx: ConversationContext
): Promise<ToolResult> {
  const check = checkPreconditions(toolName, ctx);
 
  if (!check.pass) {
    return {
      error: true,
      action: check.failAction,
      message: `Precondition ${check.ruleId} failed for ${toolName}`,
    };
  }
 
  return await tools[toolName](args, ctx);
}

The agent doesn't know the contract exists. Its outputs and tool calls are intercepted externally. This matters because you don't want the agent to reason about the contract. You want the contract enforced independently of the agent's judgment about what's appropriate.

Drift Detection as a Monitoring Signal

Every invariant violation gets logged. That means you accumulate a continuous signal: how often is each contract rule being triggered? A well-calibrated agent violates rarely, so a rising violation rate tells you something changed before you'd otherwise know.

Pass HIGH severity fail LOW severity fail Yes No Agent Response Generated Invariant Check Deliver to Caller Block and Correct Log Violation Correction or Escalation Sent Violation Rate Counter Rate > Threshold? Alert: Possible Model Drift Continue Monitoring Review Prompts or Model Version
Contract violation rate as a behavioral drift indicator

The concrete tell is a single rule that suddenly fires more often. If the formality invariant hits 3% of turns this week versus 0.8% last week, that's worth investigating before any caller notices. It's the one advantage this signal has over post-hoc scoring: it's continuous. You're measuring every production conversation, not a weekly sample, and catching drift while it's happening.

Chanl's monitoring dashboard tracks this signal across your agent fleet. You set alerts on violation rate thresholds and get notified when an agent starts drifting, before the drift produces complaints.

Testing Contracts Before You Deploy

Writing a contract is only useful if you've tested it. Scenario testing can run your agent through conversation paths specifically designed to trigger drift: repeated requests (callers who ask four times for something the agent should decline), tone escalation (callers who start formal and shift casual), and social pressure ("you helped the last caller with exactly this").

For each scenario, you verify that invariants hold throughout, preconditions gate the right actions, and recovery behaviors fire when expected. This is the equivalent of integration testing for the enforcement layer.

Scorecards can also evaluate contract adherence as a quality dimension across production calls. If your scorecard includes "no refund promise without eligibility check," that criterion runs on every conversation automatically. You see cases where enforcement prevented an issue and cases where something slipped through that your contract rules didn't cover.

Multi-Agent Contracts

In multi-agent pipelines, each agent gets its own contract. The orchestrator contract defines which worker agents it can invoke and what actions each can take. Worker agents have their own invariants, enforced locally.

The key addition for multi-agent systems is composition checks: before an orchestrator passes a worker's output to the next stage, it verifies that the output satisfies the downstream component's preconditions. A worker agent's poorly formatted or policy-violating output doesn't propagate through the pipeline.

Writing the specification before you write the code, as covered in agent specification before you write the prompt, gives you a natural starting point for contract invariants. The specification and the contract should describe the same agent. If they don't match, you've found a gap.

Common Invariants for CX Agents

If you're not sure where to start, these invariants cover the most common failure modes in customer service agents:

contracts/cx-invariants.ts·typescript
// The 6 invariants that appear in most CX agent contracts.
// Severity is the "high" | "medium" | "low" type from the contract above;
// the check helpers (hasPiiExposure, etc.) are yours to implement.
 
const cxInvariants = [
  {
    id: "no-commitment-without-verification",
    description: "Never commit to a refund, replacement, or credit without eligibility check",
    check: (turn: Turn) =>
      !hasFinancialCommitment(turn.agentText) || turn.ctx.eligibilityVerified,
    severity: "high" as Severity,
  },
  {
    id: "no-pii-exposure",
    description: "Never repeat back full account numbers, SSNs, or card numbers",
    check: (turn: Turn) => !hasPiiExposure(turn.agentText),
    severity: "high" as Severity,
  },
  {
    id: "escalation-path-present",
    description: "Always offer a human option when the caller asks three times",
    check: (turn: Turn) =>
      turn.ctx.escalationRequests < 3 ||
      mentionsEscalationOption(turn.agentText),
    severity: "medium" as Severity,
  },
  {
    id: "scope-maintained",
    description: "Do not attempt to resolve issues outside your tool allowlist",
    check: (turn: Turn) => !mentionsOutOfScopeCapabilities(turn.agentText),
    severity: "medium" as Severity,
  },
  {
    id: "formality-floor",
    description: "Maintain professional register throughout the conversation",
    check: (turn: Turn) => turn.metrics.formalityScore > 0.55,
    severity: "low" as Severity,
  },
  {
    id: "no-fabrication",
    description: "Do not state policy terms that can't be verified in context",
    check: (turn: Turn) =>
      !hasFabricatedPolicy(turn.agentText, turn.ctx.knowledgeBase),
    severity: "high" as Severity,
  },
];

The hasFabricatedPolicy check is the hardest to implement. It requires a reference to your actual policy knowledge base and a semantic comparison against what the agent claims. Simpler heuristics catch the most common hallucination patterns without needing semantic similarity: flag phrases like "our policy guarantees" or "you're entitled to" against known policy coverage.

Where Do Behavioral Contracts Fit in Your Stack?

Contracts don't replace evals. They address a different part of the reliability problem.

LayerWhat it catchesWhen
Unit evalsSingle-turn qualityDuring development
Scenario testingMulti-turn flow correctnessPre-deployment
Behavioral contractsRuntime violations and driftDuring production
Post-hoc scoringOutcome quality and trendsAfter conversations

The goal is defense in depth. Each row in that table covers a gap the others leave open, so leaning on any single layer means you're blind somewhere in the stack.

The most common mistake is treating post-hoc scoring as the complete quality loop: reviewing calls, finding violations, updating the system prompt, and repeating. Behavioral contracts interrupt that loop by catching violations during the conversation, before callers experience them. That difference matters most for financial commitments, compliance statements, and any action that can't be undone after the fact.

The agent that called customers "buddy" and agreed to out-of-policy refunds wasn't broken. It was drifting, gradually, across conversations, in ways that no individual eval caught. Behavioral contracts don't fix the drift by re-prompting. They intercept it at runtime, log what's happening, and give you an early warning system before callers notice. If your agent makes consequential decisions, that interception layer pays back the first time it blocks an unauthorized action. The drift rate signal pays back every week, before you'd otherwise know something had changed.

Catch agent drift before it reaches callers

Chanl's monitoring tracks behavioral signals across every production conversation, with scorecard rules that surface contract-level quality issues without manual call review.

See monitoring features
DG

Co-founder

Building the platform for AI agents at Chanl — tools, testing, and observability for customer experience.

Learn Agentic AI

Weekly. Patterns for shipping agents that work — MCP, scorecards, regression tests, prompts, model comparisons.

500+ builders subscribed

Frequently Asked Questions