import { accessSync, constants } from 'node:fs'; import path from 'node:path'; import type { AgentBrowserModule } from '../agent-browser'; import type { AgentBrowserSnapshot } from '../../shared/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 { PiProjectWriteLeaseCoordinator } from '../coding-runtime/pi/write-lease'; 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 { createDataServiceOperations } from '../services/data-service-client'; import { createPreviewDataSessionManager, type PreviewDataSessionManager } from '../services/preview-data-session'; import { archivePiConversationSession, getPiManagedPaths, } from '../coding-runtime/pi/resource-loader'; 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 { GameResourceDeliveryCoordinator, GameResourceDeliveryReceiptStore, } from '../services/game-resource-delivery'; 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'; import { CODE_OWNED_OPTIONAL_BUNDLED_RELEASES } from '../../shared/coding-plugins'; import { ModelToolRegistry } from '../coding-runtime/pi/model-tools/model-tool-registry'; import { DevicePackageManager } from '../coding-packages/device-package-manager'; import { DevicePackageTools } from '../coding-packages/device-package-tools'; export interface CodingCompositionPaths { executablePath: string; cliPath: string; serverPath: string; npmCliPath?: 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; requestAgentBrowserPresentation?(snapshot: AgentBrowserSnapshot): void; publishAgentBrowserState?(snapshot: AgentBrowserSnapshot): 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'), npmCliPath: input.isPackaged ? path.join(input.resourcesPath, 'publish-runtime', 'bin', 'npm-cli.js') : path.join(input.appPath, 'node_modules', 'npm', 'bin', 'npm-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; let invalidateDeviceResources = async (): Promise => undefined; const projectStore = options.projectStore ?? createCodingProjectStore(options.storage); const attachments = new CodingAttachmentStore( path.join(options.paths.userDataDir, 'coding-runtime', 'attachments'), ); const modelToolRegistry = new ModelToolRegistry(); const devicePackageManager = new DevicePackageManager({ rootDir: path.join(options.paths.userDataDir, 'coding-runtime', 'device-packages'), executablePath: options.paths.executablePath, cliPath: options.paths.cliPath, ...(options.paths.npmCliPath ? { npmCliPath: options.paths.npmCliPath } : {}), onGenerationChanged: async () => await invalidateDeviceResources(), }); const devicePackageTools = new DevicePackageTools(devicePackageManager); const projectWriteLeases = new PiProjectWriteLeaseCoordinator(); const productTools = new PiProductTools({ browser: options.browser, attachments, bundledSkillsDir: options.paths.bundledSkillsDir, modelToolRegistry, devicePackageTools, requestAgentBrowserPresentation: options.requestAgentBrowserPresentation, publishAgentBrowserState: options.publishAgentBrowserState, 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(projectWriteLeases); 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: [], }); let runtime: PiConversationRuntime | undefined; let plugins: CodingProjectPluginService | undefined; let previewDataSession: PreviewDataSessionManager | undefined; 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); }, 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 gameResourceClient = new GameResourceClient(); const gameResourceDelivery = new GameResourceDeliveryCoordinator({ client: gameResourceClient, receipts: new GameResourceDeliveryReceiptStore(path.join( options.paths.userDataDir, 'coding-runtime', 'game-resource', 'receipts.json', )), leases: projectWriteLeases, recordTouchedPaths: async (conversationId, runId, paths) => { await productTools.recordTouchedPaths(conversationId, runId, paths); }, ...(options.acquireBackgroundLease ? { acquireBackgroundLease: options.acquireBackgroundLease } : {}), }); const gameResourceAdapter = createGameResourcePluginAdapter({ client: gameResourceClient, delivery: gameResourceDelivery, marketplace: marketplaceClient, packageStore, makeloreVersion: options.clientVersion ?? '2.0.0', bundledReleases: CODE_OWNED_OPTIONAL_BUNDLED_RELEASES, }); 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, 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, getSkillRoots: async () => (effectiveResolver ? (await effectiveResolver.getSkillSources()).map(({ packageRoot }) => packageRoot) : []), registerActivePluginReleases, loadProviderInput, resolveCredential: resolvePiProviderCredentialFromSecretStore, createProcess: (processOptions) => agentServer.createWorker(processOptions), ...(getLocalProxyCredential ? { getLocalProxyCredential: async () => getLocalProxyCredential() } : {}), extensionHost, capabilityRegistry, modelToolRegistry, devicePackageManager, devicePackageTools: devicePackageTools.tools, }), }); 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, }); runtime = new PiConversationRuntime({ pool: workerPool, registry, extensionHost, subagentScheduler: subagents, resolveModel: async (model) => selectPiProviderModel( buildPiProviderCatalog(await loadProviderInput()), model, ), isAuthenticationError: isCodingProviderAuthenticationError, refreshCredential: refreshCodingProviderCredential, projectImage: async ({ data, mime }) => await attachments.put(Buffer.from(data, 'base64'), mime), 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 => { await runtime?.refreshResources(); 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); } }; invalidateDeviceResources = invalidateManagedResources; 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(); }, }); void gameResourceDelivery.resumePending().catch(() => undefined); const unsubscribeMarketplaceSession = subscribeWorksSquareSession(() => { void invalidateManagedResources(); void gameResourceDelivery.resumePending().catch(() => undefined); }); 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, devicePackages: devicePackageManager, 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) ))); if (reason === 'background_sleep' && runtime.hasActiveWork()) return; if (reason === 'background_sleep') { await options.browser.close().catch(() => undefined); } await agentServer.stop(); }, async shutdown() { conversations.dispose(); gameResourceDelivery.dispose(); 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 { try { await agentServer.stop(); } finally { unsubscribeMarketplaceSession(); packageStore.dispose(); marketplaceClient.dispose(); } } }, }; }