Repository navigation
refactor!: extract an event-driven core agent into bub.core - #356
Merged
Merged
Conversation
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>
Deploying with
|
| 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 |
`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>
|
感觉挺好,core 就是最小 agent runnable 实例,那就很灵活了 |
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
force-pushed
the
refactor/core-agent
branch
from
October 14, 2026 00:45
c792796 to
cd4dafa
Compare
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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 intobub.coreand rebuilds the builtin agent on top of it.bub.coreAgent: 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 theinvoke_agentspan) when the block exits.agent.start(...)returns a stream for another consumer to close, asBubAgent.run_streamdoes.republic.ChatModeldirectly. AModelClientprotocol covers callers with their own policy (fallbacks, credentials, timeouts).bub.core.events):RunStarted,StepStarted/StepFinished,Before/AfterLlmCall,Before/AfterToolCall,ChatCompleted, plus user-defined events. Handlers are registered withagent.events.on(...), either called directly or used as a decorator.Before*events can be modified or short-circuited. Tape recording is aTapeRecorderhandler.Agent(command_prefix=...), a text prompt starting with the prefix calls a tool directly (or thebashtool) instead of the model and emitsCommandFinished;None(the default) disables commands. Prefix rules move tobub.core.commands.agent.add_system_prompt(...)(also usable as a decorator). Each section receives aPromptContextwith the step's prompt, tools, model, tape and state.bub.tape/bub.store/bub.sidecars/bub.toolspaths remain as module aliases.Builtin
BubAgent(formerlybub.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 corestart().BubModelreplaces the old candidate/fallback logic ofModelRunner.bub.builtin.model_runneris removed.PluginAgentHooksadapts the pluggy interception hooks to core events.Tapeintobub.builtin.archive;TapeandAgentno longer take an archive path.bub.builtin.context, and the deprecatedbub.typesmodule along with its test.Behavior changes
reasoning_effortis now fixed at the start of a turn, the same as model switches.allowed_toolsraise before the session tape is forked.invoke_agentspan starts.tape.resetwith an archive,sidecar.archiveevents are recorded aftersidecar.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 beforeframework.running()(the CLI channel creates it on first use), soBubAgenttakes the framework's store when created; SDK users create it insideframework.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 aroundbub.core, new FastAPI tutorial, hooks/types/tape/turn-pipeline references).Verification
make check(lock, prek, ruff, mypy): passuv run pytest -q: 607 passed, 20 skippedopentelemetry-sdkinstalled (tracing tests enabled): 626 passed, 1 skippedbub run ",help"andbub run ",tape.reset archive=true"🤖 Generated with Claude Code