Topic 8 of 15 · Core patterns

Agentic workflows.

Topic 5's tool-calling loop already does something agent-like — it keeps calling tools until it has enough to answer. An agent is that same loop given a goal instead of a single question, the freedom to decide its own next step, and explicit limits on how far it's allowed to wander before you make it stop.

The mental model

A while loop with judgment instead of a condition.

A regular program's control flow is decided by your code: if, while, fixed pipelines. An agent's control flow is decided by the model, turn by turn — it looks at where it is, decides the next action, and only your guardrails (Section 5) keep it from running forever. This is a real tradeoff, not a free upgrade: you're trading predictability for the ability to handle tasks you didn't fully anticipate.

Section 0

What Makes It Agentic

PatternWho decides the next stepExample
Single callYou, entirely — one prompt in, one response outTopic 2's chat completion
Fixed chainYou, at design time — a hardcoded sequence of callsTopic 7's retrieve-then-generate pipeline
Tool-calling loopThe model, per turn, but bounded to a known tool setTopic 5's weather/calculator loop
AgentThe model, across many turns, toward an open-ended goal"Research X and summarize the findings" — the number and order of steps isn't known in advance
There's no hard line between "tool-calling loop" and "agent" — it's a spectrum of how much autonomy you hand over. Topic 5's loop becomes agentic the moment the task is open-ended enough that you can't predict the sequence of tool calls in advance.
Section 1

The ReAct Pattern

ReAct (Reason + Act) is the foundational agent pattern: at each step, the model explicitly reasons about what it knows and what it needs, then takes one action, observes the result, and repeats. Making the reasoning explicit — not just the action — is what makes the process inspectable and debuggable.

Thought: what do
I need next?
Action: call
a tool
Observation:
read the result
Repeat, or give
a final answer
system_prompt = """You are a research assistant. For each step, think about
what you need, take one action using an available tool, then observe the
result before deciding the next step. When you have enough information,
give a final answer instead of calling another tool."""
Modern tool-calling APIs (Topic 5) implement this pattern for you structurally — the model's tool call is the action, and the message you append back is the observation. You rarely hand-write "Thought:/Action:/Observation:" text anymore; the loop mechanics from Topic 5 already encode ReAct.
Section 2

Building the Loop

This is Topic 5 Section 1's loop again, with one change: the stopping condition is no longer "the model didn't call a tool" alone — it's that, or a guardrail from Section 5 kicking in.

def run_agent(goal: str, tools: dict, tool_schemas: list[dict],
              max_steps: int = 8) -> str:
    client = OpenAI()
    messages = [
        {"role": "system", "content": "Work toward the goal step by step, "
                                       "using tools as needed. Stop and answer "
                                       "once you have enough information."},
        {"role": "user", "content": goal},
    ]

    for step in range(max_steps):
        response = client.chat.completions.create(
            model="gpt-4.1-mini", messages=messages, tools=tool_schemas,
        )
        message = response.choices[0].message
        messages.append(message)

        if not message.tool_calls:
            return message.content  # the model decided it's done

        for call in message.tool_calls:
            args = json.loads(call.function.arguments)
            result = tools[call.function.name](**args)
            messages.append({"role": "tool", "tool_call_id": call.id,
                              "content": json.dumps(result)})

    return "Reached the step limit without a final answer."
Section 3

Memory: Short-Term vs Long-Term

KindWhat it isWhere it lives
Short-term (working)The current run's messages list — everything said and done so far in this taskIn memory, discarded when the run ends, bounded by the context window (Topic 2 Section 4)
Long-termFacts, past interactions, or documents that should persist across runsA vector store (Topic 6/12) the agent retrieves from as one of its tools
Long-term memory is just RAG (Topic 7) called from inside an agent loop: a search_past_notes(query) tool that retrieves relevant history and hands it back as an observation, exactly like any other tool result.
Section 4

Planning

For genuinely multi-step goals, letting the model plan explicitly before acting — write out the steps, then execute them one at a time — improves reliability over pure step-by-step improvisation, for the same reason Topic 3's chain-of-thought section improved single-call reasoning.

PLANNING_PROMPT = """Before taking any action, write a short numbered plan
for how you'll achieve the goal. Then execute it one step at a time,
revising the plan if a step's result changes what's needed."""
Planning is a prompting technique, not a different architecture — it still runs through the same loop from Section 2. The plan itself is just more context the model reasons over on each turn, the same way few-shot examples (Topic 3) are context that shapes behavior without changing the request shape.
Section 5

