« Phase 21 README · Hitchhiker's · Deep Dive · Principal Deep Dive · Core Contributor · Staff Notes
Phase 21 Warmup — The OpenAI Agents SDK, From First Principles
Who this is for: someone who can write Python and understands the agent loop (Phase 00/01) but has only called the OpenAI Agents SDK, or hasn't touched it at all. By the end you will be able to rebuild its four primitives from scratch, explain every design decision, and defend "Agents SDK vs LangGraph vs CrewAI" and "how Temporal makes it durable" in an interview. Nothing here needs an API key, a GPU, or the
openai-agentspackage — the mechanism is the point.
Table of Contents
- What the OpenAI Agents SDK is (and the philosophy)
- The four primitives: Agents, Handoffs, Guardrails, Sessions
- The agent loop inside the Runner
- The Agent: a declarative bundle
- Function tools and the auto-generated schema
- The trust boundary: proposing vs executing a tool
- Runner APIs, RunResult, and the turn budget
- Structured output
- Handoffs: the SDK's multi-agent mechanism
- Handoffs vs agents-as-tools: two multi-agent styles
- Guardrails and tripwires
- Sessions and automatic memory
- Tracing and observability
- Agents SDK vs LangGraph vs CrewAI
- The Temporal integration: durable agents
- Common misconceptions
- Lab walkthrough
- Success criteria
- Interview Q&A
- References
1. What the OpenAI Agents SDK is (and the philosophy)
The OpenAI Agents SDK (the openai-agents Python package, evolved from the "Swarm"
experiment) is a small, unopinionated framework for building agentic apps. Its entire design
philosophy fits in two sentences, straight from its docs:
"Enough features to be worth using, but few enough primitives to make it quick to learn. Python-first: use built-in language features to orchestrate and chain agents, rather than needing to learn new abstractions."
Read that as a deliberate reaction to heavier frameworks. There is no graph to declare, no DSL, no
config format. You write Python functions and decorate them; you make Agent objects; you call
Runner.run. The SDK contributes exactly four concepts on top of plain Python — Agents,
Handoffs, Guardrails, Sessions — plus a built-in tracing layer. That's it.
Why does a minimal framework deserve a whole phase? Because minimalism means there is almost nothing between you and the mechanism. When you understand the SDK, you understand agents, full stop — the loop, tool dispatch, multi-agent control transfer, safety gating, and memory, with no abstraction hiding them. And because it's the framework a big chunk of the industry (and the JDs, notably Temporal's AI Foundations team) actually reaches for, being able to rebuild it is a direct, defensible interview signal. The rest of this warmup rebuilds each primitive from first principles.
2. The four primitives: Agents, Handoffs, Guardrails, Sessions
Here is the whole SDK on one page, so the later sections have a map to hang on:
| Primitive | What it is | The one-line mechanism |
|---|---|---|
| Agent | an LLM configured with instructions, tools, handoffs, guardrails, and an output type | a declarative bundle the Runner executes |
| Handoff | a way for one agent to delegate to another | a tool named transfer_to_<agent> that switches the active agent |
| Guardrail | a fast check that runs alongside the agent | a function returning a tripwire_triggered bool that can abort the run |
| Session | automatic conversation memory | a keyed store the Runner reads before and writes after each run |
And one engine that ties them together:
Runner— the agent loop. It calls the model, runs the tools/handoffs it asked for, applies guardrails, reads/writes the session, and returns aRunResult. Every primitive above is something theRunnerknows how to interpret during that loop.
Plus one cross-cutting concern:
- Tracing — the SDK automatically wraps every run in a trace of spans (per agent turn, tool call, handoff, guardrail), so you can see what happened without instrumenting anything.
If you can rebuild the Runner loop and teach it about tools, handoffs, guardrails, and a session,
you have rebuilt the SDK. That is precisely the three labs.
3. The agent loop inside the Runner
Everything starts here. An agent is an LLM in a loop with tools and memory (Phase 00), and in the
Agents SDK that loop lives inside Runner.run. In pseudocode, stripped to its essence:
def run(agent, input, max_turns):
items = to_items(input) # the running conversation the model sees
current = agent
for turn in range(1, max_turns + 1):
output = current.model(items) # 1. call the LLM (untrusted text out)
items += output
if output.has_tool_calls(): # 2. it asked for tools -> run them
for call in output.tool_calls:
result = run_tool(call) # (on the TRUSTED side)
items += tool_output(result) # feed the observation back
continue # loop: the model sees the results
if output.is_handoff(): # 3. it asked to hand off -> switch agents
current = output.handoff.target
continue
return RunResult(final_output=output.message, # 4. a plain message -> we're done
new_items=items_since_start,
last_agent=current)
raise MaxTurnsExceeded(...) # 5. the budget guard
The official docs describe the same thing in prose: "call the LLM; if it returns a final output,
return it; if it has a handoff, switch to the new agent and re-run; if it produces tool calls, run
them, append the results, and re-run; if max_turns is exceeded, raise." That is the entire
control flow of the SDK. Streaming, structured output, guardrails, and sessions are all decorations
on this loop.
Two load-bearing details you must internalize:
- The model only produces text/JSON; the
Runnerdoes everything real. Theoutputis a proposal. Whether a tool actually runs is a decision theRunnermakes on the trusted side (§6). max_turnsis the reliability budget ([Phase 00 §5]) made concrete. Without it, a model that keeps calling tools never terminates. It is a guard against looping, not a target.
In Lab 01 you write this loop as a generator so the same
implementation backs run_sync, the async run, and run_streamed.
4. The Agent: a declarative bundle
An Agent is not code that runs — it is configuration the Runner interprets. The real class,
trimmed to the fields that matter:
agent = Agent(
name="Weather Bot",
instructions="Answer weather questions.", # the system prompt (or a callable)
tools=[get_weather], # @function_tool-decorated functions
handoffs=[billing_agent], # agents this one may delegate to
input_guardrails=[...], output_guardrails=[...],
output_type=WeatherReport, # a pydantic type for structured output
model="gpt-4.1", model_settings=ModelSettings(temperature=0.2),
)
Everything one "role" needs is in one object. instructions can be a plain string or a function
of the run context (dynamic instructions — e.g. inject the user's name). model names the LLM;
model_settings carries temperature, tool_choice, parallel_tool_calls, etc. Agents are cheap,
immutable-ish descriptions, so you make many of them (a triage agent, a billing agent, a refund
agent) and wire them together with handoffs.
In the labs the model is an injected policy — a pure function of the running item list that
returns the model's output items — rather than a real LLM name. That single substitution is what
makes the whole SDK testable offline: the loop's correctness does not depend on the model being
right, which is the entire discipline of production agent engineering (Phase 00). Real teams do the
same thing with a FakeModel / record-replay fixtures to unit-test Agents-SDK apps.
5. Function tools and the auto-generated schema
A tool is a Python function the model can ask to call. The SDK's headline ergonomic feature is
the @function_tool decorator: it turns any function into a tool by deriving the JSON schema from
the signature.
@function_tool
def get_weather(city: str, units: str = "celsius") -> str:
"""Return the current weather for a city."""
...
From that, the SDK produces (roughly):
{
"name": "get_weather",
"description": "Return the current weather for a city.",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}, "units": {"type": "string"}},
"required": ["city"]
}
}
Note the mechanics, because you rebuild them in Lab 01:
- name comes from the function name, description from the docstring.
- Each parameter becomes a property; its Python annotation maps to a JSON type
(
int→"integer",str→"string",bool→"boolean", and so on). - A parameter without a default is
required; one with a default is optional. That's the entire "required" story — it falls straight out of the signature.
The real SDK does this via pydantic, so rich types (list[int], nested models, enums,
Literal, per-argument descriptions parsed from the docstring) all work, and the arguments the
model sends are validated against the schema before your function ever sees them. In Lab 01 we use
stdlib inspect to cover the common scalars — enough to make the signature→schema step legible.
The lesson stands either way: a well-typed, well-documented Python function is a good tool,
because the model sees exactly the contract the signature encodes.
The SDK also exposes non-function tools (hosted tools like web search / file search / code
interpreter, and computer-use), but @function_tool is the one you'll write 95% of the time.
6. The trust boundary: proposing vs executing a tool
This is the Phase 00 idea, now visible in the loop. When the model "calls get_weather," nothing
physically happens — the model emits a structured request: {"tool":"get_weather","args":{...}}.
Then the Runner (your trusted code) decides whether to honor it and invokes the underlying
function. The model proposes; the Runner executes. In the SDK, the executor is literally a
field on the tool — on_invoke_tool — and it's the only place the real function is called.
Two consequences you should be able to state:
- Arguments are untrusted. They came from a stochastic model that may be following an injected instruction (Phase 10). That's why the schema validation in §5 matters — malformed or wrong-typed args become a structured error, not a crash or a bad call.
- Tool exceptions are observations, not crashes. If a tool raises, the SDK (by default, via a
failure_error_function) turns the error into a message fed back to the model, so the agent can see "that failed" and recover — exactly like theToolError-as-observation pattern in Phase 01. A hallucinated tool name (the model calls a tool the agent doesn't have) is different: that's aModelBehaviorError, because it means the model did something structurally impossible. Lab 01 implements both behaviors and tests them.
Keeping "run the tool" on the trusted side is where every later safety control plugs in:
allow-lists, sandboxes (Phase 09), authorization (Phase 13), and human approval all live in or
around on_invoke_tool, never in the prompt.
7. Runner APIs, RunResult, and the turn budget
The Runner exposes three entry points, all driving the same loop:
await Runner.run(agent, input)— the primary async API; returns aRunResult.Runner.run_sync(agent, input)— a synchronous wrapper for scripts and notebooks.Runner.run_streamed(agent, input)— returns aRunResultStreamingimmediately; you iterateasync for event in result.stream_events()to receive items as they're produced (token deltas, tool calls, tool outputs, handoffs), then read the final fields.
All three return (or eventually populate) a RunResult with the three fields you rebuild:
final_output— the agent's answer (a string, or a structured object ifoutput_typeis set).new_items— every item generated during the run (messages, tool calls, tool outputs, handoffs), excluding the original input. This is the audit trail.last_agent— which agent produced the final output. With handoffs this is not the agent you started with, and you persist it to know who to resume as next turn (§9).
The max_turns parameter caps how many times the loop calls the model before raising
MaxTurnsExceeded. It is the loop-level version of Phase 00's step budget: a bounded, catchable
failure that stops a confused agent from burning tokens forever. Raising the cap is not a fix for an
agent that legitimately needs more steps — that's a signal to add verification or a handoff, not to
let it loop more (Phase 00 §5).
In Lab 01 the loop is one generator that yields each new item
and returns the RunResult; run_sync drains it, run awaits it, and run_streamed exposes the
yielded items — one implementation, three surfaces, exactly like the real SDK.
8. Structured output
By default an agent's final_output is free text. Set output_type and the SDK forces the
final output into a schema — pass a pydantic model (or a dataclass, TypedDict, list[...], etc.)
and the SDK parses/validates the model's final message into that type. This is how you get typed
answers you can hand to downstream code without regex-scraping a string:
class WeatherReport(BaseModel):
city: str
temp_c: float
summary: str
agent = Agent(name="Weather", tools=[get_weather], output_type=WeatherReport)
result = await Runner.run(agent, "weather in Paris?")
result.final_output.temp_c # a float, validated
Mechanically, output_type changes the loop's stopping condition: the model's final message must
parse into the type, and the SDK uses that type's JSON schema to steer the model. In Lab 01 the
stand-in is a one-line callable (output_type=int) applied to the final message — enough to show
the coercion seam. The production point: structured output is where the fuzzy model hands off to
deterministic code, so it's a natural place to put a validating boundary.
9. Handoffs: the SDK's multi-agent mechanism
A single agent with a dozen tools becomes unwieldy and unreliable. The SDK's answer to "many specialists" is the handoff: one agent delegates control to another. The mechanism is the elegant part, and it's why handoffs feel native rather than bolted on:
A handoff is exposed to the model as a tool named
transfer_to_<agent>.
So when a triage agent has handoffs=[billing_agent, refund_agent], the model sees two extra tools,
transfer_to_billing_agent and transfer_to_refund_agent. When the model "calls" one, the Runner
doesn't run a function — it switches the active agent to the target and continues the same
loop, now driven by the target's model, instructions, and tools. Control does not come back;
the specialist runs until it produces a final output, so RunResult.last_agent is the specialist.
Two knobs make handoffs production-grade, both of which you build in Lab 02:
on_handoff— a callback that fires the instant control transfers (log the routing decision, warm a cache, pull a record). It can also declare aninput_typeso the LLM must pass structured handoff arguments (e.g.{"reason": "...", "order_id": 42}), which the SDK validates.input_filter— a function that reshapes the conversation the new agent inherits. By default the specialist sees the full history; a filter can strip tool-call noise (handoff_filters.remove_all_tools), summarize, or redact, so the specialist starts clean.
The naming convention (transfer_to_ + a snake-cased agent name) is not cosmetic — it's how the
Runner distinguishes a handoff call from an ordinary tool call during the loop. In
Lab 02 you generate that name, index handoffs by it, switch the active
agent on the call, and enforce max_turns across handoffs so two agents can't bounce control
forever.
10. Handoffs vs agents-as-tools: two multi-agent styles
The SDK gives you a second way to compose agents, and telling the two apart is a classic senior
interview probe. You can wrap Runner.run(sub_agent, ...) inside a @function_tool — now the
sub-agent runs as a tool call of the orchestrator, returns its result, and the orchestrator
keeps control. Contrast that with a handoff, where control transfers and never returns:
| Handoff | Agent-as-tool | |
|---|---|---|
| control after the call | transfers — the target finishes the run | returns to the orchestrator |
last_agent | the target (specialist) | the orchestrator |
| conversation | the target inherits it (optionally filtered) | the sub-agent gets a fresh sub-run; only its result comes back |
| mental model | "you take this from here" (delegation / routing) | "go compute this for me" (a callable helper) |
| best when | triage → one specialist should fully own the reply | you need to combine several sub-results, or run sub-agents in parallel |
Neither is "more advanced." Handoffs give you decentralized control (each agent owns its turn); agents-as-tools give you centralized control (one orchestrator stays in charge). A real support bot often uses both: a triage agent hands off to a billing specialist, and that specialist uses a "translate" agent as a tool. Knowing which shape a problem wants — and being able to say why — is the point of Lab 02.
11. Guardrails and tripwires
Agents are expensive and can be manipulated. Guardrails are cheap, fast checks that run
alongside the agent to catch bad inputs and bad outputs early. Each guardrail is a function that
returns a GuardrailFunctionOutput(output_info, tripwire_triggered) — the boolean is the
decision, the info is for logging. There are two kinds:
- Input guardrails run on the first agent's input. If a tripwire fires, the SDK raises
InputGuardrailTripwireTriggeredand aborts before the expensive agent runs — you don't pay for a big model on an obviously abusive or off-topic request. - Output guardrails run on the final output. A tripwire raises
OutputGuardrailTripwireTriggered, so a non-compliant answer (PII, policy violation, wrong format) never reaches the user.
The intended pattern, and the reason guardrails are a primitive, is the cost asymmetry: run a small, fast, cheap model (or a regex/classifier) as the guardrail, and let its tripwire short-circuit the smart, slow, expensive agent. "A fast model guards the smart model" — a guardrail that costs a tenth of a cent can prevent a ten-cent agent run and a policy incident. In the real SDK, input guardrails run concurrently with the first model call and cancel it on a tripwire; running them first (as in Lab 03) gives the same guarantee — no final output escapes — with simpler mechanics.
Where this sits relative to Phase 10: that phase built defense-in-depth against prompt injection (trust boundary, tool allow-lists, output exfil scanning, human-in-the-loop). SDK guardrails are the framework-native slice of that — the productized "input guard / output guard" layer. The architectural controls (least-privilege tools, the trust boundary) still live in your code around the SDK; guardrails complement them, they don't replace them.
12. Sessions and automatic memory
Out of the box, each Runner.run is stateless: it forgets everything the moment it returns. To hold
a conversation you'd have to manually thread the prior items into the next call's input.
Sessions automate exactly that. A Session is a keyed store of conversation items with a tiny
interface — get_items, add_items, pop_item, clear_session — and when you pass session= to
Runner.run, the loop:
- reads the session's stored items and prepends them to this turn's input, and
- writes this turn's input and all generated items back to the session on success.
So a second Runner.run on the same session automatically sees the prior turns — memory with no
manual plumbing. The SDK ships SQLiteSession (a real sqlite table keyed by session_id),
OpenAIConversationsSession, and lets you supply custom backends (Redis, Postgres, SQLAlchemy)
behind the same four methods.
In Lab 03 you implement SQLiteSession for real on stdlib
sqlite3 — rows keyed by session_id, ordered by an autoincrement id (deterministic, no
timestamps/uuids) — plus an InMemorySession. A counting agent then proves the memory: run 1 reports
"turn 1," run 2 on the same session reports "turn 2" because it saw the first turn. Two things worth
flagging for production: sessions are where context growth (Phase 04) shows up — you eventually
need pruning/summarization in get_items — and where multi-tenant isolation (Phase 13) matters:
the session_id is the tenant/user boundary, so one customer must never read another's rows.
13. Tracing and observability
You cannot operate what you cannot see, and agents are especially opaque (a loop over a stochastic
oracle). The SDK's answer is built-in tracing: every Runner.run is automatically wrapped in a
trace composed of spans — one per agent turn, LLM generation, tool call, handoff, and
guardrail. You get a structured, hierarchical record of what the agent did with zero
instrumentation, viewable in the OpenAI dashboard or exported to third-party processors (Logfire,
Braintrust, LangSmith, AgentOps, and OpenTelemetry backends).
Why this is a first-class feature and not an afterthought: the questions you ask at 2 a.m. — why did
it call that tool, where did it hand off, which guardrail tripped, how many turns did it take, where
did the tokens go — are all answerable from the span tree. Tracing is the SDK's seam into everything
in Phase 14 (cost/latency/observability). In the labs the analog is the new_items list on the
RunResult: the ordered record of every message, tool call, tool output, and handoff the run
produced. That list is a miniature trace — you can assert the exact sequence of events, which is
how the tests verify behavior. Real tracing adds timing, token counts, and the parent/child span
structure, but the idea — record every step of the loop as it happens — is the same.
14. Agents SDK vs LangGraph vs CrewAI
Three frameworks the JDs name, three philosophies. Being able to place them is a senior signal:
| OpenAI Agents SDK | LangGraph (Phase 18) | CrewAI | |
|---|---|---|---|
| core abstraction | few primitives over one loop | an explicit graph of nodes/edges + persisted state | roles (agents) + tasks in a crew/process |
| control flow | the Runner loop; branching via handoffs | you draw it: nodes, conditional edges, cycles, fan-out/in | a process (sequential/hierarchical) orchestrates roles |
| state | conversation items + sessions | a typed state object with per-channel reducers | task context passed between roles |
| multi-agent | handoffs (switch agent) or agents-as-tools | supervisor/worker subgraphs, explicit routing | role delegation within a crew |
| durability / HITL | via Temporal wrap (§15) or your own | checkpointers + interrupts built in (Phase 18 labs 02/03) | limited; add your own |
| feels like | writing Python with a tiny agent library | building a state machine / dataflow | assigning a team of personas |
| reach for it when | light, Python-first agent with handoffs; you don't want a graph | you need explicit state, checkpoints, complex/cyclic control flow, precise HITL | fast prototyping of a role-playing multi-agent "team" |
The honest framing: they overlap more than the marketing admits, and they interoperate — a
LangGraph node can call an Agents-SDK agent; both are elaborations of while not done: proposal = model(scratchpad); execute (Phase 00). Choose on the control-flow axis: the Agents SDK when the
flow is "an agent that occasionally delegates," LangGraph when the flow is "a state machine I need
to see and checkpoint," CrewAI when you want a quick role-based team. There is no framework that
makes the model trustworthy — that's still your trust boundary, in your code.
15. The Temporal integration: durable agents
Here is the JD-critical connection. Temporal's AI Foundations team builds plugins that connect Temporal's durable execution to AI frameworks — Pydantic AI, Vercel AI SDK, Google ADK, and the OpenAI Agents SDK by name. Why marry them?
The Agents SDK loop is, by default, an in-memory process: if the machine crashes mid-run — after three tool calls and a handoff, ten minutes into a long research task — the whole run is lost and you restart from zero, re-paying for every step. For a short chat that's fine; for a long-running or high-value agent it is unacceptable (Phase 08's entire thesis: reliability is a budget, and durability buys steps back).
Temporal makes execution durable by event-sourcing every step: each model call, tool result,
and handoff is recorded to a persistent history, and on a crash the workflow replays that history
to reconstruct state exactly and resume from where it stopped — no re-doing completed work. The
Temporal + OpenAI Agents SDK integration wraps the Runner loop so that each turn becomes a durable,
replayable step: the LLM call and each tool call run as Temporal activities (retried with backoff,
idempotent), while the loop itself runs as a workflow (deterministic, replay-safe). The payoff is
a crash-proof, infinitely resumable agent with automatic retries and full history — the Agents SDK's
ergonomics with Temporal's reliability.
The one constraint that ties back to this whole track: replay-safety requires determinism. A
workflow that reads the wall clock or an unseeded random can't replay identically (Phase 08, and the
determinism rules in LAB-STANDARD.md). That is exactly why every lab in this
phase injects the model as a pure policy and uses counters, not time.time() or uuid: the same
discipline that makes the labs testable is what makes an agent durable under Temporal. Cross-reference
Phase 08 for the event-sourcing/replay mechanism you'd
build the integration on.
16. Common misconceptions
- "The framework runs my tools." No — the model proposes a tool call (untrusted text); the
Runnerexecutes it on the trusted side viaon_invoke_tool. Every safety control lives there, not in the prompt. - "A handoff is a function call that returns." A handoff transfers control — the target agent
finishes the run and
last_agentbecomes the target. The "returns a result" shape is agents-as-tools, a different mechanism (§10). - "Guardrails make the agent safe." Guardrails are a cheap input/output gate; they are not a substitute for the architectural controls (allow-lists, sandboxing, authz, HITL) from Phases 09/10/13. Defense in depth — guardrails are one layer.
- "Guardrails should use the best model so they're accurate." Usually the opposite: run a small, fast, cheap model so the guardrail adds negligible cost/latency and can gate the expensive agent. A guardrail as slow as the agent defeats the purpose.
- "Sessions are just chat history." They're a keyed store with a real interface; the
session_idis a tenant boundary (Phase 13) and the place context growth bites (Phase 04) — production sessions need pruning and isolation, not just append. - "
max_turnsis a performance knob." It's a reliability guard against infinite loops. If an agent legitimately needs more turns, add verification or a handoff — don't just raise the cap. - "The Agents SDK is a toy because it's small." Small is the feature. Its minimalism is why it's easy to reason about, easy to make durable (Temporal), and why understanding it is understanding agents.
17. Lab walkthrough
Do the labs in order; each builds on the last and maps to sections above.
- Lab 01 — Agent Runner & Tools (§3–§8).
Build
function_tool(auto-schema from the signature),Agent, and theRunnerloop as one generator behindrun_sync/run/run_streamed, withoutput_typecoercion,max_turns, unknown-toolModelBehaviorError, and tool-exception-as-observation. 20 tests. - Lab 02 — Handoffs (§9–§10).
Generate
transfer_to_<agent>names, index handoffs, switch the active agent on a transfer call, and supporton_handoff+input_filter; multi-hop;max_turnsacross handoffs. 20 tests. - Lab 03 — Guardrails & Sessions (§11–§12).
Build input/output guardrails with tripwires (input halts before the model runs) and a real
sqlite
Sessiongiving cross-run memory; a blocked run writes nothing. 22 tests.
Run each lab's reference green first, then fill your own lab.py:
cd lab-01-agent-runner-tools
LAB_MODULE=solution pytest test_lab.py -q # reference (green)
python solution.py # the worked example
pytest test_lab.py -q # your lab.py (red until you implement)
Finish by reading each solution.py's main() output — together they are this warmup, running.
18. Success criteria
-
You can whiteboard the
Runnerloop (call model → tools/handoff → repeat → final /max_turns) and say where each primitive plugs in. -
You can explain how
@function_toolderives a schema from a signature, and why a param with a default is notrequired. -
You can state the trust boundary in SDK terms: the model proposes a tool call;
on_invoke_toolexecutes it; a tool exception is an observation, an unknown tool is aModelBehaviorError. -
You can explain a handoff as "a
transfer_to_<agent>tool that switches the active agent," and contrast it with agents-as-tools (last_agentis the tell). - You can explain the guardrail tripwire pattern and why guardrails should run a cheap model.
-
You can explain how a
Sessiongives automatic cross-run memory, and whysession_idis a tenant boundary. - You can place the Agents SDK against LangGraph and CrewAI, and explain how Temporal makes the loop durable (and why that needs determinism).
-
All three labs pass (62 tests total) under both
labandsolution.
19. Interview Q&A
Q: Walk me through what Runner.run(agent, input) actually does. A: It runs the agent loop:
compose the input into a running item list; call the agent's model; if the model returned tool calls,
execute each on the trusted side and append the results, then loop; if it returned a handoff
(transfer_to_<agent>), switch the active agent and loop; if it returned a plain final message,
return a RunResult(final_output, new_items, last_agent); and if the loop exceeds max_turns, raise
MaxTurnsExceeded. Streaming and structured output are the same loop with a different surface.
Q: How does @function_tool know the tool's schema? A: It introspects the function signature
(via pydantic in the real SDK). Parameter names + annotations become the JSON-schema properties and
types; parameters without a default are required; the docstring is the description. So a well-typed,
documented function is the tool contract, and the model's arguments get validated against that
schema before your code runs.
Q: Handoffs vs sub-agents-as-tools — when do you use each? A: A handoff transfers control —
the target agent takes over the conversation and produces the final output, so last_agent becomes
the target; use it for triage/routing where a specialist should fully own the reply. An agent-as-tool
is Runner.run(sub_agent) wrapped in a @function_tool: control returns to the orchestrator with
just the sub-result; use it when you need to combine several sub-results or keep one orchestrator in
charge. Decentralized vs centralized control.
Q: What's a guardrail tripwire, and why run guardrails on a cheap model? A: A guardrail
returns GuardrailFunctionOutput(output_info, tripwire_triggered); if the tripwire is true, the run
aborts — input guardrails before the agent even runs, output guardrails before the answer reaches the
user. You run them on a small/fast/cheap model because the whole point is cost asymmetry: a fraction-
of-a-cent check short-circuits a much more expensive agent run, so it must add negligible latency and
cost. A guardrail as slow as the agent is pointless.
Q: How do Sessions work, and what breaks at scale? A: A Session is a keyed item store;
passing session= makes the Runner prepend stored history to the input and append the new items
after the run, so the next Runner.run sees the prior turns automatically. At scale two things bite:
context growth — you must prune/summarize in get_items or every turn re-sends a longer history
(Phase 04) — and tenant isolation — session_id is a security boundary, so one user must never
read another's rows (Phase 13).
Q: The Agents SDK loop is in-memory; how do you make a long-running agent survive a crash? A: Wrap the loop in a durable-execution engine — Temporal. Each model/tool call becomes an activity (retried, idempotent) and the loop becomes a workflow whose steps are event-sourced to a persistent history; on a crash the workflow replays the history to resume exactly where it stopped, without re-doing completed work. That requires the loop to be deterministic — no wall clock, no unseeded randomness — which is the same discipline that makes the SDK unit-testable. This is precisely what Temporal's AI Foundations team builds (and the JD names the OpenAI Agents SDK explicitly).
Q: Agents SDK or LangGraph? A: Agents SDK when the shape is "an agent that occasionally delegates" and I want a light, Python-first library with handoffs and built-in tracing. LangGraph when I need an explicit, inspectable state machine — typed state with reducers, checkpoints, cyclic control flow, and precise human-in-the-loop interrupts. They interoperate and both are the same underlying loop; I pick on whether the control flow wants to be implicit (SDK) or drawn and persisted (LangGraph).
20. References
- OpenAI Agents SDK — documentation home (primitives, the agent loop, quickstart). https://openai.github.io/openai-agents-python/
- OpenAI Agents SDK — Agents. https://openai.github.io/openai-agents-python/agents/
- OpenAI Agents SDK — Running agents (
Runner,max_turns,RunResult, streaming). https://openai.github.io/openai-agents-python/running_agents/ - OpenAI Agents SDK — Tools (
@function_tool, schema generation, hosted tools). https://openai.github.io/openai-agents-python/tools/ - OpenAI Agents SDK — Handoffs (
transfer_to_*,on_handoff,input_filter). https://openai.github.io/openai-agents-python/handoffs/ - OpenAI Agents SDK — Guardrails (input/output, tripwires). https://openai.github.io/openai-agents-python/guardrails/
- OpenAI Agents SDK — Sessions (
SQLiteSession, custom memory). https://openai.github.io/openai-agents-python/sessions/ - OpenAI Agents SDK — Tracing (spans, exporters). https://openai.github.io/openai-agents-python/tracing/
- OpenAI, A practical guide to building agents (2024) — the design philosophy behind the SDK. https://cdn.openai.com/business-guides-and-resources/a-practical-guide-to-building-agents.pdf
- Temporal — durable execution + AI SDK integrations (Pydantic AI, Vercel AI SDK, ADK, OpenAI Agents SDK). https://temporal.io/ and jd.md §11.
- LangGraph docs (the explicit-graph contrast). https://langchain-ai.github.io/langgraph/
- Anthropic, Building Effective Agents (2024) — workflows vs agents; least-agentic that works. https://www.anthropic.com/research/building-effective-agents
- Cross-references in this track: Phase 00 (the loop, reliability/cost math), Phase 01 (ReAct/ReWOO runtimes), Phase 08 (durable execution / replay), Phase 10 (guardrails in depth), Phase 18 (LangGraph's explicit graph).