mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 20:01:24 +00:00
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:
parent
12805f61bd
commit
9c9e6822e3
13 changed files with 293 additions and 33 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -696,16 +696,33 @@ pi.sendMessage({
|
|||
details: { ... }, // Optional metadata (not sent to LLM)
|
||||
}, {
|
||||
triggerTurn: true, // If true and agent is idle, triggers LLM response
|
||||
deliverAs: "steer", // "steer" (default) or "followUp" when agent is streaming
|
||||
deliverAs: "steer", // "steer", "followUp", or "nextTurn"
|
||||
});
|
||||
```
|
||||
|
||||
**Storage and timing:**
|
||||
- The message is appended to the session file immediately as a `CustomMessageEntry`
|
||||
- If the agent is currently streaming:
|
||||
- `deliverAs: "steer"` (default): Delivered after current tool execution, interrupts remaining tools
|
||||
- `deliverAs: "followUp"`: Delivered only after agent finishes all work
|
||||
- If `triggerTurn` is true and the agent is idle, a new agent loop starts
|
||||
**Delivery modes (`deliverAs`):**
|
||||
|
||||
| Mode | When agent is streaming | When agent is idle |
|
||||
|------|------------------------|-------------------|
|
||||
| `"steer"` (default) | Delivered after current tool, interrupts remaining | Appended to session immediately |
|
||||
| `"followUp"` | Delivered after agent finishes all work | Appended to session immediately |
|
||||
| `"nextTurn"` | Queued as context for next user message | Queued as context for next user message |
|
||||
|
||||
The `"nextTurn"` mode is useful for notifications that shouldn't wake the agent but should be seen on the next turn. The message becomes an "aside" - included alongside the next user prompt as context, rather than appearing as a standalone entry or triggering immediate response.
|
||||
|
||||
```typescript
|
||||
// Example: Notify agent about tool changes without interrupting
|
||||
pi.sendMessage(
|
||||
{ customType: "notify", content: "Tool configuration was updated", display: true },
|
||||
{ deliverAs: "nextTurn" }
|
||||
);
|
||||
// On next user message, agent sees this as context
|
||||
```
|
||||
|
||||
**`triggerTurn` option:**
|
||||
- If `triggerTurn: true` and the agent is idle, a new agent loop starts immediately
|
||||
- Ignored when streaming (use `deliverAs` to control timing instead)
|
||||
- Ignored when `deliverAs: "nextTurn"` (the message waits for user input)
|
||||
|
||||
**LLM context:**
|
||||
- `CustomMessageEntry` is converted to a user message when building context for the LLM
|
||||
|
|
@ -869,6 +886,40 @@ pi.registerShortcut("shift+p", {
|
|||
|
||||
Shortcut format: `modifier+key` where modifier can be `shift`, `ctrl`, `alt`, or combinations like `ctrl+shift`.
|
||||
|
||||
### pi.events
|
||||
|
||||
Shared event bus for communication between hooks and custom tools. Tools can emit events, hooks can listen and wake the agent.
|
||||
|
||||
```typescript
|
||||
// Listen for events and wake agent when received
|
||||
pi.events.on("task:complete", (data) => {
|
||||
pi.sendMessage(
|
||||
{ customType: "task-notify", content: `Task done: ${data}`, display: true },
|
||||
{ triggerTurn: true } // Required to wake the agent
|
||||
);
|
||||
});
|
||||
|
||||
// Unsubscribe when needed
|
||||
const unsubscribe = pi.events.on("my:channel", handler);
|
||||
unsubscribe();
|
||||
```
|
||||
|
||||
Events are session-scoped (cleared when session ends). Channel names are arbitrary strings - use namespaced 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);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Important:** Use `{ triggerTurn: true }` when you want the agent to respond to the event. Without it, the message displays but the agent stays idle.
|
||||
|
||||
## Examples
|
||||
|
||||
### Permission Gate
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue