feat(extensions): forward message and tool execution events to extensions (#1375)

The extension system currently only forwards agent_start, agent_end,
turn_start, and turn_end events. This means extensions cannot access
streaming text (token-by-token), message lifecycle, or tool execution
progress — all of which are available to internal subscribers.

This adds forwarding for the remaining 6 agent event types:
- message_start, message_update, message_end
- tool_execution_start, tool_execution_update, tool_execution_end

These follow the exact same pattern as the existing forwarded events:
new interfaces in types.ts, exports in index.ts, and else-if blocks
in _emitExtensionEvent(). The new types are included in ExtensionEvent
and automatically flow through RunnerEmitEvent (they're not in the
exclusion list).

This enables extensions to build real-time UIs, streaming WebSocket
bridges, and other integrations that need fine-grained event access.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Mario Zechner <badlogicgames@gmail.com>
This commit is contained in:
Sumeet Agarwal 2026-02-12 11:30:46 -08:00 committed by GitHub
parent 4793f7c92d
commit ff5148e7cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 117 additions and 0 deletions

View file

@ -50,12 +50,18 @@ import {
ExtensionRunner,
type ExtensionUIContext,
type InputSource,
type MessageEndEvent,
type MessageStartEvent,
type MessageUpdateEvent,
type SessionBeforeCompactResult,
type SessionBeforeForkResult,
type SessionBeforeSwitchResult,
type SessionBeforeTreeResult,
type ShutdownHandler,
type ToolDefinition,
type ToolExecutionEndEvent,
type ToolExecutionStartEvent,
type ToolExecutionUpdateEvent,
type ToolInfo,
type TreePreparation,
type TurnEndEvent,
@ -444,6 +450,51 @@ export class AgentSession {
};
await this._extensionRunner.emit(extensionEvent);
this._turnIndex++;
} else if (event.type === "message_start") {
const extensionEvent: MessageStartEvent = {
type: "message_start",
message: event.message,
};
await this._extensionRunner.emit(extensionEvent);
} else if (event.type === "message_update") {
const extensionEvent: MessageUpdateEvent = {
type: "message_update",
message: event.message,
assistantMessageEvent: event.assistantMessageEvent,
};
await this._extensionRunner.emit(extensionEvent);
} else if (event.type === "message_end") {
const extensionEvent: MessageEndEvent = {
type: "message_end",
message: event.message,
};
await this._extensionRunner.emit(extensionEvent);
} else if (event.type === "tool_execution_start") {
const extensionEvent: ToolExecutionStartEvent = {
type: "tool_execution_start",
toolCallId: event.toolCallId,
toolName: event.toolName,
args: event.args,
};
await this._extensionRunner.emit(extensionEvent);
} else if (event.type === "tool_execution_update") {
const extensionEvent: ToolExecutionUpdateEvent = {
type: "tool_execution_update",
toolCallId: event.toolCallId,
toolName: event.toolName,
args: event.args,
partialResult: event.partialResult,
};
await this._extensionRunner.emit(extensionEvent);
} else if (event.type === "tool_execution_end") {
const extensionEvent: ToolExecutionEndEvent = {
type: "tool_execution_end",
toolCallId: event.toolCallId,
toolName: event.toolName,
result: event.result,
isError: event.isError,
};
await this._extensionRunner.emit(extensionEvent);
}
}