/** * pi SDK adapter boundary * Zones: pi agent sdk boundary, shared adapters * Owns direct pi SDK imports and exposes narrow bridge-facing helpers/types for the extension composition layer */ import type { AssistantMessageEvent } from "@earendil-works/pi-ai"; import { type AgentEndEvent, type AgentSettledEvent, type AgentStartEvent, type BeforeAgentStartEvent, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, type InputEvent, type SessionBeforeCompactEvent, type SessionCompactEvent, type SessionShutdownEvent, type SessionStartEvent, type SlashCommandInfo, SettingsManager, } from "@earendil-works/pi-coding-agent"; export type { AgentEndEvent, AgentSettledEvent, AgentStartEvent, AssistantMessageEvent, BeforeAgentStartEvent, ExtensionAPI, ExtensionCommandContext, ExtensionContext, InputEvent, SessionBeforeCompactEvent, SessionCompactEvent, SessionShutdownEvent, SessionStartEvent, SlashCommandInfo, }; export interface ToolExecutionStartEvent { type: "tool_execution_start"; toolCallId: string; toolName: string; args: unknown; } export interface ToolExecutionUpdateEvent { type: "tool_execution_update"; toolCallId: string; toolName: string; args: unknown; partialResult: unknown; } export interface ToolExecutionEndEvent { type: "tool_execution_end"; toolCallId: string; toolName: string; result: unknown; isError: boolean; } export interface PiSettingsManager { reload: () => Promise; flush: () => Promise; getEnabledModels: () => string[] | undefined; setEnabledModels: (patterns: string[] | undefined) => void; } export type PiSlashCommandInfo = SlashCommandInfo; export type PiRunMode = "tui" | "rpc" | "json" | "print"; function isPiRunMode(value: unknown): value is PiRunMode { return ( value === "tui" || value === "rpc" || value === "json" || value === "print" ); } export function getExtensionContextMode(ctx: unknown): PiRunMode | undefined { const mode = typeof ctx === "object" && ctx !== null ? (ctx as { mode?: unknown }).mode : undefined; return isPiRunMode(mode) ? mode : undefined; } export function isExtensionContextPassiveRunMode(ctx: unknown): boolean { const mode = getExtensionContextMode(ctx); return mode === "print" || mode === "json"; } export function canStartPollingInExtensionContext(ctx: unknown): boolean { return !isExtensionContextPassiveRunMode(ctx); } export function formatPollingStartBlockedByRunMode(ctx: unknown): string { const mode = getExtensionContextMode(ctx); return mode ? `Telegram polling is unavailable in Pi ${mode} mode. Use /telegram-connect from a long-lived Pi session.` : "Telegram polling is unavailable in this Pi run mode."; } export function getSessionCompactionReason( event: unknown, ): "manual" | "threshold" | "overflow" | "unknown" { const reason = event && typeof event === "object" && "reason" in event ? (event as { reason?: unknown }).reason : undefined; return reason === "manual" || reason === "threshold" || reason === "overflow" ? reason : "unknown"; } export interface PiExtensionApiRuntimePorts { sendUserMessage: ExtensionAPI["sendUserMessage"]; exec: ExtensionAPI["exec"]; getCommands: ExtensionAPI["getCommands"]; getThinkingLevel: ExtensionAPI["getThinkingLevel"]; setThinkingLevel: ExtensionAPI["setThinkingLevel"]; getActiveTools: ExtensionAPI["getActiveTools"]; setActiveTools: ExtensionAPI["setActiveTools"]; setModel: ExtensionAPI["setModel"]; } export function createExtensionApiRuntimePorts( api: Pick< ExtensionAPI, | "sendUserMessage" | "exec" | "getCommands" | "getThinkingLevel" | "setThinkingLevel" | "getActiveTools" | "setActiveTools" | "setModel" >, ): PiExtensionApiRuntimePorts { return { sendUserMessage: (content, options) => api.sendUserMessage(content, options), exec: (command, args, options) => api.exec(command, args, options), getCommands: () => api.getCommands(), getThinkingLevel: () => api.getThinkingLevel(), setThinkingLevel: (level) => api.setThinkingLevel(level), getActiveTools: () => api.getActiveTools(), setActiveTools: (names) => api.setActiveTools(names), setModel: (model) => api.setModel(model), }; } type PiSettingsManagerFactory = { create: (cwd: string) => unknown | PromiseLike; }; type HostSettingsManager = { reload?: () => void | PromiseLike; flush?: () => void | PromiseLike; getEnabledModels?: () => unknown; setEnabledModels?: (patterns: string[] | undefined) => void; get?: (key: string) => unknown; set?: (key: string, value: unknown) => void; }; function readEnabledModels(value: unknown): string[] | undefined { if (value === undefined) return undefined; if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) { return [...value]; } throw new TypeError("Host settings enabledModels must be a string array or undefined."); } export function normalizeSettingsManager(manager: unknown): PiSettingsManager { if (typeof manager !== "object" || manager === null) { throw new TypeError("Host settings manager must be an object."); } const host = manager as HostSettingsManager; if (typeof host.flush !== "function") { throw new TypeError("Host settings manager must provide flush()."); } const read = typeof host.getEnabledModels === "function" ? () => host.getEnabledModels!.call(host) : typeof host.get === "function" ? () => host.get!.call(host, "enabledModels") : undefined; const write = typeof host.setEnabledModels === "function" ? (patterns: string[] | undefined) => host.setEnabledModels!.call(host, patterns) : typeof host.set === "function" ? (patterns: string[] | undefined) => host.set!.call(host, "enabledModels", patterns ?? []) : undefined; if (!read || !write) { throw new TypeError( "Host settings manager must provide enabled-model read and write capabilities.", ); } return { reload: async () => { await host.reload?.call(host); }, flush: async () => { await host.flush!.call(host); }, getEnabledModels: () => readEnabledModels(read()), setEnabledModels: write, }; } export async function createSettingsManager( cwd: string, ): Promise { // Pi returns its legacy settings surface synchronously. Compatible hosts may // resolve a generic settings service asynchronously; normalize both once at // the SDK boundary instead of leaking host distinctions into menu domains. const factory = SettingsManager as unknown as PiSettingsManagerFactory; return normalizeSettingsManager(await factory.create(cwd)); } export function createScopedModelPatternPersister(deps: { createSettingsManager: ( cwd: string, ) => PiSettingsManager | PromiseLike; clearCachedModelMenuInputs: () => void; }): (patterns: string[], ctx: ExtensionContext) => Promise { return async (patterns, ctx) => { const settingsManager = await deps.createSettingsManager(ctx.cwd); settingsManager.setEnabledModels( patterns.length > 0 ? patterns : undefined, ); await settingsManager.flush(); deps.clearCachedModelMenuInputs(); }; } export function getExtensionContextModel( ctx: ExtensionContext, ): ExtensionContext["model"] { return ctx.model; } export function getExtensionContextCwd(ctx: ExtensionContext): string { return ctx.cwd; } export function isExtensionContextIdle(ctx: ExtensionContext): boolean { return ctx.isIdle(); } export function hasExtensionContextPendingMessages( ctx: ExtensionContext, ): boolean { return ctx.hasPendingMessages(); } export function compactExtensionContext( ctx: ExtensionContext, callbacks: Parameters[0], ): ReturnType { return ctx.compact(callbacks); }