import { accessSync, constants } from 'node:fs'; import path from 'node:path'; import type { AgentBrowserModule } from '../agent-browser'; import { CodingAttachmentStore } from '../coding-projects/attachment-store'; import { createCodingConversationStore } from '../coding-projects/conversation-store'; import { CodingProjectService } from '../coding-projects/project-service'; import { createCodingProjectStore, type CodingProjectStore, type CodingProjectStorage, } from '../coding-projects/project-store'; import { CodingConversationService } from '../coding-runtime/conversation-service'; import { PiManagedExtensionHost } from '../coding-runtime/pi/extension-host'; import { PiAgentServerProcess } from '../coding-runtime/pi/agent-server-process'; import { PiManagedInputRevisionCoordinator } from '../coding-runtime/pi/managed-input-revision'; import { PiProductTools } from '../coding-runtime/pi/product-tools'; import { buildPiProviderCatalog, resolvePiProviderCredentialFromSecretStore, selectPiProviderModel, } from '../coding-runtime/pi/provider-config'; import { createPiManagedWorkerOpener, PiConversationRuntime, } from '../coding-runtime/pi/runtime'; import { PiSessionRegistry } from '../coding-runtime/pi/session-registry'; import { createPiManagedSubagentChildOpener } from '../coding-runtime/pi/subagent-child'; import { PiSubagentScheduler } from '../coding-runtime/pi/subagent'; import { PiProcessBudget, PiWorkerPool } from '../coding-runtime/pi/worker-pool'; import { getProviderService } from '../services/providers/provider-service'; import { isCodingProviderAuthenticationError, refreshCodingProviderCredential, } from './coding-provider-auth'; import { createCodingProductHost, type CodingProductComposition } from './coding-product-services'; import { archivePiConversationSession, getPiManagedPaths, } from '../coding-runtime/pi/resource-loader'; export interface CodingCompositionPaths { executablePath: string; cliPath: string; serverPath: string; userDataDir: string; bundledSkillsDir: string; } export interface CreateCodingCompositionOptions { storage: CodingProjectStorage; projectStore?: CodingProjectStore; browser: AgentBrowserModule; paths: CodingCompositionPaths; getLocalProxyCredential?(): string | undefined; acquireBackgroundLease?(lease: { id: string; kind: 'coding-run' }): () => void; } type PiWorkerExecutableProbe = (candidate: string) => boolean; function canExecute(candidate: string): boolean { try { accessSync(candidate, constants.X_OK); return true; } catch { return false; } } export function resolvePiWorkerExecutablePath( executablePath: string, options: { platform?: NodeJS.Platform; canExecute?: PiWorkerExecutableProbe; } = {}, ): string { if ((options.platform ?? process.platform) !== 'darwin') return executablePath; // The product binary remains a Foreground LaunchServices app even in Node mode. // Electron's generic Helper is LSUIElement=true, so workers stay out of the Dock. const executableName = path.basename(executablePath); const helperExecutablePath = path.resolve( path.dirname(executablePath), '..', 'Frameworks', `${executableName} Helper.app`, 'Contents', 'MacOS', `${executableName} Helper`, ); return (options.canExecute ?? canExecute)(helperExecutablePath) ? helperExecutablePath : executablePath; } export function resolveCodingPiRuntimePaths(input: { isPackaged: boolean; resourcesPath: string; appPath: string; executablePath: string; }): Pick { return { executablePath: resolvePiWorkerExecutablePath(input.executablePath), cliPath: input.isPackaged ? path.join(input.resourcesPath, 'pi-runtime', 'dist', 'cli.js') : path.join( input.appPath, 'node_modules', '@earendil-works', 'pi-coding-agent', 'dist', 'cli.js', ), serverPath: input.isPackaged ? path.join(input.resourcesPath, 'resources', 'pi-agent-server.mjs') : path.join(input.appPath, 'resources', 'pi-agent-server.mjs'), }; } export function createCodingComposition( options: CreateCodingCompositionOptions, ): CodingProductComposition { const getLocalProxyCredential = options.getLocalProxyCredential; const projectStore = options.projectStore ?? createCodingProjectStore(options.storage); const attachments = new CodingAttachmentStore( path.join(options.paths.userDataDir, 'coding-runtime', 'attachments'), ); const productTools = new PiProductTools({ browser: options.browser, attachments, bundledSkillsDir: options.paths.bundledSkillsDir, }); const extensionHost = new PiManagedExtensionHost(); extensionHost.configureProductTools(productTools); const conversationStores = new Map>(); const conversationStoreForProject = (projectPath: string) => { const existing = conversationStores.get(projectPath); if (existing) return existing; const created = createCodingConversationStore(projectPath); conversationStores.set(projectPath, created); return created; }; const registry = new PiSessionRegistry({ projectStore, createConversationStore: conversationStoreForProject, }); const revisions = new PiManagedInputRevisionCoordinator(); const processBudget = new PiProcessBudget(); const agentServer = new PiAgentServerProcess({ executablePath: options.paths.executablePath, serverPath: options.paths.serverPath, runtimeRoot: path.dirname(path.dirname(options.paths.cliPath)), configDir: getPiManagedPaths(options.paths.userDataDir).configDir, }); const loadProviderInput = async () => ({ accounts: await getProviderService().listAccounts(), modelSummaries: [], }); const workerPool = new PiWorkerPool({ processBudget, processMode: 'shared', maxIdle: 8, revisionCoordinator: revisions, openWorker: createPiManagedWorkerOpener({ registry, executablePath: options.paths.executablePath, cliPath: options.paths.cliPath, userDataDir: options.paths.userDataDir, bundledSkillsDir: options.paths.bundledSkillsDir, loadProviderInput, resolveCredential: resolvePiProviderCredentialFromSecretStore, createProcess: (processOptions) => agentServer.createWorker(processOptions), ...(getLocalProxyCredential ? { getLocalProxyCredential: async () => getLocalProxyCredential() } : {}), extensionHost, }), }); const childOpener = createPiManagedSubagentChildOpener({ projectStore, executablePath: options.paths.executablePath, cliPath: options.paths.cliPath, userDataDir: options.paths.userDataDir, bundledSkillsDir: options.paths.bundledSkillsDir, extensionHost, loadProviderInput, resolveCredential: resolvePiProviderCredentialFromSecretStore, getRevision: () => revisions.current, ...(getLocalProxyCredential ? { getLocalProxyCredential: async () => getLocalProxyCredential() } : {}), }); const subagents = new PiSubagentScheduler({ openChild: childOpener, processBudget, }); const runtime = new PiConversationRuntime({ pool: workerPool, registry, extensionHost, subagentScheduler: subagents, resolveModel: async (model) => selectPiProviderModel( buildPiProviderCatalog(await loadProviderInput()), model, ), isAuthenticationError: isCodingProviderAuthenticationError, refreshCredential: refreshCodingProviderCredential, resolveImages: async (refs) => await Promise.all(refs.map(async ({ attachmentId }) => { const record = await attachments.read(attachmentId); return { type: 'image', data: record.data.toString('base64'), mimeType: record.mime, }; })), ...(options.acquireBackgroundLease ? { acquireBackgroundLease: options.acquireBackgroundLease } : {}), }); const projects = new CodingProjectService(projectStore, { createConversationStore: conversationStoreForProject, onResourcesChanged: async (project) => { runtime.markResourcesStale(); const conversations = await conversationStoreForProject(project.path).read() .then((file) => file.conversations) .catch(() => []); for (const conversation of conversations) registry.forget(conversation.id); }, onProjectDeactivated: async (project, reason) => { const conversations = await conversationStoreForProject(project.path).read() .then((file) => file.conversations) .catch(() => []); await Promise.allSettled([ options.browser.close(project.path), ...conversations.map(({ id }) => runtime.dispose(id, reason)), ]); }, }); const conversations = new CodingConversationService(projects, runtime, { archiveSession: async ({ projectId, sessionKey }) => { await archivePiConversationSession({ userDataDir: options.paths.userDataDir, projectId, sessionKey, }); }, }); const host = createCodingProductHost({ projects, productTools, listPiCommands: (conversationId) => conversations.listLiveCommands(conversationId), }); return { attachments, productTools, projects, conversations, runtime, host, async sleep(reason) { if (reason === 'background_sleep' && runtime.hasActiveWork()) return; const conversationIds = runtime.getDiagnostics().workers.map((worker) => worker.conversationId); await Promise.allSettled(conversationIds.map((conversationId) => ( runtime.dispose(conversationId, reason) ))); await agentServer.stop(); }, async shutdown() { await subagents.close(); try { await runtime.shutdown(); } finally { await agentServer.stop(); } }, }; }