AI EngineeringZero to ProductionHome·About·Contact
Frontier Agent Capabilities · Part 7

Modern agent SDKs

The newer typed, lightweight agent frameworks — OpenAI Agents SDK and Pydantic AI — and how they compare to the raw loop, LangChain/LangGraph and CrewAI/AutoGen. Fewer concepts, types doing the work, and observability that comes free.

⏱️ ~1.5 hours🧪 4 labs🎯 Beginner→Tech-lead

Learning objectives

  • Explain what the newer typed/lightweight agent SDKs add over a raw loop.
  • Describe the OpenAI Agents SDK primitives: agents, handoffs, guardrails, sessions, tracing.
  • Describe Pydantic AI: type-safe agents, structured outputs, dependency injection.
  • Place them on the framework landscape vs LangChain/LangGraph and CrewAI/AutoGen.
  • Choose a framework per project and reason about migration and lock-in as a lead.

1 · Why another agent framework? essential

You already met the raw agent loop (call the model, run a tool, feed the result back, repeat) and the heavier stacks — LangChain/LangGraph and CrewAI/AutoGen. The newer SDKsOpenAI Agents SDK and Pydantic AI — sit between them: more structure than a hand-written loop, far less machinery than LangChain. Their pitch is typed, lightweight, and few-concepts: a handful of primitives you can hold in your head, with types doing the heavy lifting so bugs surface at author time, not in production.

Two things to fix up front. Despite the name, the OpenAI Agents SDK is provider-agnostic — it runs against Claude and other models through a model-adapter, not just OpenAI. And Pydantic AI is model-agnostic too, built by the Pydantic team around the same validation library that already powers structured outputs across the ecosystem.

2 · The framework landscape essential

These frameworks differ less in what they do (loop over a model + tools) than in how much they impose and what they buy you. Read the row you'd reach for first, then read up.

FrameworkWeightTyped?HandoffsGuardrailsReach for it when…
Raw loop (Ch04)noneyou add ithand-rolledhand-rolledlearning; one agent; total control
LangChain / LangGraphheavypartialgraph edgescallbacks/manualcomplex stateful graphs; big ecosystem
CrewAI / AutoGenmediumlooseroles/messagesmanualmulti-agent role play, conversations
OpenAI Agents SDKlightyesfirst-classfirst-classtyped multi-agent w/ handoffs + tracing
Pydantic AIlightyes (Pydantic)via toolsvalidatorstype-safe structured outputs + DI
Raw loop control LangGraph stateful graph CrewAI/AutoGen multi-role OpenAI Agents handoffs+guardrails Pydantic AI types+DI
🗺️ How to read this diagram

This strip lines up the agent frameworks from most hand-built on the left to most typed/lightweight on the right. Every box still runs the same underlying loop; the caption under each says what it primarily buys you.

  • Raw loop — you write the model→tool→model loop yourself. Maximum control, zero help. This is the Ch04 starting point.
  • LangGraph — wraps the loop in an explicit stateful graph; reach for it when the control flow is genuinely complex.
  • CrewAI / AutoGen — organize several agents into roles that message each other.
  • OpenAI Agents SDK — adds first-class handoffs plus guardrails and tracing with little ceremony.
  • Pydantic AI — leans on types and dependency injection so outputs validate and wiring stays clean.

In short: pick the box whose caption matches your project's hardest need — that's usually the right framework, and when two fit, choose the lighter one.

Same loop underneathEvery box here still runs the Ch04 loop: model → tool → result → model. The frameworks differ in what they wrap around it — state, typing, roles, handoffs, tracing. If you understand the loop, you understand all of them.

3 · Typed structured output (Pydantic AI shape) essential

Pydantic AI's headline feature: you declare the output type, and the agent is responsible for returning data that validates against it — no fragile string parsing. We model that shape with a plain dataclass + manual validation (no external pydantic import) so it runs offline. The real SDK does this for you with a Pydantic model.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Python · typed validated output, modeled (runs)
typed_output.pyfrom dataclasses import dataclass

