mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-19 07:03:44 +00:00
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:
parent
bbdc350394
commit
57146de202
6 changed files with 170 additions and 16 deletions
|
|
@ -1,24 +1,81 @@
|
||||||
/**
|
/**
|
||||||
* Test hook that registers a /greet command.
|
* Test hook demonstrating custom commands, message rendering, and before_agent_start.
|
||||||
* Usage: /greet [name]
|
|
||||||
*/
|
*/
|
||||||
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
|
import type { HookAPI } from "@mariozechner/pi-coding-agent";
|
||||||
|
import { Box, Text } from "@mariozechner/pi-tui";
|
||||||
|
|
||||||
export default function (pi: HookAPI) {
|
export default function (pi: HookAPI) {
|
||||||
pi.registerCommand("greet", {
|
// Track whether injection is enabled
|
||||||
description: "Send a greeting message to the LLM",
|
let injectEnabled = false;
|
||||||
handler: async (ctx) => {
|
|
||||||
const name = ctx.args.trim() || "world";
|
|
||||||
|
|
||||||
// Insert a custom message and trigger LLM response
|
// Register a custom message renderer for our "test-info" type
|
||||||
pi.sendMessage(
|
pi.registerMessageRenderer("test-info", (message, options, theme) => {
|
||||||
{
|
const box = new Box(0, 0, (t) => theme.bg("success", t));
|
||||||
customType: "greeting",
|
|
||||||
content: `Hello, ${name}! Please say something nice about them.`,
|
const label = theme.fg("successText", "[TEST INFO]");
|
||||||
|
box.addChild(new Text(label, 0, 0));
|
||||||
|
|
||||||
|
const content =
|
||||||
|
typeof message.content === "string"
|
||||||
|
? message.content
|
||||||
|
: message.content.map((c) => (c.type === "text" ? c.text : "[image]")).join("");
|
||||||
|
|
||||||
|
box.addChild(new Text(theme.fg("successText", content), 0, 1));
|
||||||
|
|
||||||
|
if (options.expanded && message.details) {
|
||||||
|
box.addChild(new Text(theme.fg("dim", `Details: ${JSON.stringify(message.details)}`), 0, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
return box;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register /test-msg command
|
||||||
|
pi.registerCommand("test-msg", {
|
||||||
|
description: "Send a test custom message",
|
||||||
|
handler: async (ctx) => {
|
||||||
|
pi.sendMessage({
|
||||||
|
customType: "test-info",
|
||||||
|
content: "This is a test message with custom rendering!",
|
||||||
display: true,
|
display: true,
|
||||||
},
|
details: { timestamp: Date.now(), source: "test-command hook" },
|
||||||
true, // triggerTurn - get LLM to respond
|
});
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Register /test-hidden command
|
||||||
|
pi.registerCommand("test-hidden", {
|
||||||
|
description: "Send a hidden message (display: false)",
|
||||||
|
handler: async (ctx) => {
|
||||||
|
pi.sendMessage({
|
||||||
|
customType: "test-info",
|
||||||
|
content: "This message is in context but not displayed",
|
||||||
|
display: false,
|
||||||
|
});
|
||||||
|
ctx.ui.notify("Sent hidden message (check session file)");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register /test-inject command to toggle before_agent_start injection
|
||||||
|
pi.registerCommand("test-inject", {
|
||||||
|
description: "Toggle context injection before agent starts",
|
||||||
|
handler: async (ctx) => {
|
||||||
|
injectEnabled = !injectEnabled;
|
||||||
|
ctx.ui.notify(`Context injection ${injectEnabled ? "enabled" : "disabled"}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Demonstrate before_agent_start: inject context when enabled
|
||||||
|
pi.on("before_agent_start", async (event, ctx) => {
|
||||||
|
if (!injectEnabled) return;
|
||||||
|
|
||||||
|
// Return a message to inject before the user's prompt
|
||||||
|
return {
|
||||||
|
message: {
|
||||||
|
customType: "test-info",
|
||||||
|
content: `[Injected context for prompt: "${event.prompt.slice(0, 50)}..."]`,
|
||||||
|
display: true,
|
||||||
|
details: { injectedAt: Date.now() },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -490,6 +490,29 @@ export class AgentSession {
|
||||||
// Expand file-based slash commands if requested
|
// Expand file-based slash commands if requested
|
||||||
const expandedText = expandCommands ? expandSlashCommand(text, [...this._fileCommands]) : text;
|
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.agent.prompt(expandedText, options?.images);
|
||||||
await this.waitForRetry();
|
await this.waitForRetry();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ export { wrapToolsWithHooks, wrapToolWithHooks } from "./tool-wrapper.js";
|
||||||
export type {
|
export type {
|
||||||
AgentEndEvent,
|
AgentEndEvent,
|
||||||
AgentStartEvent,
|
AgentStartEvent,
|
||||||
|
BeforeAgentStartEvent,
|
||||||
|
BeforeAgentStartEventResult,
|
||||||
BashToolResultEvent,
|
BashToolResultEvent,
|
||||||
ContextEvent,
|
ContextEvent,
|
||||||
ContextEventResult,
|
ContextEventResult,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import type { ModelRegistry } from "../model-registry.js";
|
||||||
import type { SessionManager } from "../session-manager.js";
|
import type { SessionManager } from "../session-manager.js";
|
||||||
import type { AppendEntryHandler, LoadedHook, SendMessageHandler } from "./loader.js";
|
import type { AppendEntryHandler, LoadedHook, SendMessageHandler } from "./loader.js";
|
||||||
import type {
|
import type {
|
||||||
|
BeforeAgentStartEvent,
|
||||||
|
BeforeAgentStartEventResult,
|
||||||
ContextEvent,
|
ContextEvent,
|
||||||
ContextEventResult,
|
ContextEventResult,
|
||||||
HookError,
|
HookError,
|
||||||
|
|
@ -346,4 +348,44 @@ export class HookRunner {
|
||||||
|
|
||||||
return currentMessages;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,19 @@ export interface ContextEvent {
|
||||||
messages: AgentMessage[];
|
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.
|
* Event data for agent_start event.
|
||||||
* Fired when an agent loop starts (once per user prompt).
|
* 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 =
|
export type HookEvent =
|
||||||
| SessionEvent
|
| SessionEvent
|
||||||
| ContextEvent
|
| ContextEvent
|
||||||
|
| BeforeAgentStartEvent
|
||||||
| AgentStartEvent
|
| AgentStartEvent
|
||||||
| AgentEndEvent
|
| AgentEndEvent
|
||||||
| TurnStartEvent
|
| TurnStartEvent
|
||||||
|
|
@ -358,6 +372,15 @@ export interface ToolResultEventResult {
|
||||||
isError?: boolean;
|
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.
|
* Return type for session event handlers.
|
||||||
* Allows hooks to cancel "before_*" actions.
|
* Allows hooks to cancel "before_*" actions.
|
||||||
|
|
@ -433,6 +456,11 @@ export interface HookAPI {
|
||||||
on(event: "session", handler: HookHandler<SessionEvent, SessionEventResult | void>): void;
|
on(event: "session", handler: HookHandler<SessionEvent, SessionEventResult | void>): void;
|
||||||
// biome-ignore lint/suspicious/noConfusingVoidType: void allows handlers to not return anything
|
// biome-ignore lint/suspicious/noConfusingVoidType: void allows handlers to not return anything
|
||||||
on(event: "context", handler: HookHandler<ContextEvent, ContextEventResult | void>): void;
|
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_start", handler: HookHandler<AgentStartEvent>): void;
|
||||||
on(event: "agent_end", handler: HookHandler<AgentEndEvent>): void;
|
on(event: "agent_end", handler: HookHandler<AgentEndEvent>): void;
|
||||||
on(event: "turn_start", handler: HookHandler<TurnStartEvent>): void;
|
on(event: "turn_start", handler: HookHandler<TurnStartEvent>): void;
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ export type {
|
||||||
AgentEndEvent,
|
AgentEndEvent,
|
||||||
AgentStartEvent,
|
AgentStartEvent,
|
||||||
BashToolResultEvent,
|
BashToolResultEvent,
|
||||||
|
BeforeAgentStartEvent,
|
||||||
|
BeforeAgentStartEventResult,
|
||||||
CustomToolResultEvent,
|
CustomToolResultEvent,
|
||||||
EditToolResultEvent,
|
EditToolResultEvent,
|
||||||
FindToolResultEvent,
|
FindToolResultEvent,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue