Skip to main content

Python agent SDK with Claude Code-style loop semantics on top of OpenRouter, with MCP client support.

Project description

binho-sdk

A Python agent SDK with Claude Code-style loop semantics on top of OpenRouter, with first-class MCP support — developed by Unlegacy.

Build agents that think like Claude Code — same turn loop, same tool dispatch model, same hooks and permission surface — but pointed at any of the 300+ models OpenRouter exposes (Claude, GPT, Gemini, Llama, DeepSeek, Qwen, …). The public API is a single async def query() -> AsyncIterator[Event]. No frameworks, no graphs, no DSL.

import asyncio
from binho_sdk import query, Options

async def main():
    async for ev in query(
        prompt="Summarize what this repo does, then list the top 3 risks.",
        options=Options(
            model="anthropic/claude-sonnet-4-5",
            tools=["Read", "Glob", "Grep"],
        ),
    ):
        if ev.type == "text_delta":
            print(ev.delta, end="", flush=True)
        elif ev.type == "result":
            print(f"\n[{ev.usage.input_tokens} in / {ev.usage.output_tokens} out / ${ev.usage.cost_usd or 0:.4f}]")

asyncio.run(main())

Why binho-sdk

The agent loop is small. Most "agent frameworks" bury that loop under graphs, state machines, decorator stacks, and routing layers you didn't ask for. The Anthropic claude-agent-sdk gets the shape right but is locked to Anthropic's API.

binho-sdk keeps the shape, drops the lock-in:

binho-sdk claude-agent-sdk LangChain / LangGraph
Models Any (via OpenRouter) Anthropic only Any
Loop control Explicit, in your process Hidden behind subprocess Graph DSL
Streaming events Single async iterator Single async iterator Callbacks
Built-in tools Read/Write/Edit/Glob/Grep/Bash/Task Same set None
MCP client HTTP, tools-only stdio + HTTP Plugin
Subagents First-class (Task tool, AgentDefinition) First-class Manual
Hooks / permissions PreToolUse / PostToolUse / Stop / etc., can_use_tool callback Same DIY
Footprint ~2k LOC, 4 deps Heavier (subprocess + Node bridge) Heavy

If you've used Claude Code's hooks, permission modes, or subagent dispatch, the concepts here will be familiar — they're the same model, re-implemented as a clean Python library.


Install

pip install binho-sdk
# or
uv add binho-sdk

Requires Python 3.11+. Set OPENROUTER_API_KEY in your environment, or pass api_key= to Options.

export OPENROUTER_API_KEY=sk-or-v1-...

