Skip to content

feat: add turn admission hook#214

Open
Gezi-lzq wants to merge 1 commit into
bubbuild:mainfrom
Gezi-lzq:codex/admit-message-hook
Open

feat: add turn admission hook#214
Gezi-lzq wants to merge 1 commit into
bubbuild:mainfrom
Gezi-lzq:codex/admit-message-hook

Conversation

@Gezi-lzq
Copy link
Copy Markdown

Motivation

ChannelManager currently schedules each inbound channel message as a new turn immediately. The default concurrent behavior is simple, but plugins do not have a hook to decide whether the next message for an already-active session should be processed now, queued, dropped, or offered to the running turn as steering input.

This PR adds an optional scheduling decision point while keeping the default behavior unchanged.

Design

Add a new first-result hook:

admit_message(session_id, message, turn) -> AdmitDecision | None

None means no decision; if every implementation returns None, Bub keeps the current default concurrent scheduling behavior.

AdmitDecision supports four actions:

  • PROCESS: schedule immediately
  • DROP: discard explicitly
  • WAIT: process after the active turn finishes
  • STEER: enqueue as per-session steering input for model hooks to consume via state["_runtime_steering"]

Undrained steering messages are promoted to the front of the pending queue when the active turn finishes, preserving FIFO order.

Scope

  • admission only applies to the ChannelManager path; direct process_inbound() calls are unaffected
  • Bub does not apply size, count, rate, or capacity limits to admission queues; plugins that need such policy can return DROP explicitly
  • steering is currently per-session, not per-turn
  • builtin agent steering consumption and whether turn cancellation should become a framework capability can be discussed separately

Verification

  • uv run pytest -q
  • uv run ruff check .
  • uv run mypy src

@PsiACE PsiACE requested a review from frostming May 17, 2026 14:37
@Gezi-lzq Gezi-lzq force-pushed the codex/admit-message-hook branch from 6b1602b to 8deff5c Compare May 17, 2026 14:39
Copy link
Copy Markdown
Collaborator

@frostming frostming left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not complete reviewed yet. I think we should first resolve the fundamental design choice.

Comment thread src/bub/turn_admission.py
Comment on lines +87 to +91
class SteeringHandle:
"""Control surface exposed to model hooks through turn state."""

session_id: str
buffer: SteeringBuffer
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like too many abstraction layers, consider uniting SteeringHandle and SteeringBuffer

Copy link
Copy Markdown
Author

@Gezi-lzq Gezi-lzq May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The split between SteeringHandle and SteeringBuffer is probably premature for this PR.

This PR currently treats steering as session-scoped, but there is an unresolved design question around whether steering should eventually target a session, a specific active turn, or a forked tape/turn branch. If that becomes first-class later, a public handle over a different internal routing/storage model might make sense.

For now, since the PR only implements session-scoped steering, I agree we can collapse this into one simpler type and avoid the extra abstraction.

Comment thread src/bub/hookspecs.py
Comment on lines +112 to +123
@hookspec(firstresult=True)
def admit_message(
self,
session_id: str,
message: Envelope,
turn: TurnSnapshot,
) -> AdmitDecision | None:
"""Decide how to handle an inbound channel message for a session.
Return ``None`` to keep Bub's default concurrent scheduling behavior.
"""
raise NotImplementedError
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it better to implement this as a method of Channel?

Because steering is highly related to channels, each channel should be able to define its own steering logic. The current approach only takes the first implemented hook.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see admit_message as more than channel-side input handling. Some decisions, especially STEER, only become meaningful when they are paired with a run_model implementation that knows how to consume steering input. If the active model hook never drains the steering queue, a channel-level STEER decision cannot actually steer the running turn.

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