@dataclass
class Triage:
    urgency: str      # low | medium | high
    category: str

def validate(d):
    """Stand-in for Pydantic validation: enforce the schema by hand."""
    if d.get("urgency") not in {"low", "medium", "high"}:
        raise ValueError(f"bad urgency: {d.get('urgency')!r}")
    if not d.get("category"):
        raise ValueError("category required")
    return Triage(urgency=d["urgency"], category=d["category"])

def run_agent(ticket):
    """Pretend the model returned this dict; the SDK would coerce+validate it."""
    raw = {"urgency": "high", "category": "billing"} if "refund" in ticket \
          else {"urgency": "low", "category": "general"}
    return validate(raw)

r = run_agent("I need a refund now")
print(type(r).__name__, "->", r.urgency, "/", r.category)
try:
    validate({"urgency": "URGENT", "category": "x"})
except ValueError as e:
    print("rejected:", e)
Triage -> high / billing
rejected: bad urgency: 'URGENT'
▶ How this works

This models Pydantic AI's headline trick — a typed output the agent must return — using only a dataclass and a hand-written check, so it runs offline. The real SDK does the validation for you from a Pydantic model.

  1. @dataclass class Triage declares the shape we expect back: an urgency and a category. Nothing more, nothing less.
  2. validate(d) is our stand-in for Pydantic: it rejects any urgency outside the allowed set and any empty category, raising ValueError loudly at the boundary.
  3. run_agent pretends the model returned a dict; we push it through validate so bad data can never reach the rest of the program.
  4. The try/except shows the guard firing: "URGENT" is not in the allowed set, so validation rejects it instead of letting it slip through.

What the output means: The good ticket becomes a Triage object (high / billing); the bad one is rejected with a clear message. That loud failure at the edge is the whole point of typed outputs.

Try this: add a third urgency like "critical" to the allowed set, or feed a dict missing category and watch which guard fires.

Types are the guardrailThe win isn't just convenience: a typed output means malformed model responses fail loudly at the boundary instead of silently corrupting downstream code. That's the same discipline as validating any external input.
Python · the real Pydantic AI shape — needs the SDK (does NOT run here)
pydantic_ai_real.py# needs the SDK:  pip install pydantic-ai
from pydantic import BaseModel
from pydantic_ai import Agent

class Triage(BaseModel):
    urgency: str
    category: str

agent = Agent("anthropic:claude-sonnet-4-5", output_type=Triage)
result = agent.run_sync("I need a refund now")
print(result.output.urgency)   # -> "high"  (validated Triage instance)

4 · Handoffs — one agent routes to another intermediate

The OpenAI Agents SDK makes handoffs a first-class primitive: a triage agent decides which specialist should take over and hands the conversation off to it. That's more structured than CrewAI's message-passing between roles and lighter than wiring LangGraph edges. Model the routing shape: an agent returns a target instead of a final answer, and the harness dispatches to it.

Python · a handoff router, modeled (runs)
handoff.pydef triage(query):
    """Return either a final answer, or a handoff target for a specialist."""
    q = query.lower()
    if "refund" in q or "charge" in q:
        return ("handoff", "billing")
    if "error" in q or "crash" in q:
        return ("handoff", "tech")
    return ("answer", "Thanks! A general agent can help with that.")

SPECIALISTS = {
    "billing": lambda q: f"[billing] Looking into the charge for: {q!r}",
    "tech":    lambda q: f"[tech] Reproducing the crash for: {q!r}",
}

def run(query, hops=0):
    kind, payload = triage(query)
    if kind == "answer":
        return payload
    if hops > 3:
        return "escalate to human"          # loop guard
    return SPECIALISTS[payload](query)       # hand off to the specialist

for q in ["I want a refund", "the app crashes on login", "what are your hours?"]:
    print(run(q))
[billing] Looking into the charge for: 'I want a refund'
[tech] Reproducing the crash for: 'the app crashes on login'
Thanks! A general agent can help with that.
▶ How this works

