feat(coding): compose plugin host lifecycle

This commit is contained in:
2026-08-27 18:02:10 +08:00
parent a92cd904d3
commit a18727ecf8
9 changed files with 985 additions and 34 deletions

View File

@@ -35,6 +35,17 @@ 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,
type CodingCapabilityRegistry,
} from '../coding-plugins/registry';
import { createDataServicePluginAdapter } from '../coding-plugins/adapters/data-service';
import { PluginPolicyClient } from '../services/plugin-policy-client';
import {
createCodingProjectPluginService,
type CodingProjectPluginService,
} from './coding-product-services';
export interface CodingCompositionPaths {
executablePath: string;
@@ -106,6 +117,85 @@ export function createCodingComposition(
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 policyClient = new PluginPolicyClient();
const projectPlugins = createProjectPluginService({
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);
},
});
const capabilityRegistry = createCodingCapabilityRegistry({
policyClient,
projectPlugins,
adapters: [dataServiceAdapter],
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;
},
});
const refreshingCapabilityRegistry: CodingCapabilityRegistry = {
async resolveWorkerResources(input) {
await policyClient.refresh();
return await capabilityRegistry.resolveWorkerResources(input);
},
async invoke(input) {
return await capabilityRegistry.invoke(input);
},
};
productTools.configureCapabilityRegistry(refreshingCapabilityRegistry);
plugins = createCodingProjectPluginService({
projects,
projectPlugins,
policyClient,
adapters: [dataServiceAdapter],
});
const workerPool = new PiWorkerPool({
processBudget,
revisionCoordinator: revisions,
@@ -121,6 +211,7 @@ export function createCodingComposition(
? { getLocalProxyCredential: async () => getLocalProxyCredential() }
: {}),
extensionHost,
capabilityRegistry: refreshingCapabilityRegistry,
}),
});
const childOpener = createPiManagedSubagentChildOpener({
@@ -136,13 +227,14 @@ export function createCodingComposition(
...(getLocalProxyCredential
? { getLocalProxyCredential: async () => getLocalProxyCredential() }
: {}),
capabilityRegistry: refreshingCapabilityRegistry,
});
const subagents = new PiSubagentScheduler({
openChild: childOpener,
processBudget,
reclaimProcessCapacity: (signal) => workerPool.reclaimIdleWorker(signal),
});
const runtime = new PiConversationRuntime({
runtime = new PiConversationRuntime({
pool: workerPool,
registry,
extensionHost,
@@ -165,37 +257,6 @@ export function createCodingComposition(
? { 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({
@@ -210,8 +271,6 @@ export function createCodingComposition(
productTools,
listPiCommands: (conversationId) => conversations.listLiveCommands(conversationId),
});
const dataService = createDataServiceOperations({ projects });
productTools.configureDataService(dataService);
previewDataSession = createPreviewDataSessionManager({ projects });
if (typeof options.browser.configurePreviewDataSession === 'function') {
options.browser.configurePreviewDataSession(previewDataSession);
@@ -224,6 +283,7 @@ export function createCodingComposition(
return {
attachments,
dataService,
plugins,
previewDataSession,
productTools,
projects,
@@ -232,6 +292,10 @@ export function createCodingComposition(
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)
@@ -243,6 +307,8 @@ export function createCodingComposition(
options.browser.configurePreviewDataSession(undefined);
}
unsubscribeBrowserLifecycle();
const active = await projects.getActiveProject();
if (active) await plugins?.deactivate(active.path);
await subagents.close();
await runtime.shutdown();
},