Worked example: a first-reply support agent, start to URL
The book's own Tier-1 support agent taken through every page of this room: the three gates, the spec, the first test, the agent and its two tools, the harness, the eval that catches the wrong-account reply, and the done-when checks at the live URL.
- Status
- stable
- Trust
- human-reviewedHuman: Raza Rizwan ·
- Owner
- Human: Raza Rizwan
- Approved
- Human: Raza Rizwan ·
The book carries one agent through its deployment course: Maya, a Tier-1 support agent for a software product. She answers a customer's first message: she looks up the account, then drafts a reply. Her two tools are deliberately simple stand-ins so the harness runs without any external system behind it. 1 This page takes that same agent through this room's pages in order, so you can see the shape of each step on one small case before you do it on yours.
Gates
Gate 1. Customer messages arrive in different words about the same thing; there is no fixed rule that maps a message to its answer, so this is AI work, not a tool. And the work is not only to reply in kind: it has to look something up and then act on what it found, which is what makes it an agent shape rather than a chatbot. 2
Gate 2. The companion runs Maya one message at a time, with a person reading the draft; that is Mode 1, with a human at the end of every run. The third time the same task is done the same way, run Gate 2: if all three dials are up, often, same shape, worth it, that is the cue to cross it into a worker. 2
Gate 3. Works from: the customer's first message, and the account id when the message names one. Want at the end: a short reply in the requested tone. Done when: the draft exists, and when an account was named, the draft rests on that account's plan and status rather than on a guess. 2
Spec, in the six sections
# spec.md, First-reply support agent
## Goal
Answer a customer's first message with a correct, kind draft so a person
can send it instead of writing it.
## User Scenarios
- A message naming a known account gets a draft that reflects that
account's plan and status.
- A message naming an unknown account gets a draft that says no account
was found for that id.
- A message naming no account gets a draft without any lookup.
## Functional Requirements
FR-1 When a message references an account, look it up before drafting.
FR-2 The draft names what was found: plan and status, or "no account".
FR-3 The draft is short and in the requested tone; the default is friendly.
## Edge Cases & Rules
- The lookup returns more than one match: do not proceed with action
tools until the matching account is identified; if it cannot be, stop
and hand the message to a person.
## Out of Scope
- Sending the reply. Refunds, plan changes, or any change to an account.
## Acceptance Criteria
- [ ] acct_1001 in the message: the draft mentions plan Pro and status active.
- [ ] An unknown id: the draft says no account was found.
- [ ] No account in the message: no lookup call appears in the run.
- [ ] Two matches: no draft is produced; the run stops for a person.The multiple-match rule is the book's own, from the eval walkthrough on the evals page: the fix for the wrong-customer refund was one targeted rule, "when the lookup returns multiple results, do not proceed with action tools until you have identified which account matches; if you cannot, escalate to a human". 3 Here it is written into the spec before any code exists.
The first test, before the tool body
Keep the lookup a plain function with a tiny interface, so the test calls only that and never touches the network, as on the tests page. The two tests come straight from the first two acceptance criteria:
# tests/test_accounts.py, written FIRST
from chat_agent.accounts import find_account
def test_known_account_reports_plan_and_status():
assert "plan Pro" in find_account("acct_1001")
assert "status active" in find_account("acct_1001")
def test_unknown_account_says_so():
assert find_account("acct_9999") == "No account found for id acct_9999."Watch both fail, write the function, watch both pass. The @function_tool
below is a thin wrapper over that function.
The agent and its two tools
The companion's agent, as shipped: 1
# src/maya_harness/agent.py, from the book's companion
from agents import Agent, function_tool
from .settings import settings
# A tiny in-memory account table. In production this would be a CRM call.
_ACCOUNTS = {
"acct_1001": {"name": "Ada Lovelace", "plan": "Pro", "status": "active"},
"acct_1002": {"name": "Alan Turing", "plan": "Free", "status": "past_due"},
}
@function_tool
def lookup_account(account_id: str) -> str:
"""Look up a customer account by id and return its plan and status."""
account = _ACCOUNTS.get(account_id)
if account is None:
return f"No account found for id {account_id}."
return (
f"Account {account_id}: {account['name']}, plan {account['plan']}, "
f"status {account['status']}."
)
@function_tool
def draft_reply(customer_message: str, tone: str = "friendly") -> str:
"""Draft a short support reply to a customer message in the given tone."""
return (
f"[{tone} draft]\n"
f"Hi there, thanks for reaching out. About: '{customer_message}'. "
f"Here is what I can do to help."
)
def build_agent() -> Agent:
"""Construct Maya. Model defaults to gpt-5.4-mini (the SDK default)."""
return Agent(
name="Maya",
instructions=(
"You are Maya, a Tier-1 support agent. Be brief and kind. "
"When a customer references an account, call lookup_account first. "
"Then call draft_reply to compose your answer."
),
model=settings.model,
tools=[lookup_account, draft_reply],
)One line in that file has aged: the docstring names gpt-5.4-mini as the SDK
default, which was true for the SDK release the companion was written
against; the default has since changed, and the probe on the
build page prints the one your installed SDK will
use. Leave the model unset, as the previous pages do, and the code stays
correct across releases.
Two things to notice against the earlier pages. The instructions carry the
order of operations in words, and the spec's multiple-match rule belongs
there too as one more sentence; both are prompts, written under the
prompting guide. And neither tool changes anything in the
world, so nothing here needs needs_approval; the moment a tool that sends
the reply or changes the account is added, that tool gets the gate.
The harness and the container
Nothing changes from the FastAPI page: the same
main.py with GET /health and POST /runs, the same Dockerfile, the same
Dockerfile.vercel copy, PORT set to 8000 and OPENAI_API_KEY on the
project. The book's boot condition holds here too: with no key, /health
still answers and /runs answers 503 with the message to add one.
1
The eval that the tests cannot write
The golden set starts with the four acceptance criteria as four examples, tagged easy (known account), easy (no account), medium (unknown id) and hard (two matches), with a few reworded variants of each so the set is not one sentence per case. The hard case is the one that matters: a run can produce a perfect-looking draft for the wrong account, and only a tool-use eval that asks "was the right argument passed?" catches it. 3 Grade every example at two levels, output and tool calls, as a pass rate across several runs, and hold the change until the whole set passes, not just the case you were fixing. 3
At the URL
The end-to-end page in one line each: the smoke test
with BODY set to "Hi, I cannot log in to account acct_1001." prints a
draft that mentions Pro and active; the same test passes inside the
container; /health answers ok from your phone at the deployed URL; after
the quiet window the first request is slower and then ok again; the
golden set passes against the live /runs; and a message with two matching
accounts stops without a draft. 4
That is the whole room on one small agent. Yours will have different tools and a different spec, and exactly the same steps. Where this agent needs more than a session file, give it a record says what it grows into; how to write the skill that runs it is on write a skill.
Footnotes
-
deploying-agents-base companion:
src/maya_harness/agent.py,src/maya_harness/main.py,README.md. Panaversity, 2026. ↩ ↩2 ↩3 -
Is This an Agent Problem?, the three gates. The AI Agent Factory, Panaversity, 2026. ↩ ↩2 ↩3
-
Eval-Driven Development for AI Employees: A Multi-Track Crash Course, Concepts 3, 11 and 12. The AI Agent Factory, Panaversity, 2026. ↩ ↩2 ↩3
-
Deploy Your Agent Harness to the Cloud: A Crash Course, Decisions 1 to 3. The AI Agent Factory, Panaversity, 2026. ↩
Sources
- [^af-28]Is This an Agent Problem? The AI Agent Factory, Panaversity, 2026
- [^af-49]Eval-Driven Development for AI Employees: A Multi-Track Crash Course. The AI Agent Factory, Panaversity, 2026
- [^af-50]Deploy Your Agent Harness to the Cloud: A Crash Course. The AI Agent Factory, Panaversity, 2026
- [^af-50-companion]deploying-agents-base, the companion code for the deployment crash course (src/maya_harness/agent.py). Panaversity, 2026
Give it a record: the tables a worker runs against, the audit trail, and the MCP wire
Where the book continues after this room's basic worker, from The AI Agent Factory: every worker runs against a system of record; four tables every worker keeps; Postgres with pgvector over a separate vector store; the embedding pipeline; the audit trail written in the same transaction as the action; what MCP is and is not; a development plane that never reaches runtime; when a custom MCP server earns its keep over a plain function tool; reads run free, writes are gated, schema changes never reach production through the agent.
Write a skill: the description is the trigger, the body is the instructions
How to write a skill a coding agent will actually reach for, from The AI Agent Factory and OpenAI's skill guidance: progressive disclosure and what it costs, the description formula and the delete-the-keyword test, an imperative body with real examples and edge cases, what belongs in a script instead of a sentence, one skill or several, where skills live, how to test the trigger, and why a skill has no scope of its own.