Lab 01 — Agent + Runner Loop + Function Tools
Phase 21 · Lab 01 · Phase README · Warmup
The problem
The OpenAI Agents SDK is famous for being tiny: a few primitives (Agents, Handoffs, Guardrails,
Sessions) over one engine — the agent loop inside Runner. If you can only call
Runner.run(agent) you are a user; if you can rebuild the loop, you can own the deployment. So
build it: an Agent, a @function_tool decorator that auto-derives the tool schema from a Python
signature, and a Runner whose loop calls the model, runs the tools it asked for, feeds the
results back, and repeats until a final output — with a max_turns guard. The model is
injected as a deterministic policy, so the whole thing is offline and testable.
What you build
| Piece | What it does |
|---|---|
function_tool | decorator: Python function → FunctionTool, deriving the JSON schema from the signature (types + which params are required) and the description from the docstring |
Agent | name, instructions, tools, model (injected policy), output_type; rejects duplicate tools |
Runner._run_turns | the loop: model → tool calls → observations → repeat → final output; max_turns guard |
Runner.run_sync / run / run_streamed | the three entry points, all driving the one loop |
RunResult | final_output, new_items, last_agent — the real result surface |
File map
| File | Role |
|---|---|
lab.py | your implementation (fill the # TODOs) |
solution.py | reference + a worked example (python solution.py) driving a tool-using agent |
test_lab.py | 20 tests: schema derivation, the loop, max_turns, unknown-tool + tool-exception, streaming, async, output_type, determinism |
requirements.txt | pytest only |
Run it
pytest test_lab.py -v # your lab.py (red until you implement)
LAB_MODULE=solution pytest test_lab.py -v # the reference (green)
python solution.py # the worked example
Success criteria
-
@function_toolturnsdef add(x: int, y: int, note: str = "")into a schema whosepropertiescarry the right JSON types and whoserequiredis exactly["x", "y"]. -
A no-tool agent returns its final output in one turn; a tool-using agent loops
(tool call →
ToolCallOutputItemfed back → final message). -
RunResult.new_itemscontains theToolCallItemandToolCallOutputItem, andlast_agentis the agent that finished. -
max_turnsraisesMaxTurnsExceeded; an unknown tool raisesModelBehaviorError; a tool that itself raises becomes anERROR:observation the loop can recover from. -
run_streamed().stream_events()yields the items in order and matchesrun_sync; the asyncRunner.rundrives the same loop;output_type=intcoerces the final output. -
All 20 tests pass under both
labandsolution.
How this maps to the real stack
Agent/Runnerare the SDK's real classes.Runner.runis async,run_syncwraps it,run_streamedreturns aRunResultStreamingyou iterate withasync for event in result.stream_events()— exactly the surface here. The real loop also threads a run context, a tracing span per turn, andModelSettings; the control flow is what you built.@function_toolis real. The SDK derives the JSON schema from the signature via pydantic (solist[int], nested models, enums,Literals, and rich descriptions from docstring arg sections all work); we useinspectto make the signature→schema step legible.on_invoke_toolis the SDK's real field name for the executor.- The loop — "call the model; if it returned tool calls, run them and feed results back; else
it's the final output; stop at
max_turns" — is verbatim the SDK'sRunnerbehavior, and it is the same loop as a LangGraph ReAct node (Phase 18) and the ReAct runtime (Phase 01). The framework's value is the ergonomics around it, not a different algorithm. output_typemaps to the SDK's structured-output feature: pass a pydantic model and the final output is parsed/validated into it. Our callable (int) is the one-line stand-in.
Limits. A real model is non-deterministic and emits native tool-call JSON that must be parsed and validated (Phase 02); real tools are async and may need retries/timeouts (Phase 08); the real schema derivation is far richer than six scalar types. The engine you built — the loop, the turn budget, tool dispatch, the result surface — is the faithful part.
Extensions (your own machine)
- Install the real SDK (
pip install openai-agents), define the sameget_weathertool with@function_tool, and diff your derived schema againsttool.params_json_schema. - Add
ModelSettings(temperature,tool_choice,parallel_tool_calls) as a dataclass and have the loop respecttool_choice="required"(must call a tool) vs"none"(must not). - Add a
RunContextWrapperpassed to each tool so tools can read run-scoped dependencies — the SDK's dependency-injection story.
Interview / resume signal
"Rebuilt the OpenAI Agents SDK core —
Agent, a@function_tooldecorator that auto-derives the JSON schema from the Python signature, and aRunnerimplementing the agent loop (model → tool dispatch → feed results back → repeat, guarded bymax_turns) with sync, async, and streaming entry points over one shared generator, plus structuredoutput_typecoercion. The model is injected as a deterministic policy, so the loop is unit-tested offline."