This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Install dependencies (uses uv)
uv sync
# Run all tests
just test # or: uv run pytest
# Run a single test file
uv run pytest tests/test_observability.py
# Run a single test by name
uv run pytest tests/test_observability.py::TestTracerContextPropagation::test_nested_spans_propagate_parent
# Lint & format
just lint # or: uv run ruff check src/ tests/
just format # or: uv run ruff format src/ tests/
# Type check
uv run mypy
# Run the CLI
uv run bubBub is a collaborative coding agent built on the Republic framework. Republic provides LLM, Tool, ToolContext, Tape, and TapeEntry primitives.
Channel (CLI/Telegram/Discord)
→ AppRuntime.handle_input()
→ SessionRuntime (per-session isolation)
→ InputRouter (decides: agent loop vs direct response)
→ AgentLoop (think-act cycle)
→ ModelRunner._chat() (LLM call)
→ ToolRegistry.execute() (tool dispatch)
src/bub/app/runtime.py—AppRuntimemanages sessions, tracer lifecycle, and the mainhandle_input()entry point.SessionRuntimeholds per-session state (tape, tools, LLM).src/bub/core/model_runner.py—ModelRunnerruns the agent loop: LLM call → tool execution → repeat. Each step is traced.src/bub/core/input_router.py— Routes user input to agent loop or direct response based on heuristics.src/bub/tools/registry.py—ToolRegistrywith decorator-based registration. Tools use Pydantic models for schema, converted viatool_from_model().src/bub/tools/builtin.py— Registers all built-in tools (bash, file ops, web, task, agent).src/bub/config/settings.py— Pydantic Settings withBUB_env prefix. All config via environment variables.src/bub/channels/—BaseChannelABC with CLI, Telegram, Discord adapters.src/bub/skills/— SKILL.md-based discovery from project/global/builtin roots.
Bub uses an append-only Tape for conversation history. Key operations:
anchor()/handoff()— phase transitionsfork()/merge()— sub-agent isolation (used by agent delegation tool)
Abstract Tracer with ContextVar-based span propagation. Three backends:
NullTracer— zero overhead when disabledLangfuseBackend— Langfuse v3 API (usesstart_span(), notclient.trace())OtelBackend— OpenTelemetry with OTLP gRPC exporter
Configured via BUB_TRACE_ENABLED, BUB_TRACE_BACKEND, and backend-specific env vars.
from republic import Tool
from pydantic import BaseModel
from bub.tools.registry import ToolRegistry, tool_from_model
class MyToolInput(BaseModel):
arg: str
def register_my_tools(registry: ToolRegistry):
@registry.register(tool_from_model("my.tool", MyToolInput, description="..."))
async def my_tool(ctx, arg: str) -> str:
return "result"Tools are prompted in two phases: compact list first, then expanded schema only when the model requests $hint for a specific tool. This reduces prompt size.
- Python 3.12+, line length 120
- Ruff for linting and formatting (
ruff check,ruff format) from __future__ import annotationsin all modules- Mypy with
ignore_missing_imports = true;src/bub/skills/is excluded from type checking - Tests use
pytestwithpytest-asyncio; test files intests/
When working with the Langfuse backend (langfuse_tracer.py):
- No
client.trace()in v3 — useclient.start_span()which implicitly creates a trace handle.end()takes no arguments — callhandle.update(output=..., level=...)first, thenhandle.end()- Nesting: use
parent_handle.start_span()/parent_handle.start_generation()