Files
makelore/electron/coding-runtime/pi/product-tools.ts

205 lines
7.1 KiB
TypeScript

import type { AgentBrowserModule } from '../../agent-browser';
import type { CodingAttachmentStore } from '../../coding-projects/attachment-store';
import {
ConversationChangeTracker,
type ConversationChangesSnapshot,
} from '../../coding-projects/conversation-change-tracker';
import {
buildProductCodingCommandCatalog,
listProductCodingSkills,
} from '../../coding-projects/skill-registry';
import type {
ProductCodingCommand,
ProductCodingSkill,
ProductPiCommandInput,
} from '../../../shared/coding-product-tools';
import {
DATA_SERVICE_PI_TOOL_NAMES,
type DataServicePiToolName,
} from '../../../shared/data-service';
import {
DATA_SERVICE_PLUGIN_DEFINITION,
} from '../../../shared/coding-plugins';
import { createDataServicePluginAdapter } from '../../coding-plugins/adapters/data-service';
import {
buildCapabilityToolResult,
type AdapterInvocationResult,
type CodingCapabilityRegistry,
type TrustedCodingCapabilityContext,
} from '../../coding-plugins/registry';
import type { DataServiceOperations } from '../../services/data-service-client';
import type { KnownToolDetails, RuntimeContextDetailsV1 } from '../contracts';
import { PiAgentBrowserTool } from './extensions/agent-browser';
import { reportChangedFiles } from './extensions/changed-file';
import { PiGameAssetTools } from './extensions/game-assets';
import { projectTaskState } from './extensions/task-state';
export type PiProductToolName =
| 'agent_browser'
| 'game_asset_browser'
| 'game_asset_review'
| 'task_state'
| 'changed_file'
| 'runtime_context'
| DataServicePiToolName;
export { DATA_SERVICE_PI_TOOL_NAMES };
const PI_PRODUCT_TOOL_NAMES = new Set<string>([
'agent_browser',
'game_asset_browser',
'game_asset_review',
'task_state',
'changed_file',
'runtime_context',
...DATA_SERVICE_PI_TOOL_NAMES,
]);
export function isPiProductToolName(value: unknown): value is PiProductToolName {
return typeof value === 'string' && PI_PRODUCT_TOOL_NAMES.has(value);
}
export interface PiProductToolContext {
conversationId: string;
runId: string;
resourceId: string;
projectId: string;
projectPath: string;
skillIds: readonly string[];
}
export interface PiProductToolResult {
content: Array<{ type: 'text'; text: string }>;
details: KnownToolDetails;
}
export interface PiProductToolsOptions {
browser: AgentBrowserModule;
attachments: CodingAttachmentStore;
bundledSkillsDir: string;
changeTracker?: ConversationChangeTracker;
dataService?: DataServiceOperations;
capabilityRegistry?: CodingCapabilityRegistry;
}
export class PiProductTools {
readonly changeTracker: ConversationChangeTracker;
private readonly browser: PiAgentBrowserTool;
private readonly gameAssets = new PiGameAssetTools();
private dataServiceAdapter: ReturnType<typeof createDataServicePluginAdapter> | undefined;
private capabilityRegistry: CodingCapabilityRegistry | undefined;
constructor(private readonly options: PiProductToolsOptions) {
this.changeTracker = options.changeTracker ?? new ConversationChangeTracker();
this.browser = new PiAgentBrowserTool(options.browser, options.attachments);
this.capabilityRegistry = options.capabilityRegistry;
this.dataServiceAdapter = options.dataService
? createDataServicePluginAdapter(options.dataService)
: undefined;
}
configureDataService(dataService: DataServiceOperations): void {
this.dataServiceAdapter = createDataServicePluginAdapter(dataService);
}
configureCapabilityRegistry(registry: CodingCapabilityRegistry): void {
this.capabilityRegistry = registry;
}
beginRun(input: { conversationId: string; runId: string; projectPath: string }) {
return this.changeTracker.beginRun(input);
}
settleRun(conversationId: string, runId: string) {
return this.changeTracker.settleRun(conversationId, runId);
}
getChanges(conversationId: string): ConversationChangesSnapshot | null {
return this.changeTracker.getSnapshot(conversationId);
}
listSkills(skillIds: readonly string[]): Promise<ProductCodingSkill[]> {
return listProductCodingSkills(this.options.bundledSkillsDir, skillIds);
}
async listCommands(
skillIds: readonly string[],
piCommands: readonly ProductPiCommandInput[] = [],
): Promise<ProductCodingCommand[]> {
return buildProductCodingCommandCatalog(await this.listSkills(skillIds), piCommands);
}
async markBash(conversationId: string, runId: string): Promise<void> {
await this.changeTracker.markProjectRefresh(conversationId, runId);
}
recordTouchedPaths(conversationId: string, runId: string, paths: readonly string[]) {
return this.changeTracker.recordTouchedPaths(conversationId, runId, paths);
}
async execute(
toolName: PiProductToolName | string,
context: PiProductToolContext,
input: unknown,
): Promise<PiProductToolResult> {
if (toolName === 'agent_browser') {
return await this.browser.execute(context, input);
}
if (toolName === 'game_asset_browser') {
return await this.gameAssets.browse(context.projectPath, input, context.resourceId);
}
if (toolName === 'game_asset_review') {
return await this.gameAssets.review(context.projectPath, input, context.resourceId);
}
if (toolName === 'task_state') return projectTaskState(input);
if (toolName === 'changed_file') {
return await reportChangedFiles(this.changeTracker, context, input);
}
if (this.capabilityRegistry && toolName !== 'runtime_context') {
return await this.capabilityRegistry.invoke({
toolName,
context,
workerRole: 'parent',
effectiveSkillIds: context.skillIds,
value: input,
});
}
if (DATA_SERVICE_PI_TOOL_NAMES.includes(toolName as DataServicePiToolName)) {
const adapter = this.dataServiceAdapter;
const definition = DATA_SERVICE_PLUGIN_DEFINITION.tools.find(({ name }) => name === toolName);
if (!adapter || !definition) throw new Error('Data Service tools are unavailable');
const trustedContext: TrustedCodingCapabilityContext = {
conversationId: context.conversationId,
runId: context.runId,
resourceId: context.resourceId,
requestId: `pi:${context.runId}:${context.resourceId}`,
localProjectId: context.projectId,
projectPath: context.projectPath,
durableProjectId: context.projectId,
workerRole: 'parent',
effectiveSkillIds: [...context.skillIds],
};
const result: AdapterInvocationResult = await adapter.invoke(trustedContext, definition, input);
return buildCapabilityToolResult(
DATA_SERVICE_PLUGIN_DEFINITION,
definition,
context,
result,
{ mode: 'included', status: 'included' },
);
}
if (toolName !== 'runtime_context') throw new Error('Product tool is unavailable');
const skills = await this.listSkills(context.skillIds);
const details: RuntimeContextDetailsV1 = {
schema: 'runtime-context.v1',
skills,
commands: buildProductCodingCommandCatalog(skills),
};
return {
content: [{ type: 'text', text: JSON.stringify(details) }],
details,
};
}
}