This models the OpenAI Agents SDK's handoff primitive: a triage agent doesn't always answer — sometimes it hands the conversation to a specialist. We model "hand off" as returning a target name the harness then dispatches to.

  1. triage(query) returns a pair: either ("answer", text) to reply directly, or ("handoff", name) to route to a specialist.
  2. SPECIALISTS maps each target name to the agent that handles it — here just small functions standing in for full agents.
  3. run looks at the pair: on "answer" it returns the text; on "handoff" it calls the named specialist. The hops > 3 line is a loop guard so routing can't spin forever.
  4. The for loop feeds three queries through and prints who ended up handling each.

What the output means: The refund goes to [billing], the crash to [tech], and the generic question is answered directly — routing decided entirely by the triage step.

Try this: add a "shipping" specialist and a keyword for it in triage, then send "where is my order?" through run.

Handoff vs sub-agent-as-toolA handoff transfers control (the target agent owns the rest of the turn); calling a sub-agent as a tool returns a value and the caller stays in charge. The SDKs support both — pick handoff for routing, tool-call for delegation you want to fold back in.

5 · Guardrails — validate before and after advanced

A guardrail is a check that runs around the agent: an input guardrail can reject a prompt before it costs a model call; an output guardrail can block or repair an unsafe/malformed response. Both SDKs support this — Agents SDK as explicit input/output guardrails, Pydantic AI via validators on the output type. Model a wrapper that enforces both.

Python · a guardrail wrapper, modeled (runs)
guardrail.pyBANNED = {"password", "ssn"}

def input_guard(prompt):
    if any(w in prompt.lower() for w in BANNED):
        raise ValueError("input blocked: sensitive term")
    return prompt

def output_guard(reply):
    if len(reply) > 200:
        return reply[:197] + "..."      # repair: clamp length
    return reply

def guarded(agent):
    """Wrap an agent fn with input + output guardrails."""
    def wrapped(prompt):
        input_guard(prompt)
        return output_guard(agent(prompt))
    return wrapped

agent = lambda p: f"Echo: {p} " + "x" * 300     # a chatty agent
safe = guarded(agent)

print(safe("hello there")[:40], "...len", len(safe("hello there")))
try:
    safe("what is my password")
except ValueError as e:
    print(e)
Echo: hello there xxxxxxxxxxxxxxxxxxxxxx ...len 200
input blocked: sensitive term
▶ How this works

This models a guardrail: checks that run around the agent. An input guard can reject a prompt before it costs a model call; an output guard can block or repair the reply. guarded wraps any agent function with both.

  1. input_guard raises if the prompt contains a banned term — stopping the call before the (pretend) model even runs.
  2. output_guard repairs rather than rejects here: an over-long reply is clamped to 200 characters instead of being thrown away.
  3. guarded(agent) returns a new function that runs the input guard, calls the agent, then runs the output guard — the classic wrapper pattern.
  4. The chatty agent lambda always returns a 300-char reply, so the output guard always clamps it; the banned-word prompt trips the input guard and raises.

What the output means: The normal prompt comes back clamped to length 200; the "password" prompt is blocked before any work happens.

Try this: add "api_key" to BANNED, or lower the 200-char clamp and watch the repaired reply get shorter.

6 · Sessions & tracing (Agents SDK) professional

Two production affordances the Agents SDK ships out of the box. Sessions persist conversation history so multi-turn state survives across calls (the same problem the memory chapter tackles, handled for you). Tracing records every step — model calls, tool calls, handoffs, guardrail decisions — as a viewable trace, so debugging a misbehaving agent is reading a timeline instead of scattering print statements. This is the real reason to adopt an SDK over a hand-rolled loop at scale: the observability comes free.

Python · the real Agents SDK shape — needs the SDK (does NOT run here)
agents_sdk_real.py# needs the SDK:  pip install openai-agents
from agents import Agent, Runner, handoff

billing = Agent(name="Billing", instructions="Handle refunds and charges.")
tech    = Agent(name="Tech",    instructions="Handle crashes and errors.")
triage  = Agent(name="Triage", instructions="Route to the right specialist.",
                handoffs=[handoff(billing), handoff(tech)])

