{
  "name": "bu-agent-sdk",
  "symbol": "BUAGENTSDK",
  "description": "\n\n# bu-agent-sdk\n\n_An agent is just a for-loop._\n\n![Agent Loop](./static/agent-loop.png)\n\nThe simplest possible agent framework. No abstractions. No magic. Just a for-loop of tool calls. The framework powering [BU.app](https://bu.app).\n\n## Install\n\n```bash\nuv sync\n```\n\nor\n\n```bash\nuv add bu-agent-sdk\n```\n\n## Quick Start\n\n```python\nimport asyncio\nfrom bu_agent_sdk import Agent, tool, TaskComplete\nfrom bu_agent_sdk.llm import ChatAnthropic\n\n@tool(\"Add two numbers\")\nasync def add(a: int, b: int) -> int:\n    return a + b\n\n@tool(\"Signal task completion\")\nasync def done(message: str) -> str:\n    raise TaskComplete(message)\n\nagent = Agent(\n    llm=ChatAnthropic(model=\"claude-sonnet-4-20250514\"),\n    tools=[add, done],\n)\n\nasync def main():\n    result = await agent.query(\"What is 2 + 3?\")\n    print(result)\n\nasyncio.run(main())\n```\n\n## Philosophy\n\n**The Bitter Lesson:** All the value is in the RL'd model, not your 10,000 lines of abstractions.\n\nAgent 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.\n\n## Features\n\n### Done Tool Pattern\n\nThe naive \"stop when no tool calls\" approach fails. Agents finish prematurely. Force explicit completion:\n\n```python\n@tool(\"Signal completion\")\nasync def done(message: str) -> str:\n    raise TaskComplete(message)\n\nagent = Agent(\n    llm=llm,\n    tools=[..., done],\n    require_done_tool=True,  # Autonomous mode\n)\n```\n\n### Ephemeral Messages\n\nLarge tool outputs (browser state, screenshots) blow up context. Keep only the last N:\n\n```python\n@tool(\"Get browser state\", ephemeral=3)  # Keep last 3 only\nasync def get_state() -> str:\n    return massive_dom_and_screenshot\n```\n\n### Simple LLM Primitives\n\n~300 lines per provider. Same interface. Full control:\n\n```python\nfrom bu_agent_sdk.llm import ChatAnthropic, ChatOpenAI, ChatGoogle\n\n# All implement BaseChatModel\nagent = Agent(llm=ChatAnthropic(model=\"claude-sonnet-4-20250514\"), tools=tools)\nagent = Agent(llm=ChatOpenAI(model=\"gpt-4o\"), tools=tools)\nagent = Agent(llm=ChatGoogle(model=\"gemini-2.0-flash\"), tools=tools)\n```\n\n### Context Compaction\n\nAuto-summarize when approaching context limits:\n\n```python\nfrom bu_agent_sdk.agent import CompactionConfig\n\nagent = Agent(\n    llm=llm,\n    tools=tools,\n    compaction=CompactionConfig(threshold_ratio=0.80),\n)\n```\n\n### Dependency Injection\n\nFastAPI-style, type-safe:\n\n```python\nfrom typing import Annotated\nfrom bu_agent_sdk import Depends\n\ndef get_db():\n    return Database()\n\n@tool(\"Query users\")\nasync def get_user(id: int, db: Annotated[Database, Depends(get_db)]) -> str:\n    return await db.find(id)\n```\n\n### Streaming Events\n\n```python\nfrom bu_agent_sdk.agent import ToolCallEvent, ToolResultEvent, FinalResponseEvent\n\nasync for event in agent.query_stream(\"do something\"):\n    match event:\n        case ToolCallEvent(tool=name, args=args):\n            print(f\"Calling {name}\")\n        case ToolResultEvent(tool=name, result=result):\n            print(f\"{name} -> {result[:50]}\")\n        case FinalResponseEvent(content=text):\n            print(f\"Done: {text}\")\n```\n\n## Claude Code in 100 Lines\n\nA sandboxed coding assistant with dependency injection:\n\n```python\nimport asyncio\nimport subprocess\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import Annotated\n\nfrom bu_agent_sdk import Agent\nfrom bu_agent_sdk.llm import ChatAnthropic\nfrom bu_agent_sdk.tools import Depends, tool\n\n\n@dataclass\nclass SandboxContext:\n    \"\"\"All file operations restricted to root_dir.\"\"\"\n    root_dir: Path\n    working_dir: Path\n\n    def resolve_path(self, path: str) -> Path:\n        resolved = (self.working_dir / path).resolve()\n        resolved.relative_to(self.root_dir)  # Raises if escapes\n        return resolved\n\n\ndef get_sandbox() -> SandboxContext:\n    raise RuntimeError(\"Override via dependency_overrides\")\n\n\n@tool(\"Execute shell command\")\nasync def bash(command: str, ctx: Annotated[SandboxContext, Depends(get_sandbox)]) -> str:\n    result = subprocess.run(command, shell=True, capture_output=True, text=True, cwd=ctx.working_dir)\n    return result.stdout + result.stderr or \"(no output)\"\n\n\n@tool(\"Read file contents\")\nasync def read(path: str, ctx: Annotated[SandboxContext, Depends(get_sandbox)]) -> str:\n    return ctx.resolve_path(path).read_text()\n\n\n@tool(\"Write file contents\")\nasync def write(path: str, content: str, ctx: Annotated[SandboxContext, Depends(get_sandbox)]) -> str:\n    ctx.resolve_path(path).write_text(content)\n    return f\"Wrote {len(content)} bytes\"\n\n\n@tool(\"Find files by glob pattern\")\nasync def glob(pattern: str, ctx: Annotated[SandboxContext, Depends(get_sandbox)]) -> str:\n    files = [str(f.relative_to(ctx.root_dir)) for f in ctx.working_dir.glob(pattern)]\n    return \"\\n\".join(files) or \"No matches\"\n\n\n@tool(\"Signal task completion\")\nasync def done(message: str) -> str:\n    from bu_agent_sdk.agent import TaskComplete\n    raise TaskComplete(message)\n\n\nasync def main():\n    # Create sandbox\n    root = Path(\"./sandbox\")\n    root.mkdir(exist_ok=True)\n    ctx = SandboxContext(root_dir=root.resolve(), working_dir=root.resolve())\n\n    agent = Agent(\n        llm=ChatAnthropic(model=\"claude-sonnet-4-20250514\"),\n        tools=[bash, read, write, glob, done],\n        system_prompt=f\"Coding assistant. Working dir: {ctx.working_dir}\",\n        dependency_overrides={get_sandbox: lambda: ctx},\n    )\n\n    print(\"Agent ready. Ctrl+C to exit.\")\n    while True:\n        task = input(\"\\n> \")\n        async for event in agent.query_stream(task):\n            if hasattr(event, \"tool\"):\n                print(f\"  → {event.tool}\")\n            elif hasattr(event, \"content\") and event.content:\n                print(f\"\\n{event.content}\")\n\n\nasyncio.run(main())\n```\n\nSee [`bu_agent_sdk/examples/claude_code.py`](./bu_agent_sdk/examples/claude_code.py) for the full version with grep, edit, and todo tools.\n\n## Examples\n\nSee [`bu_agent_sdk/examples/`](./bu_agent_sdk/examples/) for more:\n\n- `claude_code.py` - Full Claude Code clone with sandboxed filesystem\n- `dependency_injection.py` - FastAPI-style dependency injection\n\n## The Bitter Truth\n\nEvery abstraction is a liability. Every \"helper\" is a failure point.\n\nThe models got good. Really good. They were RL'd on computer use, coding, browsing. They don't need your guardrails. They need:\n\n- A complete action space\n- A for-loop\n- An explicit exit\n- Context management\n\n**The bitter lesson: The less you build, the more it works.**\n\n## License\n\nMIT\n\n## Credits\n\nBuilt by [Browser Use](https://browser-use.com). Inspired by reverse-engineering Claude Code and Gemini CLI.\n\n\nSource: https://github.com/browser-use/agent-sdk",
  "image": "https://static-create.jup.ag/images/DqEMsw7JBiCnLakUbNcZ563HmE4m9bdCBWqhhDnLswrm",
  "twitter": "https://x.com/browser_use"
}