From 1cea7fb8bc8b83ba671b49ccc0d36d02d37b86a8 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 26 Mar 2026 17:21:30 -0500 Subject: [PATCH] feat(01-04): add BeforeModelSelectEvent to extension API and wire emission - Add BeforeModelSelectEvent interface and BeforeModelSelectResult type to types.ts - Add on('before_model_select') subscription overload to ExtensionAPI interface - Add emitBeforeModelSelect() method to ExtensionAPI interface and ExtensionRuntimeState - Implement emitBeforeModelSelect() on ExtensionRunner using invokeHandlers (first-override-wins) - Bind runner's emitBeforeModelSelect into shared runtime at construction time - Wire emitBeforeModelSelect delegation through createExtensionAPI in loader.ts --- .../src/core/extensions/loader.ts | 6 +++++ .../src/core/extensions/runner.ts | 19 ++++++++++++++ .../src/core/extensions/types.ts | 26 +++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/packages/pi-coding-agent/src/core/extensions/loader.ts b/packages/pi-coding-agent/src/core/extensions/loader.ts index 96d689e67..d87eca9e4 100644 --- a/packages/pi-coding-agent/src/core/extensions/loader.ts +++ b/packages/pi-coding-agent/src/core/extensions/loader.ts @@ -428,6 +428,8 @@ export function createExtensionRuntime(): ExtensionRuntime { unregisterProvider: (name) => { runtime.pendingProviderRegistrations = runtime.pendingProviderRegistrations.filter((r) => r.name !== name); }, + // Stub replaced by ExtensionRunner at construction time via bindEmitMethods(). + emitBeforeModelSelect: async () => undefined, }; return runtime; @@ -579,6 +581,10 @@ function createExtensionAPI( runtime.unregisterProvider(name); }, + async emitBeforeModelSelect(event: Omit): Promise { + return runtime.emitBeforeModelSelect(event); + }, + events: eventBus, } as ExtensionAPI; diff --git a/packages/pi-coding-agent/src/core/extensions/runner.ts b/packages/pi-coding-agent/src/core/extensions/runner.ts index da06f0f13..048ad534c 100644 --- a/packages/pi-coding-agent/src/core/extensions/runner.ts +++ b/packages/pi-coding-agent/src/core/extensions/runner.ts @@ -13,6 +13,8 @@ import type { SessionManager } from "../session-manager.js"; import type { BeforeAgentStartEvent, BeforeAgentStartEventResult, + BeforeModelSelectEvent, + BeforeModelSelectResult, BeforeProviderRequestEvent, CompactOptions, ContextEvent, @@ -230,6 +232,8 @@ export class ExtensionRunner { this.cwd = cwd; this.sessionManager = sessionManager; this.modelRegistry = modelRegistry; + // Bind emit methods into the shared runtime so createExtensionAPI can delegate to them. + this.runtime.emitBeforeModelSelect = (event) => this.emitBeforeModelSelect(event); } bindCore(actions: ExtensionActions, contextActions: ExtensionContextActions): void { @@ -694,6 +698,21 @@ export class ExtensionRunner { return currentPayload; } + async emitBeforeModelSelect(event: Omit): Promise { + let result: BeforeModelSelectResult | undefined; + await this.invokeHandlers("before_model_select", () => ({ + type: "before_model_select" as const, + ...event, + } satisfies BeforeModelSelectEvent), (handlerResult) => { + if (handlerResult) { + result = handlerResult as BeforeModelSelectResult; + return { done: true }; // first override wins + } + return { done: false }; + }); + return result; + } + async emitBeforeAgentStart( prompt: string, images: ImageContent[] | undefined, diff --git a/packages/pi-coding-agent/src/core/extensions/types.ts b/packages/pi-coding-agent/src/core/extensions/types.ts index 8b6ff6ff1..037e9718c 100644 --- a/packages/pi-coding-agent/src/core/extensions/types.ts +++ b/packages/pi-coding-agent/src/core/extensions/types.ts @@ -603,6 +603,22 @@ export interface ModelSelectEvent { source: ModelSelectSource; } +/** Fired before model selection runs capability scoring. Extensions can override the selected model. */ +export interface BeforeModelSelectEvent { + type: "before_model_select"; + unitType: string; + unitId: string; + classification: { tier: string; reason: string; downgraded: boolean }; + taskMetadata?: Record; + eligibleModels: string[]; + phaseConfig?: { primary: string; fallbacks: string[] }; +} + +/** Result from before_model_select event handler. Return { modelId } to override selection. */ +export interface BeforeModelSelectResult { + modelId: string; +} + // ============================================================================ // User Bash Events // ============================================================================ @@ -1052,6 +1068,14 @@ export interface ExtensionAPI { on(event: "tool_result", handler: ExtensionHandler): void; on(event: "user_bash", handler: ExtensionHandler): void; on(event: "input", handler: ExtensionHandler): void; + on(event: "before_model_select", handler: ExtensionHandler): void; + + // ========================================================================= + // Event Emission (for host extensions that orchestrate model selection) + // ========================================================================= + + /** Emit before_model_select event. Returns override model ID or undefined. */ + emitBeforeModelSelect(event: Omit): Promise; // ========================================================================= // Tool Registration @@ -1367,6 +1391,8 @@ export interface ExtensionRuntimeState { */ registerProvider: (name: string, config: ProviderConfig) => void; unregisterProvider: (name: string) => void; + /** Emit before_model_select event to all registered handlers. Bound by ExtensionRunner. */ + emitBeforeModelSelect: (event: Omit) => Promise; } /**