# provider-agnostic despite the name: point at any model, e.g. Claude
result = Runner.run_sync(triage, "I want a refund")
print(result.final_output)
Don't rebuild tracing by handSessions and tracing are the parts that are tedious and error-prone to hand-roll. If your agent is going to production, the observability an SDK gives you is worth more than the hundred lines of loop it replaces.

7 · Choosing — and the lock-in question tech-lead

A lead's job here is a deliberate choice, not a fashion follow. Rough guidance: raw loop for learning or a single tightly-controlled agent; Pydantic AI when your value is in reliable typed outputs and clean dependency injection; OpenAI Agents SDK when you want multi-agent handoffs, guardrails, sessions and tracing with little ceremony; LangGraph when you genuinely need a complex stateful graph; CrewAI/AutoGen for role-playing multi-agent conversations. When two fit, prefer the lighter one.

Python · pick a framework by requirement (runs)
choose_framework.pydef choose(multi_agent, need_handoffs, typed_output, complex_state, learning):
    if learning:
        return "raw loop (Ch04) — understand the mechanics first"
    if complex_state:
        return "LangGraph — you need an explicit stateful graph"
    if multi_agent and need_handoffs:
        return "OpenAI Agents SDK — first-class handoffs + tracing"
    if typed_output:
        return "Pydantic AI — type-safe structured outputs + DI"
    if multi_agent:
        return "CrewAI / AutoGen — role-based multi-agent"
    return "raw loop — one agent, full control"

print(choose(False, False, False, False, True))    # learning
print(choose(True,  True,  False, False, False))    # handoffs
print(choose(False, False, True,  False, False))    # typed
print(choose(False, False, False, True,  False))    # stateful graph
raw loop (Ch04) — understand the mechanics first
OpenAI Agents SDK — first-class handoffs + tracing
Pydantic AI — type-safe structured outputs + DI
LangGraph — you need an explicit stateful graph
▶ How this works

This turns the "which framework?" decision into a small function you can reason about. Each if encodes one rule of thumb, checked in priority order, so the first matching condition wins.

  1. learning is checked first: if you're still learning, the answer is the raw loop — understand the mechanics before adopting a framework.
  2. complex_state routes to LangGraph; genuine stateful-graph needs are what justify its weight.
  3. multi_agent and need_handoffs picks the OpenAI Agents SDK — first-class handoffs plus tracing are its sweet spot.
  4. typed_output picks Pydantic AI; a bare multi_agent falls to CrewAI/AutoGen; everything else stays a raw loop. Order matters — the top rules pre-empt the lower ones.

What the output means: The four calls print the pick for learning, handoffs, typed output and complex state — the same guidance the tech-lead section gives, expressed as code.

Try this: reorder the ifs (e.g. move typed_output above need_handoffs) and note how the priority changes which framework a mixed requirement selects.

On lock-in: an SDK owns your control flow (its Agent/Runner/handoff types thread through everything), so switching later is a rewrite, not a config change. Mitigate it the way you would any dependency — keep your domain logic (tools, prompts, validation, business rules) in plain functions the SDK merely orchestrates, so the framework is a thin shell you can peel off. The typed shapes actually help here: a Pydantic output type or a plain tool function is portable; the orchestration around it is what you'd rewrite.

Choose wisely — the framework is the lock-inThe models are swappable (both these SDKs are provider-agnostic), but the framework is not. Its abstractions run through your whole codebase. Adopt one because you need what it adds — handoffs, guardrails, tracing, typed outputs — not for the logo. Keep domain logic in plain, portable functions so a future migration is bounded, and don't reach for a heavy stack when a raw loop or a light SDK does the job.

Exercise FA7.1 — Route with typed handoffs

Context: The Agents SDK's core — typed output, handoff routing, and guardrails — can be assembled from the three stdlib labs you already wrote, which is the best way to see the SDK isn't magic.

Your task: Combine the typed-output, handoff, and guardrail labs into one stdlib-only pipeline: a triage router returns a validated object, dispatches to the specialist it names, and the whole thing is wrapped by input/output guards.

