« Phase 21 README · Warmup · Hitchhiker's · Deep Dive · Principal Deep Dive · Core Contributor

Phase 21 — Staff Engineer Notes: the OpenAI Agents SDK

Anyone can pip install openai-agents, decorate a function, and call Runner.run. That is using the SDK. Owning the platform around it — the thing an interviewer for a Staff role is actually probing for — is a different skill: knowing which framework the problem wants, where the SDK stops and your code starts, and which "fix" is really a symptom being suppressed. These are the judgment calls, the review reflexes, and the war stories that separate the two.

The decisions you actually own

A staff engineer is not paid to know the API. You are paid to make four or five decisions that are expensive to reverse and that a mid-level engineer will get wrong by defaulting.

1. Agents SDK vs LangGraph vs CrewAI. Decide on the control-flow axis, not on vibes or GitHub stars. Reach for the Agents SDK when the shape is "an agent that occasionally delegates" and you want a light, Python-first library with handoffs and built-in tracing — the flow is implicit and you are happy for it to be. Reach for LangGraph when you need the control flow to be drawn and persisted: typed state with reducers, checkpoints, cyclic graphs, precise human-in-the-loop interrupts. Reach for CrewAI to prototype a role-playing team fast. The honest part, which is itself the senior signal: they overlap more than the marketing admits, they interoperate (a LangGraph node can call an SDK agent), and all three are while not done: proposal = model(scratchpad); execute with different ergonomics. If your answer is "LangGraph because it's more powerful," you have failed the question — power is not the axis; do you need to see and checkpoint the state machine is the axis.

2. Handoff vs agents-as-tool. This is the classic probe because the wrong choice compiles and runs. A handoff transfers control — the target owns the reply, last_agent becomes the target — use it for triage/routing where a specialist should fully own the answer. 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. The tell in code review is last_agent: if someone expects the specialist to own the conversation but built it as a tool, last_agent will always be the orchestrator and the routing is a lie.

3. When to wrap in Temporal. Not "always" — that is over-engineering a chat endpoint. Wrap the loop in Temporal when the run is long (minutes, not seconds), high-value (re-paying for it hurts), or has non-idempotent side effects mid-run (it charged a card at step 4 and you cannot afford to double-charge on retry). For a 3-second chat, in-memory is correct and Temporal is a liability. Knowing where that line is — and being able to say "this run doesn't need durability" with confidence — is as senior as knowing how to add it.

4. Where safety lives. Guardrails are one layer, not the layer. The architectural controls — least-privilege tools, the trust boundary at on_invoke_tool, sandboxing, authorization, human approval — live in your code around the SDK. A candidate who says "we added guardrails, so it's safe" has told you they think safety is a feature you install rather than a property you design.

Code-review red flags

These are the lines that make me stop the review and ask a question:

  • Raising max_turns to fix a run that hit the cap. The cap is a reliability guard, not a throttle. Hitting it means the model is looping without converging; more turns buys more identical loops. The fix is verification, a handoff, or a better tool result — change what the model sees, not the budget.
  • A guardrail on the same expensive model as the agent. The entire economic point of a guardrail is that it sits on the cheap side of the cost curve. A guardrail as slow and costly as the agent doubles your bill to save nothing. If it needs a yes/no, give it a yes/no model.
  • session_id that isn't scoped to an authenticated user. That is a cross-tenant data leak dressed up as memory. session_id is a security boundary; if it is derived from a conversation id without tying to the principal, one customer can read another's history.
  • get_items that returns the entire history unpruned. Every turn now re-sends a growing context — linear cost per turn, quadratic over the conversation. Production sessions prune or summarize inside get_items. If there is no pruning policy, ask what happens at turn 200.
  • Non-idempotent tools with no idempotency key, run without durability. The first crash-and-retry double-fires the side effect. Either make the tool idempotent or make the run durable; "it hasn't crashed yet" is not a plan.
  • Collapsing ModelBehaviorError into a generic tool error. You have lost the ability to distinguish "the world failed, retry" from "the model is broken, alert." Those want different responses.

War stories

  • The agent that "handed off" but kept answering. Someone built an agent-as-tool expecting a handoff. Control kept returning to the orchestrator, the specialist never owned the reply, and last_agent was always the triage agent. One line — make it a real handoff — fixed the routing. The lesson: know which mechanism you're using; both compile.
  • The guardrail that doubled the bill. A team ran their input guardrail on the same big model as the agent "to be accurate." Every request now paid for two big-model calls. Swapping to a tiny fast model cut the cost back and lost nothing — the guardrail only ever needed a yes/no.
  • The two agents that ping-ponged forever. Triage handed off to billing, billing handed back to triage, repeat, until the timeout. There was no max_turns counting across handoffs. The budget is a guard, and it must count across the whole run, not per agent.
  • The crash that cost real money. A 12-minute research agent died at minute 10 on a deploy. The in-memory loop restarted from zero and re-paid for every step. Wrapping the Runner in Temporal made every turn a durable step; the restart resumed at minute 10. This is the exact problem Temporal's AI Foundations team ships an integration for — and the JD names the OpenAI Agents SDK by name.

The signal an interviewer is listening for

When asked "walk me through the Agents SDK," a weak answer recites the four primitives as a feature list. A strong answer says: it is few primitives over one loop; the loop lives in the Runner; tools, handoffs, guardrails, and sessions are all things that loop knows how to interpret; and the model only ever proposes — my code executes, at the trust boundary. The interviewer is listening for whether you locate the mechanism (the loop, the boundary) rather than the surface (the decorators). The follow-up they love is "when would you not use this?" — and the senior answer is a control-flow argument (need to checkpoint state → LangGraph; need durability → Temporal wrap) capped with the most senior move of all: sometimes the right answer is that this shouldn't be an agent at all — a workflow with one fuzzy step beats an agent for anything you can enumerate.

Closing takeaways

  1. The loop is the product. Everything else is a perturbation of one generator. Own the loop and the boundary, and the whole SDK — and the case for wrapping it — falls out.
  2. Choose frameworks on the control-flow axis. Implicit-and-light (SDK), drawn-and-persisted (LangGraph), roles-and-tasks (CrewAI). Not on power.
  3. Guardrails are cost-shaping, and they belong on the cheap side. A guardrail on the expensive model is a review-stopping mistake.
  4. session_id is a tenant boundary and a context bomb. Scope it to the principal; prune inside get_items.
  5. Durability is a decision, not a default. Wrap in Temporal for long, high-value, or side-effecting runs; leave it off for a 3-second chat.
  6. The most senior instinct is "this shouldn't be an agent." The best agent engineers reach for the least-agentic thing that works.