Lesson 06 A Graph-Shaped Pipeline Agent
Theme:
Lesson 06 · Slide 1

Lesson 06 — A Graph-Shaped Pipeline Agent

LangGraph at its sweet spot: planner → fetcher → critique branch → writer, with checkpoint resume.

Agents Module 3 LangGraph Pipeline

Lesson 06 · Slide 2

Why this is LangGraph's sweet spot

Hits every condition on L05 Slide 11's “Pick it when” list.

Lesson 06 · Slide 3

The agent we're building

Goal

Given a research question, produce a structured short report — but unlike L04, do it as an explicit pipeline: plan the sources, fetch them, critique the draft, revise if needed, then write the final.

Real example question: “What changed in Linux kernel 6.14's networking stack?”

Same question as L04 — so the reader can compare the two agents' shapes, not their outputs.

Lesson 06 · Slide 4

The four pipeline stages

StageWhat it doesNode function
planLLM picks 2–4 search queries given the question.plan_sources
fetchRun each search query against the mocked PKB / web index, collect snippets.fetch_sources
critiqueLLM looks at the draft + sources, decides if revision is needed.critique_draft
writeLLM produces a 3-bullet structured draft from current notes.write_draft

Four nodes, each a plain Python function. No subclassing, no decorators — just functions of state.

Lesson 06 · Slide 5

The graph topology

START
  -> plan_sources
  -> fetch_sources
  -> write_draft
  -> critique_draft
       |-- revise == True  -> write_draft  (loop, max 2 revisions)
       |-- revise == False -> END

Note the critique-then-loop-back-to-write edge. This is the shape you cannot draw in a chain.

Lesson 06 · Slide 6

The state schema

from typing import TypedDict, Annotated
import operator

class PipelineState(TypedDict):
    question: str
    plan: list[str]                            # search queries
    notes: Annotated[list[str], operator.add]  # accumulating snippets
    draft: dict                                # {intro, bullets, sources}
    revise: bool                               # critique sets this
    revisions: int                             # bailout counter

notes uses the operator.add reducer so each fetch appends; revisions is the loop bailout.

Lesson 06 · Slide 7

The conditional edge after critique

def route_after_critique(s: PipelineState) -> str:
    if s["revise"] and s["revisions"] < 2:
        return "revise"
    return "finish"

g.add_conditional_edges(
    "critique_draft",
    route_after_critique,
    {"revise": "write_draft", "finish": END},
)

The revisions < 2 bailout matters: even if the critique keeps demanding revisions, the loop stops after two passes.

Lesson 06 · Slide 8

What's in a node — the writer

def write_draft(s: PipelineState) -> dict:
    notes_blob = "\n".join(s["notes"])
    prompt = (
        f"Question: {s['question']}\n\nNotes:\n{notes_blob}\n\n"
        "Write a JSON object with keys 'intro' (string), "
        "'bullets' (list of 3 short strings), 'sources' (list of strings)."
    )
    msg = client.messages.create(
        model="claude-3-5-sonnet-latest",
        max_tokens=600,
        messages=[{"role": "user", "content": prompt}],
    )
    draft = json.loads(msg.content[0].text)
    return {"draft": draft, "revisions": s["revisions"] + 1}

Node calls anthropic directly, parses JSON, returns a partial state update. No SDK abstraction. Increments the revision counter on every write.

Lesson 06 · Slide 9

Compile + checkpoint setup

memory = SqliteSaver.from_conn_string("pipeline.db")
app = g.compile(checkpointer=memory)

config = {"configurable": {"thread_id": f"pipeline-{question[:30]}"}}
result = app.invoke(
    {"question": question, "plan": [], "notes": [], "draft": {},
     "revise": False, "revisions": 0},
    config=config,
)

thread_id is per-question: reruns of the same question resume; reruns of new questions start fresh.

Lesson 06 · Slide 10

A real run trace

[node plan_sources]      plan=['kernel 6.14 networking', 'BIG TCP IPv4']
[node fetch_sources]     notes_count=4
[node write_draft]       draft.bullets=3  revisions=1
[node critique_draft]    revise=True
[node write_draft]       draft.bullets=3  revisions=2
[node critique_draft]    revise=False
[END]                    final report ready

Two write passes; the second clears critique. The bailout counter would have stopped a third even if critique stayed unhappy.

Lesson 06 · Slide 11

Crash resume — the killer demo

Notes: This is the single best argument for spending the extra lines on a graph framework — partial-failure recovery is a one-liner instead of an architecture change.
Lesson 06 · Slide 12

Human-in-the-loop with interrupt_before

app = g.compile(
    checkpointer=memory,
    interrupt_before=["critique_draft"],
)
Lesson 06 · Slide 13

What's left to wire (Module 6 hook)

Plan F wires this exact agent.py into agents/_eval/run_all.py as the LangGraph row of the cross-framework scorecard. The L11 lab compares this agent's cost / latency / lines-of-code against the L04 Claude SDK agent and the L08 / L10 agents from Modules 4–5.

Today's agent.py is the canonical LangGraph entry. Don't rename run_pipeline_agent — L11 imports it by name.

Lesson 06 · Slide 14

Recap & what's next

Coming up: Module 4 — OpenAI Agents SDK. Same problem shape becomes a triage-and-handoff between specialist sub-agents.
1 / ?