An agent is just a for-loop. The simplest possible agent framework.
Project description
the-agent-sdk
An agent is just a for-loop.
The simplest possible agent framework. No abstractions. No magic. Just a for-loop of tool calls. The framework powering BU.app.
Install
uv sync
or
uv add the-agent-sdk
Quick Start
import asyncio
from bu_agent_sdk import Agent, tool, TaskComplete
from bu_agent_sdk.llm import ChatAnthropic
@tool("Add two numbers")
async def add(a: int, b: int) -> int:
return a + b
@tool("Signal task completion")
async def done(message: str) -> str:
raise TaskComplete(message)
agent = Agent(
llm=ChatAnthropic(model="claude-sonnet-4-20250514"),
tools=[add, done],
)
async def main():
result = await agent.query("What is 2 + 3?")
print(result)
asyncio.run(main())
Philosophy
The Bitter Lesson: All the value is in the RL'd model, not your 10,000 lines of abstractions.
Agent frameworks fail not because models are weak, but because their action spaces are incomplete. Give the LLM as much freedom as possible, then vibe-restrict based on evals.
Features
Done Tool Pattern
The naive "stop when no tool calls" approach fails. Agents finish prematurely. Force explicit completion:
@tool("Signal completion")
async def done(message: str) -> str:
raise TaskComplete(message)
agent = Agent(
llm=llm,
tools=[..., done],
require_done_tool=True, # Autonomous mode
)
Ephemeral Messages
Large tool outputs (browser state, screenshots) blow up context. Keep only the last N:
@tool("Get browser state", ephemeral=3) # Keep last 3 only
async def get_state() -> str:
return massive_dom_and_screenshot
Simple LLM Primitives
~300 lines per provider. Same interface. Full control:
from bu_agent_sdk.llm import ChatAnthropic, ChatOpenAI, ChatGoogle, ChatGrok
# All implement BaseChatModel
agent = Agent(llm=ChatAnthropic(model="claude-sonnet-4-20250514"), tools=tools)
agent = Agent(llm=ChatOpenAI(model="gpt-4o"), tools=tools)
agent = Agent(llm=ChatGoogle(model="gemini-2.0-flash"), tools=tools)
agent = Agent(llm=ChatGrok(model="grok-4-latest"), tools=tools)
Context Compaction
Auto-summarize when approaching context limits:
from bu_agent_sdk.agent import CompactionConfig
agent = Agent(
llm=llm,
tools=tools,
compaction=CompactionConfig(threshold_ratio=0.80),
)
Dependency Injection
FastAPI-style, type-safe:
from typing import Annotated
from bu_agent_sdk import Depends
def get_db():
return Database()
@tool("Query users")
async def get_user(id: int, db: Annotated[Database, Depends(get_db)]) -> str:
return await db.find(id)
Streaming Events
from bu_agent_sdk.agent import ToolCallEvent, ToolResultEvent, FinalResponseEvent
async for event in agent.query_stream("do something"):
match event:
case ToolCallEvent(tool=name, args=args):
print(f"Calling {name}")
case ToolResultEvent(tool=name, result=result):
print(f"{name} -> {result[:50]}")
case FinalResponseEvent(content=text):
print(f"Done: {text}")
Claude Code in 100 Lines
A sandboxed coding assistant with dependency injection:
import asyncio
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Annotated
from bu_agent_sdk import Agent
from bu_agent_sdk.llm import ChatAnthropic
from bu_agent_sdk.tools import Depends, tool
@dataclass
class SandboxContext:
"""All file operations restricted to root_dir."""
root_dir: Path
working_dir: Path
def resolve_path(self, path: str) -> Path:
resolved = (self.working_dir / path).resolve()
resolved.relative_to(self.root_dir) # Raises if escapes
return resolved
def get_sandbox() -> SandboxContext:
raise RuntimeError("Override via dependency_overrides")
@tool("Execute shell command")
async def bash(command: str, ctx: Annotated[SandboxContext, Depends(get_sandbox)]) -> str:
result = subprocess.run(command, shell=True, capture_output=True, text=True, cwd=ctx.working_dir)
return result.stdout + result.stderr or "(no output)"
@tool("Read file contents")
async def read(path: str, ctx: Annotated[SandboxContext, Depends(get_sandbox)]) -> str:
return ctx.resolve_path(path).read_text()
@tool("Write file contents")
async def write(path: str, content: str, ctx: Annotated[SandboxContext, Depends(get_sandbox)]) -> str:
ctx.resolve_path(path).write_text(content)
return f"Wrote {len(content)} bytes"
@tool("Find files by glob pattern")
async def glob(pattern: str, ctx: Annotated[SandboxContext, Depends(get_sandbox)]) -> str:
files = [str(f.relative_to(ctx.root_dir)) for f in ctx.working_dir.glob(pattern)]
return "\n".join(files) or "No matches"
@tool("Signal task completion")
async def done(message: str) -> str:
from bu_agent_sdk.agent import TaskComplete
raise TaskComplete(message)
async def main():
# Create sandbox
root = Path("./sandbox")
root.mkdir(exist_ok=True)
ctx = SandboxContext(root_dir=root.resolve(), working_dir=root.resolve())
agent = Agent(
llm=ChatAnthropic(model="claude-sonnet-4-20250514"),
tools=[bash, read, write, glob, done],
system_prompt=f"Coding assistant. Working dir: {ctx.working_dir}",
dependency_overrides={get_sandbox: lambda: ctx},
)
print("Agent ready. Ctrl+C to exit.")
while True:
task = input("\n> ")
async for event in agent.query_stream(task):
if hasattr(event, "tool"):
print(f" → {event.tool}")
elif hasattr(event, "content") and event.content:
print(f"\n{event.content}")
asyncio.run(main())
See bu_agent_sdk/examples/claude_code.py for the full version with grep, edit, and todo tools.
Examples
See bu_agent_sdk/examples/ for more:
claude_code.py- Full Claude Code clone with sandboxed filesystemdependency_injection.py- FastAPI-style dependency injection
The Bitter Truth
Every abstraction is a liability. Every "helper" is a failure point.
The models got good. Really good. They were RL'd on computer use, coding, browsing. They don't need your guardrails. They need:
- A complete action space
- A for-loop
- An explicit exit
- Context management
The bitter lesson: The less you build, the more it works.
License
MIT
Credits
Maintained by Milind Sharma. Inspired by reverse-engineering Claude Code and Gemini CLI.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file the_agent_sdk-0.2.0.tar.gz.
File metadata
- Download URL: the_agent_sdk-0.2.0.tar.gz
- Upload date:
- Size: 79.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
10f418be6f4c70da08dd072ebd5db37f373414a72090f1cb638c312028108d52
|
|
| MD5 |
12ea8fedaa6ba8026b635a474519520f
|
|
| BLAKE2b-256 |
56dfc4de1afe306794abbd7e9f306f3b9c6a90ae7a6550af7148b03aeea35367
|
Provenance
The following attestation bundles were made for the_agent_sdk-0.2.0.tar.gz:
Publisher:
publish-pypi.yml on Milind220/agent-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
the_agent_sdk-0.2.0.tar.gz -
Subject digest:
10f418be6f4c70da08dd072ebd5db37f373414a72090f1cb638c312028108d52 - Sigstore transparency entry: 1006586384
- Sigstore integration time:
-
Permalink:
Milind220/agent-sdk@055fcc071a320789739563826cc4ec2872a8199d -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Milind220
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@055fcc071a320789739563826cc4ec2872a8199d -
Trigger Event:
push
-
Statement type:
File details
Details for the file the_agent_sdk-0.2.0-py3-none-any.whl.
File metadata
- Download URL: the_agent_sdk-0.2.0-py3-none-any.whl
- Upload date:
- Size: 98.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c1cce2df0f795ccbb7c32163d7abcba4c65c7514490152be99b3945bd07cb230
|
|
| MD5 |
9e3aeeb5d6eccde3ac7be66afa5990d8
|
|
| BLAKE2b-256 |
9dd6c8c7d69ae3071a4c91f677abcc42f2f8b3e719b0c61da2e5919dd5213e59
|
Provenance
The following attestation bundles were made for the_agent_sdk-0.2.0-py3-none-any.whl:
Publisher:
publish-pypi.yml on Milind220/agent-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
the_agent_sdk-0.2.0-py3-none-any.whl -
Subject digest:
c1cce2df0f795ccbb7c32163d7abcba4c65c7514490152be99b3945bd07cb230 - Sigstore transparency entry: 1006586385
- Sigstore integration time:
-
Permalink:
Milind220/agent-sdk@055fcc071a320789739563826cc4ec2872a8199d -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Milind220
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@055fcc071a320789739563826cc4ec2872a8199d -
Trigger Event:
push
-
Statement type: