Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@
# BUB_MAX_STEPS=50
# BUB_MAX_TOKENS=16384
# BUB_MODEL_TIMEOUT_SECONDS=300
# Estimated tokens (4 chars each) above which a tool result is spilled to the spill tape. 0 disables.
# BUB_TOOL_SPILL_THRESHOLD=4096
# Hard cap on the serialized model request body in bytes; oversized tool messages are clamped. 0 disables.
# BUB_MAX_REQUEST_BYTES=262144
# BUB_HOME=~/.bub

# ---------------------------------------------------------------------------
Expand Down
1 change: 0 additions & 1 deletion src/bub/builtin/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
"""Bub builtin runtime package."""
5 changes: 0 additions & 5 deletions src/bub/builtin/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,6 @@ async def run_stream(
tape = self.tape.session_tape(
session_id, workspace_from_state(state), context=replace(self.tape.context, state=state)
)
tape_store = self.framework.get_tape_store()
if tape_store is not None:
if not is_async_tape_store(tape_store):
tape_store = AsyncTapeStoreAdapter(tape_store)
state.setdefault("_runtime_spill_store", tape_store)
merge_back = not session_id.startswith("temp/")
stack = AsyncExitStack()
# The fork_tape context manager must not be exited until the last chunk of the stream is consumed.
Expand Down
47 changes: 1 addition & 46 deletions src/bub/builtin/model_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ async def fire_after(error: Exception | None = None) -> None:
async with asyncio.timeout(self.settings.model_timeout_seconds):
completion = await self.completion_response(
model=request.model,
messages=self._clamp_oversized_messages(list(request.messages)),
messages=list(request.messages),
tools=tools,
max_tokens=request.max_tokens,
reasoning_effort=tape.context.state.get("reasoning_effort"),
Expand Down Expand Up @@ -241,51 +241,6 @@ async def fire_after(error: Exception | None = None) -> None:
def generate_run_id() -> str:
return f"run-{datetime.now(UTC).strftime('%Y%m%dT%H%M%S%fZ')}"

def _clamp_oversized_messages(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Clamp tool messages so the serialized request body stays under a hard cap.

This is the last-resort fuse against provider/reverse-proxy ``413``: it
shrinks the largest oversized ``role: tool`` messages with a head+tail
clamp and an explicit marker, so even a plugin that bypasses spilling
can never send an unbounded request body.
"""
cap = self.settings.max_request_bytes
if cap <= 0 or not messages:
return messages

import json as _json

def size() -> int:
try:
return len(_json.dumps(messages, ensure_ascii=False, default=str))
except TypeError:
return sum(len(str(m.get("content", ""))) for m in messages if isinstance(m, dict))

if size() <= cap:
return messages

def clamp_tool_messages(messages: list[dict[str, Any]], budget: int) -> list[dict[str, Any]]:
"""Head+tail clamp every oversized tool message to ``budget`` chars."""
result: list[dict[str, Any]] = []
for message in messages:
content = message.get("content")
if isinstance(content, str) and message.get("role") == "tool" and len(content) > budget:
head = content[: budget // 2]
tail = content[-(budget - budget // 2) :]
message = dict(message)
message["content"] = (
f"{head}\n\n[clamped: {len(content) - budget:,} chars removed to keep the request body bounded]\n\n{tail}"
)
result.append(message)
return result

clamped = clamp_tool_messages(messages, budget=2000)
if size() <= cap:
return clamped

# Still over: shrink every tool message to a per-message budget within the cap.
return clamp_tool_messages(clamped, budget=cap // max(1, len(clamped)))

async def _fire_after_llm_call(
self,
request: LlmCallRequest,
Expand Down
8 changes: 0 additions & 8 deletions src/bub/builtin/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,6 @@ class AgentSettings(Settings):
max_steps: int = 50
max_tokens: int = DEFAULT_MAX_TOKENS
model_timeout_seconds: int | None = None
tool_spill_threshold: int = Field(
4096,
description="Estimated tokens (4 chars each) above which a tool result is spilled to the spill tape. 0 disables spilling.",
)
max_request_bytes: int = Field(
262_144,
description="Hard cap on the serialized model request body in bytes; oversized tool messages are clamped before sending. 0 disables.",
)
client_args: dict[str, Any] = Field(default_factory=dict)
completion_args: dict[str, Any] = Field(default_factory=dict)
verbose: int = Field(default=0, description="Verbosity level for logging. Higher means more verbose.", ge=0, le=2)
Expand Down
150 changes: 0 additions & 150 deletions src/bub/builtin/spill.py

This file was deleted.

5 changes: 1 addition & 4 deletions src/bub/builtin/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,7 @@ def _redact_payload(payload: dict) -> None:

async def append(self, tape: str, entry: TapeEntry) -> None:
self._redact_payload(entry.payload)
if tape == self._tape:
self._store.append(tape, entry)
return
await self._parent.append(tape, entry)
self._store.append(tape, entry)

async def merge_back(self) -> None:
if self._tape_was_reset:
Expand Down
43 changes: 0 additions & 43 deletions src/bub/builtin/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@
from pydantic import BaseModel, Field

from bub.builtin.shell_manager import shell_manager
from bub.builtin.spill import (
MAX_READ_LINES,
read_slice,
read_spilled,
)
from bub.skills import discover_skills
from bub.tools import REGISTRY, Tool, ToolContext, tool

Expand Down Expand Up @@ -200,44 +195,6 @@ async def kill_bash(shell_id: str) -> str:
return f"id: {shell.shell_id}\nstatus: {shell.status}\nexit_code: {shell.returncode}"


@tool(context=True, name="read_tool_result")
async def read_tool_result(
handle: str,
offset: int = 0,
limit: int = 200,
from_end: bool = False,
pattern: str | None = None,
*,
context: ToolContext,
) -> str:
"""Read a bounded slice of a spilled tool result.

Args:
handle: The handle from a `[tool output spilled ...]` ref.
offset: Number of lines to skip from the start (or end when `from_end`). Must be >= 0.
limit: Maximum number of lines to return (>= 1; clamped to a built-in cap).
from_end: Count `offset`/`limit` from the end of the result.
pattern: Optional literal substring; only lines containing it are returned.
"""
if offset < 0:
return "`offset` must be >= 0."
if limit < 1:
return "`limit` must be >= 1."
limit = min(limit, MAX_READ_LINES)

store = context.state.get("_runtime_spill_store")
if store is None:
return "spill store unavailable in this context."
output = await read_spilled(store=store, handle=handle)
if output is None:
return (
f"[No stored tool result for handle {handle!r}. Use the exact handle from a "
'"[tool output spilled ...]" marker; if the result is no longer available, '
"re-run the original tool.]"
)
return read_slice(output, offset=offset, limit=limit, from_end=from_end, pattern=pattern)


@tool(context=True, name="fs.read")
def fs_read(path: str, offset: int = 0, limit: int | None = None, *, context: ToolContext) -> str:
"""Read a text file and return its content. Supports optional pagination with offset and limit."""
Expand Down
8 changes: 0 additions & 8 deletions src/bub/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
from loguru import logger
from pydantic import BaseModel, ConfigDict, TypeAdapter, ValidationError, validate_call

from bub.builtin.spill import maybe_spill
from bub.builtin.tape import Tape
from bub.errors import BubError, ErrorKind
from bub.hooks.interception import ToolCall, ToolCallResult
Expand Down Expand Up @@ -257,13 +256,6 @@ async def _handle_tool_response_async(
raise
else:
await self._fire_after_tool_call(call, hook_state, started, result=result)
if context is not None and isinstance(result, str):
result = await maybe_spill(
tool=call.tool,
run_id=context.run_id,
result=result,
store=context.state.get("_runtime_spill_store"),
)
return result

async def _invoke_normalized(self, tool_obj: Tool, call: ToolCall, context: ToolContext | None) -> Any:
Expand Down
Loading
Loading