Requirements:

  • Reuse typed_output.py's dataclass validation for the triage result
  • Dispatch to the specialist named by the validated category field
  • Wrap the pipeline with guardrail.py's input and output guards
  • Keep it stdlib-only — no SDK import
  • The result mirrors the Agents SDK core: validate → route → guard

💡 Hint: You already have all three pieces from earlier labs; the exercise is the wiring order, not new code.

Exercise FA7.2 — Make the framework call

Context: The framework choice is a judgement call a lead has to defend, and the smartest hedge against lock-in is deciding up front what logic stays in plain functions.

Your task: For three real projects — a typed extraction endpoint, a multi-agent support desk, and a complex stateful workflow — run your framework selector and justify each pick in one sentence, then name what you'd keep in plain functions for the one you'd actually build.

Requirements:

  • Run choose_framework.py for all three projects
  • Give a one-sentence justification per pick
  • For your real build, name the domain logic you'd keep framework-agnostic
  • Tie the lock-in bound back to the portable model→tool→model core

💡 Hint: The logic worth protecting is whatever a migration would otherwise force you to rewrite — usually tools and prompts, not the loop.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Place a need on the framework landscapeBeginner

Context: Every agent framework — raw loop, LangGraph, CrewAI/AutoGen, the Agents SDK, Pydantic AI — runs the same model→tool→model loop; you pick one by the project's hardest need, and reach for the lighter option when two fit.

Your task: Write a selector that recommends a framework from a one-line description of a project's hardest need, defaulting to the simplest option when nothing stronger is required.

Requirements:

  • Encode the landscape as a lookup from need → framework (control, stateful graph, multi-agent roles, typed handoffs, structured output)
  • Prefer the lighter framework when two would satisfy the need
  • Fall back to a “start simple” default for an unrecognized need
  • Pure decision logic — no SDK import required
  • Show the pick for two or three contrasting needs

💡 Hint: A plain dict keyed on the need string with a .get(need, default) is the whole selector; the teaching point is the ordering, not the code.

Show solution

Encode the landscape table as a chooser (no SDK needed — pure decision logic):

def choose_framework(need):
    table = {
        "learning / total control":       "Raw loop (Ch04)",
        "complex stateful graph":         "LangChain / LangGraph",
        "multi-agent role play":          "CrewAI / AutoGen",
        "typed multi-agent + handoffs":   "OpenAI Agents SDK",
        "type-safe structured output":    "Pydantic AI",
    }
    return table.get(need, "Raw loop — start simple, add structure only when it hurts")

print(choose_framework("type-safe structured output"))  # Pydantic AI
print(choose_framework("complex stateful graph"))        # LangGraph

Every framework runs the same model→tool→model loop underneath; pick the one whose primary benefit matches your hardest need, and when two fit, choose the lighter one.

Exercise 2 · Typed structured output (Pydantic AI shape)Intermediate

Context: Pydantic AI makes the model's reply a validated type, so a malformed field is caught at author time instead of corrupting everything downstream.

Your task: Show the correct Pydantic-AI shape: a typed result model plus an agent bound to it, and label the rung as requiring the SDK.

Requirements:

  • Define a Pydantic BaseModel result with a few typed fields (e.g. a category string, a bounded int priority, a bool)
  • Construct an Agent(model, output_type=Model) and call run_sync
  • Read the validated result off result.output
  • Note the code needs pip install pydantic-ai
  • The model id is provider-agnostic (the lesson runs it against Claude)

💡 Hint: The single load-bearing idea is output_type= — it forces the reply through the model's validation before your code ever sees it.

Show solution

The typed-output pattern — needs pip install pydantic-ai (Pydantic AI is model-agnostic):

from pydantic import BaseModel
from pydantic_ai import Agent

class Ticket(BaseModel):
    category: str
    priority: int      # 1-5
    needs_human: bool

agent = Agent(
    "anthropic:claude-sonnet-4-5",   # model-agnostic; Claude here
    output_type=Ticket,               # output is validated into Ticket
)
result = agent.run_sync("Card declined twice, customer furious.")
print(result.output.category, result.output.priority)   # a validated Ticket

