feat(pi): materialize effective plugin worker tools
This commit is contained in:
@@ -13,13 +13,20 @@ import {
|
||||
parsePiSubagentDispatchRequest,
|
||||
type PiSubagentScheduler,
|
||||
} from './subagent';
|
||||
import {
|
||||
isPiProductToolName,
|
||||
type PiProductToolName,
|
||||
type PiProductTools,
|
||||
} from './product-tools';
|
||||
import type { PiProductTools } from './product-tools';
|
||||
import type { CodingPluginToolDefinition } from '../../../shared/coding-plugins';
|
||||
import type { PiSkillEntry } from './resource-loader';
|
||||
|
||||
const MAX_REQUEST_BYTES = 64 * 1024;
|
||||
const PRODUCT_TOOL_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9._:-]{0,63}$/u;
|
||||
const CORE_PRODUCT_TOOL_NAMES = new Set([
|
||||
'agent_browser',
|
||||
'game_asset_browser',
|
||||
'game_asset_review',
|
||||
'task_state',
|
||||
'changed_file',
|
||||
'runtime_context',
|
||||
]);
|
||||
|
||||
interface WorkerRegistrationRecord {
|
||||
token: string;
|
||||
@@ -28,6 +35,10 @@ interface WorkerRegistrationRecord {
|
||||
projectId: string;
|
||||
projectPath: string | null;
|
||||
skillIds: string[];
|
||||
catalogRevision?: number;
|
||||
allowedToolNames: string[];
|
||||
tools: CodingPluginToolDefinition[];
|
||||
projectWriteLeaseToolNames: string[];
|
||||
role: 'parent' | 'child';
|
||||
contextFile: string;
|
||||
runId: string | null;
|
||||
@@ -39,6 +50,7 @@ export interface PiExtensionWorkerRegistration {
|
||||
extensionPath: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
sensitiveValues: string[];
|
||||
allowedToolNames: readonly string[];
|
||||
dispose(): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -47,7 +59,9 @@ export interface RegisterPiExtensionWorkerInput {
|
||||
generation: number;
|
||||
projectId: string;
|
||||
projectPath?: string;
|
||||
skillIds?: readonly string[];
|
||||
skillEntries?: readonly PiSkillEntry[];
|
||||
catalogRevision?: number;
|
||||
tools?: readonly CodingPluginToolDefinition[];
|
||||
extensionsDir: string;
|
||||
role?: 'parent' | 'child';
|
||||
runId?: string;
|
||||
@@ -77,7 +91,7 @@ interface ProductToolBridgeRequest {
|
||||
workerGeneration: number;
|
||||
runId: string;
|
||||
resourceId: string;
|
||||
toolName: PiProductToolName;
|
||||
toolName: string;
|
||||
input: unknown;
|
||||
}
|
||||
|
||||
@@ -122,7 +136,9 @@ function bridgeRequest(value: unknown): value is BridgeRequest {
|
||||
if (!common) return false;
|
||||
if (value.action === 'subagent.dispatch') return 'request' in value;
|
||||
if (value.action === 'product.invoke') {
|
||||
return isPiProductToolName(value.toolName) && 'input' in value;
|
||||
return typeof value.toolName === 'string'
|
||||
&& PRODUCT_TOOL_NAME_PATTERN.test(value.toolName)
|
||||
&& 'input' in value;
|
||||
}
|
||||
if (value.action === 'changes.bash') return true;
|
||||
if (value.action === 'changes.touched') {
|
||||
@@ -201,6 +217,23 @@ export class PiManagedExtensionHost {
|
||||
if (this.productTools && !input.projectPath?.trim()) {
|
||||
throw new Error('Product tools require a worker project path');
|
||||
}
|
||||
const skillEntries = [...new Map(
|
||||
(input.skillEntries ?? [])
|
||||
.filter((entry) => entry.id.trim() && entry.entryPath.trim())
|
||||
.map((entry) => [entry.id.trim(), {
|
||||
id: entry.id.trim(),
|
||||
entryPath: entry.entryPath.trim().replaceAll('\\', '/'),
|
||||
}]),
|
||||
).values()];
|
||||
const tools = role === 'child'
|
||||
? []
|
||||
: [...new Map(
|
||||
(input.tools ?? []).map((tool) => [tool.name, structuredClone(tool)]),
|
||||
).values()];
|
||||
const allowedToolNames = tools.map(({ name }) => name);
|
||||
const projectWriteLeaseToolNames = tools
|
||||
.filter(({ projectWriteLease }) => projectWriteLease)
|
||||
.map(({ name }) => name);
|
||||
const token = randomBytes(32).toString('base64url');
|
||||
const contextFile = path.join(input.extensionsDir, `worker-${randomUUID()}.json`);
|
||||
const record: WorkerRegistrationRecord = {
|
||||
@@ -209,7 +242,11 @@ export class PiManagedExtensionHost {
|
||||
generation: input.generation,
|
||||
projectId: input.projectId,
|
||||
projectPath: input.projectPath?.trim() || null,
|
||||
skillIds: [...new Set((input.skillIds ?? []).map((id) => id.trim()).filter(Boolean))],
|
||||
skillIds: skillEntries.map(({ id }) => id),
|
||||
...(input.catalogRevision === undefined ? {} : { catalogRevision: input.catalogRevision }),
|
||||
allowedToolNames,
|
||||
tools,
|
||||
projectWriteLeaseToolNames,
|
||||
role,
|
||||
contextFile,
|
||||
runId: role === 'child'
|
||||
@@ -230,6 +267,7 @@ export class PiManagedExtensionHost {
|
||||
MAKELORE_PI_WORKER_ROLE: role,
|
||||
},
|
||||
sensitiveValues: [token],
|
||||
allowedToolNames: [...allowedToolNames],
|
||||
dispose: async () => {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
@@ -370,6 +408,11 @@ export class PiManagedExtensionHost {
|
||||
this.respond(response, 403, { error: 'Child workers cannot invoke parent product tools' });
|
||||
return;
|
||||
}
|
||||
if (!CORE_PRODUCT_TOOL_NAMES.has(value.toolName)
|
||||
&& !record.allowedToolNames.includes(value.toolName)) {
|
||||
this.respond(response, 403, { error: 'Product tool is not enabled for this worker' });
|
||||
return;
|
||||
}
|
||||
if (!this.productTools || !record.projectPath) {
|
||||
this.respond(response, 503, { error: 'Product tools are unavailable' });
|
||||
return;
|
||||
@@ -544,6 +587,11 @@ export class PiManagedExtensionHost {
|
||||
conversationId: record.conversationId,
|
||||
workerGeneration: record.generation,
|
||||
role: record.role,
|
||||
skillIds: record.skillIds,
|
||||
...(record.catalogRevision === undefined ? {} : { catalogRevision: record.catalogRevision }),
|
||||
allowedToolNames: record.allowedToolNames,
|
||||
tools: record.tools,
|
||||
projectWriteLeaseToolNames: record.projectWriteLeaseToolNames,
|
||||
...(record.runId ? { runId: record.runId } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,19 +14,22 @@ const MUTATION_TOOLS = new Set([
|
||||
'write',
|
||||
'game_asset_browser',
|
||||
'game_asset_review',
|
||||
'data_service_configure',
|
||||
'data_service_put_document',
|
||||
'data_service_delete_document',
|
||||
'data_service_remove_collection',
|
||||
'data_service_reset',
|
||||
'data_service_remove_project',
|
||||
]);
|
||||
const WORKER_ROLE = process.env.MAKELORE_PI_WORKER_ROLE || 'parent';
|
||||
const leases = new Map();
|
||||
const touchedPaths = new Map();
|
||||
let dynamicLeaseTools = new Set();
|
||||
|
||||
async function readWorkerContext() {
|
||||
const value = JSON.parse(await readFile(process.env.MAKELORE_PI_CONTEXT_FILE, 'utf8'));
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw new Error('Makelore worker context is invalid');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function runtimeContext() {
|
||||
const value = JSON.parse(await readFile(process.env.MAKELORE_PI_CONTEXT_FILE, 'utf8'));
|
||||
const value = await readWorkerContext();
|
||||
if (!value.runId) throw new Error('Makelore run context is unavailable');
|
||||
return value;
|
||||
}
|
||||
@@ -131,7 +134,47 @@ function registerProductTool(pi, name, label, description, parameters) {
|
||||
});
|
||||
}
|
||||
|
||||
export default function makeloreRuntime(pi) {
|
||||
function isRecord(value) {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isToolDeclaration(value) {
|
||||
return isRecord(value)
|
||||
&& typeof value.name === 'string'
|
||||
&& typeof value.label === 'string'
|
||||
&& typeof value.description === 'string'
|
||||
&& isRecord(value.inputSchema);
|
||||
}
|
||||
|
||||
async function registerDynamicProductTools(pi) {
|
||||
if (WORKER_ROLE !== 'parent') return;
|
||||
const context = await readWorkerContext();
|
||||
const allowedToolNames = new Set(
|
||||
Array.isArray(context.allowedToolNames)
|
||||
? context.allowedToolNames.filter((name) => typeof name === 'string')
|
||||
: [],
|
||||
);
|
||||
const declarations = Array.isArray(context.tools) ? context.tools : [];
|
||||
dynamicLeaseTools = new Set(
|
||||
declarations
|
||||
.filter((declaration) => isToolDeclaration(declaration)
|
||||
&& declaration.projectWriteLease === true
|
||||
&& allowedToolNames.has(declaration.name))
|
||||
.map((declaration) => declaration.name),
|
||||
);
|
||||
for (const declaration of declarations) {
|
||||
if (!isToolDeclaration(declaration) || !allowedToolNames.has(declaration.name)) continue;
|
||||
registerProductTool(
|
||||
pi,
|
||||
declaration.name,
|
||||
declaration.label,
|
||||
declaration.description,
|
||||
declaration.inputSchema,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default async function makeloreRuntime(pi) {
|
||||
if (WORKER_ROLE === 'parent') pi.registerTool({
|
||||
name: 'ask_user',
|
||||
label: 'Ask user',
|
||||
@@ -290,139 +333,11 @@ export default function makeloreRuntime(pi) {
|
||||
'Read the safe selected-skill and command catalog for this managed worker.',
|
||||
{ type: 'object', additionalProperties: false, properties: {} },
|
||||
);
|
||||
if (WORKER_ROLE === 'parent') registerProductTool(
|
||||
pi,
|
||||
'data_service_configure',
|
||||
'Data Service configure',
|
||||
'Configure collections for the active Makelore project Data Service instance.',
|
||||
{
|
||||
type: 'object', additionalProperties: false, required: ['collections'],
|
||||
properties: {
|
||||
collections: {
|
||||
type: 'array', minItems: 0, maxItems: 20,
|
||||
items: { type: 'string', minLength: 1, maxLength: 48, pattern: '^[a-z][a-z0-9_-]{0,47}$' },
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
if (WORKER_ROLE === 'parent') registerProductTool(
|
||||
pi,
|
||||
'data_service_inspect',
|
||||
'Data Service inspect',
|
||||
'Inspect the active Makelore project Data Service instance.',
|
||||
{ type: 'object', additionalProperties: false, properties: {} },
|
||||
);
|
||||
if (WORKER_ROLE === 'parent') registerProductTool(
|
||||
pi,
|
||||
'data_service_list_projects',
|
||||
'Data Service projects',
|
||||
'List Data Service instances available to the signed-in account.',
|
||||
{ type: 'object', additionalProperties: false, properties: {} },
|
||||
);
|
||||
if (WORKER_ROLE === 'parent') registerProductTool(
|
||||
pi,
|
||||
'data_service_get_document',
|
||||
'Data Service get document',
|
||||
'Read one document from a collection in the active Makelore project.',
|
||||
{
|
||||
type: 'object', additionalProperties: false, required: ['collection', 'document_id'],
|
||||
properties: {
|
||||
collection: { type: 'string', minLength: 1, maxLength: 48, pattern: '^[a-z][a-z0-9_-]{0,47}$' },
|
||||
document_id: {
|
||||
type: 'string', minLength: 1, maxLength: 128, pattern: '^[A-Za-z0-9._~-]{1,128}$',
|
||||
not: { enum: ['.', '..'] },
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
if (WORKER_ROLE === 'parent') registerProductTool(
|
||||
pi,
|
||||
'data_service_list_documents',
|
||||
'Data Service list documents',
|
||||
'List documents from a collection in the active Makelore project.',
|
||||
{
|
||||
type: 'object', additionalProperties: false, required: ['collection'],
|
||||
properties: {
|
||||
collection: { type: 'string', minLength: 1, maxLength: 48, pattern: '^[a-z][a-z0-9_-]{0,47}$' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100 },
|
||||
cursor: { type: 'string', minLength: 1, maxLength: 1024 },
|
||||
},
|
||||
},
|
||||
);
|
||||
if (WORKER_ROLE === 'parent') registerProductTool(
|
||||
pi,
|
||||
'data_service_put_document',
|
||||
'Data Service put document',
|
||||
'Create or update one document in a collection in the active Makelore project.',
|
||||
{
|
||||
type: 'object', additionalProperties: false,
|
||||
required: ['collection', 'document_id', 'data'],
|
||||
properties: {
|
||||
collection: { type: 'string', minLength: 1, maxLength: 48, pattern: '^[a-z][a-z0-9_-]{0,47}$' },
|
||||
document_id: {
|
||||
type: 'string', minLength: 1, maxLength: 128, pattern: '^[A-Za-z0-9._~-]{1,128}$',
|
||||
not: { enum: ['.', '..'] },
|
||||
},
|
||||
data: { type: 'object' },
|
||||
if_revision: { type: 'integer', minimum: 1, maximum: 9007199254740991 },
|
||||
},
|
||||
},
|
||||
);
|
||||
if (WORKER_ROLE === 'parent') registerProductTool(
|
||||
pi,
|
||||
'data_service_delete_document',
|
||||
'Data Service delete document',
|
||||
'Delete one document from the active Makelore project after explicit confirmation.',
|
||||
{
|
||||
type: 'object', additionalProperties: false,
|
||||
required: ['collection', 'document_id', 'confirmed'],
|
||||
properties: {
|
||||
collection: { type: 'string', minLength: 1, maxLength: 48, pattern: '^[a-z][a-z0-9_-]{0,47}$' },
|
||||
document_id: {
|
||||
type: 'string', minLength: 1, maxLength: 128, pattern: '^[A-Za-z0-9._~-]{1,128}$',
|
||||
not: { enum: ['.', '..'] },
|
||||
},
|
||||
if_revision: { type: 'integer', minimum: 1, maximum: 9007199254740991 },
|
||||
confirmed: { type: 'boolean', const: true },
|
||||
},
|
||||
},
|
||||
);
|
||||
if (WORKER_ROLE === 'parent') registerProductTool(
|
||||
pi,
|
||||
'data_service_remove_collection',
|
||||
'Data Service remove collection',
|
||||
'Remove a collection from the active Makelore project after explicit confirmation.',
|
||||
{
|
||||
type: 'object', additionalProperties: false, required: ['collection', 'confirmed'],
|
||||
properties: {
|
||||
collection: { type: 'string', minLength: 1, maxLength: 48, pattern: '^[a-z][a-z0-9_-]{0,47}$' },
|
||||
confirmed: { type: 'boolean', const: true },
|
||||
},
|
||||
},
|
||||
);
|
||||
if (WORKER_ROLE === 'parent') registerProductTool(
|
||||
pi,
|
||||
'data_service_reset',
|
||||
'Data Service reset',
|
||||
'Reset all collections in the active Makelore project after explicit confirmation.',
|
||||
{
|
||||
type: 'object', additionalProperties: false, required: ['confirmed'],
|
||||
properties: { confirmed: { type: 'boolean', const: true } },
|
||||
},
|
||||
);
|
||||
if (WORKER_ROLE === 'parent') registerProductTool(
|
||||
pi,
|
||||
'data_service_remove_project',
|
||||
'Data Service remove project',
|
||||
'Remove the active Makelore project Data Service instance after explicit confirmation.',
|
||||
{
|
||||
type: 'object', additionalProperties: false, required: ['confirmed'],
|
||||
properties: { confirmed: { type: 'boolean', const: true } },
|
||||
},
|
||||
);
|
||||
|
||||
await registerDynamicProductTools(pi);
|
||||
|
||||
pi.on('tool_call', async (event, ctx) => {
|
||||
if (!MUTATION_TOOLS.has(event.toolName)) return;
|
||||
if (!MUTATION_TOOLS.has(event.toolName) && !dynamicLeaseTools.has(event.toolName)) return;
|
||||
const input = event.input || event.arguments || event.args || {};
|
||||
const touchedPath = (event.toolName === 'write' || event.toolName === 'edit')
|
||||
? projectRelativePath(input.path) : undefined;
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { mkdir, rename, stat } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
BUNDLED_CODING_SKILL_IDS,
|
||||
type BundledCodingSkillId,
|
||||
} from '../../../shared/coding-skills';
|
||||
import { atomicWriteJson, atomicWriteText } from '../../coding-projects/atomic-json';
|
||||
import { validateSessionKey } from '../../coding-projects/conversation-store';
|
||||
import { resolveBundledCodingPluginRootPaths } from '../../coding-plugins/manifest';
|
||||
import type { PiProviderSelection } from './provider-config';
|
||||
import type { PiManagedInputRevision } from './managed-input-revision';
|
||||
|
||||
const MANAGED_SEGMENT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
||||
|
||||
export interface PiSkillEntry {
|
||||
id: string;
|
||||
entryPath: string;
|
||||
}
|
||||
|
||||
export interface BundledCodingSkillsPathInput {
|
||||
isPackaged: boolean;
|
||||
resourcesPath: string;
|
||||
@@ -33,7 +35,8 @@ export interface MaterializePiAgentResourcesOptions {
|
||||
projectId: string;
|
||||
agentId: string;
|
||||
prompt: string;
|
||||
skillIds: readonly string[];
|
||||
skillEntries: readonly PiSkillEntry[];
|
||||
catalogRevision: number;
|
||||
bundledSkillsDir: string;
|
||||
revision: PiManagedInputRevision;
|
||||
}
|
||||
@@ -43,7 +46,9 @@ export interface PiAgentResourceManifest {
|
||||
projectId: string;
|
||||
agentId: string;
|
||||
promptFile: string;
|
||||
skillIds: BundledCodingSkillId[];
|
||||
skillIds: string[];
|
||||
skillEntries: PiSkillEntry[];
|
||||
catalogRevision: number;
|
||||
revision: PiManagedInputRevision;
|
||||
}
|
||||
|
||||
@@ -52,13 +57,17 @@ export interface PiAgentResourceSnapshot {
|
||||
projectSessionsDir: string;
|
||||
promptPath: string;
|
||||
manifestPath: string;
|
||||
skillIds: BundledCodingSkillId[];
|
||||
skillIds: string[];
|
||||
skillEntries: PiSkillEntry[];
|
||||
skillPaths: string[];
|
||||
catalogRevision: number;
|
||||
revision: PiManagedInputRevision;
|
||||
summary: {
|
||||
projectId: string;
|
||||
agentId: string;
|
||||
skillIds: BundledCodingSkillId[];
|
||||
skillIds: string[];
|
||||
skillEntries: PiSkillEntry[];
|
||||
catalogRevision: number;
|
||||
revision: PiManagedInputRevision;
|
||||
};
|
||||
}
|
||||
@@ -130,33 +139,85 @@ export async function archivePiConversationSession(input: {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSkillIds(skillIds: readonly string[]): BundledCodingSkillId[] {
|
||||
const result: BundledCodingSkillId[] = [];
|
||||
function normalizeSkillEntries(skillEntries: readonly PiSkillEntry[]): PiSkillEntry[] {
|
||||
const result: PiSkillEntry[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const rawSkillId of skillIds) {
|
||||
const skillId = rawSkillId.trim();
|
||||
if (!BUNDLED_CODING_SKILL_IDS.includes(skillId as BundledCodingSkillId)) {
|
||||
throw new Error(`Unknown bundled coding skill: ${skillId || '(empty)'}`);
|
||||
for (const rawEntry of skillEntries) {
|
||||
if (!rawEntry || typeof rawEntry.id !== 'string' || typeof rawEntry.entryPath !== 'string') {
|
||||
throw new Error('Effective coding Skill entry is invalid');
|
||||
}
|
||||
const skillId = rawEntry.id.trim();
|
||||
const entryPath = rawEntry.entryPath.trim().replaceAll('\\', '/');
|
||||
if (!skillId || !entryPath) throw new Error('Effective coding Skill entry is invalid');
|
||||
if (entryPath.startsWith('/') || /^[A-Za-z]:\//u.test(entryPath)
|
||||
|| /^[A-Za-z][A-Za-z0-9+.-]*:/u.test(entryPath)) {
|
||||
throw new Error(`Effective coding Skill entry path must be relative: ${entryPath}`);
|
||||
}
|
||||
const relative = path.posix.normalize(entryPath);
|
||||
if (!relative || relative === '.' || relative === '..' || relative.startsWith('../')) {
|
||||
throw new Error(`Effective coding Skill entry path escapes its package: ${entryPath}`);
|
||||
}
|
||||
if (seen.has(skillId)) continue;
|
||||
seen.add(skillId);
|
||||
result.push(skillId as BundledCodingSkillId);
|
||||
result.push({ id: skillId, entryPath: relative });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function catalogRevision(value: number): number {
|
||||
if (!Number.isSafeInteger(value) || value < 0) {
|
||||
throw new Error('Catalog revision must be a non-negative safe integer');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function pathWithin(root: string, entryPath: string): string | null {
|
||||
const resolvedRoot = path.resolve(root);
|
||||
const resolved = path.resolve(resolvedRoot, ...entryPath.split('/'));
|
||||
const relative = path.relative(resolvedRoot, resolved);
|
||||
if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
||||
return null;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
async function resolveSkillEntryPath(
|
||||
bundledSkillsDir: string,
|
||||
entry: PiSkillEntry,
|
||||
): Promise<string> {
|
||||
const roots = [
|
||||
path.resolve(bundledSkillsDir),
|
||||
...resolveBundledCodingPluginRootPaths(path.join(
|
||||
path.dirname(path.resolve(bundledSkillsDir)),
|
||||
'coding-plugins',
|
||||
)),
|
||||
];
|
||||
for (const root of roots) {
|
||||
const candidate = pathWithin(root, entry.entryPath);
|
||||
if (!candidate) continue;
|
||||
try {
|
||||
const metadata = await stat(candidate);
|
||||
if (metadata.isFile()) return candidate;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
||||
}
|
||||
}
|
||||
throw new Error(`Bundled coding Skill entry is not a file: ${entry.entryPath}`);
|
||||
}
|
||||
|
||||
export async function resolveExplicitCodingSkillPaths(
|
||||
bundledSkillsDir: string,
|
||||
skillIds: readonly string[],
|
||||
): Promise<{ skillIds: BundledCodingSkillId[]; skillPaths: string[] }> {
|
||||
const normalizedSkillIds = normalizeSkillIds(skillIds);
|
||||
const root = path.resolve(bundledSkillsDir);
|
||||
const skillPaths = normalizedSkillIds.map((skillId) => path.join(root, skillId, 'SKILL.md'));
|
||||
await Promise.all(skillPaths.map(async (skillPath) => {
|
||||
const metadata = await stat(skillPath);
|
||||
if (!metadata.isFile()) throw new Error(`Bundled coding skill entry is not a file: ${skillPath}`);
|
||||
}));
|
||||
return { skillIds: normalizedSkillIds, skillPaths };
|
||||
skillEntries: readonly PiSkillEntry[],
|
||||
): Promise<{ skillIds: string[]; skillEntries: PiSkillEntry[]; skillPaths: string[] }> {
|
||||
const normalizedEntries = normalizeSkillEntries(skillEntries);
|
||||
const skillPaths = await Promise.all(normalizedEntries.map((entry) => (
|
||||
resolveSkillEntryPath(bundledSkillsDir, entry)
|
||||
)));
|
||||
return {
|
||||
skillIds: normalizedEntries.map(({ id }) => id),
|
||||
skillEntries: normalizedEntries,
|
||||
skillPaths,
|
||||
};
|
||||
}
|
||||
|
||||
export async function materializePiAgentResources(
|
||||
@@ -171,10 +232,11 @@ export async function materializePiAgentResources(
|
||||
mkdir(projectSessionsDir, { recursive: true }),
|
||||
mkdir(projectPromptsDir, { recursive: true }),
|
||||
]);
|
||||
const { skillIds, skillPaths } = await resolveExplicitCodingSkillPaths(
|
||||
const { skillIds, skillEntries, skillPaths } = await resolveExplicitCodingSkillPaths(
|
||||
options.bundledSkillsDir,
|
||||
options.skillIds,
|
||||
options.skillEntries,
|
||||
);
|
||||
const resolvedCatalogRevision = catalogRevision(options.catalogRevision);
|
||||
const promptPath = path.join(projectPromptsDir, `${agentId}.md`);
|
||||
const manifestPath = path.join(projectPromptsDir, `${agentId}.manifest.json`);
|
||||
const manifest: PiAgentResourceManifest = {
|
||||
@@ -183,6 +245,8 @@ export async function materializePiAgentResources(
|
||||
agentId,
|
||||
promptFile: path.basename(promptPath),
|
||||
skillIds: [...skillIds],
|
||||
skillEntries: structuredClone(skillEntries),
|
||||
catalogRevision: resolvedCatalogRevision,
|
||||
revision: { ...options.revision },
|
||||
};
|
||||
await atomicWriteText(promptPath, options.prompt);
|
||||
@@ -193,12 +257,16 @@ export async function materializePiAgentResources(
|
||||
promptPath,
|
||||
manifestPath,
|
||||
skillIds: [...skillIds],
|
||||
skillEntries: structuredClone(skillEntries),
|
||||
skillPaths,
|
||||
catalogRevision: resolvedCatalogRevision,
|
||||
revision: { ...options.revision },
|
||||
summary: {
|
||||
projectId,
|
||||
agentId,
|
||||
skillIds: [...skillIds],
|
||||
skillEntries: structuredClone(skillEntries),
|
||||
catalogRevision: resolvedCatalogRevision,
|
||||
revision: { ...options.revision },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -60,6 +60,7 @@ import type {
|
||||
} from './rpc-client';
|
||||
import {
|
||||
PiWorkerProcess,
|
||||
PI_CORE_TOOL_NAMES,
|
||||
type PiWorkerProcessOptions,
|
||||
type PiWorkerProofFailure,
|
||||
type PiWorkerStopReason,
|
||||
@@ -84,6 +85,7 @@ import {
|
||||
} from './session-projector';
|
||||
import { PiManagedExtensionHost } from './extension-host';
|
||||
import type { PiSubagentScheduler } from './subagent';
|
||||
import type { CodingCapabilityRegistry, ResolvedWorkerResources } from '../../coding-plugins/registry';
|
||||
import {
|
||||
PiInteractionStore,
|
||||
} from './interaction';
|
||||
@@ -147,6 +149,7 @@ export interface PiManagedWorkerOpenerOptions {
|
||||
now?: () => number;
|
||||
onTelemetry?: (event: PiRuntimeTelemetryEvent) => void;
|
||||
extensionHost: PiManagedExtensionHost;
|
||||
capabilityRegistry?: CodingCapabilityRegistry;
|
||||
}
|
||||
|
||||
interface PiRpcSessionStateProjection {
|
||||
@@ -154,6 +157,17 @@ interface PiRpcSessionStateProjection {
|
||||
sessionFile?: string;
|
||||
}
|
||||
|
||||
function fallbackWorkerResources(skillIds: readonly string[]): ResolvedWorkerResources {
|
||||
const effectiveSkillIds = [...new Set(skillIds.map((id) => id.trim()).filter(Boolean))];
|
||||
return {
|
||||
catalogRevision: 0,
|
||||
pluginIds: [],
|
||||
effectiveSkillIds,
|
||||
skillEntries: effectiveSkillIds.map((id) => ({ id, entryPath: `${id}/SKILL.md` })),
|
||||
tools: [],
|
||||
};
|
||||
}
|
||||
|
||||
class ManagedPiConversationWorker implements PiConversationWorker {
|
||||
constructor(
|
||||
readonly id: string,
|
||||
@@ -229,12 +243,20 @@ export function createPiManagedWorkerOpener(
|
||||
if (!account || !descriptor) throw new Error('Selected Provider account is unavailable');
|
||||
const managedPaths = await ensurePiManagedPaths(options.userDataDir);
|
||||
await writePiProviderCatalog(managedPaths.modelsFile, catalog, model);
|
||||
const workerResources = options.capabilityRegistry
|
||||
? await options.capabilityRegistry.resolveWorkerResources({
|
||||
projectPath: registered.projectPath,
|
||||
assignedSkillIds: registered.agent.skillIds,
|
||||
role: 'parent',
|
||||
})
|
||||
: fallbackWorkerResources(registered.agent.skillIds);
|
||||
const resources = await materializePiAgentResources({
|
||||
userDataDir: options.userDataDir,
|
||||
projectId: input.conversation.projectId,
|
||||
agentId: registered.agent.id,
|
||||
prompt: registered.agent.prompt,
|
||||
skillIds: registered.agent.skillIds,
|
||||
skillEntries: workerResources.skillEntries,
|
||||
catalogRevision: workerResources.catalogRevision,
|
||||
bundledSkillsDir: options.bundledSkillsDir,
|
||||
revision: input.revision,
|
||||
});
|
||||
@@ -251,7 +273,9 @@ export function createPiManagedWorkerOpener(
|
||||
generation: input.generation,
|
||||
projectId: input.conversation.projectId,
|
||||
projectPath: registered.projectPath,
|
||||
skillIds: registered.agent.skillIds,
|
||||
skillEntries: workerResources.skillEntries,
|
||||
catalogRevision: workerResources.catalogRevision,
|
||||
tools: workerResources.tools,
|
||||
extensionsDir: managedPaths.extensionsDir,
|
||||
});
|
||||
recordManagedMilestone(
|
||||
@@ -274,6 +298,10 @@ export function createPiManagedWorkerOpener(
|
||||
cwd: registered.projectPath,
|
||||
configDir: resources.paths.configDir,
|
||||
sessionDir: resources.projectSessionsDir,
|
||||
tools: [...new Set([
|
||||
...PI_CORE_TOOL_NAMES,
|
||||
...extension.allowedToolNames,
|
||||
])],
|
||||
additionalArgs: [
|
||||
...buildPiManagedInputArgs(selection, resources),
|
||||
'--extension', extension.extensionPath,
|
||||
|
||||
@@ -26,6 +26,7 @@ import type {
|
||||
PiSubagentChildResult,
|
||||
} from './subagent';
|
||||
import { PiSubagentChildError } from './subagent';
|
||||
import type { CodingCapabilityRegistry, ResolvedWorkerResources } from '../../coding-plugins/registry';
|
||||
import {
|
||||
PiWorkerProcess,
|
||||
type PiWorkerProcessOptions,
|
||||
@@ -61,6 +62,18 @@ export interface PiManagedSubagentChildOpenerOptions {
|
||||
getRevision(): PiManagedInputRevision;
|
||||
getLocalProxyCredential?(): Promise<string | undefined>;
|
||||
createProcess?(options: PiWorkerProcessOptions): PiSubagentProcessAdapter;
|
||||
capabilityRegistry?: CodingCapabilityRegistry;
|
||||
}
|
||||
|
||||
function fallbackWorkerResources(skillIds: readonly string[]): ResolvedWorkerResources {
|
||||
const effectiveSkillIds = [...new Set(skillIds.map((id) => id.trim()).filter(Boolean))];
|
||||
return {
|
||||
catalogRevision: 0,
|
||||
pluginIds: [],
|
||||
effectiveSkillIds,
|
||||
skillEntries: effectiveSkillIds.map((id) => ({ id, entryPath: `${id}/SKILL.md` })),
|
||||
tools: [],
|
||||
};
|
||||
}
|
||||
|
||||
function recordValue(value: unknown): Record<string, unknown> | null {
|
||||
@@ -178,12 +191,20 @@ export function createPiManagedSubagentChildOpener(
|
||||
if (!account || !descriptor) throw new PiSubagentChildError('SUBAGENT_MODEL_UNAVAILABLE');
|
||||
const managedPaths = await ensurePiManagedPaths(options.userDataDir);
|
||||
await writePiProviderCatalog(managedPaths.modelsFile, catalog, agent.model);
|
||||
const workerResources = options.capabilityRegistry
|
||||
? await options.capabilityRegistry.resolveWorkerResources({
|
||||
projectPath: project.path,
|
||||
assignedSkillIds: agent.skillIds,
|
||||
role: 'child',
|
||||
})
|
||||
: fallbackWorkerResources(agent.skillIds);
|
||||
const resources = await materializePiAgentResources({
|
||||
userDataDir: options.userDataDir,
|
||||
projectId: input.projectId,
|
||||
agentId: agent.id,
|
||||
prompt: agent.prompt,
|
||||
skillIds: agent.skillIds,
|
||||
skillEntries: workerResources.skillEntries,
|
||||
catalogRevision: workerResources.catalogRevision,
|
||||
bundledSkillsDir: options.bundledSkillsDir,
|
||||
revision: options.getRevision(),
|
||||
});
|
||||
@@ -200,7 +221,9 @@ export function createPiManagedSubagentChildOpener(
|
||||
generation: input.workerGeneration,
|
||||
projectId: input.projectId,
|
||||
projectPath: project.path,
|
||||
skillIds: agent.skillIds,
|
||||
skillEntries: workerResources.skillEntries,
|
||||
catalogRevision: workerResources.catalogRevision,
|
||||
tools: [],
|
||||
extensionsDir: managedPaths.extensionsDir,
|
||||
role: 'child',
|
||||
runId: input.runId,
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
type ChildProcessWithoutNullStreams,
|
||||
} from 'node:child_process';
|
||||
import { platform } from 'node:os';
|
||||
import { DATA_SERVICE_PI_TOOL_NAMES } from '../../../shared/data-service';
|
||||
import { logger } from '../../utils/logger';
|
||||
import type { CodingRuntimeDisposeReason } from '../contracts';
|
||||
import { PiProcessError, type PiProcessErrorCode } from './process-errors';
|
||||
@@ -122,15 +121,17 @@ export type PiWorkerProcessOptions = {
|
||||
onLifecycleEvent?(event: PiWorkerLifecycleEvent): void;
|
||||
};
|
||||
|
||||
/** Tools available to every managed parent worker before plugin materialization. */
|
||||
export const PI_CORE_TOOL_NAMES = Object.freeze([
|
||||
'read', 'bash', 'edit', 'write', 'grep', 'find', 'ls', 'ask_user', 'subagent',
|
||||
'agent_browser', 'game_asset_browser', 'game_asset_review',
|
||||
'task_state', 'changed_file', 'runtime_context',
|
||||
] as const);
|
||||
|
||||
export function buildPiRpcArgs(
|
||||
sessionDir: string,
|
||||
additionalArgs: readonly string[] = [],
|
||||
tools: readonly string[] = [
|
||||
'read', 'bash', 'edit', 'write', 'grep', 'find', 'ls', 'ask_user', 'subagent',
|
||||
'agent_browser', 'game_asset_browser', 'game_asset_review',
|
||||
'task_state', 'changed_file', 'runtime_context',
|
||||
...DATA_SERVICE_PI_TOOL_NAMES,
|
||||
],
|
||||
tools: readonly string[] = PI_CORE_TOOL_NAMES,
|
||||
): string[] {
|
||||
return [
|
||||
'--mode', 'rpc',
|
||||
|
||||
Reference in New Issue
Block a user