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([ '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 | 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 { return listProductCodingSkills(this.options.bundledSkillsDir, skillIds); } async listCommands( skillIds: readonly string[], piCommands: readonly ProductPiCommandInput[] = [], ): Promise { return buildProductCodingCommandCatalog(await this.listSkills(skillIds), piCommands); } async markBash(conversationId: string, runId: string): Promise { 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 { 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, }; } }