Because output_type is a Pydantic model, a malformed field is caught by validation instead of blowing up downstream — types do the heavy lifting so bugs surface early.

Exercise 3 · Handoffs — one agent routes to anotherAdvanced

Context: The Agents SDK makes handoffs first-class: a triage agent can transfer control to a specialist instead of you hand-rolling a routing table.

Your task: Show the correct handoff shape — two specialist agents and a triage agent that routes to them — and label the rung as requiring the SDK.

Requirements:

  • Create two specialist Agents with distinct instructions
  • Create a triage Agent with handoffs=[a, b]
  • Run through Runner.run_sync(triage, …) and read final_output
  • Note the code needs pip install openai-agents
  • The SDK is provider-agnostic; handoff routing is declarative, not hand-coded

💡 Hint: Let the triage agent decide the transfer — you declare the candidates in handoffs, you don't write the if/else.

Show solution

First-class handoffs — needs pip install openai-agents (the SDK is provider-agnostic; runs against Claude via an adapter):

from agents import Agent, Runner

billing = Agent(name="Billing", instructions="Handle refunds and charges.")
tech    = Agent(name="Tech",    instructions="Handle login and errors.")

triage = Agent(
    name="Triage",
    instructions="Route the user to the right specialist.",
    handoffs=[billing, tech],      # first-class: triage can hand off to either
)

result = Runner.run_sync(triage, "I was double-charged for my subscription.")
print(result.final_output)          # answered by the Billing agent after handoff

Instead of hand-rolling routing, you declare the specialists in handoffs and the triage agent transfers control — the structure the SDK buys you over a raw loop.

Exercise 4 · Guardrails — validate before and afterExpert

Context: Guardrails validate inputs before the agent runs (and outputs before they return), giving you an author-defined boundary that sits outside the model call.

Your task: Show the correct input-guardrail shape: a guard that trips on a simple policy violation and halts the run before the model executes. Label it as requiring the SDK.

Requirements:

  • Use the @input_guardrail decorator on a guard function
  • Return a GuardrailFunctionOutput with a tripwire_triggered flag
  • Implement one naive check (e.g. a 9-digit all-numeric token looks like an SSN)
  • Attach it via Agent(…, input_guardrails=[guard])
  • A tripped tripwire raises and stops before the model runs

💡 Hint: The tripwire boolean is the contract — when it's true the runner raises, so the expensive model call never happens.

Show solution

Input/output guardrails — needs pip install openai-agents:

from agents import Agent, Runner, input_guardrail, GuardrailFunctionOutput

@input_guardrail
def block_pii(ctx, agent, user_input: str) -> GuardrailFunctionOutput:
    contains_ssn = any(part.isdigit() and len(part) == 9
                       for part in user_input.split())
    return GuardrailFunctionOutput(
        output_info={"pii": contains_ssn},
        tripwire_triggered=contains_ssn,      # True -> halt before the model runs
    )

agent = Agent(name="Support", instructions="Help the user.",
              input_guardrails=[block_pii])
# Runner.run_sync raises if the tripwire trips, stopping unsafe input early.

Guardrails run outside the model call, so a tripwire (like detected PII) halts the request before it ever reaches the model — validation at author-defined boundaries, not buried in prompts.

Exercise 5 · Sessions & tracing (Agents SDK)Professional

Context: Sessions persist conversation state across runs and tracing records every step, so multi-turn memory and observability come for free instead of being threaded by hand.

Your task: Show the correct shape for a persisted session: two turns where the second recalls a fact stated in the first. Label the rung as requiring the SDK.

Requirements:

  • Create a SQLiteSession(id, db_path)
  • Pass session= into two consecutive Runner.run_sync calls
  • Turn two answers using a fact only given in turn one
  • State is recalled automatically — no manual history threading
  • Note tracing records each run's steps and tool calls for later inspection

💡 Hint: The same session object handed to both runs is what carries memory across the gap; you never rebuild the transcript yourself.

