#23559: feat(plugins): add before_context_send hook and model routing via before_prompt_build
agents
size: L
trusted-contributor
Cluster:
Plugin and Hook Enhancements
## Summary
Add two new plugin hook surfaces for per-LLM-call context transformation and dynamic model routing, enabling plugins to inspect/modify messages and switch models based on conversation context — without touching core.
**Split from previous PRs (#20850, #20851, #20859) where this feature was inadvertently bundled with unrelated bug fixes.**
## New hooks
### `before_context_send` (sync, per-LLM-call)
Fires on every LLM invocation (including tool continuations) via a pi-agent-core `context` event bridge. Plugins can inspect or transform the message array before it reaches the model.
- Sync execution — no async overhead on the hot path
- Receives `modelId`, `provider`, `contextWindowTokens` for routing-aware decisions
- Independent registry via `session-manager-runtime-registry` (no conflicts with existing extensions)
### `before_prompt_build` enrichment (per-turn)
Enriched with `modelId`, `provider`, and `contextWindowTokens` parameters. Can now return `modelOverride` and `providerOverride` for dynamic per-turn model routing.
## Architecture
- New pi-extension `context-hooks` bridges the sync pi-agent-core `context` event to the plugin hook system
- `ContextHooksRuntimeValue` stores mutable model state per session manager (updated on routing)
- `buildContextHooksFactory()` wired into `buildEmbeddedExtensionFactories()`
- Model routing wrapper installed in `attempt.ts` when `before_prompt_build` returns `modelOverride`
- Standalone from PR #14544; coexists via independent registries
## Changes
- `src/agents/pi-extensions/context-hooks/extension.ts` — new pi-extension (31 lines)
- `src/agents/pi-extensions/context-hooks/runtime.ts` — session-scoped runtime registry (17 lines)
- `src/agents/pi-embedded-runner/extensions.ts` — wire context-hooks factory
- `src/agents/pi-embedded-runner/run/attempt.ts` — model routing wrapper installation
- `src/plugins/hooks.ts` — `runBeforeContextSend` hook runner method
- `src/plugins/types.ts` — hook type definitions
- `src/plugin-sdk/index.ts` — export new hook types
## Known limitations (to address in follow-ups)
- Model routing only swaps the model arg in `streamFn`; provider-specific stream wrappers from `applyExtraParamsToAgent` are not rebuilt (same-provider routing only)
- Compaction/pruning context windows not updated after routing (only `contextHooksRuntime` is mutated)
- Per steipete's review: cross-provider routing needs explicit validation or full "effective model" state rebuild
## Test plan
- [x] 6 tests in `context-hooks.test.ts`:
- Extension calls hook runner on context event
- Passes model metadata to hook
- Skips when no hooks registered
- Handles missing runtime gracefully
- Returns modified messages from hook
- Preserves original messages when hook returns undefined
- [x] `pnpm build` passes
- [x] `pnpm vitest run src/agents/pi-extensions/context-hooks.test.ts` — 6/6 pass
- [x] Rebased on latest `main`, single focused commit
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
Adds two new plugin hooks for dynamic model routing and per-LLM-call message transformation. The `before_context_send` hook fires synchronously on every LLM invocation via pi-agent-core's context event bridge, allowing plugins to inspect or modify messages before they reach the model. The `before_prompt_build` hook gains model metadata (`modelId`, `provider`, `contextWindowTokens`) and can return `modelOverride`/`providerOverride` for dynamic per-turn model routing.
**Key changes:**
- New pi-extension `context-hooks` bridges pi-agent-core context events to plugin hooks
- Session-scoped `ContextHooksRuntimeValue` stores mutable model routing state
- Model routing wrapper installed in `attempt.ts` when `before_prompt_build` returns `modelOverride`
- Well-tested with 6 focused unit tests covering hook execution, model metadata passing, and runtime mutations
**Architecture concerns:**
- Model routing only swaps the streamFn wrapper—provider-specific wrappers from `applyExtraParamsToAgent` are not rebuilt (acknowledged limitation)
- Context window state not updated after routing (only `contextHooksRuntime` is mutated)
- Model override merge logic in hooks.ts:151-152 prioritizes the first plugin's override, which is inconsistent with how `systemPrompt` is merged (where later plugins override earlier ones)
<h3>Confidence Score: 4/5</h3>
- Safe to merge with one logic issue that should be addressed
- The implementation is well-structured with good test coverage and proper error handling. The model override merge logic inconsistency is a logical error that could cause unexpected behavior when multiple plugins try to route models, but it's contained to a specific edge case. The acknowledged architectural limitations (provider-specific wrappers not rebuilt, context windows not updated) are documented and acceptable for initial implementation.
- Check `src/plugins/hooks.ts:151-152` for the model override merge priority issue
<sub>Last reviewed commit: c3b7ef5</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#22624: feat(plugins): add before_context_send hook and model routing via b...
by davidrudduck · 2026-02-21
94.6%
#14647: feat(plugins): allow before_agent_start hook to override model (#14...
by lailoo · 2026-02-12
85.5%
#17614: feat: allow before_agent_start hook to override model selection
by plc · 2026-02-16
84.4%
#14544: feat: add before_context_send plugin hook
by Windelly · 2026-02-12
82.8%
#14873: [Feature]: Extend before_agent_start hook context with Model, Tools...
by akv2011 · 2026-02-12
81.6%
#8022: feat: implement before_model_select plugin hook
by dead-pool-aka-wilson · 2026-02-03
81.4%
#11155: feat(hooks): before_agent_start model/provider override (run-scoped...
by alanranger · 2026-02-07
81.1%
#11124: feat(plugins): add before_llm_request hook for custom LLM headers
by johnlanni · 2026-02-07
80.3%
#20067: feat(plugins): add before_agent_reply hook for message interception
by JoshuaLelon · 2026-02-18
77.8%
#20426: feat: make llm_input/llm_output modifying hooks for middleware patt...
by chandika · 2026-02-18
77.7%