Skip to content

Pipeline Contract

Dusan Milicevic edited this page Apr 17, 2026 · 1 revision

Pipeline Contract

The AgentContract system is how OTAIP keeps an LLM on rails. Agents declare their input/output schemas and domain rules as data; the pipeline validator enforces those declarations as six gates around every execution.

Why contracts exist

An LLM driving a booking pipeline can:

  • Pass an invalid airport code to a search agent
  • Invent an offer ID that never existed
  • Try to ticket a booking before pricing has run
  • Claim confidence on a low-quality result
  • Drift off the original booking intent mid-conversation

Without a contract, each agent has to defend against all of these in its own code. With a contract, defense is declarative: Zod schemas, a validate() function, confidence thresholds, and an action type. The pipeline validator turns those into hard gates.

The AgentContract shape

interface AgentContract<TInputSchema, TOutputSchema> {
  agentId: string;
  inputSchema: TInputSchema;              // Zod
  outputSchema: TOutputSchema;            // Zod
  actionType: ActionType;
  confidenceThreshold: number;
  outputContract: string[];               // fields the next agent relies on
  validate(input, ctx): Promise<SemanticValidationResult>;
}

type ActionType =
  | 'query'                   // read-only
  | 'mutation_reversible'     // can be rolled back
  | 'mutation_irreversible';  // cannot be undone (ticketing, settlement)

The 6 gates

flowchart LR
    In[LLM tool_use] --> G1[1. Intent Lock]
    G1 --> G2[2. Schema In]
    G2 --> G3[3. Semantic In]
    G3 --> G4[4. Cross-Agent Consistency]
    G4 --> Exec[Agent.execute]
    Exec --> G5[5. Schema Out + Confidence]
    G5 --> G6[6. Action Classification]
    G6 --> Ev[EventStore append]
    Ev --> Out[tool_result]
    G1 -. reject .-> Fail[ContractViolation]
    G2 -. reject .-> Fail
    G3 -. reject .-> Fail
    G4 -. reject .-> Fail
    G5 -. reject .-> Fail
    G6 -. reject .-> Fail
Loading
# Gate Where What
1 Intent Lock before execute Session carries a locked intent (origin, destination, dates, cabin). Gate verifies the agent is relevant and the locked values haven't shifted.
2 Schema In before execute contract.inputSchema.safeParse(input). Rejects malformed data before domain logic sees it.
3 Semantic In before execute contract.validate(input, ctx) — domain checks (IATA code exists, departure is future, passenger count is positive). Reference agents supply ctx.
4 Cross-Agent Consistency before execute Input references prior agent outputs in the same session (e.g. offer ID must have been produced by a prior availability_search).
5 Schema Out + Confidence after execute Output passes outputSchema. output.confidence >= contract.confidenceThreshold. Floor enforced by action type (below).
6 Action Classification after execute Irreversible mutations require a session-level approval flag; otherwise the result is withheld.

Confidence floors

Confidence thresholds are enforced per action type. Contracts may raise the floor but not lower it.

Action type Floor
query 0.70
mutation_reversible 0.90
mutation_irreversible 0.95
Reference agents (Stage 0) 0.90 (additional)

Tool bridge

agentToTool() converts a contracted agent into an LLM tool without hand-written JSON schemas.

import { agentToTool, zodToJsonSchema } from '@otaip/core';
import { availabilitySearchContract } from '@otaip/agents-search';

const tool = agentToTool(agent, availabilitySearchContract);
// tool.name = 'availability_search' (from AGENT_TOOL_NAMES)
// tool.inputSchema = Zod schema

// For Anthropic:
const anthropicTool = {
  name: tool.name,
  description: tool.description,
  input_schema: zodToJsonSchema(tool.inputSchema),
};

The agent loop (@otaip/core/agent-loop) drives the Anthropic Messages API. Each tool_use block is routed through the pipeline validator via runAgent(session, agentId, input); the result becomes a tool_result block. Gate failures return a structured error that the model can see and recover from.

The 14 contracted agents

ID Tool name Action type
0.1 airport_code_resolver query
0.2 airline_code_mapper query
0.3 fare_basis_decoder query
1.1 availability_search query
2.1 fare_rule_agent query
2.4 offer_builder mutation_reversible
3.1 gds_ndc_router query
3.2 pnr_builder mutation_reversible
3.8 pnr_retrieval query
4.1 ticket_issuance mutation_irreversible
9.6 performance_audit query
9.7 routing_audit query
9.8 recommendation query
9.9 alert query

Adding a contract to an agent

Create a contract.ts next to the agent:

import { z } from 'zod';
import type { AgentContract, SemanticValidationResult, ValidationContext } from '@otaip/core';

const inputSchema = z.object({
  origin: z.string().length(3),
  destination: z.string().length(3),
  // ...
});

const outputSchema = z.object({
  taxes: z.array(/* ... */),
  total: z.string(),
  currency: z.string().length(3),
  confidence: z.number().min(0).max(1),
});

export const taxCalculationContract: AgentContract<typeof inputSchema, typeof outputSchema> = {
  agentId: '2.3',
  inputSchema,
  outputSchema,
  actionType: 'query',
  confidenceThreshold: 0.8,
  outputContract: ['total', 'currency'],
  async validate(input, ctx): Promise<SemanticValidationResult> {
    const origin = await ctx.reference.resolveAirport(input.origin);
    if (!origin) {
      return { ok: false, reason: `Unknown origin airport: ${input.origin}`, field: 'origin' };
    }
    return { ok: true };
  },
};

Once registered, the validator runs all six gates automatically.

See also

  • Architecture — where the validator sits
  • Agents — the 75-agent inventory and which have contracts
  • Offers and Orders — AIDM 24.1 types referenced by pricing and booking contracts

Clone this wiki locally