/** * [WHO]: ExtensionRunner class, lifecycle management, event emission, slash-command dispatch chokepoint (invokeCommand), telemetry sink wiring (setTelemetrySink) * [FROM]: Depends on agent-core, ai, tui, modes/theme, session-manager, types.ts, core/platform/telemetry (ExtensionTelemetrySink + classifyArgsSignature for the P1 ext_command_events writer) * [TO]: Consumed by core/extensions-host/index.ts, core/extensions-host/wrapper.ts, core/runtime/agent-session.ts (delegates command dispatch via invokeCommand) * [HERE]: core/extensions-host/runner.ts - extension execution and lifecycle management; owns the single try/catch around command.handler so telemetry can wrap every invocation regardless of caller mode */ import type { AgentMessage } from "@catui/agent-core"; import type { ImageContent } from "@catui/ai/types"; import type { KeyId } from "@catui/tui"; import type { ResourceDiagnostic } from "../platform/config/diagnostics.js"; import type { KeybindingsConfig } from "../platform/keybindings.js"; import type { ModelRegistry } from "../model-registry.js"; import type { SessionManager } from "../session/session-manager.js"; import { type ExtensionTelemetrySink, type LlmCallEventInput } from "../platform/telemetry/index.js"; import type { BeforeAgentStartEvent, BeforeAgentStartEventResult, ContextEvent, Extension, ExtensionActions, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFlag, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, InputEvent, InputEventResult, InputSource, MessageRenderer, RegisteredCommand, RegisteredTool, ResourcesDiscoverEvent, SessionBeforeCompactResult, SessionBeforeForkResult, SessionBeforeSwitchResult, SessionBeforeTreeResult, ToolCallEvent, ToolCallEventResult, ToolResultEvent, ToolResultEventResult, UserBashEvent, UserBashEventResult } from "./types.js"; /** Combined result from all before_agent_start handlers */ interface BeforeAgentStartCombinedResult { messages?: NonNullable[]; systemPrompt?: string; } /** * Events handled by the generic emit() method. * Events with dedicated emitXxx() methods are excluded for stronger type safety. */ type RunnerEmitEvent = Exclude; type RunnerEmitResult = TEvent extends { type: "session_before_switch"; } ? SessionBeforeSwitchResult | undefined : TEvent extends { type: "session_before_fork"; } ? SessionBeforeForkResult | undefined : TEvent extends { type: "session_before_compact"; } ? SessionBeforeCompactResult | undefined : TEvent extends { type: "session_before_tree"; } ? SessionBeforeTreeResult | undefined : undefined; export type ExtensionErrorListener = (error: ExtensionError) => void; export type NewSessionHandler = (options?: { parentSession?: string; setup?: (sessionManager: SessionManager) => Promise; }) => Promise<{ cancelled: boolean; }>; export type ForkHandler = (entryId: string) => Promise<{ cancelled: boolean; }>; export type NavigateTreeHandler = (targetId: string, options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string; }) => Promise<{ cancelled: boolean; }>; export type SwitchSessionHandler = (sessionPath: string) => Promise<{ cancelled: boolean; }>; export type ReloadHandler = () => Promise; export type ShutdownHandler = () => void; /** * Helper function to emit session_shutdown event to extensions. * Returns true if the event was emitted, false if there were no handlers. */ export declare function emitSessionShutdownEvent(extensionRunner: ExtensionRunner | undefined): Promise; export declare class ExtensionRunner { private extensions; private runtime; private uiContext; private cwd; private agentDir; private sessionManager; private modelRegistry; private errorListeners; private getModel; private completeSimpleFn; private completeSimpleWithUsageFn; private completeJsonFn; private isIdleFn; private waitForIdleFn; private abortFn; private clearFollowUpQueueFn; private hasPendingMessagesFn; private getContextUsageFn; private compactFn; private getSystemPromptFn; private getSoulManagerFn; private getSettingsFn; private getSkillsFn; private newSessionHandler; private forkHandler; private navigateTreeHandler; private switchSessionHandler; private reloadHandler; private shutdownHandler; private shortcutDiagnostics; private commandDiagnostics; private telemetrySink?; private _beforeAgentStartTimeoutMs; private get beforeAgentStartTimeoutMs(); private readonly beforeAgentStartTimeoutSentinel; private beforeAgentStartTimeoutLastReported; constructor(extensions: Extension[], runtime: ExtensionRuntime, cwd: string, agentDir: string, sessionManager: SessionManager, modelRegistry: ModelRegistry); private withTimeout; private reportBeforeAgentStartTimeout; bindCore(actions: ExtensionActions, contextActions: ExtensionContextActions): void; bindCommandContext(actions?: ExtensionCommandContextActions): void; setUIContext(uiContext?: ExtensionUIContext): void; getUIContext(): ExtensionUIContext; hasUI(): boolean; getExtensionPaths(): string[]; /** Get all registered tools from all extensions (first registration per name wins). */ getAllRegisteredTools(): RegisteredTool[]; /** Get a tool definition by name. Returns undefined if not found. */ getToolDefinition(toolName: string): RegisteredTool["definition"] | undefined; getFlags(): Map; setFlagValue(name: string, value: boolean | string): void; getFlagValues(): Map; getShortcuts(effectiveKeybindings: Required): Map; getShortcutDiagnostics(): ResourceDiagnostic[]; onError(listener: ExtensionErrorListener): () => void; emitError(error: ExtensionError): void; /** Set the timeout for before_agent_start extension hooks (default: 1500ms). */ setBeforeAgentStartTimeout(ms: number): void; /** Clean up all handlers, listeners, and diagnostics. Call on session shutdown. */ dispose(): void; hasHandlers(eventType: string): boolean; getMessageRenderer(customType: string): MessageRenderer | undefined; getRegisteredCommands(reserved?: Set): RegisteredCommand[]; getCommandDiagnostics(): ResourceDiagnostic[]; getRegisteredCommandsWithPaths(): Array<{ command: RegisteredCommand; extensionPath: string; }>; getCommand(name: string): RegisteredCommand | undefined; /** * Returns the owning Extension for a given slash command name. Used by the * telemetry middleware to stamp `extension_name` on each ext_command_events * row. Falls back to undefined when no extension claims the command. */ private findCommandOwner; /** * Derive a short, stable extension name from an Extension record. For * built-ins (extensions/builtin/) and most user extensions * (packages/) the directory basename is the right answer. */ private deriveExtensionName; /** * Attach (or replace) the extension telemetry sink. The runner owns sink * lifecycle from this point — invokeCommand() will fire-and-forget one * `ext_command_events` row per invocation, and writeLlmCallEvent() (called * from extension-core-bindings) writes one `ext_llm_calls` row per * extension-initiated LLM call. Passing the noop sink (the factory's * default when no insforge credentials exist) is the safe way to disable * telemetry without scattering null-checks at the call sites. */ setTelemetrySink(sink: ExtensionTelemetrySink): void; /** * Passthrough used by core/runtime/extension-core-bindings.ts after each * extension-initiated LLM call. The runner owns the sink; the binding * doesn't import it directly to keep its concerns scoped to LLM plumbing. */ writeLlmCallEvent(input: LlmCallEventInput): void; /** * Wrap an extension hook handler invocation in three layers: * * 1. AsyncLocalStorage frame so any LLM call placed by the handler gets * attributed to this extension + hook + isUserInitiated=false in * ext_llm_calls. * 2. Wall-clock timing measurement (only when the sample roll passes). * 3. One fire-and-forget ext_hook_events row per sampled invocation, * capturing duration_ms + ok + error_code + sample_rate. * * High-frequency hooks (tool_*) are sampled at 10% so the table doesn't * drown in tool-execution rows; the sample_rate column on each row lets * dashboards extrapolate with `count(*) * 1/sample_rate`. Sampling decision * sits inside the AsyncLocalStorage frame so even skipped-emit hooks still * attribute their LLM calls. */ private invokeHookHandler; /** * Single chokepoint for slash command dispatch. All modes (interactive / * print / rpc / acp) funnel through agent-session._tryExecuteExtensionCommand, * which calls this method. The wrapper measures wall-clock duration, * captures outcome (ok / error / cancelled), and emits one telemetry row * per invocation. Errors are still routed through emitError() so existing * UI surfaces (toasts, logs) keep working unchanged. * * Returns `{ found: false }` when no extension owns the command, letting * the caller fall through to built-in command handling without emitting a * telemetry row for an unknown command. */ invokeCommand(commandName: string, args: string, ctx: ExtensionCommandContext, metadata?: { sessionId?: string | null; runId?: string | null; variant?: string | null; }): Promise<{ found: boolean; }>; /** * Request a graceful shutdown. Called by extension tools and event handlers. * The actual shutdown behavior is provided by the mode via bindExtensions(). */ shutdown(): void; /** * Create an ExtensionContext for use in event handlers and tool execution. * Context values are resolved at call time, so changes via bindCore/bindUI are reflected. */ createContext(): ExtensionContext; createCommandContext(): ExtensionCommandContext; private isSessionBeforeEvent; emit(event: TEvent): Promise>; emitToolResult(event: ToolResultEvent): Promise; emitToolCall(event: ToolCallEvent): Promise; emitUserBash(event: UserBashEvent): Promise; emitContext(messages: AgentMessage[]): Promise; emitBeforeAgentStart(prompt: string, images: ImageContent[] | undefined, systemPrompt: string): Promise; private appendSystemPrompt; emitResourcesDiscover(cwd: string, reason: ResourcesDiscoverEvent["reason"]): Promise<{ skillPaths: Array<{ path: string; extensionPath: string; }>; promptPaths: Array<{ path: string; extensionPath: string; }>; themePaths: Array<{ path: string; extensionPath: string; }>; }>; /** Emit input event. Transforms chain, "handled" short-circuits. */ emitInput(text: string, images: ImageContent[] | undefined, source: InputSource): Promise; } export {};