@@ -610,6 +610,22 @@ export class AgentSession {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _replaceMessageInPlace(target: AgentMessage, replacement: AgentMessage): void {
|
||||
// Agent-core stores the finalized message object in its state before emitting message_end.
|
||||
// SessionManager persistence happens later in _processAgentEvent() with event.message.
|
||||
// Mutating this object in place keeps agent state, later turn/agent events, listeners,
|
||||
// and the eventual SessionManager.appendMessage(event.message) persistence in sync.
|
||||
if (target === replacement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetRecord = target as unknown as Record<string, unknown>;
|
||||
for (const key of Object.keys(targetRecord)) {
|
||||
delete targetRecord[key];
|
||||
}
|
||||
Object.assign(targetRecord, replacement);
|
||||
}
|
||||
|
||||
/** Emit extension events based on agent events */
|
||||
private async _emitExtensionEvent(event: AgentEvent): Promise<void> {
|
||||
if (event.type === "agent_start") {
|
||||
@@ -651,7 +667,10 @@ export class AgentSession {
|
||||
type: "message_end",
|
||||
message: event.message,
|
||||
};
|
||||
await this._extensionRunner.emit(extensionEvent);
|
||||
const replacement = await this._extensionRunner.emitMessageEnd(extensionEvent);
|
||||
if (replacement) {
|
||||
this._replaceMessageInPlace(event.message, replacement);
|
||||
}
|
||||
} else if (event.type === "tool_execution_start") {
|
||||
const extensionEvent: ToolExecutionStartEvent = {
|
||||
type: "tool_execution_start",
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user