Build Your First Autonomous AI Agent in a Weekend
The 2026 edition: a real, runnable LangGraph agent with tools, memory, and a human-in-the-loop gate - verified against the official quickstart, not pseudo-code.
Note
Before you start
- Python 3.10+ and a terminal
- A model endpoint - your local Ollama from our install guide works perfectly via localhost:11434/v1
- `uv` installed (or pip - the docs use uv; both work)
Jump to section
- 1
Install the stack
`uv init && uv add langchain langchain-openai langgraph`. One command gets you the model layer, the tool layer, and the graph runtime.
- 2
Define tools and model
Decorate 2-3 real functions with @tool and bind them to the model. Keep the tool surface small - every tool is a new attack surface for prompt injection.
- 3
Define state and nodes
A TypedDict for messages (appended, never replaced) plus two nodes: the model call and the tool executor. This is the loop: model proposes, tool executes, result feeds back.
- 4
Wire the conditional edge
If the last message carries tool calls, route to the tool node; otherwise end. Compile the graph and invoke it. You now have a ReAct agent.
- 5
Add memory and a human gate
Attach a checkpointer for cross-session recall and an interrupt for any write/delete tool, so a human approves before anything destructive runs.
- 6
Ship it safe
Log every tool call with payloads, cap steps at the graph level, and run it inside Docker Sandboxes if it touches the network - never on your bare host.
A genuinely useful autonomous agent is a weekend project in 2026 - if you keep the scope tight and the loop disciplined. This guide builds the official LangGraph quickstart agent verbatim (with one swap: it runs against your local Ollama), then adds memory and a human-in-the-loop permission gate. No pseudo-code, no framework roulette: this is the canonical scaffold, documented.
The mental model
An agent is: a model, a loop, tools, memory, and a permission layer. Almost all failures - agentic runaway, hallucinations on tool results, prompt injection - live in one of those five boxes. Respect them.
The loop is the product
The model is interchangeable; the loop is what makes an agent an agent. Get the loop boring and solid before you optimize the model choice.
Step 1: Install the stack
uv init
uv add langchain langchain-openai langgraph
This installs the framework that now documents itself as “build resilient agents” - the low-level graph primitives behind most production agent stacks in 2026. Running against your local model from our Ollama guide:
from langchain_openai import ChatOpenAI
model = ChatOpenAI(
base_url="http://localhost:11434/v1", # your local Ollama
model="deepseek-v4-flash",
temperature=0,
)
Step 2: Define tools and bind them
Keep the surface small - every tool is a new attack surface for prompt injection and a new failure mode to debug. Two well-typed tools beat ten loose ones.
from langchain.tools import tool
@tool
def multiply(a: int, b: int) -> int:
"""Multiply `a` and `b`."""
return a * b
@tool
def add(a: int, b: int) -> int:
"""Adds `a` and `b`."""
return a + b
tools = [add, multiply]
tools_by_name = {tool.name: tool for tool in tools}
model_with_tools = model.bind_tools(tools)
Step 3: State and the two nodes
State persists through the whole run. We use operator.add so messages append instead of replacing - the agent’s full history survives each loop iteration.
from langchain.messages import AnyMessage, SystemMessage, ToolMessage
from typing_extensions import TypedDict, Annotated
import operator
class MessagesState(TypedDict):
messages: Annotated[list[AnyMessage], operator.add]
def llm_call(state: MessagesState):
"""LLM decides whether to call a tool or not."""
return {"messages": [model_with_tools.invoke(state["messages"])]}
def tool_node(state: MessagesState):
"""Performs the tool calls."""
result = []
for tool_call in state["messages"][-1].tool_calls:
tool = tools_by_name[tool_call["name"]]
observation = tool.invoke(tool_call["args"])
result.append(ToolMessage(content=observation, tool_call_id=tool_call["id"]))
return {"messages": result}
Step 4: Wire the loop and compile
If the last message carries tool calls, route back to the tool node; otherwise stop. That conditional edge is the entire ReAct loop, made explicit.
from typing import Literal
from langgraph.graph import StateGraph, START, END
def should_continue(state: MessagesState) -> Literal["tool_node", END]:
if state["messages"][-1].tool_calls:
return "tool_node"
return END
agent_builder = StateGraph(MessagesState)
agent_builder.add_node("llm_call", llm_call)
agent_builder.add_node("tool_node", tool_node)
agent_builder.add_edge(START, "llm_call")
agent_builder.add_conditional_edges("llm_call", should_continue, ["tool_node", END])
agent_builder.add_edge("tool_node", "llm_call")
agent = agent_builder.compile()
Add a step cap before you invoke
Wrap the invoke in a loop that counts iterations and throws past your budget - the graph runs until it converges, and an agent stuck looping on a tool result will happily spend your token budget. A hard cap is your kill switch.
Step 5: Memory and the human gate
Two additions turn this from a demo into something you can leave running:
- Memory: attach a checkpointer from
langgraph.checkpoint.memoryso the agent recalls this session later - the official “memory & persistence” page shows the one-line change. - Human gate: wrap any write/delete tool in an
interrupt()- the graph pauses mid-run, you approve in your terminal, and it resumes. Read-only tools skip the gate.
Prompt injection is real
Anything the agent reads - web pages, files, emails - is untrusted input. Treat model output as code to be validated: pass tool arguments through schema checks, never let fetched content rewrite your instructions. The step cap and the human gate are not optional.
Step 6: Ship it safe
Log every tool call with payloads and a trace ID. If your agent touches the network or the filesystem, run it inside Docker’s Sandboxes - disposable microVMs with filesystem and network controls built for exactly this - never on your bare host.
When you’re done
You have a stateful, safe, extensible agent that can answer research questions with citations from live search - the same pattern underpinning today’s enterprise agent platforms, and the same code path the official docs teach. This is Terminal-Bench 2.1’s test subject, and now it’s yours.
Questions, answered first
Do I need a big budget model for this to work?
No. A 30B-class model (or even a strong 8B) handles multi-step tool use fine when the loop is disciplined - see our local DeepSeek V4-Flash and Ollama guides. Reserve frontier models for the reasoning tasks, not the orchestration.
Why LangGraph instead of a raw loop?
Because the graph runtime gives you persistence, interrupts, streaming, and a step budget for free - the four things that separate a toy loop from something you can leave running. The official quickstart we mirror here is a supported path, not our invention.
What stops the agent from running away?
Three independent guards: a hard step cap, the interrupt gate on every non-read-only tool, and running inside an isolated sandbox when the agent touches the network. All three are non-negotiable before shipping.
Can I point this at my local Ollama instead of a hosted API?
Yes. Swap the model constructor for ChatOpenAI with base_url set to your Ollama endpoint (http://localhost:11434/v1) as shown in step 1. No other code changes - the graph, tools, and memory are model-agnostic.
Where do agents like this run safely in 2026?
For anything that reads web pages or runs commands, use Docker's Sandboxes - disposable microVMs built exactly for unsupervised coding agents, with filesystem and network controls. We covered it here.
You did it
- `uv run` your agent and it completes a multi-step research task using at least two tools
- It asks for confirmation before any write or delete action (interrupt, then resume)
- Every tool call is logged with payloads and a trace ID
- A step cap stops a runaway loop in a test
- You can point the same code at a hosted model by changing only the base URL