Skip to content

feat(hooks): add after_build_prompt to observe built prompts - #400

Open
Fengzdadi wants to merge 1 commit into
bubbuild:mainfrom
Fengzdadi:feat/after-build-prompt-hook
Open

Fengzdadi wants to merge 1 commit into
bubbuild:mainfrom
Fengzdadi:feat/after-build-prompt-hook

Conversation

@Fengzdadi

@Fengzdadi Fengzdadi commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

What changed and why

BubFramework.build_prompt builds the model-facing user content for every inbound message: the turn's message in process_inbound, and each steering message that AgentPlugins._drain_steering adds to a running turn. Only the turn's message goes through load_state. So a plugin that needs the inbound message behind a model-facing user message can capture the turn's message, but not a steered follow-up.

PowerContext's Bub plugin hit this case. It recalls memory with what the user wrote, not with the framed prompt, whose channel metadata and date outweigh the user's words in lexical recall. Reading the turn's message in load_state worked until a follow-up was steered into the turn. The model then saw the follow-up while the plugin still recalled with the first message (reported in the review of oceanbase/powercontext#2488).

  • New hookspec after_build_prompt(message, prompt, session_id, state). It runs every implementation after build_prompt and ignores their return values.
  • prompt is a copy of the built content as list[UserContent], not the legacy form, so an observer cannot change what the model receives. A plugin can match its text against the user messages in LLMCallRequest.messages and read message, including ChannelMessage.text from feat(channels): expose what the sender wrote on Telegram messages #399, for the original words.
  • build_prompt, the prompt, and the tape are unchanged.
  • The hooks reference documents the hook in English and Chinese. The steering row of the event mapping and the AgentPlugins docstring now say that drained messages are built through build_prompt and after_build_prompt; the docstring previously said build_prompt only runs in process_inbound().

Impacted modules

  • src/bub/hooks/specs.py
  • src/bub/framework.py
  • src/bub/hooks/agent.py (docstring)
  • tests/test_framework.py, tests/test_agent_plugins.py
  • website/src/content/docs/docs/reference/hooks.mdx, website/src/content/docs/zh-cn/docs/reference/hooks.mdx

Verification

  • make check: lock check, prek (ruff, ruff format), and mypy passed.
  • uv run pytest -q: 731 passed, 20 skipped.
  • New and updated tests, both failing without the framework change:
    • test_after_build_prompt_observes_a_copy_of_the_built_content_with_its_message checks that an observer gets the message and the content another plugin built, and that clearing its copy leaves the prompt unchanged.
    • test_agent_plugins_connect_loop_hooks_to_core_agent now checks that the hook sees both steering messages drained into the run.
  • Not run: make docs-test, since the website dependencies are not installed here.

🤖 Generated with Claude Code

Steering messages drained into a running turn are built through
build_prompt without load_state, so a plugin cannot relate them to the
inbound message they came from. after_build_prompt fires after every
prompt the framework builds, with the message and the content the model
receives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Fengzdadi
Fengzdadi force-pushed the feat/after-build-prompt-hook branch from 4178448 to be773de Compare November 5, 2026 17:25

This branch has not been deployed

No deployments
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.

1 participant