1. JSON Mode vs. Function Calling: Two Different Contract Enforcement Strategies

Choosing between JSON mode and function calling isn't a preference — it's a decision about where your schema enforcement lives and what breaks when the model drifts.

  • JSON mode is optimistic — it tells the model to produce valid JSON but doesn't constrain the shape, so your downstream parser is the first line of defense against schema drift.
  • Function calling is a structural contract — the model must conform to a declared schema before the response leaves the inference layer, which means validation failures are explicit and early rather than silent and late.
  • The failure modes diverge sharply: JSON mode failures show up as parse errors or missing fields at consumption time; function calling failures surface as refusals or malformed tool invocations that are easier to observe and route around.
TECHNICAL DEEP DIVE & CODE ARTIFACTS Expand [+]

When you're integrating a new agent into an existing async processor — say, a Node.js Lambda reading from SQS — the difference between these two approaches determines whether your dead-letter queue fills up with malformed payloads or whether you get a clean validation error at the agent boundary that your supervisor can handle. JSON mode asks the model to be cooperative; function calling asks the runtime to be the enforcer. For production systems where downstream components have their own typed contracts (a Java REST API expecting a specific DTO, a PostgreSQL insert expecting a specific column set), function calling is the right default because it externalizes the contract definition and makes it version-controllable alongside your service interfaces.

AIThe practical implication for multi-agent systems is that each agent's output should be treated like an API response — with a schema, a version, and a documented set of nullable fields. When you define a tool schema for function calling, you're essentially writing an OpenAPI spec for a single operation. That spec becomes the handshake between the producing agent and the consuming component, and it can be validated, mocked, and evolved with backward-compatibility rules — the same discipline you'd apply to any service boundary.

Reference Architecture
// TypeScript: defining a function-calling contract as a versioned schema
// This is the agent's output contract — treat it like an API DTO

import { z } from 'zod';

// v1 contract — explicit, versionable, shareable across services
export const ResearchQueryResultV1 = z.object({
  schema_version: z.literal('v1'),
  query_id: z.string().uuid(),
  answer: z.string().min(1),
  confidence: z.enum(['high', 'medium', 'low']),
  source_document_ids: z.array(z.string()).min(1),
  requires_human_review: z.boolean(),
  citations: z.array(z.object({
    doc_id: z.string(),
    page: z.number().int().positive().optional(),
    excerpt: z.string().optional()
  }))
});

export type ResearchQueryResult = z.infer<typeof ResearchQueryResultV1>;

// The function spec you pass to the LLM — generated from the same schema
// so contract drift between spec and consumer is impossible
export const researchQueryTool = {
  name: 'submit_research_result',
  description: 'Submit a structured answer to a pharmaceutical research query',
  parameters: zodToJsonSchema(ResearchQueryResultV1) // use zod-to-json-schema
};
State Interaction Chart
flowchart TD A[Agent LLM Call] --> B{Output Strategy} B -->|JSON Mode| C[Raw JSON String] B -->|Function Calling| D[Schema-Validated Tool Call] C --> E[Consumer Parser] D --> F[Runtime Schema Enforcer] E -->|Parse Failure| G[DLQ / Error Handler] E -->|Field Missing| H[Silent Bad State] F -->|Schema Mismatch| I[Explicit Refusal — Observable] F -->|Schema Match| J[Typed Output to Consumer] H --> K[Downstream Corruption] I --> L[Supervisor Recovery Path]

2. Evolving Agent Pipelines Without Breaking Production: Lessons from the Bayer Research System

The Bayer pharmaceutical pipeline's evolution from keyword search to regulatory document drafting is a case study in how output contracts let you add agent capability without renegotiating every downstream integration.

  • Capability expansion breaks contracts when new agents produce richer outputs that existing consumers weren't built to handle — the contract must version before the capability ships.
  • Regulatory document drafting introduces a qualitatively different output type — not a query answer but a draft artifact — which means the agent pipeline needs a branching output contract, not a single schema.
  • Observable contract violations at the agent boundary are far cheaper to handle than silent schema drift discovered when a researcher gets a malformed regulatory submission.
TECHNICAL DEEP DIVE & CODE ARTIFACTS Expand [+]

