Hook API: replace send() with sendMessage(), add appendEntry() and registerCommand()

Breaking changes to Hook API:
- pi.send(text, attachments?) replaced with pi.sendMessage(message, triggerTurn?)
  - Creates CustomMessageEntry instead of user messages
  - Properly handles queuing during streaming via agent loop
  - Supports optional turn triggering when idle
- New pi.appendEntry(customType, data?) for hook state persistence
- New pi.registerCommand(name, options) for custom slash commands
- Handler types renamed: SendHandler -> SendMessageHandler, new AppendEntryHandler

Implementation:
- AgentSession.sendHookMessage() handles all three cases:
  - Streaming: queues message with _hookData marker, agent loop processes it
  - Not streaming + triggerTurn: appends to state/session, calls agent.continue()
  - Not streaming + no trigger: appends to state/session only
- message_end handler routes based on _hookData presence to correct persistence
- HookRunner gains getRegisteredCommands() and getCommand() methods

New types: HookMessage<T>, RegisteredCommand, CommandContext
This commit is contained in:
Mario Zechner 2025-12-27 00:52:10 +01:00
parent d43a5e47a1
commit ba185b0571
13 changed files with 412 additions and 77 deletions

View file

@ -3,7 +3,7 @@
*/
import { spawn } from "node:child_process";
import type { LoadedHook, SendHandler } from "./loader.js";
import type { AppendEntryHandler, LoadedHook, SendMessageHandler } from "./loader.js";
import type {
CustomMessageRenderer,
ExecOptions,
@ -12,6 +12,7 @@ import type {
HookEvent,
HookEventContext,
HookUIContext,
RegisteredCommand,
SessionEvent,
SessionEventResult,
ToolCallEvent,
@ -164,12 +165,22 @@ export class HookRunner {
}
/**
* Set the send handler for all hooks' pi.send().
* Set the send message handler for all hooks' pi.sendMessage().
* Call this when the mode initializes.
*/
setSendHandler(handler: SendHandler): void {
setSendMessageHandler(handler: SendMessageHandler): void {
for (const hook of this.hooks) {
hook.setSendHandler(handler);
hook.setSendMessageHandler(handler);
}
}
/**
* Set the append entry handler for all hooks' pi.appendEntry().
* Call this when the mode initializes.
*/
setAppendEntryHandler(handler: AppendEntryHandler): void {
for (const hook of this.hooks) {
hook.setAppendEntryHandler(handler);
}
}
@ -218,6 +229,33 @@ export class HookRunner {
return undefined;
}
/**
* Get all registered commands from all hooks.
*/
getRegisteredCommands(): RegisteredCommand[] {
const commands: RegisteredCommand[] = [];
for (const hook of this.hooks) {
for (const command of hook.commands.values()) {
commands.push(command);
}
}
return commands;
}
/**
* Get a registered command by name.
* Returns the first command found across all hooks, or undefined if none.
*/
getCommand(name: string): RegisteredCommand | undefined {
for (const hook of this.hooks) {
const command = hook.commands.get(name);
if (command) {
return command;
}
}
return undefined;
}
/**
* Create the event context for handlers.
*/