feat(coding): add core host api composition
This commit is contained in:
176
electron/api/coding-composition.ts
Normal file
176
electron/api/coding-composition.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
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 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 { createCodingProductHost, type CodingProductComposition } from './coding-product-services';
|
||||
|
||||
export interface CodingCompositionPaths {
|
||||
executablePath: string;
|
||||
cliPath: string;
|
||||
userDataDir: string;
|
||||
bundledSkillsDir: string;
|
||||
}
|
||||
|
||||
export interface CreateCodingCompositionOptions {
|
||||
storage: CodingProjectStorage;
|
||||
browser: AgentBrowserModule;
|
||||
paths: CodingCompositionPaths;
|
||||
localProxyCredential?: string;
|
||||
}
|
||||
|
||||
export function resolveCodingPiRuntimePaths(input: {
|
||||
isPackaged: boolean;
|
||||
resourcesPath: string;
|
||||
appPath: string;
|
||||
executablePath: string;
|
||||
}): Pick<CodingCompositionPaths, 'executablePath' | 'cliPath'> {
|
||||
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 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 registry = new PiSessionRegistry({ projectStore });
|
||||
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,
|
||||
...(options.localProxyCredential
|
||||
? { getLocalProxyCredential: async () => options.localProxyCredential }
|
||||
: {}),
|
||||
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,
|
||||
...(options.localProxyCredential
|
||||
? { getLocalProxyCredential: async () => options.localProxyCredential }
|
||||
: {}),
|
||||
});
|
||||
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,
|
||||
),
|
||||
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,
|
||||
};
|
||||
})),
|
||||
});
|
||||
const projects = new CodingProjectService(projectStore, {
|
||||
onResourcesChanged: async (project) => {
|
||||
runtime.markResourcesStale();
|
||||
const conversations = await createCodingConversationStore(project.path).read()
|
||||
.then((file) => file.conversations)
|
||||
.catch(() => []);
|
||||
for (const conversation of conversations) registry.forget(conversation.id);
|
||||
},
|
||||
onProjectDeactivated: async (project) => {
|
||||
const conversations = await createCodingConversationStore(project.path).read()
|
||||
.then((file) => file.conversations)
|
||||
.catch(() => []);
|
||||
await Promise.allSettled([
|
||||
options.browser.close(project.path),
|
||||
...conversations.map(({ id }) => runtime.dispose(id)),
|
||||
]);
|
||||
},
|
||||
});
|
||||
const conversations = new CodingConversationService(projects, runtime);
|
||||
const host = createCodingProductHost({
|
||||
projects,
|
||||
productTools,
|
||||
listPiCommands: (conversationId) => conversations.listLiveCommands(conversationId),
|
||||
});
|
||||
return {
|
||||
attachments,
|
||||
productTools,
|
||||
projects,
|
||||
conversations,
|
||||
runtime,
|
||||
host,
|
||||
async shutdown() {
|
||||
await subagents.close();
|
||||
await runtime.shutdown();
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user