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
This commit is contained in:
Jeremy 2026-03-26 17:21:30 -05:00
parent 1866ccf781
commit 1cea7fb8bc
3 changed files with 51 additions and 0 deletions

View file

@ -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<import("./types.js").BeforeModelSelectEvent, "type">): Promise<import("./types.js").BeforeModelSelectResult | undefined> {
return runtime.emitBeforeModelSelect(event);
},
events: eventBus,
} as ExtensionAPI;

View file

@ -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<BeforeModelSelectEvent, "type">): Promise<BeforeModelSelectResult | undefined> {
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,

View file

@ -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<string, unknown>;
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<ToolResultEvent, ToolResultEventResult>): void;
on(event: "user_bash", handler: ExtensionHandler<UserBashEvent, UserBashEventResult>): void;
on(event: "input", handler: ExtensionHandler<InputEvent, InputEventResult>): void;
on(event: "before_model_select", handler: ExtensionHandler<BeforeModelSelectEvent, BeforeModelSelectResult>): void;
// =========================================================================
// Event Emission (for host extensions that orchestrate model selection)
// =========================================================================
/** Emit before_model_select event. Returns override model ID or undefined. */
emitBeforeModelSelect(event: Omit<BeforeModelSelectEvent, "type">): Promise<BeforeModelSelectResult | undefined>;
// =========================================================================
// 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<BeforeModelSelectEvent, "type">) => Promise<BeforeModelSelectResult | undefined>;
}
/**