import { Emitter, type Event } from '#/_base/event'; import { UserFileSkillSource } from '#/features/skill/catalog/userFileSkillSource'; import { FileProjectLocalConfigService } from '#/persistence/backends/node-fs/projectLocalConfigService'; import type { RuntimeBinding, RuntimeLease } from '#/runtime/runtime'; import { RuntimeError, type RuntimeGenerationSnapshot, type RuntimeRegistry, type RuntimeRegistryChange } from '#/runtime/runtimeRegistry'; import type { SessionLifecycleService } from '#/workspace/sessionLifecycle/sessionLifecycleService'; import { WorkspaceStateService } from '#/workspace/state/workspaceStateService'; import type { IWorkspaceStateService } from '#/workspace/state/workspaceState'; import type { IWorkspaceContext } from '#/workspace/workspaceContext/workspaceContext'; import type { IWorkspaceDirs } from '#/workspace/workspaceDirs/workspaceDirs'; import { WorkspaceDirsService } from '#/workspace/workspaceDirs/workspaceDirsService'; import type { IWorkspaceFsService } from '#/workspace/workspaceFs/fs'; import { WorkspaceFsService } from '#/workspace/workspaceFs/fsService'; import type { IWorkspaceGitService } from '#/workspace/workspaceGit/workspaceGit'; import { WorkspaceGitService } from '#/workspace/workspaceGit/workspaceGitService'; import type { IWorkspaceInstructionsService } from '#/workspace/workspaceInstructions/workspaceInstructions'; import { WorkspaceInstructionsService } from '#/workspace/workspaceInstructions/workspaceInstructionsService'; import type { IWorkspaceMcpService } from '#/workspace/workspaceMcp/workspaceMcp'; import { WorkspaceMcpService } from '#/workspace/workspaceMcp/workspaceMcpService'; import type { IWorkspaceMcpConfigService } from '#/workspace/workspaceMcpConfig/workspaceMcpConfig'; import { WorkspaceMcpConfigService } from '#/workspace/workspaceMcpConfig/workspaceMcpConfigService'; import type { IWorkspaceTrust } from '#/workspace/workspaceTrust/workspaceTrust'; import { WorkspaceTrustService } from '#/workspace/workspaceTrust/workspaceTrustService'; import type { IExtraAgentProfileLoader } from '#/workspace/workspaceAgentProfileLoader/extraAgentProfileLoader'; import { ExtraAgentProfileLoaderService } from '#/workspace/workspaceAgentProfileLoader/extraAgentProfileLoaderService'; import type { IExplicitAgentProfileLoader } from '#/workspace/workspaceAgentProfileLoader/explicitAgentProfileLoader'; import { ExplicitAgentProfileLoaderService } from '#/workspace/workspaceAgentProfileLoader/explicitAgentProfileLoaderService'; import type { IPluginAgentProfileLoader } from '#/workspace/workspaceAgentProfileLoader/pluginAgentProfileLoader'; import { PluginAgentProfileLoaderService } from '#/workspace/workspaceAgentProfileLoader/pluginAgentProfileLoaderService'; import type { IUserAgentProfileLoader } from '#/workspace/workspaceAgentProfileLoader/userAgentProfileLoader'; import { UserAgentProfileLoaderService } from '#/workspace/workspaceAgentProfileLoader/userAgentProfileLoaderService'; import type { IWorkspaceAgentProfileLoader } from '#/workspace/workspaceAgentProfileLoader/workspaceAgentProfileLoader'; import { WorkspaceAgentProfileLoaderService } from '#/workspace/workspaceAgentProfileLoader/workspaceAgentProfileLoaderService'; import { ExplicitFileSkillSource } from '#/features/skill/workspace/explicitFileSkillSource'; import { ExtraFileSkillSource } from '#/features/skill/workspace/extraFileSkillSource'; import { PluginSkillSource } from '#/features/skill/workspace/pluginSkillSource'; import { WorkspaceRootSkillSource } from '#/features/skill/workspace/rootFileSkillSource'; import { RuntimeSkillDiscovery } from '#/features/skill/workspace/runtimeSkillDiscovery'; import type { IWorkspaceSkillCatalog } from '#/features/skill/workspace/workspaceSkillCatalog'; import { WorkspaceSkillCatalogService } from '#/features/skill/workspace/workspaceSkillCatalogService'; import type { IRuntimeResolver } from '#/workspace/workspaceInstance/workspaceInstanceManager'; import type { ProgramDependencies } from './programDependencies'; export type ProgramStatus = 'preparing' | 'ready' | 'degraded'; export interface ProgramCatalogSnapshot { readonly skills: { readonly total: number; readonly invocable: number; readonly skipped: number; }; readonly agentProfiles: number; readonly mcpServers: number; } export interface ProgramSourceProvenanceSnapshot { readonly skills: readonly { readonly source: string; readonly count: number; }[]; readonly skillRoots: readonly string[]; readonly agentProfiles: readonly { readonly sourceId: string; readonly priority: number; readonly profiles: readonly string[]; }[]; readonly instructionPaths: readonly string[]; readonly mcpServers: readonly string[]; } export interface ProgramSnapshot { readonly workspaceId: string; readonly binding: RuntimeBinding; readonly status: ProgramStatus; readonly ready: boolean; readonly generation?: string; readonly trusted?: boolean; readonly catalog: ProgramCatalogSnapshot; readonly sources: ProgramSourceProvenanceSnapshot; readonly runtimes: readonly RuntimeGenerationSnapshot[]; } interface ProgramGeneration { readonly id: string; readonly lease: RuntimeLease; readonly state: IWorkspaceStateService; readonly dirs: IWorkspaceDirs; readonly fs: IWorkspaceFsService; readonly git: IWorkspaceGitService; readonly instructions: IWorkspaceInstructionsService; readonly mcpConfig: IWorkspaceMcpConfigService; readonly mcp: IWorkspaceMcpService; readonly trust: IWorkspaceTrust; readonly skills: IWorkspaceSkillCatalog; readonly agentProfiles: IWorkspaceAgentProfileLoader; readonly userAgentProfiles: IUserAgentProfileLoader; readonly pluginAgentProfiles: IPluginAgentProfileLoader; readonly explicitAgentProfiles: IExplicitAgentProfileLoader; readonly extraAgentProfiles: IExtraAgentProfileLoader; readonly disposables: readonly { dispose(): void | Promise }[]; ready: boolean; failed: boolean; references: number; retired: boolean; } const PROGRAM_CAPABILITIES = ['fs', 'process'] as const; export class Program { readonly binding: RuntimeBinding; private currentStatus: ProgramStatus = 'preparing'; private readonly changeEmitter = new Emitter(); readonly onDidChange: Event = this.changeEmitter.event; private readonly registrySubscription; private readonly resolver: IRuntimeResolver; private generation?: ProgramGeneration; private generationFailed = false; private disposed = false; private resolveReady?: () => void; readonly ready = new Promise((resolve) => { this.resolveReady = resolve; }); constructor( readonly workspaceId: string, private readonly runtimes: RuntimeRegistry, private readonly context: IWorkspaceContext, private readonly dependencies: ProgramDependencies, ) { this.binding = Object.freeze({ workspaceId, runtimeId: 'local' }); this.resolver = { _serviceBrand: undefined, inspect: (binding) => this.runtimes.inspect(binding), acquire: (binding, required) => this.runtimes.acquire(binding, required), }; this.registrySubscription = runtimes.onDidChange((change) => this.onRuntimeChange(change)); this.reconcileGeneration(); } get status(): ProgramStatus { return this.currentStatus; } get state(): IWorkspaceStateService { return this.requireGeneration().state; } get dirs(): IWorkspaceDirs { return this.requireGeneration().dirs; } get fs(): IWorkspaceFsService { return this.requireGeneration().fs; } get git(): IWorkspaceGitService { return this.requireGeneration().git; } get instructions(): IWorkspaceInstructionsService { return this.requireGeneration().instructions; } get mcpConfig(): IWorkspaceMcpConfigService { return this.requireGeneration().mcpConfig; } get mcp(): IWorkspaceMcpService { return this.requireGeneration().mcp; } get trust(): IWorkspaceTrust { return this.requireGeneration().trust; } get skills(): IWorkspaceSkillCatalog { return this.requireGeneration().skills; } get agentProfiles(): IWorkspaceAgentProfileLoader { return this.requireGeneration().agentProfiles; } get sessionControllerGeneration(): string { return this.requireGeneration().id; } createSessionController(): SessionLifecycleService { const generation = this.requireGeneration(); generation.references += 1; let released = false; const release = (): void => { if (released) return; released = true; this.releaseGeneration(generation); }; try { const runtime = generation.lease.runtime; return this.dependencies.createSessionController({ context: this.context, fs: runtime.fs!, workspaceAgentProfiles: generation.agentProfiles, extraAgentProfiles: generation.extraAgentProfiles, explicitAgentProfiles: generation.explicitAgentProfiles, userAgentProfiles: generation.userAgentProfiles, pluginAgentProfiles: generation.pluginAgentProfiles, dirs: generation.dirs, skills: generation.skills, instructions: generation.instructions, mcp: generation.mcp, onDispose: release, }); } catch (error) { release(); throw error; } } snapshot(): ProgramSnapshot { const generation = this.generation; const skills = generation?.skills.catalog.listSkills() ?? []; const skillsBySource = new Map(); for (const skill of skills) { skillsBySource.set(skill.source, (skillsBySource.get(skill.source) ?? 0) + 1); } const agentProfiles = this.dependencies.agentProfiles.entries() .filter((entry) => entry.workspaceKey === undefined || entry.workspaceKey === this.workspaceId) .map((entry) => ({ sourceId: entry.sourceId, priority: entry.priority, profiles: entry.contribution.profiles.map((profile) => profile.name), })); const mcpServers = Object.keys(generation?.mcpConfig.servers() ?? {}); return { workspaceId: this.workspaceId, binding: this.binding, status: this.currentStatus, ready: generation?.ready === true, generation: generation?.id, trusted: generation?.trust.isTrusted(), catalog: { skills: { total: skills.length, invocable: generation?.skills.catalog.listInvocableSkills().length ?? 0, skipped: generation?.skills.catalog.getSkippedByPolicy().length ?? 0, }, agentProfiles: agentProfiles.reduce((total, source) => total + source.profiles.length, 0), mcpServers: mcpServers.length, }, sources: { skills: [...skillsBySource].map(([source, count]) => ({ source, count })), skillRoots: generation?.skills.catalog.getSkillRoots() ?? [], agentProfiles, instructionPaths: generation?.instructions.snapshot.agentsMdPaths ?? [], mcpServers, }, runtimes: this.runtimes.snapshot().runtimes, }; } dispose(): void { if (this.disposed) return; this.disposed = true; this.registrySubscription.dispose(); const generation = this.generation; this.generation = undefined; if (generation !== undefined) this.retireGeneration(generation); this.changeEmitter.dispose(); } private requireGeneration(): ProgramGeneration { if (this.generation === undefined) throw new Error(`program ${this.workspaceId} has no available local runtime generation`); return this.generation; } private onRuntimeChange(change: RuntimeRegistryChange): void { if (change.runtimeId !== 'local' || this.disposed) return; this.reconcileGeneration(); } private reconcileGeneration(): void { const local = this.runtimes.current('local'); if (local === undefined) { const previous = this.generation; this.generation = undefined; if (previous !== undefined) this.retireGeneration(previous); this.refresh(); return; } if (this.generation?.id !== local.identity.generation) { const previous = this.generation; this.generationFailed = false; try { const next = this.createGeneration(); this.generation = next; if (previous !== undefined) this.retireGeneration(previous); this.observeReadiness(next); } catch (error) { if (!(error instanceof RuntimeError && error.code === 'runtime.unavailable')) { this.generationFailed = true; this.resolveProgramReady(); } } } this.refresh(); } private createGeneration(): ProgramGeneration { const lease = this.resolver.acquire(this.binding, PROGRAM_CAPABILITIES); const runtime = lease.runtime; const disposables: { dispose(): void | Promise }[] = []; const own = }>(value: T): T => { disposables.push(value); return value; }; try { const state = own(new WorkspaceStateService(this.dependencies.appState)); const localConfig = new FileProjectLocalConfigService(this.dependencies.bootstrap, runtime.fs!); const dirs = own(new WorkspaceDirsService(this.context, localConfig, this.dependencies.log, state)); const git = new WorkspaceGitService(this.context, this.dependencies.git); const fs = new WorkspaceFsService(this.context, dirs, runtime.fs!, this.resolver, this.dependencies.telemetry, git); const instructions = own(new WorkspaceInstructionsService(this.context, runtime.fs!, runtime.environment, this.dependencies.bootstrap, this.dependencies.log, state)); const trust = own(new WorkspaceTrustService(this.context, this.dependencies.docs, state, this.dependencies.telemetry)); const mcpConfig = own(new WorkspaceMcpConfigService(this.context, this.dependencies.bootstrap, this.dependencies.plugins, this.dependencies.log, this.dependencies.config, runtime.fs!, trust, this.dependencies.configStore)); const mcp = own(new WorkspaceMcpService(this.context, this.resolver, mcpConfig, this.dependencies.oauth, this.dependencies.log, this.dependencies.telemetry, this.dependencies.identity, this.dependencies.sessionManager)); const userAgentProfiles = own(new UserAgentProfileLoaderService(this.dependencies.bootstrap, runtime.fs!, this.dependencies.log, this.dependencies.builtinAgentProfiles, this.context, this.dependencies.agentProfiles)); const pluginAgentProfiles = own(new PluginAgentProfileLoaderService(this.dependencies.plugins, runtime.fs!, this.dependencies.log, userAgentProfiles, this.context, this.dependencies.agentProfiles)); const explicitAgentProfiles = own(new ExplicitAgentProfileLoaderService(this.context, this.dependencies.bootstrap, runtime.fs!, this.dependencies.log, userAgentProfiles, this.dependencies.agentProfiles)); const extraAgentProfiles = own(new ExtraAgentProfileLoaderService(this.dependencies.config, this.context, this.dependencies.bootstrap, runtime.fs!, this.dependencies.log, userAgentProfiles, this.dependencies.agentProfiles)); const agentProfiles = own(new WorkspaceAgentProfileLoaderService(this.context, runtime.fs!, this.dependencies.log, userAgentProfiles, this.dependencies.agentProfiles)); const skillDiscovery = new RuntimeSkillDiscovery(this.dependencies.log, runtime.fs!); const userSkills = own(new UserFileSkillSource(skillDiscovery, this.dependencies.bootstrap, this.dependencies.config)); const explicitSkills = new ExplicitFileSkillSource(skillDiscovery, this.context, this.dependencies.bootstrap); const extraSkills = own(new ExtraFileSkillSource(skillDiscovery, this.dependencies.config, this.context, this.dependencies.bootstrap)); const workspaceSkills = own(new WorkspaceRootSkillSource(skillDiscovery, this.context, this.dependencies.config, this.dependencies.bootstrap)); const pluginSkills = new PluginSkillSource(skillDiscovery, this.dependencies.plugins); const skills = own(new WorkspaceSkillCatalogService(this.dependencies.builtinSkills, userSkills, explicitSkills, extraSkills, workspaceSkills, pluginSkills, state)); return { id: runtime.identity.generation, lease, state, dirs, fs, git, instructions, mcpConfig, mcp, trust, skills, agentProfiles, userAgentProfiles, pluginAgentProfiles, explicitAgentProfiles, extraAgentProfiles, disposables, ready: false, failed: false, references: 1, retired: false, }; } catch (error) { for (const disposable of disposables.reverse()) void disposable.dispose(); lease.dispose(); throw error; } } private observeReadiness(generation: ProgramGeneration): void { void Promise.all([ readiness(generation.dirs), readiness(generation.instructions), readiness(generation.mcpConfig), readiness(generation.mcp), readiness(generation.skills), readiness(generation.agentProfiles), ]).then( () => { if (this.generation !== generation) return; generation.ready = true; this.resolveProgramReady(); this.refresh(); }, () => { if (this.generation !== generation) return; generation.failed = true; this.resolveProgramReady(); this.refresh(); }, ); } private retireGeneration(generation: ProgramGeneration): void { if (generation.retired) return; generation.retired = true; this.releaseGeneration(generation); } private releaseGeneration(generation: ProgramGeneration): void { generation.references -= 1; if (generation.references !== 0 || !generation.retired) return; for (const disposable of [...generation.disposables].reverse()) void disposable.dispose(); generation.lease.dispose(); } private resolveProgramReady(): void { this.resolveReady?.(); this.resolveReady = undefined; } private refresh(): void { const local = this.runtimes.current('local'); if (local === undefined || local.status === 'connecting') this.currentStatus = 'preparing'; else if (this.generationFailed || this.generation?.failed === true) this.currentStatus = 'degraded'; else if (this.generation?.ready !== true) this.currentStatus = this.generation === undefined && local.status !== 'ready' ? 'degraded' : 'preparing'; else this.currentStatus = local.status === 'ready' ? 'ready' : 'degraded'; this.changeEmitter.fire(this.snapshot()); } } function readiness(value: unknown): Promise { const ready = (value as { readonly ready?: unknown }).ready; return ready !== null && typeof ready === 'object' && 'then' in ready ? Promise.resolve(ready as PromiseLike).then(() => {}) : Promise.resolve(); }