« 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-agents package — the mechanism is the point.

Table of Contents

  1. What the OpenAI Agents SDK is (and the philosophy)
  2. The four primitives: Agents, Handoffs, Guardrails, Sessions
  3. The agent loop inside the Runner
  4. The Agent: a declarative bundle
  5. Function tools and the auto-generated schema
  6. The trust boundary: proposing vs executing a tool
  7. Runner APIs, RunResult, and the turn budget
  8. Structured output
  9. Handoffs: the SDK's multi-agent mechanism
  10. Handoffs vs agents-as-tools: two multi-agent styles
  11. Guardrails and tripwires
  12. Sessions and automatic memory
  13. Tracing and observability
  14. Agents SDK vs LangGraph vs CrewAI
  15. The Temporal integration: durable agents
  16. Common misconceptions
  17. Lab walkthrough
  18. Success criteria
  19. Interview Q&A
  20. 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:

PrimitiveWhat it isThe one-line mechanism
Agentan LLM configured with instructions, tools, handoffs, guardrails, and an output typea declarative bundle the Runner executes
Handoffa way for one agent to delegate to anothera tool named transfer_to_<agent> that switches the active agent
Guardraila fast check that runs alongside the agenta function returning a tripwire_triggered bool that can abort the run
Sessionautomatic conversation memorya 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 a RunResult. Every primitive above is something the Runner knows 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 Runner does everything real. The output is a proposal. Whether a tool actually runs is a decision the Runner makes on the trusted side (§6).
  • max_turns is 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 the ToolError-as-observation pattern in Phase 01. A hallucinated tool name (the model calls a tool the agent doesn't have) is different: that's a ModelBehaviorError, 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 a RunResult.
  • Runner.run_sync(agent, input) — a synchronous wrapper for scripts and notebooks.
  • Runner.run_streamed(agent, input) — returns a RunResultStreaming immediately; you iterate async 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 if output_type is 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 an input_type so 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:

HandoffAgent-as-tool
control after the calltransfers — the target finishes the runreturns to the orchestrator
last_agentthe target (specialist)the orchestrator
conversationthe 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 whentriage → one specialist should fully own the replyyou 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 InputGuardrailTripwireTriggered and 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:

  1. reads the session's stored items and prepends them to this turn's input, and
  2. 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 SDKLangGraph (Phase 18)CrewAI
core abstractionfew primitives over one loopan explicit graph of nodes/edges + persisted stateroles (agents) + tasks in a crew/process
control flowthe Runner loop; branching via handoffsyou draw it: nodes, conditional edges, cycles, fan-out/ina process (sequential/hierarchical) orchestrates roles
stateconversation items + sessionsa typed state object with per-channel reducerstask context passed between roles
multi-agenthandoffs (switch agent) or agents-as-toolssupervisor/worker subgraphs, explicit routingrole delegation within a crew
durability / HITLvia Temporal wrap (§15) or your owncheckpointers + interrupts built in (Phase 18 labs 02/03)limited; add your own
feels likewriting Python with a tiny agent librarybuilding a state machine / dataflowassigning a team of personas
reach for it whenlight, Python-first agent with handoffs; you don't want a graphyou need explicit state, checkpoints, complex/cyclic control flow, precise HITLfast 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 Runner executes it on the trusted side via on_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_agent becomes 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_id is a tenant boundary (Phase 13) and the place context growth bites (Phase 04) — production sessions need pruning and isolation, not just append.
  • "max_turns is 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.

  1. Lab 01 — Agent Runner & Tools (§3§8). Build function_tool (auto-schema from the signature), Agent, and the Runner loop as one generator behind run_sync / run / run_streamed, with output_type coercion, max_turns, unknown-tool ModelBehaviorError, and tool-exception-as-observation. 20 tests.
  2. Lab 02 — Handoffs (§9§10). Generate transfer_to_<agent> names, index handoffs, switch the active agent on a transfer call, and support on_handoff + input_filter; multi-hop; max_turns across handoffs. 20 tests.
  3. Lab 03 — Guardrails & Sessions (§11§12). Build input/output guardrails with tripwires (input halts before the model runs) and a real sqlite Session giving 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 Runner loop (call model → tools/handoff → repeat → final / max_turns) and say where each primitive plugs in.
  • You can explain how @function_tool derives a schema from a signature, and why a param with a default is not required.
  • You can state the trust boundary in SDK terms: the model proposes a tool call; on_invoke_tool executes it; a tool exception is an observation, an unknown tool is a ModelBehaviorError.
  • You can explain a handoff as "a transfer_to_<agent> tool that switches the active agent," and contrast it with agents-as-tools (last_agent is the tell).
  • You can explain the guardrail tripwire pattern and why guardrails should run a cheap model.
  • You can explain how a Session gives automatic cross-run memory, and why session_id is 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 lab and solution.

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 isolationsession_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).