What it gives you

  • query() async generator — stateless entry point. A single iterator yields every event the loop produces and exhausts after the final Result.
  • Event streamTextDelta, ToolUse, ToolResult, TurnStart / TurnEnd, SubagentStart / SubagentEnd, Result. Discriminated union keyed by .type.
  • Built-in toolsRead, Write, Edit, Glob, Grep, Bash, Task (subagent dispatcher). The read-before-edit invariant is enforced by a per-call ReadTracker.
  • Custom tools@tool decorator on an async function with a Pydantic input model; the JSON Schema is generated automatically.
  • MCP client — HTTP transport, tools-only. Reference an MCP tool by mcp__<server>__<tool> in Options.tools after configuring Options.mcp_servers.
  • Hooks — middleware-style interception around tool calls and turn boundaries: PreToolUse, PostToolUse, Stop, SubagentStart, SubagentStop. Hooks can allow / deny / modify inputs and outputs.
  • Permissions — four modes (default, acceptEdits, bypassPermissions, plan) plus a pluggable can_use_tool callback. The default policy mirrors Claude Code's behavior (read-only in plan, ask on Bash in acceptEdits, etc.).
  • Subagents — named AgentDefinitions in Options.agents. The model dispatches them through the Task tool; their events are forwarded into the parent's stream wrapped in SubagentStart / SubagentEnd.
  • Token budget guard — opt-in via Options.context_window, stops the loop with stop_reason="context_full" before you blow the model's context window.
  • OpenRouter routingprovider_routing, allow_provider_fallback, max_tokens (to keep OpenRouter's per-request credit reservation small at high concurrency).

Quickstart: a code-exploration agent with subagents and MCP

import asyncio
from binho_sdk import query, Options, AgentDefinition, McpServerConfig

async def main():
    options = Options(
        model="anthropic/claude-sonnet-4-5",
        tools=["Read", "Grep", "Glob", "Task", "mcp__docs__bm25_search"],
        agents={
            "explorer": AgentDefinition(
                description="Investigates code, returns findings.",
                prompt="You are an exploration subagent. Read aggressively, summarize tightly.",
                tools=["Read", "Grep", "Glob", "mcp__docs__bm25_search"],
            ),
            "reviewer": AgentDefinition(
                description="Skeptically reviews findings.",
                prompt="You are a review subagent. Distrust the narrative.",
                tools=["Read", "Grep"],
                permission_mode="plan",
            ),
        },
        mcp_servers={
            "docs": McpServerConfig(
                url="https://example.com/mcp",
                headers={"Authorization": "Bearer ..."},
            ),
        },
        max_turns=30,
        context_window=200_000,
    )

    async for ev in query("Plan the migration of module X.", options):
        match ev.type:
            case "text_delta":
                print(ev.delta, end="", flush=True)
            case "tool_use":
                print(f"\n{ev.name}({ev.arguments})")
            case "subagent_start":
                print(f"\n┌── subagent {ev.agent_type}")
            case "subagent_end":
                print("\n└── done")
            case "result":
                print(f"\n[stop={ev.stop_reason} cost=${ev.usage.cost_usd or 0:.4f}]")

asyncio.run(main())

Use cases

Code agents. Read/Grep/Edit/Bash plus a Task tool dispatching focused subagents is the same loop Claude Code uses. Point it at any model OpenRouter offers — useful for benchmarking, cost optimization, or running against a model that isn't Anthropic's.

Research / RAG with MCP. Plug in an MCP doc-search server, add mcp__server__search to your tools list, and the model has structured retrieval without you writing a retriever loop.

CI / repo bots. Run as a one-shot inside GitHub Actions: spawn query(), stream the result, post a comment. The single-async-iterator shape makes this trivial — no subprocess, no daemon.

Doc & data pipelines. Use the Stop hook plus the tool_terminate stop reason to wire the loop into longer pipelines that need an agent step in the middle.

Custom shells / TUIs. The event stream is everything you need to render a Claude Code-style UI on top: text deltas, tool calls, subagent envelopes, final result.

Evaluations. Inject a mock provider via the test seam (_provider=) and replay deterministic transcripts against your tool definitions.


Custom tools

from pydantic import BaseModel
from binho_sdk import tool, ToolContext, Options

class WeatherArgs(BaseModel):
    city: str

@tool(name="weather", description="Look up current weather for a city.")
async def weather(args: WeatherArgs, ctx: ToolContext) -> str:
    # ctx gives you cwd, options, the running message history, agent_id, etc.
    return f"Cloudy, 14°C in {args.city}"

# Pass the Tool directly:
options = Options(model="anthropic/claude-sonnet-4-5", tools=[weather])

Hooks

from binho_sdk import HookContext, HookResult

async def block_rm(ctx: HookContext) -> HookResult | None:
    if ctx.tool_name == "Bash" and "rm -rf" in (ctx.tool_input or {}).get("command", ""):
        return HookResult(decision="deny", reason="rm -rf is denied here")
    return None

options = Options(
    model="anthropic/claude-sonnet-4-5",
    tools=["Bash"],
    hooks={"PreToolUse": [block_rm]},
)

Five events: PreToolUse, PostToolUse, Stop, SubagentStart, SubagentStop. Each callback can return None (pass), HookResult(decision="deny", ...) (block), or HookResult(decision="modify", modified_input=..., modified_output=...) (rewrite the tool's input or output before it reaches the next hook / the loop).


Permissions

from binho_sdk import CanUseToolInput, PermissionResult

async def policy(inp: CanUseToolInput) -> PermissionResult:
    if inp.tool_name == "Bash" and "sudo" in str(inp.tool_input.get("command", "")):
        return PermissionResult(behavior="deny", message="no sudo")
    return PermissionResult(behavior="allow")

options = Options(model="...", tools=["Bash"], can_use_tool=policy)

Without a can_use_tool callback, a small default policy is applied per permission_mode:

Mode Behavior
default Allow all.
plan Allow Read / Glob / Grep only. Everything else denied.
acceptEdits Allow read+edit built-ins, ask on Bash, deny MCP.
bypassPermissions Allow all. Use only when you trust the input.

Stop reasons

Result.stop_reason is one of:

  • end_turn — assistant returned no tool calls.
  • max_turnsOptions.max_turns exceeded.
  • tool_terminate — every runnable tool in a batch returned terminate=True (a clean way for a tool to end the loop — e.g. a "submit answer" tool).
  • context_full — token estimate exceeded context_window * context_safety_margin.

Project status

binho-sdk is 0.1.x. The public surface (query, Options, event types, @tool, hooks, permissions) is intended to be stable, but expect minor additive changes before 1.0. Breaking changes will be flagged in the changelog.

Not in v1:

  • MCP stdio transport (HTTP only).
  • Persistent permission allowlists (the PermissionUpdate type is wired but ignored).
  • A non-OpenRouter provider. The Provider interface is small — adding a direct Anthropic / OpenAI / Bedrock provider is straightforward.

Versioning

This project follows SemVer. While the version is 0.x, the public API may change in minor releases (0.1 → 0.2); patch releases (0.1.0 → 0.1.1) contain bug fixes only and remain API-compatible. Pin binho-sdk>=0.1,<0.2 if you want patch updates without surprise. The version will move to 1.0.0 once the public surface is considered stable.


Development

git clone https://github.com/unlegacy/binho-sdk
cd binho-sdk
uv sync --extra dev
uv run pytest
uv run ruff check
uv run mypy src

Tests marked smoke hit the real OpenRouter API and require OPENROUTER_API_KEY; they are skipped by default. Run pytest -m smoke to include them.


License

MIT. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

binho_sdk-0.1.0.tar.gz (57.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

binho_sdk-0.1.0-py3-none-any.whl (46.6 kB view details)

Uploaded Python 3

File details

Details for the file binho_sdk-0.1.0.tar.gz.

File metadata

  • Download URL: binho_sdk-0.1.0.tar.gz
  • Upload date:
  • Size: 57.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for binho_sdk-0.1.0.tar.gz
Algorithm Hash digest
SHA256 71b8834738e7654c4168185373f3426beb4b20901f9f7518858913f1a46b6e00
MD5 ef89d8392179445e9bc40eb234f675ce
BLAKE2b-256 969cde451d959e0af2e2bfceedd6ee62ef693e633f662bc3864193af73bae47a

See more details on using hashes here.

Provenance

The following attestation bundles were made for binho_sdk-0.1.0.tar.gz:

Publisher: release.yml on unlegacy/binho-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file binho_sdk-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: binho_sdk-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 46.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for binho_sdk-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3465326f2495b6faf099ccc85b5e1f9ad1bca346dfbe0efbddd71c1225b5329d
MD5 87bbeee01ad106f382c26f0e017275eb
BLAKE2b-256 e1d2dafa522dabd9b8b512411cff71f65d7ac08fd5154f9d50eb74a7514a9395

See more details on using hashes here.

Provenance

The following attestation bundles were made for binho_sdk-0.1.0-py3-none-any.whl:

Publisher: release.yml on unlegacy/binho-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page