Skip to content
Draft
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ Lines starting with `,` enter internal command mode (`,help`, `,skill name=my-sk
| `BUB_MAX_STEPS` | unlimited | Tool-use loop limit; must be a positive integer |
| `BUB_MAX_TOKENS` | `16384` | Max tokens per model call |
| `BUB_MODEL_TIMEOUT_SECONDS` | — | Model call timeout (seconds) |
| `BUB_SPILL_THRESHOLD` | `4096` | Estimated tokens before tool output spills; `0` disables |

## Background

Expand Down
2 changes: 2 additions & 0 deletions env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
# BUB_MAX_STEPS=50
# BUB_MAX_TOKENS=16384
# BUB_MODEL_TIMEOUT_SECONDS=300
# Estimated tokens (4 chars each) above which string tool results move to a chunked spill tape. 0 disables.
# BUB_SPILL_THRESHOLD=4096
# BUB_HOME=~/.bub

# ---------------------------------------------------------------------------
Expand Down
7 changes: 6 additions & 1 deletion src/bub/builtin/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,12 @@ def tape(self) -> Tape:
tape_store = InMemoryTapeStore()
if not is_async_tape_store(tape_store):
tape_store = AsyncTapeStoreAdapter(tape_store)
return Tape(bub.home / "tapes", tape_store, self.framework.build_tape_context())
return Tape(
bub.home / "tapes",
tape_store,
self.framework.build_tape_context(),
sidecars=self.framework.get_tape_sidecars(),
)

@staticmethod
def _events_from_iterable(iterable: Iterable) -> AsyncStreamEvents:
Expand Down
36 changes: 34 additions & 2 deletions src/bub/builtin/hook_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
from bub.envelope import Envelope, content_of, field_of
from bub.framework import BubFramework
from bub.hooks import hookimpl
from bub.hooks.interception import ToolCall, ToolCallDecision
from bub.hooks.interception import ToolCall, ToolCallDecision, ToolCallResult
from bub.model_selection import ModelChoice, ModelOptions
from bub.sidecars import TapeSidecar
from bub.streaming import AsyncStreamEvents
from bub.tape import TapeContext, TapeStore
from bub.turn import TurnState
Expand Down Expand Up @@ -65,7 +66,7 @@ class BuiltinImpl:
"""Default hook implementations for basic runtime operations."""

def __init__(self, framework: BubFramework) -> None:
from bub.builtin import tools # noqa: F401
from bub.builtin import spill, tools # noqa: F401

self.framework = framework
self._agent: Agent | None = None
Expand Down Expand Up @@ -361,6 +362,13 @@ def provide_tape_store(self) -> TapeStore:

return FileTapeStore(directory=bub.home / "tapes")

@hookimpl
def provide_tape_sidecars(self) -> list[TapeSidecar]:
from bub.builtin.spill import SpillSettings, SpillStore
from bub.configure import ensure_config

return [SpillStore(ensure_config(SpillSettings))]

@hookimpl
def build_tape_context(self) -> TapeContext:
return default_tape_context()
Expand Down Expand Up @@ -408,3 +416,27 @@ async def before_tool_call(
else:
guidance = f"Tool `{call.tool}` does not exist. No similar tool is available."
return ToolCallDecision.replace(guidance)

@hookimpl
async def after_tool_call(
self,
call: ToolCall,
result: ToolCallResult,
state: TurnState,
) -> None:
from bub.builtin.spill import SpillStore

if result.error is not None or not isinstance(result.result, str):
return
tape = state.get("_runtime_tape")
if tape is None:
return
spill = SpillStore.mounted(tape)
if spill is None:
return
result.result = await spill.spill_tool_result(
tape,
result.result,
tool=call.tool,
run_id=call.run_id,
)
Loading
Loading