Summary
Allow message_sending plugin hooks to access agent-level metadata — specifically tool call count and token usage — so plugins can implement pre-send validation guards (e.g., block "done" claims without verification, warn on high context fill).
Problem to solve
Plugin hooks registered for message_sending can already cancel or modify outbound messages. However, the hook event only contains delivery-level data (to, content, metadata: { channel, accountId, mediaUrls }). There's no way to know:
- How many tools the agent used in the current turn — needed for verification guards ("agent says done but never called any tools")
- Current token usage vs context window — needed for context-size guards ("context is 72% full, agent should summarize")
This data exists in the agent subscription handler (pi-embedded-subscribe.ts: toolMetas, usageTotals, params.model.contextWindow) but is not threaded through to the delivery pipeline.
Proposed solution
Add an optional agentMetadata field to PluginHookMessageSendingEvent:
export type PluginHookMessageSendingEvent = {
to: string;
content: string;
metadata?: Record<string, unknown>;
agentMetadata?: {
toolCallCount: number;
tokenUsage?: { input: number; output: number; total: number };
contextWindow?: number;
agentId?: string;
};
};
Threading: subscription handler → block reply callback → delivery params → runMessageSending() event.
Important timing consideration: During streaming, usageTotals may not be finalized yet (populated on message_end). Options:
- Only populate
agentMetadata for non-streaming deliveries (final response)
- Populate with "last known" values (stale but non-zero after first turn)
- Add a
streaming: boolean flag so plugins know the data may be incomplete
Alternatives considered
- New internal hook
message:pre-send — Rejected: internal hooks are fire-and-forget (errors swallowed), not suitable for blocking. Plugin hooks have proper cancel/modify semantics.
- Piggybacking on
channelData — Rejected: channelData is explicitly for per-channel envelope data (Discord buttons, Telegram keyboards). Agent metadata there violates the type contract.
- Post-send via
message:sent — Works for auditing but can't block/modify. Useful as complement, not replacement.
- extraDirs hooks for plugin hooks — Not possible today; extraDirs only loads internal hooks. Would be a separate feature request.
Impact
- Affected: Self-hosted deployments with custom quality/compliance requirements
- Severity: Medium — workaround exists (prompt-based rules via bootstrap hook, ~60-70% compliance)
- Frequency: Every agent response
- Consequence without: Can't build reliable pre-send validation. Prompt-only approach has no enforcement.
Evidence/examples
Use case: 12-agent deployment where agents claim tasks are "done" without calling any verification tools. Bootstrap-injected quality standards help (~60-70%) but can't enforce.
Existing code references:
message_sending plugin hook: src/infra/outbound/deliver.ts:498-521
PluginHookMessageSendingEvent: src/plugins/types.ts:447-457
- Tool tracking:
pi-embedded-subscribe.ts (toolMetas, usageTotals)
- Context window:
pi-embedded-runner/run/attempt.ts:596-601
Additional information
We're happy to contribute a PR if the approach is agreed upon. The main question is the preferred threading path for agent metadata to the delivery pipeline — whether via delivery params, a shared session store, or a different mechanism.
Summary
Allow
message_sendingplugin hooks to access agent-level metadata — specifically tool call count and token usage — so plugins can implement pre-send validation guards (e.g., block "done" claims without verification, warn on high context fill).Problem to solve
Plugin hooks registered for
message_sendingcan already cancel or modify outbound messages. However, the hook event only contains delivery-level data (to,content,metadata: { channel, accountId, mediaUrls }). There's no way to know:This data exists in the agent subscription handler (
pi-embedded-subscribe.ts:toolMetas,usageTotals,params.model.contextWindow) but is not threaded through to the delivery pipeline.Proposed solution
Add an optional
agentMetadatafield toPluginHookMessageSendingEvent:Threading: subscription handler → block reply callback → delivery params →
runMessageSending()event.Important timing consideration: During streaming,
usageTotalsmay not be finalized yet (populated onmessage_end). Options:agentMetadatafor non-streaming deliveries (final response)streaming: booleanflag so plugins know the data may be incompleteAlternatives considered
message:pre-send— Rejected: internal hooks are fire-and-forget (errors swallowed), not suitable for blocking. Plugin hooks have proper cancel/modify semantics.channelData— Rejected:channelDatais explicitly for per-channel envelope data (Discord buttons, Telegram keyboards). Agent metadata there violates the type contract.message:sent— Works for auditing but can't block/modify. Useful as complement, not replacement.Impact
Evidence/examples
Use case: 12-agent deployment where agents claim tasks are "done" without calling any verification tools. Bootstrap-injected quality standards help (~60-70%) but can't enforce.
Existing code references:
message_sendingplugin hook:src/infra/outbound/deliver.ts:498-521PluginHookMessageSendingEvent:src/plugins/types.ts:447-457pi-embedded-subscribe.ts(toolMetas,usageTotals)pi-embedded-runner/run/attempt.ts:596-601Additional information
We're happy to contribute a PR if the approach is agreed upon. The main question is the preferred threading path for agent metadata to the delivery pipeline — whether via delivery params, a shared session store, or a different mechanism.