The First Rule: Start Narrow
The most common mistake in first agent builds is picking a task that is too broad. "Build an agent to help me with my email" will fail, because email involves reading, summarizing, drafting, sending, organizing, scheduling follow-ups, and flagging urgent items, and each of those is a separate task with different tools, different risk profiles, and different success criteria.
"Build an agent that monitors my inbox for messages from five specific clients, summarizes each one in two sentences, and adds the summary to a daily briefing document" is specific enough to build, test, and trust. Start there. Broaden it only after the narrow version works reliably.
The Six-Step Build Process
Your Pre-Launch Checklist
Before you let an agent run unsupervised on real tasks, go through this checklist. Click each item as you complete it.
When to Hand Off to a Human
One of the most important design decisions in any agent is knowing when the agent should stop and ask rather than continue and act. The answer depends on the stakes of the next action. Here are the three cases where human approval should be required before the agent proceeds:
Irreversible actions. Any action that cannot be undone without significant cost or effort. Sending an email, processing a payment, deleting data, submitting a form, publishing content. The agent should present the proposed action and wait for approval.
Ambiguous instructions. If the original task is unclear about how to handle a situation the agent has encountered, it is better to ask than to guess. A guess that is wrong in an irreversible action context is far more costly than the brief interruption of asking.
Unusual observations. If the agent encounters something that does not match the normal pattern of the task, for example, an unusually large file, an unexpected error code, or data that contradicts what it expected, it should flag this for human review rather than proceeding on a best guess.
Why System Prompt Quality Determines Agent Quality
The system prompt is the most leveraged document in your agent's design. It is the only persistent context the agent has for every reasoning step. A weak system prompt produces an agent that wanders: it interprets ambiguous situations however feels right in the moment, takes actions you did not intend, and produces outputs you cannot predict. A strong system prompt produces an agent that stays in scope, escalates appropriately, and behaves consistently across many different inputs.
A strong system prompt for an agent has five components: role definition ("You are a contract review specialist"), task definition ("Your task is to identify deviations from the standard clauses listed below"), tool descriptions ("You have access to the following tools: [list]"), escalation conditions ("Stop and ask a human if you encounter X, Y, or Z"), and exit conditions ("Once you have reviewed all clauses or after 25 reasoning steps, whichever comes first, output your findings and stop").
Notice that the escalation and exit conditions do the heavy lifting for safety. Without explicit exit conditions, the agent runs until it decides it is done, which may be later than you intended and after actions you did not anticipate. Without escalation conditions, the agent guesses when it should ask, and guessing under ambiguity is exactly the scenario most likely to produce a wrong irreversible action.
Testing Before You Trust
An agent you have not tested is a bet you have not placed yet. Before you let an agent run on real data with real consequences, you need to know how it behaves on at least three types of inputs: a normal input that represents the task as designed, an edge case that is technically within scope but unusual, and a malicious or adversarial input that represents the kind of bad input a prompt injection or user error might produce.
For a contract review agent, a normal input is a standard vendor contract. An edge case is a contract written in a language you did not anticipate, or a contract that is 400 pages rather than the typical 20. A malicious input is a contract that contains text designed to override the agent's instructions (a prompt injection attempt embedded in a clause). Knowing how the agent responds to all three categories before you deploy is what separates an enterprise pilot from a prototype.
import json, openai
from typing import Callable
# Tool registry: name -> (function, is_reversible)
TOOLS: dict[str, tuple[Callable, bool]] = {}
def register_tool(name: str, fn: Callable, reversible: bool = True):
TOOLS[name] = (fn, reversible)
class Agent:
def __init__(self, goal: str, tool_schemas: list,
max_steps: int = 20):
self.goal = goal
self.tool_schemas = tool_schemas
self.max_steps = max_steps
self.messages = [
{"role": "system", "content":
f"Goal: {goal}. Stop and ask a human if uncertain "
f"or if you encounter anything unexpected."}
]
def run(self, task: str) -> str:
self.messages.append({"role": "user", "content": task})
for step in range(self.max_steps):
response = openai.chat.completions.create(
model="gpt-4o",
messages=self.messages,
tools=self.tool_schemas
)
msg = response.choices[0].message
self.messages.append(msg)
if not msg.tool_calls:
return msg.content # agent finished
for call in msg.tool_calls:
result = self._execute(call)
self.messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": str(result)
})
return self._escalate("Step limit reached.")
def _execute(self, call) -> str:
name = call.function.name
args = json.loads(call.function.arguments)
if name not in TOOLS:
return f"Error: unknown tool {name}"
fn, reversible = TOOLS[name]
if not reversible:
if not self._human_check(name, args):
return "Action rejected by human reviewer."
return fn(**args)
def _human_check(self, name: str, args: dict) -> bool:
print(f"\n[HUMAN CHECK] About to call {name}({args})")
return input("Allow? (yes/no): ").lower() == "yes"
def _escalate(self, reason: str) -> str:
print(f"\n[ESCALATION] Agent stopped: {reason}")
return f"Escalated to human: {reason}"
# Register tools
register_tool("search_web", web_search, reversible=True)
register_tool("send_email", send_email, reversible=False)
# Run the agent
agent = Agent(
goal="Monitor inbox for messages from five named clients and summarize each",
tool_schemas=[search_schema, email_schema]
)
result = agent.run("Check for new client emails from today")
Choose one tool from the list you wrote in Module 2. Write the three test cases you would run before deploying an agent that uses that tool: (1) a normal input that represents the typical use case, (2) an edge case that is technically valid but unusual (an empty result, a very large input, an unexpected data type), and (3) a malicious input designed to confuse or override the agent's instructions. What would the correct agent response be to each one?