The evolution described in the Bayer system — from search results to complex question answering to document drafting — represents exactly the kind of incremental capability expansion that breaks naive pipeline designs. Each new capability class produces a structurally different output: a ranked list of document excerpts, a synthesized answer with citations, a full draft with section headers and regulatory framing. If these all pour into the same untyped string field consumed by the same downstream component, you've traded short-term integration speed for long-term brittleness. The right design is a discriminated union at the contract level — a `result_type` field that routes to the appropriate schema branch — so existing consumers can ignore new result types they don't handle rather than choking on them. Source: [Building Reliable Agentic AI Systems] — Sarang Sanjay Kulkarni (https://martinfowler.com/articles/reliable-llm-bayer.html)

AIThis pattern maps directly to how you'd version an event schema in an async messaging system. When a new event type lands on the queue, well-designed consumers skip it rather than fail. Agent output contracts should work the same way: backward-compatible additions (new optional fields) are safe; shape changes (renaming required fields, changing types) require a version bump and a migration window. The difference from traditional service versioning is that the producer is an LLM, so you can't rely on compile-time enforcement — you need runtime validation with explicit fallback behavior baked into the supervisor.

Reference Architecture
// TypeScript: discriminated union contract for multi-capability agent output
// Each result_type maps to its own schema — consumers ignore types they don't handle

import { z } from 'zod';

const SearchResultsV1 = z.object({
  result_type: z.literal('search_results'),
  schema_version: z.literal('v1'),
  documents: z.array(z.object({
    doc_id: z.string(),
    relevance_score: z.number().min(0).max(1),
    excerpt: z.string()
  }))
});

const SynthesizedAnswerV1 = z.object({
  result_type: z.literal('synthesized_answer'),
  schema_version: z.literal('v1'),
  answer: z.string(),
  citations: z.array(z.string())
});

const RegulatoryDraftV2 = z.object({
  result_type: z.literal('regulatory_draft'),
  schema_version: z.literal('v2'),
  sections: z.array(z.object({
    heading: z.string(),
    body: z.string(),
    requires_review: z.boolean()
  })),
  draft_status: z.enum(['draft', 'pending_review', 'approved'])
});

export const AgentOutputContract = z.discriminatedUnion('result_type', [
  SearchResultsV1,
  SynthesizedAnswerV1,
  RegulatoryDraftV2
]);

export type AgentOutput = z.infer<typeof AgentOutputContract>;

// Consumer pattern: handle known types, skip unknown gracefully
function handleAgentOutput(raw: unknown) {
  const result = AgentOutputContract.safeParse(raw);
  if (!result.success) {
    // Route to supervisor, not to a generic error handler
    supervisorChannel.send({ type: 'contract_violation', errors: result.error.issues, raw });
    return;
  }
  switch (result.data.result_type) {
    case 'regulatory_draft': return regulatoryQueue.enqueue(result.data);
    case 'synthesized_answer': return citationFormatter.process(result.data);
    // search_results handled by a different consumer — this one skips it cleanly
  }
}
State Interaction Chart
flowchart TD A[Research Query Agent] --> B[Output Contract Router] B -->|result_type: search_results v1| C[Document Ranker Consumer] B -->|result_type: synthesized_answer v1| D[Citation Formatter Consumer] B -->|result_type: regulatory_draft v2| E[Document Drafting Consumer] B -->|unknown result_type| F[Supervisor — Unknown Type Handler] C --> G[Researcher UI] D --> G E --> H[Regulatory Review Queue] F --> I[Human Review] style F fill:#f90,color:#000 style I fill:#f90,color:#000

3. Clean Room Architecture as a Contract Enforcement Model for Agentic Data Access

Stagwell's clean room design — where computation runs inside the data owner's environment rather than data moving to the computation — is a blueprint for how agentic systems can enforce data access contracts without building a trust layer from scratch.

  • Data-to-computation inversion eliminates an entire class of contract violations: if the raw data never leaves its environment, downstream agents can't accidentally expose, cache, or leak it.
  • Pre-built app orchestration within the clean room means the agent's tool calls are bounded — it can only invoke operations that the data owner pre-approved, which is a hard capability contract rather than a policy suggestion.
  • Agentic targeting systems that follow this pattern get compliance almost for free because the contract enforcement is architectural, not procedural — no agent can violate data residency rules because the infrastructure makes it structurally impossible.
TECHNICAL DEEP DIVE & CODE ARTIFACTS Expand [+]

The Stagwell Agentic Targeting System (SATS) built on Databricks Clean Rooms demonstrates a pattern that's directly applicable to any multi-agent system handling sensitive data: instead of giving agents broad data access and then trying to govern what they do with it through prompt instructions or output filters, you invert the architecture. The agent's tools are pre-vetted operations that run inside a controlled environment. The contract isn't between the agent and a policy document — it's between the agent and the infrastructure itself. The agent literally cannot call a function that moves raw data, because no such function exists in its tool registry. Source: [How Stagwell built privacy-safe ID matching on Databricks] (https://www.databricks.com/blog/how-stagwell-built-privacy-safe-id-matching-databricks)

AIThis is a fundamentally stronger governance model than output validation alone. When you pair it with structured outputs — the agent's tool results flow back as typed schemas that downstream components can verify — you get defense in depth: the data access contract is enforced at the infrastructure layer, and the output contract is enforced at the schema layer. For Sam building on AWS, the analog is an agent whose tools are Lambda functions in a VPC with no internet egress and IAM policies scoped to specific DynamoDB tables or S3 prefixes — the tool registry IS the access policy, and function calling IS the enforcement mechanism.

Reference Architecture
// TypeScript: tool registry as access contract
// Only operations in this registry are callable — the set IS the policy

import { z } from 'zod';

// Each tool definition is the contract — shape, intent, and boundary in one place
const tools = {
  match_ids: {
    description: 'Match brand first-party IDs against identity spine within clean room',
    inputSchema: z.object({
      brand_segment_id: z.string().uuid(),
      match_threshold: z.number().min(0.7).max(1.0).default(0.85)
    }),
    outputSchema: z.object({
      matched_count: z.number().int().min(0),
      match_rate: z.number().min(0).max(1),
      segment_token: z.string() // opaque token — raw IDs never returned
    }),
    // No raw data access — result is always aggregated or tokenized
    handler: cleanRoomClient.matchIds
  },
  score_audiences: {
    description: 'Score a matched segment against audience attributes',
    inputSchema: z.object({
      segment_token: z.string(),
      audience_attributes: z.array(z.string()).max(10)
    }),
    outputSchema: z.object({
      scores: z.record(z.string(), z.number().min(0).max(1)),
      confidence: z.enum(['high', 'medium', 'low'])
    }),
    handler: cleanRoomClient.scoreAudiences
  }
  // export_matched_segment intentionally omitted — not in policy for this agent role
} as const;

type ToolName = keyof typeof tools;

// Agent can only call what's in the registry — no escape hatch
async function invokeAgentTool<T extends ToolName>(
  name: T,
  rawInput: unknown
): Promise<z.infer<typeof tools[T]['outputSchema']>> {
  const tool = tools[name];
  const input = tool.inputSchema.parse(rawInput); // fail fast on bad input
  const result = await tool.handler(input);
  return tool.outputSchema.parse(result); // fail fast on bad output
}
State Interaction Chart
flowchart TD A[Agentic Targeting System] --> B[Tool Registry — Pre-approved Operations Only] B --> C{Tool Call} C -->|match_ids| D[Clean Room Compute — Brand Env] C -->|score_audiences| D C -->|export_matched_segment| E[Approved Output Sink] D -->|Raw Data| F[Never Leaves Brand Environment] D -->|Aggregated Result| G[Typed Schema Response] G --> A H[Raw Identity Graph] -->|Stays In Place| D I[Brand First-Party Data] -->|Stays In Place| D style F fill:#e74c3c,color:#fff style D fill:#27ae60,color:#fff

4. Persistent Context as Agent Infrastructure: The CLAUDE.md Pattern

Treating your AI session context — architecture decisions, naming conventions, workflow preferences — as versioned infrastructure files rather than repeated prompt preamble is the difference between agents that accumulate knowledge and agents that start from zero every session.

  • Context as config files means your agent's operating constraints are version-controlled, reviewable, and deployable alongside your code — not buried in someone's clipboard history.
  • Onboarding framing matters — structuring a CLAUDE.md like you're onboarding a capable new hire (here's the system, here's what we value, here's what not to touch) produces more consistent behavior than prompt engineering individual interactions.
  • Feedback loop closure is where compounding happens: when an agent produces a useful pattern or decision, persisting it back into the context infrastructure means the next session starts smarter, not from scratch.
TECHNICAL DEEP DIVE & CODE ARTIFACTS Expand [+]

Eugene Yan's framing of directory structures and INDEX.md files as 'context infrastructure' deserves to be taken seriously as an engineering pattern, not just a productivity tip. If you're building multi-agent systems where a coding assistant is one of the agents, the quality of that agent's outputs is directly coupled to the richness of the context it receives. A well-structured CLAUDE.md that encodes your TypeScript conventions, your async processing patterns, your PostgreSQL schema naming rules, and your preferred error handling approach is functionally equivalent to the schema validation that enforces output contracts — it constrains the agent's behavior space toward the outputs you actually want. Source: [How to Work and Compound with AI] (https://eugeneyan.com//writing/working-with-ai/)

AIThis connects directly to the structured output problem: if JSON mode gives you valid JSON but doesn't constrain shape, then a rich context file is the upstream equivalent — it doesn't enforce output structure mechanically, but it shapes the generation toward structures your downstream systems can handle. For a senior engineer mentoring others on AI dev tooling, this framing is useful: context infrastructure is the contract between the engineer and the AI assistant, just as a Zod schema is the contract between agents in a pipeline. Both deserve the same maintenance discipline — keep them current, version them, and treat drift as a defect.

Reference Architecture
// Example CLAUDE.md structure — treat this like a config file, not a readme
// Keep it under version control alongside your service code

/*
CLAUDE.md — Platform Engineering Agent Context
Version: 1.4.0 | Last updated: 2026-06-19

## System Overview
Async processing platform: Node.js/TypeScript Lambdas reading from SQS.
REST APIs: Java Spring Boot, OpenAPI 3.1 specs as source of truth.
Database: PostgreSQL 15, schema managed via Flyway migrations.
Infrastructure: AWS CDK (TypeScript), cloud-agnostic patterns preferred.

## Output Contracts
- All agent outputs must conform to Zod schemas in /src/contracts/
- Schema versions follow semver — breaking changes require new version file
- Never use `any` or untyped `object` in contract definitions
- Discriminated unions preferred for multi-type outputs (see block_2 pattern)

## Naming Conventions
- SQS message types: SCREAMING_SNAKE_CASE (e.g., RESEARCH_QUERY_RECEIVED)
- Database tables: snake_case, plural (e.g., research_queries, agent_outputs)
- TypeScript interfaces for contracts: PascalCase + V{major} suffix (e.g., AgentOutputV2)

## Do Not Touch
- /src/legacy/keyword-search/ — deprecated, migration in progress, read-only
- Any Flyway migration already applied to prod — add new ones, never edit old

## Feedback Log (persist valuable patterns here)
- 2026-06-18: Discriminated union on result_type eliminated 3 ambiguous parser branches
- 2026-06-15: zodToJsonSchema() bridging Zod contracts to LLM function specs — use this pattern
*/

// The above lives in CLAUDE.md; the pattern it references lives in code:
export const contractRegistry = {
  'agent_output:v1': AgentOutputV1Schema,
  'agent_output:v2': AgentOutputV2Schema,
  'research_result:v1': ResearchQueryResultV1
} as const;
State Interaction Chart
flowchart TD A[New AI Session] --> B[Load Context Infrastructure] B --> C[CLAUDE.md — System Constraints] B --> D[INDEX.md — Codebase Map] B --> E[decisions/ — Persisted Architectural Choices] C & D & E --> F[Agent Context Window — Constrained Behavior Space] F --> G[AI-Assisted Work Session] G --> H{Valuable Output Produced?} H -->|Yes| I[Persist Pattern Back to Context Files] H -->|No| J[Discard] I --> B style B fill:#2980b9,color:#fff style I fill:#27ae60,color:#fff