Skip to main content

bu-agent-sdk

An agent is just a for-loop.

Agent 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 bu-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

# 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)

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}")

A CLI in 60 Lines

#!/usr/bin/env python3
import asyncio
from bu_agent_sdk import Agent, tool, TaskComplete
from bu_agent_sdk.llm import ChatAnthropic

@tool("Execute shell command")
async def bash(command: str) -> str:
    proc = await asyncio.create_subprocess_shell(
        command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT,
    )
    stdout, _ = await proc.communicate()
    return stdout.decode()

@tool("Read file")
async def read(path: str) -> str:
    return open(path).read()

@tool("Write file")
async def write(path: str, content: str) -> str:
    open(path, 'w').write(content)
    return f"Wrote {path}"

@tool("Task complete")
async def done(message: str) -> str:
    raise TaskComplete(message)

async def main():
    agent = Agent(
        llm=ChatAnthropic(model="claude-sonnet-4-20250514"),
        tools=[bash, read, write, done],
        system_prompt="You are a coding assistant.",
    )
    print("Agent ready. Ctrl+C to exit.")
    while True:
        try:
            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}")
        except KeyboardInterrupt:
            break

if __name__ == "__main__":
    asyncio.run(main())

Examples

See examples/ for more:

  • 01_hello_world.py - Simplest possible agent
  • 07_minimal_cli.py - 60-line CLI

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

Built by Browser Use. Inspired by reverse-engineering Claude Code and Gemini CLI.

Metadata

Release files for bu-agent-sdk 0.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for bu-agent-sdk 0.0.1
File Size Uploaded Provenance Actions
bu_agent_sdk-0.0.1.tar.gz 69.6 kB Without provenance Details for bu_agent_sdk-0.0.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for bu-agent-sdk 0.0.1
File Interpreter ABI Platform Actions
bu_agent_sdk-0.0.1-py3-none-any.whl Python 3 none any Details for bu_agent_sdk-0.0.1-py3-none-any.whl

Total release size: 153.9 kB

Release files / bu_agent_sdk-0.0.1.tar.gz

Download URL bu_agent_sdk-0.0.1.tar.gz
Size 69.6 kB
Tags Source
SHA-256 checksum
How to use checksums
10ea4bdf7c521d6852acae7eb3b3389177b6fe1c59b89f4aa3677d1d763316d8
BLAKE2b-256 checksum
How to use checksums
e0fe9e51e6d8ca05d9c2adac1494bb5548b89ab7c1f1f0fa59af19bbf88ca5d4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.7.21

Release files / bu_agent_sdk-0.0.1-py3-none-any.whl

Download URL bu_agent_sdk-0.0.1-py3-none-any.whl
Size 84.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
62c2dabd56e81bbc007c7753aa075f3ecfe496803a425876b10259ffcba19f1d
BLAKE2b-256 checksum
How to use checksums
0defed964a6b68cfbae1b318fd892427c41a74015a740492522afa78badf0670
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.7.21

Security for bu-agent-sdk 0.0.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.0.1 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page