Tools and CodeMode
A tool gives your agent an action: look up an order, read a file, or call a service. Tool describes the action’s name, parameters, and result. You can pass it directly to an Agent or register it with the Bub application.
This page covers the shared tool contract and direct SDK use. For installation and a complete model loop, start with Build your own agent.
Define the action
Section titled “Define the action”Tool.from_callable() reads parameter types from annotations and a description from the docstring. It accepts synchronous and asynchronous functions and returns a tool for you to pass to an Agent.
from bub.core import Agent
from bub.core.tools import Tool
def add(a: int, b: int) -> int:
"""Add two integers."""
return a + b
add_tool = Tool.from_callable(add, name="math.add")
agent = Agent(tools=[add_tool])
The runtime keeps the name math.add; model-facing names replace dots with underscores, so the model sees math_add. Use description= to supply a description explicitly.
Plain Agent offers all supplied tools except those marked exposure="command". Deferred loading, the allowed_tools state value, and the state["code_mode"] switch are BubAgent policies. To choose tools per step in your own agent, override prepare_tools().
Return data the caller can use
Section titled “Return data the caller can use”Return a dictionary, list, typed value, or string that represents the result. A renderer can turn structured data into text when the model calls the tool directly:
from typing import TypedDict
class Order(TypedDict):
id: str
status: str
def lookup_order(order_id: str) -> Order:
"""Look up an order by id."""
return {"id": order_id, "status": "shipped"}
order_tool = Tool.from_callable(
lookup_order,
renderer=lambda order: f"Order {order['id']}: {order['status']}",
)
Tool.render(result) applies the renderer. By default, strings pass through, Pydantic models and JSON-serializable values become JSON, and other values use str(). The model-facing executor renders before ToolCallFinished, so handlers see the result that will be sent to the model. The executor used by CodeMode keeps structured results intact.
Tool execution errors become tool results in the model loop, giving the model a chance to respond or retry. A direct command call propagates its error to the caller. Event handlers have their own exception behavior.
Use the current conversation
Section titled “Use the current conversation”Pass context=True to receive a ToolContext as the keyword argument context. It carries the current tape, turn state, run ID, and event bus.
from bub.core.tools import ToolContext
async def remember(note: str, *, context: ToolContext) -> str:
"""Record a note for this conversation."""
await context.tape.append_event("note", {"text": note})
return "Recorded."
remember_tool = Tool.from_callable(remember, context=True)
This writes a business event to the tape. The default context selector reads conversation messages and tool exchanges; including this event in a model request requires your own selection logic. Use Tape and context to decide which saved information the model should read.
Compose tools with CodeMode
Section titled “Compose tools with CodeMode”CodeMode lets the model write Python that calls your tools. It provides a run_code tool and a prompt section describing the tools available inside that code. Add both explicitly to a plain Agent:
from bub.core.codemode import CodeMode
code_mode = CodeMode([add_tool, order_tool])
agent = Agent(
tools=[code_mode.run_code],
system_prompt=[
"Use the available tools to answer the user's question.",
code_mode.prompt,
],
)
Run this agent with the model lifecycle shown in the SDK guide. The model can call run_code with code such as:
order = await tools.lookup_order(order_id="A123")
print(order["status"])
Inside run_code, tool names use underscores, calls take keyword arguments, and results retain their structured form. Top-level await is supported. Calls pass through ToolCallStarted and ToolCallFinished with context.code_mode=True.
CodeMode takes the tools you supply, excluding exposure="command". BubAgent groups auto, direct, and code tools through its application code mode settings.
| Option | Behavior |
|---|---|
tools |
A tool collection, or a function of turn state returning that collection. Return None to disable code execution for a run. |
environment |
Where code runs. Defaults to LocalEnvironment() in the current directory. May also be a function of turn state. |
models |
Default provider:model for Republic calls made by the code, or a resolver returning a model spec and client options. Without it, code must supply a spec and Republic uses environment credentials. |
stub_dir |
Optional directory for the generated Python tool definitions. By default, the definitions appear in the system prompt. A file-based stub requires an agent that can read that file. |
media_tools |
True by default. Adds attach_image, attach_audio, and attach_video for returning media. |
The code receives a Republic bridge for chat, embedding, and decision model calls. Requests go through the host’s model resolver. The host handles provider credentials for these calls.
run_code returns printed output and attached media. Its default timeout is 120 seconds; callers may pass timeout_seconds. With LocalEnvironment, each call starts a Python process with the host user’s permissions and stops that process and its descendants on completion or cancellation. Choose another environment when you need a different execution boundary.
Choose an execution environment
Section titled “Choose an execution environment”The Environment interface in bub.environment supplies process execution, file access, and Python execution. CodeMode(environment=...) uses the environment you provide. In the Bub application, the provide_environment hook selects an environment per session.
| Operation | Responsibility |
|---|---|
spawn(command, *, cwd, env) |
Return a process with output streams, input, wait, and process-tree signalling. |
read_text, write_text, read_bytes |
Access files inside the environment. |
workspace, resolve_path |
Define its working directory and resolve relative paths. |
run_code(code, *, tools, call_tool, write) |
Run Python, route tool calls through call_tool, and send stdout to write. Stop execution when cancelled. |
close() |
Release resources owned by the environment. |
The environment runs the generated code and calls back to the host to execute your Python tools. Close custom environments when your application stops using them.
Call a tool without a model
Section titled “Call a tool without a model”Set command_prefix to accept text commands. This example uses the add_tool defined above and needs no model credentials:
import asyncio
async def calculate() -> None:
agent = Agent(tools=[add_tool], command_prefix=",")
async with agent.run(",math.add a=2 b=3", tape="calculator") as stream:
async for event in stream:
if event.kind == "text":
print(event.data["delta"])
asyncio.run(calculate())
It prints 5. Commands invoke supplied tools and emit tool events plus CommandFinished; they bypass the model loop. Prefixes must be non-empty and contain no whitespace. The parser accepts positional arguments followed by key=value arguments. A command that does not name a tool falls back to the agent’s bash tool if one was supplied.
The Bub application supplies commands such as ,skill and ,code_mode.