Stopping Conditions & Guardrails

An agent with no limits is a while(true) with an API bill attached. Every production agent needs explicit, code-enforced limits — never rely on the model to decide to stop on its own.

GuardrailProtects against
Max steps (Section 2's max_steps)Infinite tool-call loops
Cost cap (sum token usage per run, per Topic 2 Section 5)A single runaway task burning an unbounded budget
Timeout (wall-clock, not just step count)Slow tool calls stalling the whole run
Tool allowlist per task (Topic 5 Section 6)An agent reaching for a destructive tool it doesn't need for this goal
Repetition detection (same tool + same args twice in a row)The model stuck retrying an approach that isn't working
Section 6

When Not to Use an Agent

The steps are actually knownIf you can write the sequence of calls yourself — retrieve, then generate (Topic 7) — a fixed chain is more reliable, cheaper, and easier to debug than letting the model rediscover the same sequence every run.
Latency or cost is tightEach agent step is a full round trip to the model. A task an agent solves in 6 steps, a fixed pipeline often solves in 1-2 calls.
Default to the simplest pattern that solves the problem: single call → fixed chain → tool-calling loop → agent, in that order. Reach for the next level of autonomy only when the previous one demonstrably can't handle the task's variability.
Section 7 · Checkpoint

Capstone: A Small Research Agent

An agent with two tools — a fake web search and Topic 6's semantic search over a small knowledge base — that has to combine both to answer a question neither tool alone can fully answer.

import json
from openai import OpenAI

def fake_web_search(query: str) -> dict:
    fake_results = {
        "pgvector release date": "pgvector was first released in 2021.",
    }
    match = next((v for k, v in fake_results.items() if k in query.lower()), None)
    return {"result": match or "No web results found."}

def search_notes(query: str) -> dict:
    index = TinySearchIndex([  # from Topic 6
        "Our team adopted pgvector for the RAG pipeline in March 2024.",
        "The support team uses a separate ticketing system, not covered here.",
    ])
    results = index.search(query, top_k=1)
    return {"result": results[0][0] if results else "No notes found."}

TOOLS = {"web_search": fake_web_search, "search_notes": search_notes}
TOOL_SCHEMAS = [
    {"type": "function", "function": {"name": "web_search",
        "description": "Search the public web for general facts.",
        "parameters": {"type": "object", "properties":
            {"query": {"type": "string"}}, "required": ["query"]}}},
    {"type": "function", "function": {"name": "search_notes",
        "description": "Search our team's internal notes.",
        "parameters": {"type": "object", "properties":
            {"query": {"type": "string"}}, "required": ["query"]}}},
]

def run_agent(goal: str, max_steps: int = 6) -> str:
    client = OpenAI()
    messages = [
        {"role": "system", "content": "Answer using both web_search and "
                                       "search_notes as needed. Combine facts "
                                       "from both if the question needs it."},
        {"role": "user", "content": goal},
    ]
    for _ in range(max_steps):
        response = client.chat.completions.create(
            model="gpt-4.1-mini", messages=messages, tools=TOOL_SCHEMAS,
        )
        message = response.choices[0].message
        messages.append(message)
        if not message.tool_calls:
            return message.content
        for call in message.tool_calls:
            args = json.loads(call.function.arguments)
            result = TOOLS[call.function.name](**args)
            messages.append({"role": "tool", "tool_call_id": call.id,
                              "content": json.dumps(result)})
    return "Reached the step limit without a final answer."


if __name__ == "__main__":
    print(run_agent(
        "How long had pgvector existed before our team started using it?"
    ))
Expected behavior: the agent calls web_search for the release date, search_notes for the adoption date, and combines both into a final answer ("about 3 years") — a fact neither tool alone contains.
  • Ran the agent and confirmed it called both tools before answering, not just one
  • Set max_steps=1 and confirmed it hits the guardrail message instead of a partial or hallucinated answer
  • Asked a question answerable from only one tool and confirmed it doesn't waste a call on the other
  • Can explain out loud why this task needed an agent rather than a fixed two-step chain — what specifically about it is unpredictable in advance
  • Next up

    Topic 9: LangGraph

    Rebuild this topic's hand-rolled loop as a graph with explicit state, branching, and persistence.