From 02b42bfffe16580b9deb85d13efc3d1fae0e9e52 Mon Sep 17 00:00:00 2001 From: frizynn Date: Thu, 19 Mar 2026 14:55:18 -0300 Subject: [PATCH] refactor: consolidate 9 emit methods in extension runner into shared invokeHandlers Extract the duplicated loop pattern (create context, iterate extensions, iterate handlers, try/catch, emitError) into a private invokeHandlers() helper. Each emit method is now a thin wrapper that delegates the iteration to invokeHandlers and provides only its result-processing callback. Net reduction of ~124 lines with identical runtime behavior. --- .../src/core/extensions/runner.ts | 360 ++++++------------ 1 file changed, 118 insertions(+), 242 deletions(-) diff --git a/packages/pi-coding-agent/src/core/extensions/runner.ts b/packages/pi-coding-agent/src/core/extensions/runner.ts index 5ff1392bf..5eb15a7ac 100644 --- a/packages/pi-coding-agent/src/core/extensions/runner.ts +++ b/packages/pi-coding-agent/src/core/extensions/runner.ts @@ -555,216 +555,136 @@ export class ExtensionRunner { ); } - async emit(event: TEvent): Promise> { + /** + * Shared handler invocation loop. + * + * Iterates every handler registered for `eventType` across all extensions, + * calling each inside a try/catch that emits an ExtensionError on failure. + * + * `getEvent` builds the event object for each handler call — callers that + * mutate state between calls (e.g. context, before_provider_request) supply + * a function; callers with a fixed event can pass a constant. + * + * `processResult` receives each handler's return value and the owning + * extension's path. It returns `{ done: true }` to short-circuit + * or `{ done: false }` to keep iterating. + */ + private async invokeHandlers( + eventType: string, + getEvent: () => unknown, + processResult: (handlerResult: unknown, extensionPath: string) => { done: boolean }, + ): Promise { const ctx = this.createContext(); - let result: SessionBeforeEventResult | undefined; for (const ext of this.extensions) { - const handlers = ext.handlers.get(event.type); + const handlers = ext.handlers.get(eventType); if (!handlers || handlers.length === 0) continue; for (const handler of handlers) { try { + const event = getEvent(); const handlerResult = await handler(event, ctx); - - if (this.isSessionBeforeEvent(event) && handlerResult) { - result = handlerResult as SessionBeforeEventResult; - if (result.cancel) { - return result as RunnerEmitResult; - } - } + const action = processResult(handlerResult, ext.path); + if (action.done) return; } 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: event.type, + event: eventType, error: message, stack, }); } } } + } + + async emit(event: TEvent): Promise> { + let result: SessionBeforeEventResult | undefined; + const isSessionBefore = this.isSessionBeforeEvent(event); + + await this.invokeHandlers(event.type, () => event, (handlerResult) => { + if (isSessionBefore && handlerResult) { + result = handlerResult as SessionBeforeEventResult; + if (result.cancel) return { done: true }; + } + return { done: false }; + }); return result as RunnerEmitResult; } async emitToolResult(event: ToolResultEvent): Promise { - const ctx = this.createContext(); const currentEvent: ToolResultEvent = { ...event }; let modified = false; - for (const ext of this.extensions) { - const handlers = ext.handlers.get("tool_result"); - if (!handlers || handlers.length === 0) continue; + await this.invokeHandlers("tool_result", () => currentEvent, (handlerResult) => { + const r = handlerResult as ToolResultEventResult | undefined; + if (!r) return { done: false }; - for (const handler of handlers) { - try { - const handlerResult = (await handler(currentEvent, ctx)) as ToolResultEventResult | undefined; - if (!handlerResult) continue; + if (r.content !== undefined) { currentEvent.content = r.content; modified = true; } + if (r.details !== undefined) { currentEvent.details = r.details; modified = true; } + if (r.isError !== undefined) { currentEvent.isError = r.isError; modified = true; } + return { done: false }; + }); - if (handlerResult.content !== undefined) { - currentEvent.content = handlerResult.content; - modified = true; - } - if (handlerResult.details !== undefined) { - currentEvent.details = handlerResult.details; - modified = true; - } - if (handlerResult.isError !== undefined) { - currentEvent.isError = handlerResult.isError; - 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: "tool_result", - error: message, - stack, - }); - } - } - } - - if (!modified) { - return undefined; - } - - return { - content: currentEvent.content, - details: currentEvent.details, - isError: currentEvent.isError, - }; + if (!modified) return undefined; + return { content: currentEvent.content, details: currentEvent.details, isError: currentEvent.isError }; } async emitToolCall(event: ToolCallEvent): Promise { - const ctx = this.createContext(); let result: ToolCallEventResult | undefined; - for (const ext of this.extensions) { - const handlers = ext.handlers.get("tool_call"); - if (!handlers || handlers.length === 0) continue; - - for (const handler of handlers) { - try { - const handlerResult = await handler(event, ctx); - - if (handlerResult) { - result = handlerResult as ToolCallEventResult; - if (result.block) { - return result; - } - } - } 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: "tool_call", - error: message, - stack, - }); - } + await this.invokeHandlers("tool_call", () => event, (handlerResult) => { + if (handlerResult) { + result = handlerResult as ToolCallEventResult; + if (result.block) return { done: true }; } - } + return { done: false }; + }); return result; } async emitUserBash(event: UserBashEvent): Promise { - const ctx = this.createContext(); + let result: UserBashEventResult | undefined; - for (const ext of this.extensions) { - const handlers = ext.handlers.get("user_bash"); - if (!handlers || handlers.length === 0) continue; - - for (const handler of handlers) { - try { - const handlerResult = await handler(event, ctx); - if (handlerResult) { - return handlerResult as UserBashEventResult; - } - } 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: "user_bash", - error: message, - stack, - }); - } + await this.invokeHandlers("user_bash", () => event, (handlerResult) => { + if (handlerResult) { + result = handlerResult as UserBashEventResult; + return { done: true }; } - } + return { done: false }; + }); - return undefined; + return result; } async emitContext(messages: AgentMessage[]): Promise { - const ctx = this.createContext(); let currentMessages = structuredClone(messages); - for (const ext of this.extensions) { - const handlers = ext.handlers.get("context"); - if (!handlers || handlers.length === 0) continue; - - for (const handler of handlers) { - try { - const event: ContextEvent = { type: "context", messages: currentMessages }; - const handlerResult = await handler(event, ctx); - - if (handlerResult && (handlerResult as ContextEventResult).messages) { - currentMessages = (handlerResult as ContextEventResult).messages!; - } - } 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: "context", - error: message, - stack, - }); - } + await this.invokeHandlers("context", () => ({ type: "context", messages: currentMessages } satisfies ContextEvent), (handlerResult) => { + if (handlerResult && (handlerResult as ContextEventResult).messages) { + currentMessages = (handlerResult as ContextEventResult).messages!; } - } + return { done: false }; + }); return currentMessages; } async emitBeforeProviderRequest(payload: unknown, model?: { provider: string; id: string }): Promise { - const ctx = this.createContext(); let currentPayload = payload; - for (const ext of this.extensions) { - const handlers = ext.handlers.get("before_provider_request"); - if (!handlers || handlers.length === 0) continue; - - for (const handler of handlers) { - try { - const event: BeforeProviderRequestEvent = { - type: "before_provider_request", - payload: currentPayload, - model, - }; - const handlerResult = await handler(event, ctx); - if (handlerResult !== undefined) { - currentPayload = handlerResult; - } - } 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: "before_provider_request", - error: message, - stack, - }); - } - } - } + await this.invokeHandlers("before_provider_request", () => ({ + type: "before_provider_request", + payload: currentPayload, + model, + } satisfies BeforeProviderRequestEvent), (handlerResult) => { + if (handlerResult !== undefined) currentPayload = handlerResult; + return { done: false }; + }); return currentPayload; } @@ -774,47 +694,26 @@ export class ExtensionRunner { images: ImageContent[] | undefined, systemPrompt: string, ): Promise { - const ctx = this.createContext(); const messages: NonNullable[] = []; let currentSystemPrompt = systemPrompt; let systemPromptModified = false; - for (const ext of this.extensions) { - const handlers = ext.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, - systemPrompt: currentSystemPrompt, - }; - const handlerResult = await handler(event, ctx); - - if (handlerResult) { - const result = handlerResult as BeforeAgentStartEventResult; - if (result.message) { - messages.push(result.message); - } - if (result.systemPrompt !== undefined) { - currentSystemPrompt = result.systemPrompt; - systemPromptModified = 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: "before_agent_start", - error: message, - stack, - }); + await this.invokeHandlers("before_agent_start", () => ({ + type: "before_agent_start", + prompt, + images, + systemPrompt: currentSystemPrompt, + } satisfies BeforeAgentStartEvent), (handlerResult) => { + if (handlerResult) { + const r = handlerResult as BeforeAgentStartEventResult; + if (r.message) messages.push(r.message); + if (r.systemPrompt !== undefined) { + currentSystemPrompt = r.systemPrompt; + systemPromptModified = true; } } - } + return { done: false }; + }); if (messages.length > 0 || systemPromptModified) { return { @@ -822,7 +721,6 @@ export class ExtensionRunner { systemPrompt: systemPromptModified ? currentSystemPrompt : undefined, }; } - return undefined; } @@ -834,72 +732,50 @@ export class ExtensionRunner { promptPaths: Array<{ path: string; extensionPath: string }>; themePaths: Array<{ path: string; extensionPath: string }>; }> { - const ctx = this.createContext(); const skillPaths: Array<{ path: string; extensionPath: string }> = []; const promptPaths: Array<{ path: string; extensionPath: string }> = []; const themePaths: Array<{ path: string; extensionPath: string }> = []; - for (const ext of this.extensions) { - const handlers = ext.handlers.get("resources_discover"); - if (!handlers || handlers.length === 0) continue; - - for (const handler of handlers) { - try { - const event: ResourcesDiscoverEvent = { type: "resources_discover", cwd, reason }; - const handlerResult = await handler(event, ctx); - const result = handlerResult as ResourcesDiscoverResult | undefined; - - if (result?.skillPaths?.length) { - skillPaths.push(...result.skillPaths.map((path) => ({ path, extensionPath: ext.path }))); - } - if (result?.promptPaths?.length) { - promptPaths.push(...result.promptPaths.map((path) => ({ path, extensionPath: ext.path }))); - } - if (result?.themePaths?.length) { - themePaths.push(...result.themePaths.map((path) => ({ path, extensionPath: ext.path }))); - } - } 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: "resources_discover", - error: message, - stack, - }); - } - } - } + await this.invokeHandlers("resources_discover", () => ({ + type: "resources_discover", + cwd, + reason, + } satisfies ResourcesDiscoverEvent), (handlerResult, extensionPath) => { + const r = handlerResult as ResourcesDiscoverResult | undefined; + if (r?.skillPaths?.length) skillPaths.push(...r.skillPaths.map((path) => ({ path, extensionPath }))); + if (r?.promptPaths?.length) promptPaths.push(...r.promptPaths.map((path) => ({ path, extensionPath }))); + if (r?.themePaths?.length) themePaths.push(...r.themePaths.map((path) => ({ path, extensionPath }))); + return { done: false }; + }); return { skillPaths, promptPaths, themePaths }; } /** Emit input event. Transforms chain, "handled" short-circuits. */ async emitInput(text: string, images: ImageContent[] | undefined, source: InputSource): Promise { - const ctx = this.createContext(); let currentText = text; let currentImages = images; + let handled: InputEventResult | undefined; - for (const ext of this.extensions) { - for (const handler of ext.handlers.get("input") ?? []) { - try { - const event: InputEvent = { type: "input", text: currentText, images: currentImages, source }; - const result = (await handler(event, ctx)) as InputEventResult | undefined; - if (result?.action === "handled") return result; - if (result?.action === "transform") { - currentText = result.text; - currentImages = result.images ?? currentImages; - } - } catch (err) { - this.emitError({ - extensionPath: ext.path, - event: "input", - error: err instanceof Error ? err.message : String(err), - stack: err instanceof Error ? err.stack : undefined, - }); - } + await this.invokeHandlers("input", () => ({ + type: "input", + text: currentText, + images: currentImages, + source, + } satisfies InputEvent), (handlerResult) => { + const r = handlerResult as InputEventResult | undefined; + if (r?.action === "handled") { + handled = r; + return { done: true }; } - } + if (r?.action === "transform") { + currentText = r.text; + currentImages = r.images ?? currentImages; + } + return { done: false }; + }); + + if (handled) return handled; return currentText !== text || currentImages !== images ? { action: "transform", text: currentText, images: currentImages } : { action: "continue" };