feat(coding-agent): allow message_end replacements

closes #3982
This commit is contained in:
Mario Zechner
2026-04-30 21:19:09 +02:00
parent 24dec9fcd5
commit 40c6eabb8f
7 changed files with 188 additions and 4 deletions

View File

@@ -34,6 +34,8 @@ import type {
InputEvent,
InputEventResult,
InputSource,
MessageEndEvent,
MessageEndEventResult,
MessageRenderer,
ProviderConfig,
RegisteredCommand,
@@ -118,6 +120,7 @@ type RunnerEmitEvent = Exclude<
| ContextEvent
| BeforeProviderRequestEvent
| BeforeAgentStartEvent
| MessageEndEvent
| ResourcesDiscoverEvent
| InputEvent
>;
@@ -708,6 +711,48 @@ export class ExtensionRunner {
return result as RunnerEmitResult<TEvent>;
}
async emitMessageEnd(event: MessageEndEvent): Promise<AgentMessage | undefined> {
const ctx = this.createContext();
let currentMessage = event.message;
let modified = false;
for (const ext of this.extensions) {
const handlers = ext.handlers.get("message_end");
if (!handlers || handlers.length === 0) continue;
for (const handler of handlers) {
try {
const currentEvent: MessageEndEvent = { ...event, message: currentMessage };
const handlerResult = (await handler(currentEvent, ctx)) as MessageEndEventResult | undefined;
if (!handlerResult?.message) continue;
if (handlerResult.message.role !== currentMessage.role) {
this.emitError({
extensionPath: ext.path,
event: "message_end",
error: "message_end handlers must return a message with the same role",
});
continue;
}
currentMessage = handlerResult.message;
modified = true;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const stack = err instanceof Error ? err.stack : undefined;
this.emitError({
extensionPath: ext.path,
event: "message_end",
error: message,
stack,
});
}
}
}
return modified ? currentMessage : undefined;
}
async emitToolResult(event: ToolResultEvent): Promise<ToolResultEventResult | undefined> {
const ctx = this.createContext();
const currentEvent: ToolResultEvent = { ...event };

View File

@@ -993,6 +993,11 @@ export interface ToolResultEventResult {
isError?: boolean;
}
export interface MessageEndEventResult {
/** Replace the finalized message. The replacement must keep the original message role. */
message?: AgentMessage;
}
export interface BeforeAgentStartEventResult {
message?: Pick<CustomMessage, "customType" | "content" | "display" | "details">;
/** Replace the system prompt for this turn. If multiple extensions return this, they are chained. */
@@ -1101,7 +1106,7 @@ export interface ExtensionAPI {
on(event: "turn_end", handler: ExtensionHandler<TurnEndEvent>): void;
on(event: "message_start", handler: ExtensionHandler<MessageStartEvent>): void;
on(event: "message_update", handler: ExtensionHandler<MessageUpdateEvent>): void;
on(event: "message_end", handler: ExtensionHandler<MessageEndEvent>): void;
on(event: "message_end", handler: ExtensionHandler<MessageEndEvent, MessageEndEventResult>): void;
on(event: "tool_execution_start", handler: ExtensionHandler<ToolExecutionStartEvent>): void;
on(event: "tool_execution_update", handler: ExtensionHandler<ToolExecutionUpdateEvent>): void;
on(event: "tool_execution_end", handler: ExtensionHandler<ToolExecutionEndEvent>): void;