Module 8: Project — Architect an AI Feature in Mercado
Place the component and its contract
Overview
The method starts where the guide started: placing the AI component. Before giving it a budget, an eval, or a guardrail, you have to answer three module 1 questions for Mercado's support agent —the capstone's feature—: how much non-determinism does it tolerate?, how does it split into core and shell?, and what's the contract of its output? From the answers comes the property sheet, the ten-field artifact lessons 3 through 7 will fill and lesson 8 will execute whole. This lesson does that step 1 in full, executed.
And there's an underlying decision made here that governs everything else: the support agent is the guide's intolerant feature. In module 1 you classified it with a tolerance of 3 out of 15 —it touches money directly, an error hurts the business a great deal, and its blast radius is the whole payments system and the customer's trust—. That low tolerance isn't a curious datum: it's what dictates that its shell be thick, that it need all the guide's mechanisms and not a light version. Placing the component well is what makes it so that in the coming lessons you won't ask "do I need an eval gate for this?, do I need to validate every proposal?" —the tolerance already answered yes—.
Connection with the module. This is the capstone's foundation lesson. Lesson 1 gave you the map and the teaser; this one lays the first stone: the property sheet that structures the whole project. Each field of the sheet points to a coming lesson: latency_budget/cost_budget → lesson 3, eval_gate → lesson 4, guardrail → lesson 5, fallback → lesson 6, deterministic_shell and feedback_loop → lesson 7. By the end of this lesson you'll have the feature placed, with its contract clear and its sheet issued —ready for the following layers to fill each field well—. And the boundary is respected from the first step: we place the component and define the contract of its output; we do not build the core that produces it (that's AI Engineering). This lesson's ai_component, as throughout the guide, is a stub that proposes.
An analogy: an employee's file before giving them a position
When a company is going to hire someone for a sensitive position —say, someone who will handle customer refunds— it doesn't sit them at the desk on the first day and give them access to the cash. First it opens a file: it defines their position (where do they fit in the org chart?, who do they report to?), their level of authority (can they approve alone, or does a supervisor review everything they propose?), their budget (how much can they move without authorization?), and the rules that frame them (what happens if they make a mistake?, who covers for them if they're out?). That file doesn't describe how the person does their job —they know that—; it describes where they fit and what contains them. And it's filled before giving them a single real responsibility, because the file is exactly what makes it safe to give them responsibilities.
The property sheet of an AI component is that file. It doesn't describe how the support agent generates its responses —that's AI Engineering, the "how the person does their job"—; it describes where it lives, how much authority it has (its tolerance, which dictates the thickness of its shell), its latency and cost budget, and the rules that frame it (its eval gate, its guardrails, its fallback). And like the file, it's filled before the component touches anything real, because it's what makes it safe to connect it to the refunds system. An architect who puts an AI feature in without its sheet is like a manager who gives the cash to an employee without a file: it might go well, but no one decided why it should go well. In this capstone, the sheet is the first thing you produce, and everything else fills it.
Worked example: place the support agent and issue its sheet
We're going to execute step 1 of the method in four parts: classify the agent's tolerance (to know the thickness of its shell), separate core and shell, demonstrate its probabilistic contract, and issue the sheet. All with the LLM simulated by a deterministic stub, literal output.
# M8 Lesson 2 — place the component and its contract.
# Capstone feature: Mercado's SUPPORT AGENT (the intolerant one).
# 1) Its tolerance to non-determinism (M1) -> THICK shell.
# 2) Core (proposes) vs shell (disposes).
# 3) The PROBABILISTIC contract (M1): the same message gives different proposals;
# the exact assert breaks; the property-based contract passes.
# All SIMULATED with deterministic stubs. Zero network, zero API, zero keys.
import random
from dataclasses import dataclass, asdict
_RNG = random.Random(8)
# ------------------------------------------------------------------
# Step 1 — Tolerance to non-determinism (M1).
# ------------------------------------------------------------------
touches_money_or_state, error_cost, blast_if_wrong = 5, 5, 5
nd_tolerance = 18 - (touches_money_or_state + error_cost + blast_if_wrong)
print("=== Step 1: non-determinism tolerance (M1) ===")
print(f" touches_money_or_state={touches_money_or_state} "
f"error_cost={error_cost} blast_if_wrong={blast_if_wrong}")
print(f" nd_tolerance = {nd_tolerance}/15 -> INTOLERANT feature, THICK shell")
print(" (touches money directly: every proposal must be validated against the policy)")
# ------------------------------------------------------------------
# Step 2 — Probabilistic core vs deterministic shell (M1/M6).
# ------------------------------------------------------------------
print()
print("=== Step 2: core (LLM) vs shell (deterministic) ===")
print(" core : read the ticket and PROPOSE an action (refund/reply)")
print(" shell : validate the schema, the policy, the trust boundary,")
print(" the budget, the eval-gate, the fallback and the feedback")
# ------------------------------------------------------------------
# Step 3 — The probabilistic contract (M1): proposals, not free text.
# ------------------------------------------------------------------
# The same ticket -> different STRUCTURED proposals (same meaning, different form).
# We don't assert the exact proposal; we assert its PROPERTIES.
_PROPOSALS = [
{"action": "refund", "order_id": "A-1001", "amount": 50.0},
{"action": "refund", "order_id": "A-1001", "amount": 50.0, "reason": "broken"},
{"action": "refund", "order_id": "A-1001", "amount": 50},
]
_RNG.shuffle(_PROPOSALS)
_i = 0
def ai_component(ticket):
# SIMULATES the core: same ticket -> different structured proposals.
global _i
p = _PROPOSALS[_i % len(_PROPOSALS)]
_i += 1
return p
KNOWN_ACTIONS = {"refund", "reply"}
outs = [ai_component("My order A-1001 arrived broken") for _ in range(3)]
print()
print("=== Step 3: the probabilistic contract (M1) ===")
for k, o in enumerate(outs, 1):
print(f" proposal {k}: {o}")
try:
assert outs[0] == outs[1]
print(" assert outs[0] == outs[1] -> PASSES")
except AssertionError:
print(" exact assert outs[0] == outs[1] -> BREAKS (non-determinism)")
def satisfies_contract(p):
# The contract is NOT exact equality; it's a set of properties.
if not isinstance(p, dict):
return False
if p.get("action") not in KNOWN_ACTIONS:
return False
if p["action"] == "refund":
return ("order_id" in p and isinstance(p.get("amount"), (int, float))
and p["amount"] > 0)
return isinstance(p.get("text"), str)
assert all(satisfies_contract(o) for o in outs)
print(" property-based contract (known action, order_id, amount>0) -> PASSES for all 3")
# ------------------------------------------------------------------
# Step 4 — The component's property sheet (M1, now complete).
# ------------------------------------------------------------------
@dataclass
class AIComponentSheet:
name: str
location: str
nd_tolerance: int
latency_budget_ms: int
cost_budget_usd_month: int
eval_gate: str
guardrail: str
fallback: str
deterministic_shell: str
feedback_loop: str
sheet = AIComponentSheet(
name="support_agent",
location="behind support-service; the LLM NEVER touches the refunds API",
nd_tolerance=nd_tolerance,
latency_budget_ms=4000,
cost_budget_usd_month=3000,
eval_gate="eval-set of tickets; score < 0.80 -> blocks the deploy",
guardrail="input: injection signal; output: schema + deterministic boundary",
fallback="model -> FAQ template -> escalate to human (never auto-approves)",
deterministic_shell="thick: validates every refund proposal against the policy",
feedback_loop="human agent thumbs/corrections feed the eval-set",
)
print()
print("=== Step 4: the component's property sheet (M1) ===")
for field, value in asdict(sheet).items():
if field == "name":
continue
print(f" {field:<22} {value}")
print()
print("Each field points to a module: budget->M2, eval->M3, guardrail->M4,")
print("fallback->M5, shell->M6, feedback->M7. The rest of the module FILLS the sheet.")
What to expect. When you run the file, the output is exactly this:
=== Step 1: non-determinism tolerance (M1) ===
touches_money_or_state=5 error_cost=5 blast_if_wrong=5
nd_tolerance = 3/15 -> INTOLERANT feature, THICK shell
(touches money directly: every proposal must be validated against the policy)
=== Step 2: core (LLM) vs shell (deterministic) ===
core : read the ticket and PROPOSE an action (refund/reply)
shell : validate the schema, the policy, the trust boundary,
the budget, the eval-gate, the fallback and the feedback
=== Step 3: the probabilistic contract (M1) ===
proposal 1: {'action': 'refund', 'order_id': 'A-1001', 'amount': 50}
proposal 2: {'action': 'refund', 'order_id': 'A-1001', 'amount': 50.0, 'reason': 'broken'}
proposal 3: {'action': 'refund', 'order_id': 'A-1001', 'amount': 50.0}
exact assert outs[0] == outs[1] -> BREAKS (non-determinism)
property-based contract (known action, order_id, amount>0) -> PASSES for all 3
=== Step 4: the component's property sheet (M1) ===
location behind support-service; the LLM NEVER touches the refunds API
nd_tolerance 3
latency_budget_ms 4000
cost_budget_usd_month 3000
eval_gate eval-set of tickets; score < 0.80 -> blocks the deploy
guardrail input: injection signal; output: schema + deterministic boundary
fallback model -> FAQ template -> escalate to human (never auto-approves)
deterministic_shell thick: validates every refund proposal against the policy
feedback_loop human agent thumbs/corrections feed the eval-set
Each field points to a module: budget->M2, eval->M3, guardrail->M4,
fallback->M5, shell->M6, feedback->M7. The rest of the module FILLS the sheet.
Let's walk through the output step by step, because each block is an architecture decision.
Step 1 — the tolerance, which dictates the thickness of the shell. The support agent scores 5 on the three axes at the same time: touches_money_or_state=5 (it executes refunds, touches money directly), error_cost=5 (a miscalculated refund hurts the business and the customer's trust a great deal), and blast_if_wrong=5 (its blast radius is the whole payments system). Tolerance = 18 − 15 = 3 out of 15: the intolerant feature, the one with the thick shell. And that figure is what governs the rest of the capstone: since the feature tolerates almost no non-determinism without containment, it will need all the mechanisms —not a light version—. The semantic search, with high tolerance, would settle for a thin shell; the refunds agent, no.
Step 2 — the core/shell partition. The core's responsibility is stated in one line: read the ticket and propose an action (refund or reply). That, and nothing more, is the only thing the LLM contributes. Everything else —validate the schema, the policy, the trust boundary, the budget, the eval gate, the fallback, and the feedback— is the shell, deterministic. Notice the asymmetry: the core has one responsibility; the shell has seven. That imbalance is on purpose and it's module 1's reactor rule: small, contained core, robust shell. The entire capstone is building that seven-layer shell around a single-responsibility core.
Step 3 — the probabilistic contract. The same ticket ("My order A-1001 arrived broken") enters the ai_component three times and three different proposals come out: one with amount: 50 (integer), another with an extra reason field, another with amount: 50.0 (float). All three propose the same thing —refund 50 of A-1001— but in different forms. The assert outs[0] == outs[1] —the normal-code reflex— breaks. And here there's an important nuance of the capstone versus module 1: the contract of a feature that proposes actions doesn't verify properties of a text, but of a structured proposal —that the action be known (refund or reply), that a refund carry order_id and a positive numeric amount—. That contract passes for all three proposals. You don't assert the exact proposal; you assert that it's a valid and dispatchable proposal. It's what lets the shell validate it in the following lessons.
Step 4 — the sheet, with its ten fields. The sheet summarizes the whole design and —crucial— each field points to the lesson that will fill it well. The location makes explicit the most important containment decision: the LLM lives behind support-service and never touches the refunds API directly. The nd_tolerance (3) justifies the thickness of everything else. And the other eight fields are promises lessons 3 through 7 will fulfill: the budget (M2), the eval gate (M3), the guardrails (M4), the fallback (M5), the deterministic shell (M6), the feedback loop (M7). Right now they're one-line statements; by the end of the module they'll be executed mechanisms.
Going deeper: why the sheet goes before any mechanism
The sheet is the architectural contract, not the implementation. It's tempting to skip the sheet and start directly assembling the eval gate or the guardrails —"what matters is the code, not the paper"—. But the sheet does a job no loose mechanism does: it decides which mechanisms the feature needs and why, before building any. Without the sheet, you assemble mechanisms out of habit (or forget them out of carelessness). With the sheet, each mechanism exists because a field asked for it, and each field exists because the tolerance justified it. It's the difference between "I gave it an eval gate just because" and "I gave it an eval gate because its error_cost is 5 and a change that degrades the quality can't reach production without being blocked".
The tolerance is the master variable. Of the ten fields, the nd_tolerance is the one that governs the others. A tolerance of 3 (support) demands the thickest shell: guardrails at input and output, a deterministic boundary that validates every proposal against the policy, a fallback that degrades to a human (never auto-approves), and a strict eval gate. A tolerance of 12 (like "describe your product") would allow lighter fields: a medium shell, a two-rule guardrail, a fallback to a template. The same method, sized to the risk. That's why step 1 goes first: it sets the scale of everything that follows. Designing the shell before measuring the tolerance is like buying the safe before knowing how much money you're going to keep.
The structured contract is what makes the proposal dispatchable. A detail step 3 let show and worth underscoring: the core doesn't propose free text that someone interprets, but a structured proposal —a named command (action) and typed fields (order_id, amount)—. This isn't a whim; it's what lets the shell validate it deterministically. A {"action": "refund", "order_id": "A-1001", "amount": 50.0} can be validated against the policy field by field; an "I think we should refund about 50 pesos to this customer" can't. The capstone's probabilistic contract, then, has two layers: the proposal must be structurally valid (schema, lesson 5) and policy-compliant (the shell, lesson 7). Step 3 establishes the first; the rest of the module builds the second.
Common mistakes
Issuing the sheet with blank fields "because I haven't designed them yet". What happens: the student fills location and nd_tolerance and leaves eval_gate, fallback, or feedback_loop empty, thinking "those are from lessons I haven't seen yet". Why it happens: "I haven't built it" is confused with "I can't state it". How to detect it: your sheet has empty fields. How to fix it: all ten fields carry a statement from step 1, even a one-line one. The sheet is a statement of intent —"this feature will have an eval gate with a 0.80 threshold"—, not the implementation. Lessons 3 through 7 turn each statement into an executed mechanism, but the statement exists from the start, because it's what tells you what you still need to build. A sheet with holes is an incomplete file: you don't know what authority the employee has.
Setting the tolerance "by eye" instead of with the three axes. What happens: the student declares "the support agent is risky, thick shell" without going through the three axes (touches_money_or_state, error_cost, blast_if_wrong). They reach the correct conclusion, but without the reasoning that sustains it. Why it happens: the conclusion seems obvious, so the calculation is skipped. How to detect it: you can't say why the tolerance is 3 and not 8. How to fix it: score the three axes explicitly. The value isn't what matters; what matters is that the number is justified, because when someone asks "why does this feature need so much ceremony?" the answer is "because it scores 5-5-5 on the three risk axes", not "because it seems risky to me". Module 1 insisted on this: measure the tolerance, don't opine it.
Testing the core's proposal with an exact assert. What happens: when validating step 3, the student writes assert ai_component(ticket) == {"action": "refund", "order_id": "A-1001", "amount": 50.0} and gets frustrated because it fails intermittently. Why it happens: it's the normal-code reflex, and module 1 warned against it —except here the output is a structured proposal, not a text, which makes it even more tempting (a dict seems like something you can assert)—. How to detect it: your core test fixes an exact expected proposal. How to fix it: verify properties of the proposal —known action, required fields, correct types, positive amount—, not its literal equality. satisfies_contract passes for any valid proposal (the three different formats) and fails only when the proposal really doesn't work. It's module 1's lesson 2, reaffirmed for structured proposals.
Exercises
Exercise 1 — Change the feature, recompute the tolerance. Take Mercado's semantic search (it interprets queries and returns products, doesn't touch money) and recompute its nd_tolerance with the three axes. Then say which fields of its sheet would be lighter than the support agent's, and which one would be almost identical.
See solution
The semantic search on the three axes: touches_money_or_state=1 (doesn't touch money or state; it reorders results), error_cost=2 (an irrelevant result annoys, but doesn't cost money or break trust), blast_if_wrong=2 (it affects one search, not half the platform). Tolerance = 18 − 5 = 13 out of 15: tolerant feature, thin shell.
Fields lighter than in the support agent:
deterministic_shell: thin, not thick. There's no action that touches money to validate; the model proposes an order of results, and the shell only filters retired or unpermitted products. It's the biggest difference.guardrail: output only (filter forbidden results), without the support agent's strong trust boundary —the search doesn't execute actions, so an injection has much less to gain—.fallback: degrades to keywords (deterministic, no AI), not to a human —a keyword search is a cheap and sufficient fallback; escalating to a human would be absurd for a search—.
Field almost identical:
eval_gate: both features need an eval gate that blocks a change that degrades the quality. The search measures relevance and the support agent measures correctness, but the form of the mechanism —a score against a threshold that governs the deploy— is the same. The quality has to be governed the same, whether the feature is tolerant or not.
Module 1's moral: the tolerance sizes the shell. Less tolerance, thicker shell; same form of the method, different thickness.
Exercise 2 — The contract of a reply. Step 3's contract verifies a refund (known action, order_id, positive numeric amount). Design the property-based contract for a reply-type proposal —when the agent answers a question instead of refunding— and explain why it's still a property-based contract and not an exact assert.
See solution
A property-based contract for a reasonable reply:
def satisfies_reply_contract(p):
return (isinstance(p, dict)
and p.get("action") == "reply"
and isinstance(p.get("text"), str)
and 0 < len(p["text"]) <= 500 # not empty, within a limit
and "\n\n\n" not in p["text"]) # no excessive-format noise
It verifies that the proposal is a reply, that it carries a text that's a string, not empty, within a length limit, and without obvious noise. (In lesson 5 it's added that the text not contain the system prompt —the trust boundary—.)
Why it's still a property-based contract and not an exact assert: because the same ticket can produce many different valid responses —"You can see the tracking in your profile", "Find your order's tracking in the Profile section", etc., all correct—. There's no the expected response you can fix with ==; there's a set of properties a good response meets (not empty, bounded, no noise, doesn't leak the prompt). Asserting p["text"] == "an exact response" would fail with the first legitimate variation, exactly module 1's error. The correct contract verifies that the response works, not that it's a specific string. The difference with the refund is only which properties you verify (a bounded text vs a typed action), not the nature of the contract: in both cases, properties, not equality.
Exercise 3 — The sheet as a containment argument. A colleague looks at the support agent's sheet and says: "this is a lot of ceremony for a feature; let's give it direct access to the refunds API and be done, the model is good". Using two concrete fields of the sheet, build the argument for why the ceremony isn't optional for this feature.
See solution
The argument, anchored in two fields of the sheet:
-
nd_tolerance = 3. The sheet records that this feature scores 5 on the three risk axes: it touches money directly, an error hurts a great deal, and its blast radius is the whole payments system. A tolerance of 3 means, by definition, that the feature can't withstand the model varying or making a mistake without containment —every proposal can become real money that goes out wrong—. "Giving it direct access to the refunds API" is exactly what a tolerance of 3 forbids. The ceremony isn't optional because the risk, measured, is maximal. -
deterministic_shell = "thick: validates every proposal against the policy". The sheet records that the chosen containment is a thick shell where the model proposes and the shell disposes. Removing it —connecting the model directly to the API— is module 6's root antipattern: coupling the execution to the model's output. The day the model proposes a refund of 9999 (from a hallucination or a prompt injection), that money goes out, because there's no shell to validate it. The sheet doesn't describe "ceremony"; it describes the only difference between a system that goes down with the first creative injection and one that holds.
The argument's close: the sheet is the answer to "why so much ceremony?". Each field exists because a measured risk asked for it. For a tolerant feature (semantic search) the sheet would be light and the colleague would be partly right; for this feature, tolerance 3, the thick sheet is what makes it legally responsible to put it in production. The ceremony isn't excess: it's proportional to the risk the sheet documents.
Summary and next step
In this lesson you executed step 1 of the method for the capstone's feature: you placed the support agent and gave it its contract. You classified its tolerance with the three axes (5-5-5 → 3 out of 15, the intolerant feature, thick shell), separated its core (propose an action) from its shell (the seven responsibilities that contain it), demonstrated its probabilistic contract —the same ticket gave three different structured proposals, the exact assert broke and the property-based contract passed for all three—, and issued its property sheet with the ten fields, each pointing to the lesson that will fill it. The feature was placed, with its contract clear and its file opened —ready for the following layers to turn each statement of the sheet into an executed mechanism—.
Before moving on you should be able to: classify a feature's tolerance with the three axes and derive from it the thickness of its shell; separate the core (one responsibility: propose) from the shell (everything that contains it); write a property-based contract for a structured proposal; and issue a property sheet with the ten fields stated.
Lesson 3 takes the first two fields of the sheet —latency_budget and cost_budget— and turns them into first-class architecture: the budget, the cascade, and the cache. You'll see, measured over 1000 tickets, how the support agent —slow and expensive— fits into its budget with a model cascade (cheap first) and a cache, and an honest lesson module 2 anticipated: no single lever is enough —the cascade trims but doesn't suffice, and only cascade + cache fits the feature into its cost margin—. The component is already placed; now you learn to pay for it.
Resources
- Anthropic, "Building Effective Agents" (2024) — anthropic.com/engineering/building-effective-agents. The reference for placing an AI component well: keeping the core bounded (one responsibility: propose) and loading the containment onto what surrounds it. This lesson's core/shell partition is a direct application of that principle. In English.
- Michael Nygard, "Documenting Architecture Decisions" (2011) — cognitect.com/blog/2011/11/15/documenting-architecture-decisions. The property sheet is a relative of the ADR lesson 8 writes in full; documenting the placement decision before building is the same discipline. In English.
- Martin Fowler and Bharani Subramaniam, "Emerging Patterns in Building GenAI Apps" — martinfowler.com/articles/gen-ai-patterns. The treatment of the AI component as a piece with architectural properties —contract, boundary, containment— that this lesson's sheet summarizes. In English.
- Chip Huyen, AI Engineering (O'Reilly, 2024). For when you cross the boundary and want to build the core you only placed here —the agent's prompt, its tool use, its RAG—: that's the book on the other side of the boundary. In English.