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 { 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 { createDataServiceOperations } from '../services/data-service-client'; import { createPreviewDataSessionManager, type PreviewDataSessionManager } from '../services/preview-data-session'; import { archivePiConversationSession } from '../coding-runtime/pi/resource-loader'; import { resolveLegacyProjectModel } from '../coding-projects/legacy-v1'; export interface CodingCompositionPaths { executablePath: string; cliPath: 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; } export function resolveCodingPiRuntimePaths(input: { isPackaged: boolean; resourcesPath: string; appPath: string; executablePath: string; }): Pick { return { executablePath: 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', ), }; } 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 loadProviderInput = async () => ({ accounts: await getProviderService().listAccounts(), modelSummaries: [], }); const workerPool = new PiWorkerPool({ processBudget, revisionCoordinator: revisions, openWorker: createPiManagedWorkerOpener({ registry, executablePath: options.paths.executablePath, cliPath: options.paths.cliPath, userDataDir: options.paths.userDataDir, bundledSkillsDir: options.paths.bundledSkillsDir, loadProviderInput, resolveCredential: resolvePiProviderCredentialFromSecretStore, ...(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, reclaimProcessCapacity: (signal) => workerPool.reclaimIdleWorker(signal), }); 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 } : {}), }); let previewDataSession: PreviewDataSessionManager | undefined; const projects = new CodingProjectService(projectStore, { migration: { resolveLegacyModel: async ({ legacyModel }) => resolveLegacyProjectModel( legacyModel, await getProviderService().listAccounts(), ), }, 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); }, onProjectIdentityChanging: async (project) => { previewDataSession?.invalidate('project_identity_changed'); await options.browser.close(project.path); }, onProjectDeactivated: async (project, reason) => { previewDataSession?.invalidate('project_deactivated'); 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), }); const dataService = createDataServiceOperations({ projects }); productTools.configureDataService(dataService); previewDataSession = createPreviewDataSessionManager({ projects }); const unsubscribeBrowserLifecycle = typeof options.browser.subscribeLifecycle === 'function' ? options.browser.subscribeLifecycle((event) => { previewDataSession?.handleAgentBrowserLifecycle(event); }) : () => undefined; return { attachments, dataService, previewDataSession, 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) ))); }, async shutdown() { previewDataSession?.dispose(); unsubscribeBrowserLifecycle(); await subagents.close(); await runtime.shutdown(); }, }; }