Implement before_agent_start hook event

- Add BeforeAgentStartEvent and BeforeAgentStartEventResult types
- Add emitBeforeAgentStart to HookRunner
- Call in AgentSession.prompt() before agent.prompt()
- Hook can return a message to inject into context (persisted + visible)
- Add test hook demonstrating custom message rendering and before_agent_start
This commit is contained in:
Mario Zechner 2025-12-28 14:50:22 +01:00
parent bbdc350394
commit 57146de202
6 changed files with 170 additions and 16 deletions

View file

@ -490,6 +490,29 @@ export class AgentSession {
// Expand file-based slash commands if requested
const expandedText = expandCommands ? expandSlashCommand(text, [...this._fileCommands]) : text;
// Emit before_agent_start hook event
if (this._hookRunner) {
const result = await this._hookRunner.emitBeforeAgentStart(expandedText, options?.images);
if (result?.message) {
// Append hook message to agent state and session
const hookMessage: HookMessage = {
role: "hookMessage",
customType: result.message.customType,
content: result.message.content,
display: result.message.display,
details: result.message.details,
timestamp: Date.now(),
};
this.agent.appendMessage(hookMessage);
this.sessionManager.appendCustomMessageEntry(
result.message.customType,
result.message.content,
result.message.display,
result.message.details,
);
}
}
await this.agent.prompt(expandedText, options?.images);
await this.waitForRetry();
}

View file

@ -12,6 +12,8 @@ export { wrapToolsWithHooks, wrapToolWithHooks } from "./tool-wrapper.js";
export type {
AgentEndEvent,
AgentStartEvent,
BeforeAgentStartEvent,
BeforeAgentStartEventResult,
BashToolResultEvent,
ContextEvent,
ContextEventResult,

View file

@ -7,6 +7,8 @@ import type { ModelRegistry } from "../model-registry.js";
import type { SessionManager } from "../session-manager.js";
import type { AppendEntryHandler, LoadedHook, SendMessageHandler } from "./loader.js";
import type {
BeforeAgentStartEvent,
BeforeAgentStartEventResult,
ContextEvent,
ContextEventResult,
HookError,
@ -346,4 +348,44 @@ export class HookRunner {
return currentMessages;
}
/**
* Emit before_agent_start event to all hooks.
* Returns the first message to inject (if any handler returns one).
*/
async emitBeforeAgentStart(
prompt: string,
images?: import("@mariozechner/pi-ai").ImageContent[],
): Promise<BeforeAgentStartEventResult | undefined> {
const ctx = this.createContext();
let result: BeforeAgentStartEventResult | undefined;
for (const hook of this.hooks) {
const handlers = hook.handlers.get("before_agent_start");
if (!handlers || handlers.length === 0) continue;
for (const handler of handlers) {
try {
const event: BeforeAgentStartEvent = { type: "before_agent_start", prompt, images };
const timeout = createTimeout(this.timeout);
const handlerResult = await Promise.race([handler(event, ctx), timeout.promise]);
timeout.clear();
// Take the first message returned
if (handlerResult && (handlerResult as BeforeAgentStartEventResult).message && !result) {
result = handlerResult as BeforeAgentStartEventResult;
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
this.emitError({
hookPath: hook.path,
event: "before_agent_start",
error: message,
});
}
}
}
return result;
}
}

View file

@ -158,6 +158,19 @@ export interface ContextEvent {
messages: AgentMessage[];
}
/**
* Event data for before_agent_start event.
* Fired after user submits a prompt but before the agent loop starts.
* Allows hooks to inject context that will be persisted and visible in TUI.
*/
export interface BeforeAgentStartEvent {
type: "before_agent_start";
/** The user's prompt text */
prompt: string;
/** Any images attached to the prompt */
images?: ImageContent[];
}
/**
* Event data for agent_start event.
* Fired when an agent loop starts (once per user prompt).
@ -314,6 +327,7 @@ export function isLsToolResult(e: ToolResultEvent): e is LsToolResultEvent {
export type HookEvent =
| SessionEvent
| ContextEvent
| BeforeAgentStartEvent
| AgentStartEvent
| AgentEndEvent
| TurnStartEvent
@ -358,6 +372,15 @@ export interface ToolResultEventResult {
isError?: boolean;
}
/**
* Return type for before_agent_start event handlers.
* Allows hooks to inject context before the agent runs.
*/
export interface BeforeAgentStartEventResult {
/** Message to inject into context (persisted to session, visible in TUI) */
message?: Pick<HookMessage, "customType" | "content" | "display" | "details">;
}
/**
* Return type for session event handlers.
* Allows hooks to cancel "before_*" actions.
@ -433,6 +456,11 @@ export interface HookAPI {
on(event: "session", handler: HookHandler<SessionEvent, SessionEventResult | void>): void;
// biome-ignore lint/suspicious/noConfusingVoidType: void allows handlers to not return anything
on(event: "context", handler: HookHandler<ContextEvent, ContextEventResult | void>): void;
// biome-ignore lint/suspicious/noConfusingVoidType: void allows handlers to not return anything
on(
event: "before_agent_start",
handler: HookHandler<BeforeAgentStartEvent, BeforeAgentStartEventResult | void>,
): void;
on(event: "agent_start", handler: HookHandler<AgentStartEvent>): void;
on(event: "agent_end", handler: HookHandler<AgentEndEvent>): void;
on(event: "turn_start", handler: HookHandler<TurnStartEvent>): void;

View file

@ -42,6 +42,8 @@ export type {
AgentEndEvent,
AgentStartEvent,
BashToolResultEvent,
BeforeAgentStartEvent,
BeforeAgentStartEventResult,
CustomToolResultEvent,
EditToolResultEvent,
FindToolResultEvent,