feat(coding-agent): expose deliverAs option in hook sendMessage() API

- pi.sendMessage(msg, options?) now accepts { triggerTurn?, deliverAs? }
- deliverAs: 'steer' (default) or 'followUp' controls delivery timing
- Update all mode handlers to pass options through
- Update file-trigger example to use new API
- Update CHANGELOG
This commit is contained in:
Mario Zechner 2026-01-02 23:56:51 +01:00
parent d404f8fcfa
commit 9d8230dfc6
8 changed files with 29 additions and 18 deletions

View file

@ -15,7 +15,7 @@
- `queuedMessageCount``pendingMessageCount`
- `getQueuedMessages()``getSteeringMessages()` and `getFollowUpMessages()`
- `clearQueue()` now returns `{ steering: string[], followUp: string[] }`
- **sendHookMessage() signature changed**: Second parameter changed from `triggerTurn?: boolean` to `options?: { triggerTurn?, deliverAs? }`. Use `deliverAs: "followUp"` for follow-up delivery.
- **Hook API signature changed**: `pi.sendMessage()` second parameter changed from `triggerTurn?: boolean` to `options?: { triggerTurn?, deliverAs? }`. Use `deliverAs: "followUp"` for follow-up delivery. Affects both hooks and internal `sendHookMessage()` method.
- **RPC API changes**:
- `queue_message` command → `steer` and `follow_up` commands
- `set_queue_mode` command → `set_steering_mode` and `set_follow_up_mode` commands

View file

@ -25,7 +25,7 @@ export default function (pi: HookAPI) {
content: `External trigger: ${content}`,
display: true,
},
true, // triggerTurn - get LLM to respond
{ triggerTurn: true }, // triggerTurn - get LLM to respond
);
fs.writeFileSync(triggerFile, ""); // Clear after reading
}

View file

@ -53,7 +53,7 @@ type HandlerFn = (...args: unknown[]) => Promise<unknown>;
*/
export type SendMessageHandler = <T = unknown>(
message: Pick<HookMessage<T>, "customType" | "content" | "display" | "details">,
triggerTurn?: boolean,
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" },
) => void;
/**
@ -177,8 +177,11 @@ function createHookAPI(
list.push(handler);
handlers.set(event, list);
},
sendMessage<T = unknown>(message: HookMessage<T>, triggerTurn?: boolean): void {
sendMessageHandler(message, triggerTurn);
sendMessage<T = unknown>(
message: HookMessage<T>,
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" },
): void {
sendMessageHandler(message, options);
},
appendEntry<T = unknown>(customType: string, data?: T): void {
appendEntryHandler(customType, data);

View file

@ -692,12 +692,15 @@ export interface HookAPI {
* @param message.content - Message content (string or TextContent/ImageContent array)
* @param message.display - Whether to show in TUI (true = styled display, false = hidden)
* @param message.details - Optional hook-specific metadata (not sent to LLM)
* @param triggerTurn - If true and agent is idle, triggers a new LLM turn. Default: false.
* If agent is streaming, message is queued and triggerTurn is ignored.
* @param options.triggerTurn - If true and agent is idle, triggers a new LLM turn. Default: false.
* If agent is streaming, message is queued and triggerTurn is ignored.
* @param options.deliverAs - How to deliver when agent is streaming. Default: "steer".
* - "steer": Interrupt mid-run, delivered after current tool execution.
* - "followUp": Wait until agent finishes all work before delivery.
*/
sendMessage<T = unknown>(
message: Pick<HookMessage<T>, "customType" | "content" | "display" | "details">,
triggerTurn?: boolean,
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" },
): void;
/**

View file

@ -344,7 +344,10 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa
const handlers = new Map<string, Array<(...args: unknown[]) => Promise<unknown>>>();
const messageRenderers = new Map<string, any>();
const commands = new Map<string, any>();
let sendMessageHandler: (message: any, triggerTurn?: boolean) => void = () => {};
let sendMessageHandler: (
message: any,
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" },
) => void = () => {};
let appendEntryHandler: (customType: string, data?: any) => void = () => {};
let newSessionHandler: (options?: any) => Promise<{ cancelled: boolean }> = async () => ({ cancelled: false });
let branchHandler: (entryId: string) => Promise<{ cancelled: boolean }> = async () => ({ cancelled: false });
@ -358,8 +361,8 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa
list.push(handler);
handlers.set(event, list);
},
sendMessage: (message: any, triggerTurn?: boolean) => {
sendMessageHandler(message, triggerTurn);
sendMessage: (message: any, options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" }) => {
sendMessageHandler(message, options);
},
appendEntry: (customType: string, data?: any) => {
appendEntryHandler(customType, data);
@ -383,7 +386,9 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa
handlers,
messageRenderers,
commands,
setSendMessageHandler: (handler: (message: any, triggerTurn?: boolean) => void) => {
setSendMessageHandler: (
handler: (message: any, options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" }) => void,
) => {
sendMessageHandler = handler;
},
setAppendEntryHandler: (handler: (customType: string, data?: any) => void) => {

View file

@ -404,10 +404,10 @@ export class InteractiveMode {
hookRunner.initialize({
getModel: () => this.session.model,
sendMessageHandler: (message, triggerTurn) => {
sendMessageHandler: (message, options) => {
const wasStreaming = this.session.isStreaming;
this.session
.sendHookMessage(message, { triggerTurn })
.sendHookMessage(message, options)
.then(() => {
// For non-streaming cases with display=true, update UI
// (streaming cases update via message_end event)

View file

@ -32,8 +32,8 @@ export async function runPrintMode(
if (hookRunner) {
hookRunner.initialize({
getModel: () => session.model,
sendMessageHandler: (message, triggerTurn) => {
session.sendHookMessage(message, { triggerTurn }).catch((e) => {
sendMessageHandler: (message, options) => {
session.sendHookMessage(message, options).catch((e) => {
console.error(`Hook sendMessage failed: ${e instanceof Error ? e.message : String(e)}`);
});
},

View file

@ -181,8 +181,8 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
if (hookRunner) {
hookRunner.initialize({
getModel: () => session.agent.state.model,
sendMessageHandler: (message, triggerTurn) => {
session.sendHookMessage(message, { triggerTurn }).catch((e) => {
sendMessageHandler: (message, options) => {
session.sendHookMessage(message, options).catch((e) => {
output(error(undefined, "hook_send", e.message));
});
},