-
Notifications
You must be signed in to change notification settings - Fork 13
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.
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.
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)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
| # | 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 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) |
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.
| 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 |
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.
- 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