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'; import { createProjectPluginService } from '../coding-plugins/project-service'; import { createCodingCapabilityRegistry, } from '../coding-plugins/registry'; import { createDataServicePluginAdapter } from '../coding-plugins/adapters/data-service'; import { createGameResourcePluginAdapter } from '../coding-plugins/adapters/game-resource'; import { GameResourceClient } from '../services/game-resource-client'; import { AccountPluginCache } from '../coding-plugins/account-plugin-cache'; import { createMarketplaceClient, type MarketplaceClient, } from '../coding-plugins/marketplace-client'; import { PluginPackageStore, type PluginPackageStore as PluginPackageStoreType, } from '../coding-plugins/package-store'; import { createEffectivePluginResolver, type EffectivePluginResolver, } from '../coding-plugins/effective-resolver'; import { subscribeWorksSquareSession } from '../services/works-square-session'; import { PluginPolicyClient } from '../services/plugin-policy-client'; import { loadBundledCodingPluginDefinitionsSync, resolveBundledCodingPluginRootPaths, } from '../coding-plugins/manifest'; import { createCodingProjectPluginService, createCodingPluginMarketplaceService, type CodingProjectPluginService, type CodingPluginMarketplaceService, } from './coding-product-services'; 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; marketplaceClient?: MarketplaceClient; packageStore?: PluginPackageStoreType; accountCache?: AccountPluginCache; clientVersion?: string; policyClient?: PluginPolicyClient; } 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 bundledPluginsDir = path.join( path.dirname(path.resolve(options.paths.bundledSkillsDir)), 'coding-plugins', ); const pluginDefinitions = loadBundledCodingPluginDefinitionsSync(bundledPluginsDir); const pluginRoots = resolveBundledCodingPluginRootPaths(bundledPluginsDir); const pluginSkillSources = pluginDefinitions.flatMap((definition, index) => ( definition.skills.map((skill) => ({ id: skill.id, pluginId: definition.id, directory: path.join(pluginRoots[index] as string, path.dirname(skill.entryPath)), entryPath: path.basename(skill.entryPath), })) )); const accountCache = options.accountCache ?? new AccountPluginCache(); const marketplaceClient = options.marketplaceClient ?? createMarketplaceClient({ accountCache }); const packageStore = options.packageStore ?? new PluginPackageStore({ rootDir: path.join(options.paths.userDataDir, 'coding-plugins'), marketplace: marketplaceClient, accountCache, clientVersion: options.clientVersion ?? '2.0.0', }); const activePluginReleaseCounts = new Map(); const registerActivePluginReleases = (releaseIds: readonly string[]): (() => Promise) => { const uniqueReleaseIds = [...new Set(releaseIds)]; for (const releaseId of uniqueReleaseIds) { const count = activePluginReleaseCounts.get(releaseId) ?? 0; if (count === 0) packageStore.registerActiveWorker(releaseId); activePluginReleaseCounts.set(releaseId, count + 1); } let released = false; return async () => { if (released) return; released = true; const cleanup: Array> = []; for (const releaseId of uniqueReleaseIds) { const count = activePluginReleaseCounts.get(releaseId) ?? 0; if (count <= 1) { activePluginReleaseCounts.delete(releaseId); cleanup.push(packageStore.releaseActiveWorker(releaseId)); } else { activePluginReleaseCounts.set(releaseId, count - 1); } } await Promise.all(cleanup); }; }; let effectiveResolver: EffectivePluginResolver | undefined; 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, pluginSkillSources, getPluginSkillSources: async () => effectiveResolver ? (await effectiveResolver.getSkillSources()).map((source) => ({ id: source.id, pluginId: source.pluginId, directory: source.directory, entryPath: source.entryPath, })) : [], }); 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: [], }); let runtime: PiConversationRuntime | undefined; let plugins: CodingProjectPluginService | undefined; 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 plugins?.deactivate(project.path); 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([ plugins?.deactivate(project.path), options.browser.close(project.path), ...conversations.map(({ id }) => runtime?.dispose(id, reason)), ]); }, }); const dataService = createDataServiceOperations({ projects }); const dataServiceAdapter = createDataServicePluginAdapter(dataService); const gameResourceAdapter = createGameResourcePluginAdapter({ client: new GameResourceClient(), marketplace: marketplaceClient, packageStore, makeloreVersion: options.clientVersion ?? '2.0.0', }); const policyClient = options.policyClient ?? new PluginPolicyClient(); const knownPluginIds = new Set(pluginDefinitions.map(({ id }) => id)); // Existing user Releases are discovered from the device index at startup; // unlike the bundled catalog they cannot be enumerated synchronously. Keep // the ProjectPluginService's known-ID gate accurate for a reopened app while // preserving unknown IDs in existing project files. void packageStore.readInstalledIndex() .then((records) => records.forEach(({ pluginId }) => knownPluginIds.add(pluginId))) .catch(() => undefined); const projectPlugins = createProjectPluginService({ knownPluginIds: () => [...knownPluginIds], onManagedInputsChanged: async ({ projectPath }) => { runtime?.markResourcesStale(); const conversations = await conversationStoreForProject(projectPath).read() .then((file) => file.conversations) .catch(() => []); for (const conversation of conversations) registry.forget(conversation.id); }, onAdapterDeactivated: async ({ projectPath, pluginId }) => { previewDataSession?.invalidate('project_deactivated'); await plugins?.deactivate(projectPath, pluginId); }, }); effectiveResolver = createEffectivePluginResolver({ definitions: pluginDefinitions, packageStore, marketplace: marketplaceClient, accountCache, getAccountBinding: () => marketplaceClient.getCurrentAccountBinding(), getEnabledPluginIds: (projectPath) => projectPlugins.getEnabledPluginIds(projectPath), policyClient, }); const capabilityRegistry = createCodingCapabilityRegistry({ policyClient, projectPlugins, adapters: [dataServiceAdapter, gameResourceAdapter], definitions: pluginDefinitions, effectiveResolver, getDurableProjectId: async (projectPath, localProjectId) => { const active = await projects.requireActiveRealProjectWithIdentity(projectPath); if (active.project.id !== localProjectId) { throw new Error('The trusted coding project does not match the active project'); } return active.projectId; }, }); productTools.configureCapabilityRegistry(capabilityRegistry); plugins = createCodingProjectPluginService({ projects, projectPlugins, policyClient, adapters: [dataServiceAdapter, gameResourceAdapter], definitions: pluginDefinitions, effectiveResolver, getDefinitions: async () => { const installed = await packageStore.readInstalledIndex(); const definitions = await Promise.all(installed.map(async ({ pluginId }) => { const release = await packageStore.getInstalled(pluginId); return release?.definition ?? null; })); return definitions.flatMap((definition) => definition ? [definition] : []); }, }); 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, getSkillRoots: async () => (effectiveResolver ? (await effectiveResolver.getSkillSources()).map(({ packageRoot }) => packageRoot) : []), registerActivePluginReleases, loadProviderInput, resolveCredential: resolvePiProviderCredentialFromSecretStore, ...(getLocalProxyCredential ? { getLocalProxyCredential: async () => getLocalProxyCredential() } : {}), extensionHost, capabilityRegistry, }), }); 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() } : {}), capabilityRegistry, getSkillRoots: async () => (effectiveResolver ? (await effectiveResolver.getSkillSources()).map(({ packageRoot }) => packageRoot) : []), }); const subagents = new PiSubagentScheduler({ openChild: childOpener, processBudget, reclaimProcessCapacity: (signal) => workerPool.reclaimIdleWorker(signal), }); 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 conversations = new CodingConversationService(projects, runtime, { archiveSession: async ({ projectId, sessionKey }) => { await archivePiConversationSession({ userDataDir: options.paths.userDataDir, projectId, sessionKey, }); }, }); const invalidateManagedResources = async (): Promise => { runtime?.markResourcesStale(); const projectPaths = [...conversationStores.entries()]; for (const [, store] of projectPaths) { const conversationsInProject = await store.read() .then((file) => file.conversations) .catch(() => []); for (const conversation of conversationsInProject) registry.forget(conversation.id); } }; const pluginMarketplace: CodingPluginMarketplaceService = createCodingPluginMarketplaceService({ marketplace: marketplaceClient, packageStore, clientVersion: options.clientVersion ?? '2.0.0', onChanged: async ({ pluginId, kind }) => { if (kind === 'install' || kind === 'update') knownPluginIds.add(pluginId); await invalidateManagedResources(); }, }); const unsubscribeMarketplaceSession = subscribeWorksSquareSession(() => { void invalidateManagedResources(); }); const host = createCodingProductHost({ projects, productTools, getEnabledPluginIds: (projectPath) => projectPlugins.getEnabledPluginIds(projectPath), effectiveResolver, listPiCommands: (conversationId) => conversations.listLiveCommands(conversationId), }); previewDataSession = createPreviewDataSessionManager({ projects }); if (typeof options.browser.configurePreviewDataSession === 'function') { options.browser.configurePreviewDataSession(previewDataSession); } const unsubscribeBrowserLifecycle = typeof options.browser.subscribeLifecycle === 'function' ? options.browser.subscribeLifecycle((event) => { previewDataSession?.handleAgentBrowserLifecycle(event); }) : () => undefined; return { attachments, dataService, plugins, pluginMarketplace, marketplace: pluginMarketplace, previewDataSession, productTools, projects, conversations, runtime, host, async sleep(reason) { if (reason === 'background_sleep' && runtime.hasActiveWork()) return; if (reason === 'auth_cleanup') { const active = await projects.getActiveProject(); if (active) await plugins?.deactivate(active.path); } const conversationIds = runtime.getDiagnostics().workers.map((worker) => worker.conversationId); await Promise.allSettled(conversationIds.map((conversationId) => ( runtime.dispose(conversationId, reason) ))); }, async shutdown() { previewDataSession?.dispose(); if (typeof options.browser.configurePreviewDataSession === 'function') { options.browser.configurePreviewDataSession(undefined); } unsubscribeBrowserLifecycle(); const active = await projects.getActiveProject(); if (active) await plugins?.deactivate(active.path); try { await subagents.close(); await runtime.shutdown(); } finally { unsubscribeMarketplaceSession(); packageStore.dispose(); marketplaceClient.dispose(); } }, }; }