Skip to content

refactor!: extract an event-driven core agent into bub.core - #356

Merged
frostming merged 14 commits into
devfrom
refactor/core-agent
Oct 14, 2026
Merged

frostming merged 14 commits into
devfrom
refactor/core-agent

Conversation

@frostming

@frostming frostming commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

What and why

Bub's agent loop was tangled with settings, plugin hooks, skills and commands in bub.builtin.Agent / ModelRunner. This PR extracts a low-level, reusable agent kernel into bub.core and rebuilds the builtin agent on top of it.

bub.core

  • Agent: the agent loop over tools, system prompt sections and a tape store. It holds no settings, model or session. Each run gets the model to call and works on a fork of a named tape: async with agent.run(prompt, model=..., options=..., tape=..., state=...) as stream: closes the stream (merging the fork and ending the invoke_agent span) when the block exits. agent.start(...) returns a stream for another consumer to close, as BubAgent.run_stream does.
  • Models: a run accepts a republic.ChatModel directly. A ModelClient protocol covers callers with their own policy (fallbacks, credentials, timeouts).
  • Events (bub.core.events): RunStarted, StepStarted/StepFinished, Before/AfterLlmCall, Before/AfterToolCall, ChatCompleted, plus user-defined events. Handlers are registered with agent.events.on(...), either called directly or used as a decorator. Before* events can be modified or short-circuited. Tape recording is a TapeRecorder handler.
  • Commands: with Agent(command_prefix=...), a text prompt starting with the prefix calls a tool directly (or the bash tool) instead of the model and emits CommandFinished; None (the default) disables commands. Prefix rules move to bub.core.commands.
  • System prompt: ordered sections, registered with agent.add_system_prompt(...) (also usable as a decorator). Each section receives a PromptContext with the step's prompt, tools, model, tape and state.
  • Kernel modules moved in: tape, store, sidecars, tools and tape context. The old bub.tape / bub.store / bub.sidecars / bub.tools paths remain as module aliases.

Builtin

  • BubAgent (formerly bub.builtin.Agent, alias kept) subclasses the core agent. It adds settings, plugin hooks, skills, code mode, deferred tools and steering, and passes its command prefix to the core agent. run_stream() prepares the turn and calls the core start().
  • BubModel replaces the old candidate/fallback logic of ModelRunner. bub.builtin.model_runner is removed.
  • Plugin hooks: hookspecs are unchanged. PluginAgentHooks adapts the pluggy interception hooks to core events.
  • Tape archiving moves out of Tape into bub.builtin.archive; Tape and Agent no longer take an archive path.
  • Removed: bub.builtin.context, and the deprecated bub.types module along with its test.

Behavior changes

  • A session's reasoning_effort is now fixed at the start of a turn, the same as model switches.
  • Unknown allowed_tools raise before the session tape is forked.
  • Loading turn state happens before the invoke_agent span starts.
  • On tape.reset with an archive, sidecar.archive events are recorded after sidecar.reset.
  • agent.tape(name, state=...) returns a conversation's tape for use outside runs (no unscoped tape factory); session_tape_name() computes a session's tape name.
  • Agent(store=...) takes a store instance. Builtin channels no longer create the agent before framework.running() (the CLI channel creates it on first use), so BubAgent takes the framework's store when created; SDK users create it inside framework.running().

Impacted modules

bub.core.* (new), bub.builtin.agent, bub.builtin.model (new), bub.builtin.archive (new), bub.builtin.settings, bub.builtin.tools, bub.builtin.hook_impl, bub.builtin.codemode, bub.hooks.interception, bub.framework, bub.channels.cli, docs (SDK guide rewritten around bub.core, new FastAPI tutorial, hooks/types/tape/turn-pipeline references).

Verification

  • make check (lock, prek, ruff, mypy): pass
  • uv run pytest -q: 607 passed, 20 skipped
  • with opentelemetry-sdk installed (tracing tests enabled): 626 passed, 1 skipped
  • Smoke tests: bub run ",help" and bub run ",tape.reset archive=true"
  • SDK doc examples were run against a mocked provider.
  • The docs site build was not run locally because of a pnpm version mismatch in this environment.

🤖 Generated with Claude Code

Add `bub.core`, an agent kernel with no settings or plugin dependencies:
the agent loop runs over tools, system prompt sections and a tape store,
and each run is given the model to call (a `republic.ChatModel` or a
`ModelClient`). Interception hooks and tape recording are event handlers
on `agent.events`; system prompt sections are registered on the agent.

`BubAgent` (formerly `bub.builtin.Agent`) subclasses the core agent and
adds settings, plugin hooks, skills, commands, code mode, deferred tools
and steering. `ModelRunner` is replaced by the core loop and `BubModel`.
Tape archiving moves from `Tape` to `bub.builtin.archive`.

The tape, store, sidecar and tool modules move to `bub.core`; their old
`bub.*` paths remain as aliases. `bub.builtin.context` and the deprecated
`bub.types` module are removed.

BREAKING CHANGE: `bub.builtin.Agent` is now `BubAgent` (alias kept),
`bub.builtin.model_runner` and `bub.builtin.context` are removed,
`Tape` no longer takes an archive path, and `bub.types` is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
bub 4e6cfae Commit Preview URL

Branch Preview URL
Oct 14 2026, 12:51 AM

frostming and others added 8 commits October 13, 2026 11:43
`agent.tape` exposed an unscoped tape that only worked as a factory.
`agent.tape(name, state=...)` now returns a conversation's tape for use
outside runs, and `session_tape_name()` computes a session's tape name
without building a tape.

