feat(coding-agent): add event bus for tool/hook communication (#431)

* feat(coding-agent): add event bus for tool/hook communication

Adds pi.events API enabling custom tools and hooks to communicate via
pub/sub. Tools can emit events, hooks can listen. Shared EventBus instance
created per session in createAgentSession().

- EventBus interface with emit() and on() methods
- on() returns unsubscribe function
- Threaded through hook and tool loaders
- Documented in hooks.md and custom-tools.md

* fix(coding-agent): wrap event handlers to catch errors

* docs: note async handler error handling for event bus

* feat(coding-agent): add sendMessage to tools, nextTurn delivery mode

- Custom tools now have pi.sendMessage() for direct agent notifications
- New deliverAs: 'nextTurn' queues messages for next user prompt
- Fix: hooks and tools now share the same eventBus (was isolated before)

* fix(coding-agent): nextTurn delivery should always queue, even when streaming
This commit is contained in:
Nico Bailon 2026-01-04 12:36:19 -08:00 committed by GitHub
parent 12805f61bd
commit 9c9e6822e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 293 additions and 33 deletions

View file

@ -159,6 +159,8 @@ interface CustomToolAPI {
exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
ui: ToolUIContext;
hasUI: boolean; // false in --print or --mode rpc
events: EventBus; // Shared event bus for tool/hook communication
sendMessage(message, options?): void; // Send messages to the agent session
}
interface ToolUIContext {
@ -184,6 +186,52 @@ interface ExecResult {
Always check `pi.hasUI` before using UI methods.
### Event Bus
Tools can emit events that hooks (or other tools) listen for via `pi.events`:
```typescript
// Emit an event
pi.events.emit("mytool:completed", { result: "success", itemCount: 42 });
// Listen for events (tools can also subscribe)
const unsubscribe = pi.events.on("other:event", (data) => {
console.log("Received:", data);
});
```
Events are session-scoped. Use namespaced channel names like `"toolname:event"` to avoid collisions.
Handler errors are caught and logged. For async handlers, handle errors internally:
```typescript
pi.events.on("mytool:event", async (data) => {
try {
await doSomething(data);
} catch (err) {
console.error("Handler failed:", err);
}
});
```
### Sending Messages
Tools can send messages to the agent session via `pi.sendMessage()`:
```typescript
pi.sendMessage({
customType: "mytool-notify",
content: "Configuration was updated",
display: true,
}, {
deliverAs: "nextTurn",
});
```
**Delivery modes:** `"steer"` (default) interrupts streaming, `"followUp"` waits for completion, `"nextTurn"` queues for next user message. Use `triggerTurn: true` to wake an idle agent immediately.
See [hooks documentation](hooks.md#pisendmessagemessage-options) for full details.
### Cancellation Example
Pass the `signal` from `execute` to `pi.exec` to support cancellation: