#14544: feat: add before_context_send plugin hook
agents
stale
size: M
Cluster:
Plugin and Hook Enhancements
## Summary
Add a new `before_context_send` plugin hook that fires after context pruning but before messages are sent to the LLM. This enables plugins to implement zero-cost token optimization strategies (deduplication, superseding stale writes, purging old error inputs) without modifying core code.
## Motivation
Projects like [opencode-dynamic-context-pruning](https://github.com/Opencode-DCP/opencode-dynamic-context-pruning) demonstrate that significant token savings can be achieved through simple message-level transformations applied just before the LLM call. Currently, OpenClaw's plugin system has no hook that allows modifying the full messages array at this stage — `tool_result_persist` only sees individual messages at write time, and `before_agent_start` can only prepend context.
This PR adds the minimal hook needed to unlock this class of plugins.
## Changes
- **`src/plugins/types.ts`**: Add `PluginHookBeforeContextSend{Event,Result,Context}` types and `before_context_send` to `PluginHookName` union and `PluginHookHandlerMap`
- **`src/plugins/hooks.ts`**: Add `runBeforeContextSend()` — synchronous, sequential, priority-ordered (same pattern as `tool_result_persist`)
- **`src/agents/pi-extensions/context-pruning/extension.ts`**: Wire the hook after pruner runs; plugins receive the (possibly pruned) messages array and can return a modified one
- **`src/agents/pi-embedded-runner/extensions.ts`**: Load the context-pruning extension when `before_context_send` hooks are registered, even if pruning itself is disabled
- **`src/plugin-sdk/index.ts`**: Export new types for plugin authors
- **`src/agents/pi-extensions/context-pruning.before-context-send-hook.test.ts`**: Tests for no-op, filtering, and priority composition
## Hook API
```typescript
api.on("before_context_send", (event, ctx) => {
// event.messages: AgentMessage[] — full messages array after pruning
// Return { messages: [...] } to replace, or void to pass through
const filtered = event.messages.filter(m => !isDuplicate(m));
return { messages: filtered };
});
```
The hook is **synchronous** (runs inside pi-agent-core's `context` event) and **sequential** (handlers execute in priority order, each receiving the previous handler's output).
## Plugin Use Case Example
A DCP-style plugin could register:
```typescript
api.on("before_context_send", (event) => {
let messages = deduplicateToolResults(event.messages);
messages = supersedeStaleWrites(messages);
messages = purgeOldErrorInputs(messages);
return { messages };
}, { priority: 10 });
```
## Testing
- Added 3 test cases covering: no-op when no hooks registered, message filtering, and multi-handler priority composition
- Existing context-pruning tests are unaffected (pruning logic unchanged)
<!-- greptile_comment -->
<h2>Greptile Overview</h2>
<h3>Greptile Summary</h3>
This PR adds a new synchronous plugin hook, `before_context_send`, intended to let plugins transform the full `AgentMessage[]` after context pruning and right before the LLM call. It wires the hook into the `context-pruning` pi-extension and conditionally loads that extension in the embedded runner when the hook is registered, plus exports the new hook types from the plugin SDK and adds focused tests for no-op/filtering/priority composition.
Key integration points:
- Hook runner: `createHookRunner()` gains `runBeforeContextSend()` (sync, sequential, priority-ordered) alongside existing `tool_result_persist` patterns.
- Agent pipeline: `src/agents/pi-extensions/context-pruning/extension.ts` calls the hook both when pruning runs and when pruning is skipped (e.g., no runtime / TTL not elapsed).
- Embedded runner: `buildEmbeddedExtensionPaths()` attempts to load the context-pruning extension when any `before_context_send` hooks exist, even if pruning is disabled.
<h3>Confidence Score: 3/5</h3>
- This PR is close to mergeable, but has a couple integration gaps that can prevent the hook from behaving as intended.
- Core hook runner/type additions look consistent and the extension wiring follows existing patterns, but (1) the hook context is currently always `{}` so advertised ctx fields aren’t usable, and (2) conditional extension loading via `hasGlobalHooks()` can fail if plugin loading happens after embedded extensions are constructed, preventing hooks from ever running when pruning is disabled.
- src/agents/pi-extensions/context-pruning/extension.ts; src/agents/pi-embedded-runner/extensions.ts
<!-- greptile_other_comments_section -->
<sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub>
<!-- /greptile_comment -->
Most Similar PRs
#22624: feat(plugins): add before_context_send hook and model routing via b...
by davidrudduck · 2026-02-21
87.0%
#23559: feat(plugins): add before_context_send hook and model routing via b...
by davidrudduck · 2026-02-22
82.8%
#11124: feat(plugins): add before_llm_request hook for custom LLM headers
by johnlanni · 2026-02-07
82.3%
#20067: feat(plugins): add before_agent_reply hook for message interception
by JoshuaLelon · 2026-02-18
81.6%
#18004: feat: Add before_message_dispatch hook for blocking inbound messages
by Andy-Haigh · 2026-02-16
81.5%
#14873: [Feature]: Extend before_agent_start hook context with Model, Tools...
by akv2011 · 2026-02-12
81.5%
#11732: feat(plugins): add injectMessages to before_agent_start hook
by antra-tess · 2026-02-08
79.4%
#16028: feat/before-tool-result
by ambushalgorithm · 2026-02-14
78.7%
#8022: feat: implement before_model_select plugin hook
by dead-pool-aka-wilson · 2026-02-03
78.2%
#17667: feat: tool-hooks extension — run shell commands on tool calls
by FaradayHunt · 2026-02-16
77.0%