BREAKING CHANGE: `Agent.tape` is a method taking the tape name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Builtin channels no longer create the agent while they are constructed,
before `framework.running()`: the CLI channel creates it on first use
and Telegram reads the command prefix from settings. `BubAgent` can then
take the framework's store when it is created, and the core `Agent`
only accepts a store instance.

BREAKING CHANGE: `Agent(store=...)` no longer accepts a function, and
`BubAgent` must be created inside `framework.running()` to use the
framework's store. `CliChannel` takes `get_agent` and `workspace`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`async with agent.run(...) as stream` closes the stream when the block
exits, including after an early break, so the tape fork merges back and
the trace span ends without `aclosing`. `Agent.start(...)` keeps the
previous behavior for code that hands the stream to another consumer,
such as `BubAgent.run_stream`.

BREAKING CHANGE: `await agent.run(...)` is now `await agent.start(...)`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Agent(command_prefix=...)` runs a text prompt starting with the prefix
as a command: a known tool is called directly with parsed arguments,
any other line runs through the `bash` tool, and the outcome is recorded
as a `command` event. Without a prefix (the default) commands are off.
`BubAgent` passes its configured prefix instead of handling commands
itself. Prefix rules and argument parsing move to `bub.core.commands`.

BREAKING CHANGE: `bub.builtin.commands` is now `bub.core.commands`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Frost Ming <me@frostming.com>
The tutorial builds a self-contained `bub.core` app with one lock per
session, so different sessions run concurrently, and walks through
running and calling it. The SDK guide links to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`run_model_stream` runs turns through `BubAgent.run_stream()` and the
builtin plugin implements no `run_model`; `build_prompt` only marks
command prompts for the agent to run; `load_state` restores session
settings from the session tape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rainzee

rainzee commented Oct 13, 2026

Copy link
Copy Markdown

感觉挺好,core 就是最小 agent runnable 实例,那就很灵活了

frostming and others added 4 commits October 13, 2026 17:45
Drop the per-tool logging wrapper and the contextvar tool call reporter.
ToolExecutor now logs tool calls after BeforeToolCall handlers run, and the
CLI renders tool calls from BeforeToolCall/AfterToolCall events on the agent.

Commands now run their tools through ToolExecutor, so tool events and hooks
apply to them; positional command arguments bind to the tool's parameters in
order. ToolContext.command marks these calls, and the builtin unknown-tool and
spill hooks skip them. Tool.run is now always async.

BREAKING CHANGE: tool_call_reporter and ToolCallReporter are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rename ToolExecutor.execute_async to execute_many and add execute for a
single call, which returns the result or raises the call's BubError.
Commands and code mode tool calls use execute.

BREAKING CHANGE: ToolExecutor.execute_async is renamed to execute_many.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove Agent.add_system_prompt and Agent.remove_system_prompt; add and
remove sections with agent.system_prompt.add and agent.system_prompt.remove.

BREAKING CHANGE: Agent.add_system_prompt and Agent.remove_system_prompt are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Frost Ming <me@frostming.com>
@frostming
frostming force-pushed the refactor/core-agent branch from c792796 to cd4dafa Compare October 14, 2026 00:45
Agent(model=...) sets the model of runs not given one; run, start, run_on
and run_once take model as optional. A run that calls a model without
either raises ValueError, while commands need no model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frostming
frostming merged commit f2416ec into dev Oct 14, 2026
6 checks passed
@frostming
frostming deleted the refactor/core-agent branch October 14, 2026 00:57
frostming added a commit that referenced this pull request Oct 14, 2026
* refactor: use Republic for builtin models and authentication

* refactor: consume native model outputs

* refactor: delegate request extras to Republic

* refactor: simplify builtin options and naming

* chore: use Republic 0.6 preview

* Refactor prompt handling and improve compatibility with legacy content

- Introduced `republic` module to manage media types and user content.
- Updated tests to validate new prompt structure and legacy content conversion.
- Modified `build_prompt` to return a list of `UserContent` instead of legacy formats.
- Enhanced documentation to reflect changes in prompt handling and hook signatures.
- Adjusted various tests to ensure compatibility with new prompt formats and legacy support.

Signed-off-by: Frost Ming <me@frostming.com>

* fix: replay Chat Completions tool calls from older tapes

Older Bub versions recorded tool calls as
{"id", "type": "function", "function": {"name", "arguments"}}, which
republic.Message.from_dict rejects with KeyError: 'name', so sessions
with such tapes could not build their context. Convert them to
Republic's form in to_message, next to the other legacy conversions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: identify Republic model clients as bub/{version}

Send User-Agent: bub/{version} instead of Republic's default through
AgentSettings.model_client_kwargs, used by the model runner, code mode
and onboarding. A User-Agent in client_args.headers still wins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: expose republic models and media attachments in code mode (#351)

- Add a stdlib `republic` module to run_code: media classes, messages,
  get_model() (chat and stream), get_embedding_model() and
  get_decision_model(). Model calls are proxied to Bub, which uses its
  provider settings and credentials; they never reach the code.
- Add code-only tools attach_image/attach_audio/attach_video that accept
  republic media or a path/URL/data URL and attach it to the run_code
  result, sent to the model as a multimodal tool message.
- Add content results (bub.tools.content_result) for tool results with
  media; spilling bounds only their text.
- Add Environment.read_bytes with a default that runs the environment's
  python.
- Document the republic module in the code mode stub and prompt.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* refactor!: extract an event-driven core agent into bub.core (#356)

* docs: update README to improve clarity and organization of content

Signed-off-by: Frost Ming <me@frostming.com>

* feat: accept republic built-in tools in core Agent

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants