{"slug": "what-is-an-ai-agent-how-to-build-one-from-scratch-in-python", "title": "What Is an AI Agent? How to Build One from Scratch in Python", "summary": "A developer published a from-scratch guide to building an AI agent in roughly 50 lines of Python using the Anthropic SDK, defining an agent as a large language model that decides its own next steps and acts through tools in a loop until a goal is reached. The walkthrough builds a file agent with list, read and write tools confined to a workspace folder, and argues that teams should prefer cheaper, more predictable workflows whenever the steps can be drawn in advance.", "body_md": "*Originally published at [GeekCoding101](https://geekcoding101.com/posts/what-is-an-ai-agent-and-how-to-build-one).*\n\nEvery product launch in 2026 seems to include the word *agent*: coding agents, browser agents, research agents, \"agentic workflows\". In my [Agentic Frameworks guide](https://geekcoding101.com/posts/a-quick-guide) I compared the big frameworks for building them. But before you pick a framework, it's worth understanding what an agent really is, because the core idea fits in about 50 lines of Python. In this post we'll define it, look at the loop that powers every agent, and build one from scratch. 🚀\n\nAn **AI agent** is a system where a large language model **decides its own next steps** and **takes actions through tools** in a loop, until a goal is reached.\n\nCompare three levels:\n\n|  | Chatbot | Workflow | Agent | \n|---|---|---|---|\n| Who decides the steps? | Nobody, one answer | You, in code | The model | \n| Can it act? | No | Yes, fixed actions | Yes, picks its own actions | \n| Example | \"Explain OAuth\" | \"Summarize every new ticket, then post to Slack\" | \"Find out why the build is failing and fix it\" | \n\nA good rule of thumb: if you can draw the flowchart in advance, build a **workflow**; it's cheaper, faster and more predictable. Reach for an **agent** when the path depends on what the model discovers along the way, like debugging, research, or cleaning up messy data.\n\nMemory, planning and multi-agent setups are refinements on top of these four. Everything else is detail.\n\nThe model never runs anything itself. It *asks* for a tool call (in the Claude API, a `tool_use` block with `stop_reason: \"tool_use\"`), your code runs it, and you send back a `tool_result`. The whole conversation, including every tool call and result, is resent on each turn, which is how the agent \"remembers\" what it already did.\n\nLet's build a small **file agent**: it can list, read and write text files inside a `workspace/` folder, and we'll ask it to clean up a messy to-do list. I'm using the official Anthropic Python SDK, but the same loop works with any provider that supports tool calling.\n\n```\nuv init file-agent\ncd file-agent\nuv add anthropic\nexport ANTHROPIC_API_KEY=sk-ant-...   # from console.anthropic.com\n```\n\nSave this as `agent.py`:\n\n```\n\"\"\"A minimal AI agent: Claude plus three file tools, in a loop.\"\"\"\nfrom pathlib import Path\n\nimport anthropic\n\nWORKDIR = Path(\"workspace\").resolve()\nWORKDIR.mkdir(exist_ok=True)\n\nclient = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY from the environment\n\nTOOLS = [\n    {\n        \"name\": \"list_files\",\n        \"description\": \"List the files in the workspace folder.\",\n        \"input_schema\": {\"type\": \"object\", \"properties\": {}, \"additionalProperties\": False},\n    },\n    {\n        \"name\": \"read_file\",\n        \"description\": \"Read a text file from the workspace folder.\",\n        \"input_schema\": {\n            \"type\": \"object\",\n            \"properties\": {\"path\": {\"type\": \"string\", \"description\": \"File name inside the workspace\"}},\n            \"required\": [\"path\"],\n            \"additionalProperties\": False,\n        },\n    },\n    {\n        \"name\": \"write_file\",\n        \"description\": \"Create or overwrite a text file in the workspace folder.\",\n        \"input_schema\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"path\": {\"type\": \"string\", \"description\": \"File name inside the workspace\"},\n                \"content\": {\"type\": \"string\", \"description\": \"Full new content of the file\"},\n            },\n            \"required\": [\"path\", \"content\"],\n            \"additionalProperties\": False,\n        },\n    },\n]\n\ndef safe_path(name: str) -> Path:\n    \"\"\"Resolve a file name inside WORKDIR and refuse anything that escapes it.\"\"\"\n    path = (WORKDIR / name).resolve()\n    if not path.is_relative_to(WORKDIR):\n        raise ValueError(f\"{name} is outside the workspace\")\n    return path\n\ndef run_tool(name: str, args: dict) -> str:\n    if name == \"list_files\":\n        return \"\\n\".join(p.name for p in sorted(WORKDIR.iterdir())) or \"(empty)\"\n    if name == \"read_file\":\n        return safe_path(args[\"path\"]).read_text(encoding=\"utf-8\")\n    if name == \"write_file\":\n        safe_path(args[\"path\"]).write_text(args[\"content\"], encoding=\"utf-8\")\n        return f\"Wrote {len(args['content'])} characters to {args['path']}\"\n    raise ValueError(f\"Unknown tool {name}\")\n\ndef run_agent(task: str, max_steps: int = 20) -> str:\n    messages = [{\"role\": \"user\", \"content\": task}]\n    for step in range(max_steps):\n        response = client.messages.create(\n            model=\"claude-opus-5-5\",\n            max_tokens=16000,\n            system=\"You are a careful assistant that works on files in a workspace folder. \"\n            \"Look at what exists before changing anything.\",\n            tools=TOOLS,\n            messages=messages,\n        )\n        # Keep the whole assistant turn (text, thinking and tool calls) in the history.\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n\n        if response.stop_reason != \"tool_use\":\n            # end_turn (done), max_tokens, refusal...: stop and return what we have.\n            return \"\".join(b.text for b in response.content if b.type == \"text\")\n\n        results = []\n        for block in response.content:\n            if block.type != \"tool_use\":\n                continue\n            print(f\"[step {step}] {block.name}({block.input})\")\n            try:\n                output, is_error = run_tool(block.name, block.input), False\n            except Exception as e:  # report failures to the model instead of crashing\n                output, is_error = f\"Error: {e}\", True\n            results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": output, \"is_error\": is_error})\n        # All results of one turn go back together in a single user message.\n        messages.append({\"role\": \"user\", \"content\": results})\n    return \"Stopped: too many steps.\"\n\nif __name__ == \"__main__\":\n    (WORKDIR / \"todo.txt\").write_text(\"buy milk\\nwrite blog post about agents\\nbuy milk\\n\", encoding=\"utf-8\")\n    print(run_agent(\"Clean up todo.txt: remove duplicates, sort it, and save the result as todo-clean.txt.\"))\nuv run agent.py\n```\n\nOn a run you'll see something like this (the exact steps vary, since the model decides them):\n\n```\n[step 0] list_files({})\n[step 1] read_file({'path': 'todo.txt'})\n[step 2] write_file({'path': 'todo-clean.txt', 'content': 'buy milk\\nwrite blog post about agents\\n'})\nDone! I removed the duplicate \"buy milk\" entry, sorted the list, and saved it to todo-clean.txt.\n```\n\nNobody told it to call `list_files` first or to read before writing. It decided that, partly because the system prompt says to look before changing anything.\n\n**Tool definitions are prompts.** The model only knows a tool through its `name`, `description` and `input_schema`. Clear descriptions (\"Full new content of the file\") prevent most wrong calls. `additionalProperties: False` keeps the model from inventing parameters.\n\n**Append the whole assistant turn.** `messages.append({\"role\": \"assistant\", \"content\": response.content})` keeps the text, any thinking blocks and the `tool_use` blocks exactly as returned. Extracting only the text would break the next request, because every `tool_result` must match a `tool_use` id from the previous turn.\n\n**Return all results together.** One assistant turn can contain several tool calls (the model reads two files in parallel, for example). Send all their results back in **one** user message.\n\n**Errors are results, not crashes.** When a tool fails, we send `\"is_error\": True` with the message. The model reads it and usually recovers, for example by listing files after guessing a wrong name.\n\n**Stop on anything but `tool_use`.** `end_turn` means done, but `max_tokens` or `refusal` also end the loop here instead of spinning forever.\n\n**`safe_path` is the guardrail.** The model chooses file names, so never trust them: resolving the path and checking it stays inside `WORKDIR` blocks `../../.ssh/id_rsa`-style escapes. Together with `max_steps`, this is the minimum safety every agent needs.\n\nOnce you understand the loop, you don't have to write it every time. The Anthropic SDK's **tool runner** (beta) builds the schemas from type hints and docstrings and runs the loop for you:\n\n``` python\nimport anthropic\nfrom anthropic import beta_tool\n\nfrom agent import WORKDIR, safe_path\n\nclient = anthropic.Anthropic()\n\n@beta_tool\ndef list_files() -> str:\n    \"\"\"List the files in the workspace folder.\"\"\"\n    return \"\\n\".join(p.name for p in sorted(WORKDIR.iterdir())) or \"(empty)\"\n\n@beta_tool\ndef read_file(path: str) -> str:\n    \"\"\"Read a text file from the workspace folder.\n\n    Args:\n        path: File name inside the workspace.\n    \"\"\"\n    return safe_path(path).read_text(encoding=\"utf-8\")\n\n@beta_tool\ndef write_file(path: str, content: str) -> str:\n    \"\"\"Create or overwrite a text file in the workspace folder.\n\n    Args:\n        path: File name inside the workspace.\n        content: Full new content of the file.\n    \"\"\"\n    safe_path(path).write_text(content, encoding=\"utf-8\")\n    return f\"Wrote {len(content)} characters to {path}\"\n\nrunner = client.beta.messages.tool_runner(\n    model=\"claude-opus-5-5\",\n    max_tokens=16000,\n    tools=[list_files, read_file, write_file],\n    messages=[{\"role\": \"user\", \"content\": \"Clean up todo.txt: remove duplicates, sort it, save as todo-clean.txt.\"}],\n)\nfinal = runner.until_done()\nprint(final.content[-1].text)\n```\n\nFrameworks like LangGraph, CrewAI, the OpenAI Agents SDK or Google ADK add more on top (state graphs, multiple agents, tracing, deployment), but underneath they all run this same loop.\n\n`max_steps`, and log token usage per run.` write_file`.\nA chatbot answers one message at a time. An agent pursues a goal across many steps, choosing and calling tools, reading the results and deciding what to do next on its own.\n\nNo. As shown above, a working agent is a loop around a model API plus a few tool functions. Frameworks help once you need persistence, multiple agents, tracing or deployment.\n\n\"Agentic AI\" describes systems with this agent behavior: models that plan, use tools and act with some autonomy, as opposed to single-shot text generation.\n\nMCP standardizes how agents get tools. Instead of writing `read_file` yourself, your agent can connect to an existing MCP server for files, GitHub, databases and more. See [What Is an MCP Server?](https://geekcoding101.com/posts/what-is-an-mcp-server)\n\nMore in the **Agent Stack Insider** series 👇", "url": "https://wpnews.pro/news/what-is-an-ai-agent-how-to-build-one-from-scratch-in-python", "canonical_source": "https://dev.to/geekcoding101/what-is-an-ai-agent-how-to-build-one-from-scratch-in-python-48ce", "published_at": "2026-10-19 23:29:09+00:00", "updated_at": "2026-10-19 23:45:47.850541+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "large-language-models", "developer-tools"], "entities": ["Anthropic", "Claude", "GeekCoding101"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/what-is-an-ai-agent-how-to-build-one-from-scratch-in-python", "markdown": "https://wpnews.pro/news/what-is-an-ai-agent-how-to-build-one-from-scratch-in-python.md", "text": "https://wpnews.pro/news/what-is-an-ai-agent-how-to-build-one-from-scratch-in-python.txt", "jsonld": "https://wpnews.pro/news/what-is-an-ai-agent-how-to-build-one-from-scratch-in-python.jsonld"}}