Show solution

Sessions + tracing — needs pip install openai-agents:

from agents import Agent, Runner, SQLiteSession

agent = Agent(name="Assistant", instructions="Be helpful and remember context.")
session = SQLiteSession("user-123", "conversations.db")  # persisted across runs

# Turn 1
Runner.run_sync(agent, "My name is Dana.", session=session)
# Turn 2 — the session carries the earlier turn's context automatically
r = Runner.run_sync(agent, "What's my name?", session=session)
print(r.final_output)     # knows "Dana" from the persisted session

# Tracing is built in: each run records steps/tool-calls for later inspection.

A session stores the running conversation so the next turn has context without you threading it manually, and built-in tracing gives you a step-by-step record to debug and audit what the agent did.

Exercise 6 · Choose a framework and reason about lock-inIndustry scenario

Context: As a lead you don't just pick a framework — you plan for the day you leave it. Lock-in lives in a framework's proprietary primitives; the portable model→tool→model core survives any migration.

Your task: Write logic that both recommends a framework for a project and scores its lock-in risk, emitting a short migration plan alongside the pick.

Requirements:

  • Recommend from the project's needs (types wanted? multi-agent?)
  • Rate lock-in per framework: Pydantic AI LOW (portable models), Agents SDK MEDIUM (SDK-specific handoffs/sessions), raw loop NONE
  • Return a (framework, lock-in, migration-plan) triple
  • The migration plan keeps tool functions and prompts framework-agnostic
  • Reinforce that lock-in concentrates in proprietary extras, not the core loop

💡 Hint: Ask what a rewrite would have to re-implement: if it's just glue around your own tool functions and prompts, lock-in is low.

Show solution

Recommend a framework and quantify how hard it would be to leave (pure decision logic):

def evaluate(need, team_wants_types, multi_agent):
    if need == "structured output" and team_wants_types:
        fw, lockin = "Pydantic AI", "LOW — Pydantic models are portable"
    elif multi_agent:
        fw, lockin = "OpenAI Agents SDK", "MEDIUM — handoffs/sessions are SDK-specific"
    else:
        fw, lockin = "Raw loop (Ch04)", "NONE — you own the loop"
    plan = ("keep tool functions + prompts framework-agnostic so the model→tool→model "
            "core ports even if you switch")
    return fw, lockin, plan

fw, lock, plan = evaluate("structured output", team_wants_types=True, multi_agent=False)
print("framework:", fw)
print("lock-in  :", lock)
print("migration:", plan)

Every SDK wraps the same loop, so lock-in lives in the proprietary extras (handoffs, sessions). Keep your tools and prompts framework-agnostic and the portable core survives a migration — choose the lighter framework when two fit.

✓ Checkpoint — you can move on when you can…

  • Explain what typed/lightweight SDKs add over a raw loop and over LangChain/CrewAI.
  • Describe agents, handoffs, guardrails, sessions and tracing (Agents SDK).
  • Describe Pydantic AI's typed outputs and dependency injection.
  • Model a handoff router, a typed validated output, and a guardrail wrapper on stdlib.
  • Pick a framework per project and bound the lock-in by keeping domain logic portable.

Knowledge check check yourself

✓ Knowledge check

Where do the newer SDKs (OpenAI Agents SDK, Pydantic AI) sit relative to the raw loop and LangGraph/CrewAI, and are they tied to one provider?

Show answer
They sit between the hand-built raw loop and the heavier stacks: fewer concepts, with types doing the work and observability built in. Both are provider-agnostic despite their names — the OpenAI Agents SDK runs against Claude and others via a model adapter, and Pydantic AI is model-agnostic too.
✓ Knowledge check

What does declaring a typed output (the Pydantic AI shape) buy you, and where does a guardrail run?

Show answer
Declaring the output type makes the agent responsible for returning data that validates against the schema, replacing fragile string parsing with a loud failure at the edge. A guardrail runs around the agent: an input guard can reject a prompt before it costs a model call, and an output guard can block or repair an unsafe/malformed reply.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in