197 lines
7.1 KiB
TypeScript
197 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 {
|
|
type ProductCodingPluginSkillSource,
|
|
} from '../../coding-projects/skill-registry';
|
|
import {
|
|
type CodingCapabilityRegistry,
|
|
} from '../../coding-plugins/registry';
|
|
import type { EffectivePluginSnapshot } from '../../coding-plugins/effective-resolver';
|
|
import type { KnownToolDetails, RuntimeContextDetailsV1 } from '../contracts';
|
|
import { BUNDLED_CODING_SKILL_IDS } from '../../../shared/coding-skills';
|
|
import { PiAgentBrowserTool } from './extensions/agent-browser';
|
|
import { reportChangedFiles } from './extensions/changed-file';
|
|
import { projectTaskState } from './extensions/task-state';
|
|
import type { ModelToolRegistryPort } from './model-tools/model-tool-registry';
|
|
import type { DevicePackageTools } from '../../coding-packages/device-package-tools';
|
|
import { DEVICE_PACKAGE_TOOL_NAMES } from '../../../shared/device-packages';
|
|
|
|
export type PiProductToolName =
|
|
| 'agent_browser'
|
|
| 'task_state'
|
|
| 'changed_file'
|
|
| 'runtime_context';
|
|
|
|
const PI_PRODUCT_TOOL_NAMES = new Set<string>([
|
|
'agent_browser',
|
|
'task_state',
|
|
'changed_file',
|
|
'runtime_context',
|
|
]);
|
|
|
|
export function isPiProductToolName(value: unknown): value is PiProductToolName {
|
|
return typeof value === 'string' && PI_PRODUCT_TOOL_NAMES.has(value);
|
|
}
|
|
|
|
export interface PiProductToolContext {
|
|
conversationId: string;
|
|
workerGeneration?: number;
|
|
runId: string;
|
|
resourceId: string;
|
|
projectId: string;
|
|
projectPath: string;
|
|
skillIds: readonly string[];
|
|
effectiveSnapshot?: EffectivePluginSnapshot;
|
|
}
|
|
|
|
export interface PiProductToolResult {
|
|
content: Array<{ type: 'text'; text: string }>;
|
|
details: KnownToolDetails;
|
|
}
|
|
|
|
export interface PiProductToolsOptions {
|
|
browser: AgentBrowserModule;
|
|
attachments: CodingAttachmentStore;
|
|
bundledSkillsDir: string;
|
|
changeTracker?: ConversationChangeTracker;
|
|
pluginSkillSources?: readonly ProductCodingPluginSkillSource[];
|
|
getPluginSkillSources?(): readonly ProductCodingPluginSkillSource[]
|
|
| Promise<readonly ProductCodingPluginSkillSource[]>;
|
|
capabilityRegistry?: CodingCapabilityRegistry;
|
|
modelToolRegistry?: ModelToolRegistryPort;
|
|
devicePackageTools?: DevicePackageTools;
|
|
}
|
|
|
|
export class PiProductTools {
|
|
readonly changeTracker: ConversationChangeTracker;
|
|
private readonly browser: PiAgentBrowserTool;
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
async listSkills(
|
|
skillIds: readonly string[],
|
|
availablePluginSkillIds: readonly string[] = [],
|
|
): Promise<ProductCodingSkill[]> {
|
|
const available = new Set(availablePluginSkillIds);
|
|
const dynamicSources = this.options.getPluginSkillSources
|
|
? await this.options.getPluginSkillSources()
|
|
: [];
|
|
const uniqueSources = [...new Map(
|
|
[...(this.options.pluginSkillSources ?? []), ...dynamicSources]
|
|
.map((source) => [source.id, source] as const),
|
|
).values()];
|
|
const sources = uniqueSources.map((source) => ({
|
|
...source,
|
|
available: available.has(source.pluginId ?? source.id) || available.has(source.id),
|
|
}));
|
|
const selectedSkillIds = this.options.getPluginSkillSources
|
|
? skillIds.filter((id) => BUNDLED_CODING_SKILL_IDS.includes(id)
|
|
|| uniqueSources.some((source) => source.id === id))
|
|
: skillIds;
|
|
return listProductCodingSkills(this.options.bundledSkillsDir, selectedSkillIds, sources);
|
|
}
|
|
|
|
async listCommands(
|
|
skillIds: readonly string[],
|
|
piCommands: readonly ProductPiCommandInput[] = [],
|
|
availablePluginSkillIds: readonly string[] = [],
|
|
): Promise<ProductCodingCommand[]> {
|
|
return buildProductCodingCommandCatalog(
|
|
await this.listSkills(skillIds, availablePluginSkillIds),
|
|
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,
|
|
onUpdate?: (result: PiProductToolResult) => void,
|
|
): Promise<PiProductToolResult> {
|
|
if (DEVICE_PACKAGE_TOOL_NAMES.includes(toolName as typeof DEVICE_PACKAGE_TOOL_NAMES[number])) {
|
|
if (!this.options.devicePackageTools) throw new Error('Device package management is unavailable');
|
|
return await this.options.devicePackageTools.invoke(toolName, context.runId, input);
|
|
}
|
|
if (toolName === 'web_search') {
|
|
if (!this.options.modelToolRegistry) throw new Error('Model Web Search is unavailable');
|
|
return await this.options.modelToolRegistry.invoke('web_search', {
|
|
conversationId: context.conversationId,
|
|
workerGeneration: context.workerGeneration ?? 0,
|
|
runId: context.runId,
|
|
resourceId: context.resourceId,
|
|
}, input);
|
|
}
|
|
if (toolName === 'agent_browser') {
|
|
return await this.browser.execute(context, input);
|
|
}
|
|
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,
|
|
...(onUpdate ? { onUpdate } : {}),
|
|
});
|
|
}
|
|
if (toolName !== 'runtime_context') throw new Error('Product tool is unavailable');
|
|
const skills = await this.listSkills(context.skillIds, context.skillIds);
|
|
const details: RuntimeContextDetailsV1 = {
|
|
schema: 'runtime-context.v1',
|
|
skills,
|
|
commands: buildProductCodingCommandCatalog(skills),
|
|
};
|
|
return {
|
|
content: [{ type: 'text', text: JSON.stringify(details) }],
|
|
details,
|
|
};
|
|
}
|
|
|
|
}
|