feat(coding): add model tools and device packages
This commit is contained in:
@@ -46,8 +46,6 @@ import {
|
||||
import { createDataServicePluginAdapter } from '../coding-plugins/adapters/data-service';
|
||||
import { createGameResourcePluginAdapter } from '../coding-plugins/adapters/game-resource';
|
||||
import { GameResourceClient } from '../services/game-resource-client';
|
||||
import { createWebSearchPluginAdapter } from '../coding-plugins/adapters/web-search';
|
||||
import { WebSearchClient } from '../services/web-search-client';
|
||||
import { AccountPluginCache } from '../coding-plugins/account-plugin-cache';
|
||||
import {
|
||||
createMarketplaceClient,
|
||||
@@ -74,11 +72,15 @@ import {
|
||||
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;
|
||||
}
|
||||
@@ -139,7 +141,7 @@ export function resolveCodingPiRuntimePaths(input: {
|
||||
resourcesPath: string;
|
||||
appPath: string;
|
||||
executablePath: string;
|
||||
}): Pick<CodingCompositionPaths, 'executablePath' | 'cliPath' | 'serverPath'> {
|
||||
}): Pick<CodingCompositionPaths, 'executablePath' | 'cliPath' | 'serverPath' | 'npmCliPath'> {
|
||||
return {
|
||||
executablePath: resolvePiWorkerExecutablePath(input.executablePath),
|
||||
cliPath: input.isPackaged
|
||||
@@ -155,6 +157,9 @@ export function resolveCodingPiRuntimePaths(input: {
|
||||
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'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -210,14 +215,26 @@ export function createCodingComposition(
|
||||
};
|
||||
};
|
||||
let effectiveResolver: EffectivePluginResolver | undefined;
|
||||
let invalidateDeviceResources = async (): Promise<void> => 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 productTools = new PiProductTools({
|
||||
browser: options.browser,
|
||||
attachments,
|
||||
bundledSkillsDir: options.paths.bundledSkillsDir,
|
||||
modelToolRegistry,
|
||||
devicePackageTools,
|
||||
pluginSkillSources,
|
||||
getPluginSkillSources: async () => effectiveResolver
|
||||
? (await effectiveResolver.getSkillSources()).map((source) => ({
|
||||
@@ -292,13 +309,6 @@ export function createCodingComposition(
|
||||
makeloreVersion: options.clientVersion ?? '2.0.0',
|
||||
bundledReleases: CODE_OWNED_OPTIONAL_BUNDLED_RELEASES,
|
||||
});
|
||||
const webSearchAdapter = createWebSearchPluginAdapter({
|
||||
client: new WebSearchClient(),
|
||||
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;
|
||||
@@ -334,7 +344,7 @@ export function createCodingComposition(
|
||||
const capabilityRegistry = createCodingCapabilityRegistry({
|
||||
policyClient,
|
||||
projectPlugins,
|
||||
adapters: [dataServiceAdapter, gameResourceAdapter, webSearchAdapter],
|
||||
adapters: [dataServiceAdapter, gameResourceAdapter],
|
||||
definitions: pluginDefinitions,
|
||||
effectiveResolver,
|
||||
getDurableProjectId: async (projectPath, localProjectId) => {
|
||||
@@ -350,7 +360,7 @@ export function createCodingComposition(
|
||||
projects,
|
||||
projectPlugins,
|
||||
policyClient,
|
||||
adapters: [dataServiceAdapter, gameResourceAdapter, webSearchAdapter],
|
||||
adapters: [dataServiceAdapter, gameResourceAdapter],
|
||||
definitions: pluginDefinitions,
|
||||
effectiveResolver,
|
||||
getDefinitions: async () => {
|
||||
@@ -385,6 +395,9 @@ export function createCodingComposition(
|
||||
: {}),
|
||||
extensionHost,
|
||||
capabilityRegistry,
|
||||
modelToolRegistry,
|
||||
devicePackageManager,
|
||||
devicePackageTools: devicePackageTools.tools,
|
||||
}),
|
||||
});
|
||||
const childOpener = createPiManagedSubagentChildOpener({
|
||||
@@ -442,7 +455,7 @@ export function createCodingComposition(
|
||||
},
|
||||
});
|
||||
const invalidateManagedResources = async (): Promise<void> => {
|
||||
runtime?.markResourcesStale();
|
||||
await runtime?.refreshResources();
|
||||
const projectPaths = [...conversationStores.entries()];
|
||||
for (const [, store] of projectPaths) {
|
||||
const conversationsInProject = await store.read()
|
||||
@@ -451,6 +464,7 @@ export function createCodingComposition(
|
||||
for (const conversation of conversationsInProject) registry.forget(conversation.id);
|
||||
}
|
||||
};
|
||||
invalidateDeviceResources = invalidateManagedResources;
|
||||
const pluginMarketplace: CodingPluginMarketplaceService = createCodingPluginMarketplaceService({
|
||||
marketplace: marketplaceClient,
|
||||
packageStore,
|
||||
@@ -482,6 +496,7 @@ export function createCodingComposition(
|
||||
return {
|
||||
attachments,
|
||||
dataService,
|
||||
devicePackages: devicePackageManager,
|
||||
plugins,
|
||||
pluginMarketplace,
|
||||
marketplace: pluginMarketplace,
|
||||
|
||||
@@ -47,6 +47,7 @@ import type {
|
||||
} from '../coding-plugins/package-store';
|
||||
import type { MarketplaceLibrarySnapshot } from '../coding-plugins/account-plugin-cache';
|
||||
import type { EffectivePluginResolver } from '../coding-plugins/effective-resolver';
|
||||
import type { DevicePackageManager } from '../coding-packages/device-package-manager';
|
||||
|
||||
export interface ActiveCodingProject {
|
||||
id: string;
|
||||
@@ -66,6 +67,7 @@ export interface CodingProductHost {
|
||||
export interface CodingProductComposition {
|
||||
attachments: CodingAttachmentStore;
|
||||
dataService: DataServiceOperations;
|
||||
devicePackages: DevicePackageManager;
|
||||
previewDataSession?: PreviewDataSessionManager;
|
||||
productTools: PiProductTools;
|
||||
pluginMarketplace: CodingPluginMarketplaceService;
|
||||
|
||||
@@ -22,6 +22,7 @@ import { handleCodingProjectRoutes } from './routes/coding-projects';
|
||||
import { handleCodingConversationRoutes } from './routes/coding-conversations';
|
||||
import { handleCodingPluginRoutes } from './routes/coding-plugins';
|
||||
import { handlePluginMarketplaceRoutes } from './routes/plugin-marketplace';
|
||||
import { handleDevicePackageRoutes } from './routes/device-packages';
|
||||
|
||||
export type HostApiRouteHandler = (
|
||||
req: IncomingMessage,
|
||||
@@ -51,6 +52,7 @@ export const hostApiRouteHandlers: readonly HostApiRouteHandler[] = [
|
||||
handleCodingAttachmentRoutes,
|
||||
handleCodingProjectRoutes,
|
||||
handlePluginMarketplaceRoutes,
|
||||
handleDevicePackageRoutes,
|
||||
handleCodingPluginRoutes,
|
||||
handleCodingConversationRoutes,
|
||||
handleCodingFileRoutes,
|
||||
|
||||
112
electron/api/routes/device-packages.ts
Normal file
112
electron/api/routes/device-packages.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||
import { DevicePackageError } from '../../coding-packages/device-package-manager';
|
||||
import type { HostApiContext } from '../context';
|
||||
import { parseJsonBody, sendJson } from '../route-utils';
|
||||
|
||||
const ROOT = '/api/coding/device-packages';
|
||||
const PACKAGE = /^\/api\/coding\/device-packages\/([^/]+)$/u;
|
||||
const PACKAGE_ID = /^[a-z0-9][a-z0-9._-]{0,127}$/u;
|
||||
|
||||
class DevicePackageRouteError extends Error {
|
||||
constructor(readonly status: 400 | 404, message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
function packageId(value: string): string {
|
||||
try {
|
||||
const decoded = decodeURIComponent(value);
|
||||
if (PACKAGE_ID.test(decoded)) return decoded;
|
||||
} catch {
|
||||
// Project a single bounded request error below.
|
||||
}
|
||||
throw new DevicePackageRouteError(400, 'Device package id is invalid');
|
||||
}
|
||||
|
||||
function noQuery(url: URL): void {
|
||||
if (url.search) throw new DevicePackageRouteError(400, 'Query parameters are not supported');
|
||||
}
|
||||
|
||||
async function exactBody(req: IncomingMessage, keys: readonly string[]): Promise<Record<string, unknown>> {
|
||||
const body = await parseJsonBody<unknown>(req);
|
||||
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
||||
throw new DevicePackageRouteError(400, 'Request body is invalid');
|
||||
}
|
||||
const record = body as Record<string, unknown>;
|
||||
const actual = Object.keys(record).sort();
|
||||
const expected = [...keys].sort();
|
||||
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
||||
throw new DevicePackageRouteError(400, 'Request body has unexpected fields');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
function sendError(res: ServerResponse, error: unknown): void {
|
||||
if (error instanceof DevicePackageRouteError) {
|
||||
sendJson(res, error.status, {
|
||||
success: false, code: 'local_package_request_invalid', error: error.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (error instanceof DevicePackageError) {
|
||||
const status = error.code === 'local_package_not_installed' || error.code === 'local_package_not_found'
|
||||
? 404
|
||||
: error.code === 'local_package_in_use' || error.code === 'local_package_confirmation_required'
|
||||
? 409
|
||||
: error.code === 'local_package_dependency_failed' || error.code === 'local_package_install_failed'
|
||||
? 503
|
||||
: 422;
|
||||
sendJson(res, status, { success: false, code: error.code, error: error.message });
|
||||
return;
|
||||
}
|
||||
if (error instanceof SyntaxError) {
|
||||
sendJson(res, 400, {
|
||||
success: false, code: 'local_package_request_invalid', error: 'Request body is invalid',
|
||||
});
|
||||
return;
|
||||
}
|
||||
sendJson(res, 503, {
|
||||
success: false, code: 'local_package_install_failed', error: 'Device package service is unavailable',
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleDevicePackageRoutes(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
url: URL,
|
||||
ctx: HostApiContext,
|
||||
): Promise<boolean> {
|
||||
const match = PACKAGE.exec(url.pathname);
|
||||
const known = (url.pathname === ROOT && req.method === 'GET')
|
||||
|| Boolean(match && (req.method === 'PATCH' || req.method === 'DELETE'));
|
||||
if (!known) return false;
|
||||
const manager = ctx.codingProducts?.devicePackages;
|
||||
if (!manager) {
|
||||
sendJson(res, 503, {
|
||||
success: false, code: 'local_package_install_failed', error: 'Device package service is unavailable',
|
||||
});
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
noQuery(url);
|
||||
if (url.pathname === ROOT) {
|
||||
sendJson(res, 200, await manager.list());
|
||||
return true;
|
||||
}
|
||||
const id = packageId(match?.[1] ?? '');
|
||||
if (req.method === 'PATCH') {
|
||||
const body = await exactBody(req, ['enabled']);
|
||||
if (typeof body.enabled !== 'boolean') {
|
||||
throw new DevicePackageRouteError(400, 'enabled is invalid');
|
||||
}
|
||||
sendJson(res, 200, await manager.setEnabled(id, body.enabled));
|
||||
return true;
|
||||
}
|
||||
await exactBody(req, []);
|
||||
sendJson(res, 200, await manager.uninstall(id));
|
||||
return true;
|
||||
} catch (error) {
|
||||
sendError(res, error);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user