{"slug": "the-claude-api-multi-agent-loop-without-the-framework", "title": "The Claude API multi-agent loop, without the framework", "summary": "A developer released an 80-line Python implementation of the Claude API multi-agent loop that exposes the tool-calling cycle without framework abstractions. The open-source project on GitHub includes example agents for research and code tasks, and is designed to be readable, modifiable, and production-ready.", "body_md": "Most Claude API tutorials show a single tool call. Most frameworks hide the loop behind abstractions you can't read. This post shows the loop directly — what actually happens between \"Claude requests a tool\" and \"Claude finishes.\"\n\nWhen you give Claude tools, a single API call isn't always enough. Claude decides whether to call a tool, you execute it, then you send the result back. Claude might call another tool, or it might answer. That cycle is the agent loop.\n\n```\nuser message\n    ↓\nClaude responds\n    ↓\nstop_reason == \"tool_use\"?  →  execute tools  →  back to Claude\n    ↓\nstop_reason == \"end_turn\"\n    ↓\nreturn final text\n```\n\nThe entire loop is in [ agent.py](https://github.com/espanhol6/claude-multiagent-loop/blob/main/agent.py) — about 80 lines.\n\n``` python\ndef run_agent(\n    system: str,\n    user_message: str,\n    tools: list[dict],\n    tool_handlers: dict[str, Callable],\n    max_rounds: int = 10,\n) -> str:\n    messages = [{\"role\": \"user\", \"content\": user_message}]\n\n    for round_num in range(max_rounds):\n        response = client.messages.create(\n            model=MODEL,\n            max_tokens=4096,\n            system=system,\n            tools=tools,\n            messages=messages,\n        )\n\n        messages.append({\"role\": \"assistant\", \"content\": response.content})\n\n        if response.stop_reason == \"end_turn\":\n            return _extract_text(response.content)\n\n        if response.stop_reason == \"tool_use\":\n            tool_results = []\n            for block in response.content:\n                if block.type == \"tool_use\":\n                    result = _call_tool(block, tool_handlers)\n                    tool_results.append({\n                        \"type\": \"tool_result\",\n                        \"tool_use_id\": block.id,\n                        \"content\": json.dumps(result),\n                    })\n            messages.append({\"role\": \"user\", \"content\": tool_results})\n            continue\n\n        break\n\n    return _extract_text(response.content)\n```\n\nThat's the core. The rest of the file is `_call_tool`\n\n(dispatch to your Python function) and `_extract_text`\n\n(pull text blocks from the response).\n\nDefine tools in Anthropic's schema format:\n\n```\nTOOLS = [\n    {\n        \"name\": \"read_file\",\n        \"description\": \"Read the contents of a file.\",\n        \"input_schema\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"path\": {\"type\": \"string\", \"description\": \"File path to read\"},\n            },\n            \"required\": [\"path\"],\n        },\n    },\n]\n```\n\nDefine handlers as plain Python functions:\n\n``` php\ndef read_file(path: str) -> dict:\n    return {\"content\": Path(path).read_text()}\n\ntool_handlers = {\"read_file\": read_file}\n```\n\nRun the agent:\n\n```\nresult = run_agent(\n    system=\"You are a helpful assistant.\",\n    user_message=\"What's in README.md?\",\n    tools=TOOLS,\n    tool_handlers=tool_handlers,\n)\n```\n\nFrameworks aren't wrong. But when something breaks in production — and it will — you want to know exactly what message went to Claude and exactly what came back. Abstractions make that harder.\n\nThis implementation is meant to be read, modified, and owned. The loop is visible. You can add logging, approval gates, retry logic, or conditional execution exactly where you need it.\n\nThe repo includes:\n\n`example_research.py`\n\n`search`\n\nand `read_page`\n\ntools (swap in your real implementations)`example_code.py`\n\n`read_file`\n\n, `write_file`\n\n, and `list_files`\n\ntoolsBoth run end-to-end with real Claude API calls.\n\n```\npip install anthropic\nexport ANTHROPIC_API_KEY=sk-ant-...\npython example_code.py\n```\n\nThe repo: [github.com/espanhol6/claude-multiagent-loop](https://github.com/espanhol6/claude-multiagent-loop)\n\nThis pattern is what I used as the foundation for [Cluster OS Jarvis](https://github.com/espanhol6) — a production multi-agent framework with SSE streaming, up to 6 tool-calling rounds, and cron-scheduled autonomous agents. The loop here is the simplified, standalone version.\n\nIf you're building something with Claude and want to understand what's happening under the hood before adding abstractions, this is a good starting point.\n\n[João Daniel Espanhol Miguel](https://linkedin.com/in/jo%C3%A3o-espanhol-miguel) — AI engineer, Lisbon. Also wrote about [debugging a silent native crash in ctranslate2 + WinRT](https://dev.to/espanhol/debugging-a-silent-native-crash-when-combining-faster-whisper-and-winrt-on-windows-2ik9).", "url": "https://wpnews.pro/news/the-claude-api-multi-agent-loop-without-the-framework", "canonical_source": "https://dev.to/espanhol/the-claude-api-multi-agent-loop-without-the-framework-2nh5", "published_at": "2026-06-15 14:56:59+00:00", "updated_at": "2026-06-15 15:07:26.599060+00:00", "lang": "en", "topics": ["large-language-models", "ai-agents", "developer-tools"], "entities": ["Claude", "Anthropic", "GitHub", "Cluster OS Jarvis"], "alternates": {"html": "https://wpnews.pro/news/the-claude-api-multi-agent-loop-without-the-framework", "markdown": "https://wpnews.pro/news/the-claude-api-multi-agent-loop-without-the-framework.md", "text": "https://wpnews.pro/news/the-claude-api-multi-agent-loop-without-the-framework.txt", "jsonld": "https://wpnews.pro/news/the-claude-api-multi-agent-loop-without-the-framework.jsonld"}}