import type { AgentBrowserDetailsV1, ChangedFileDetailsV1, DataServiceToolDetailsV1, GameAssetsDetailsV1, KnownToolDetails, RuntimeContextDetailsV1, TaskStateDetailsV1, } from './coding-conversation-contracts'; import { DATA_SERVICE_PI_TOOL_NAMES, type DataServiceErrorContext, type DataServiceToolData, type DataServicePiToolName, } from './data-service'; const PRODUCT_TOOL_NAMES = new Set([ 'agent_browser', 'game_asset_browser', 'game_asset_review', 'task_state', 'changed_file', 'runtime_context', ...DATA_SERVICE_PI_TOOL_NAMES, ]); const DATA_SERVICE_CONTEXT_KEYS = new Set([ 'resource', 'limit', 'current', 'attempted', 'actual', 'allowed', 'current_revision', 'retry_after_seconds', ]); const DATA_SERVICE_CONTEXT_RESOURCES = new Set(['instances', 'collections', 'documents', 'bytes']); function record(value: unknown): Record | null { return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record : null; } function text(value: unknown, max = 4096): string | null { return typeof value === 'string' && value.trim() && value.length <= max ? value : null; } function strings(value: unknown, maxItems = 200): string[] | null { if (!Array.isArray(value) || value.length > maxItems) return null; const result: string[] = []; for (const item of value) { const normalized = text(item); if (!normalized) return null; result.push(normalized); } return result; } function relativePaths(value: unknown): string[] | null { const paths = strings(value); if (!paths) return null; const result: string[] = []; for (const candidate of paths) { const normalized = candidate.replaceAll('\\', '/').replace(/^\.\//, ''); if (!normalized || normalized.startsWith('/') || /^[A-Za-z]:\//.test(normalized) || normalized.split('/').some((segment) => segment === '..')) return null; result.push(normalized); } return result; } function browserDetails(value: Record): AgentBrowserDetailsV1 | null { const actions = new Set([ 'open', 'status', 'close', 'reset_profile', 'navigate', 'send_cdp', 'read_events', 'read_payload', ]); if (!actions.has(String(value.action))) return null; const attachmentId = value.attachmentId === undefined ? undefined : text(value.attachmentId, 128); const mime = value.mime === undefined ? undefined : text(value.mime, 128); if ((value.attachmentId !== undefined && !attachmentId) || (value.mime !== undefined && !mime)) return null; if (Boolean(attachmentId) !== Boolean(mime) || (attachmentId && value.action !== 'send_cdp')) return null; return { schema: 'agent-browser.v1', action: value.action as AgentBrowserDetailsV1['action'], ...(attachmentId ? { attachmentId } : {}), ...(mime ? { mime } : {}), }; } function gameAssetDetails(value: Record): GameAssetsDetailsV1 | null { const invocationId = text(value.invocationId, 200); const candidateIds = strings(value.candidateIds); const pendingAssetIds = strings(value.pendingAssetIds); const approvedAssetIds = strings(value.approvedAssetIds); const discardedAssetIds = strings(value.discardedAssetIds); if (!invocationId || !candidateIds || !pendingAssetIds || !approvedAssetIds || !discardedAssetIds) return null; if (value.status !== 'pending' && value.status !== 'resolved') return null; return { schema: 'game-assets.v1', invocationId, candidateIds, status: value.status, pendingAssetIds, approvedAssetIds, discardedAssetIds, }; } function taskDetails(value: Record): TaskStateDetailsV1 | null { if (!Array.isArray(value.tasks) || value.tasks.length === 0 || value.tasks.length > 100) return null; const tasks: TaskStateDetailsV1['tasks'] = []; for (const candidate of value.tasks) { const item = record(candidate); const id = text(item?.id, 128); const title = text(item?.title, 500); if (!item || !id || !title || !['pending', 'running', 'complete', 'error'].includes(String(item.status))) { return null; } tasks.push({ id, title, status: item.status as TaskStateDetailsV1['tasks'][number]['status'] }); } return { schema: 'task-state.v1', tasks }; } function runtimeContextDetails(value: Record): RuntimeContextDetailsV1 | null { if (!Array.isArray(value.skills) || value.skills.length > 100 || !Array.isArray(value.commands) || value.commands.length > 200) return null; const skills: RuntimeContextDetailsV1['skills'] = []; const commands: RuntimeContextDetailsV1['commands'] = []; for (const candidate of value.skills) { const item = record(candidate); const id = text(item?.id, 128); const name = text(item?.name, 200); if (!item || !id || !name || typeof item.description !== 'string' || item.description.length > 2000 || typeof item.selected !== 'boolean') return null; skills.push({ id, name, description: item.description, selected: item.selected }); } for (const candidate of value.commands) { const item = record(candidate); const name = text(item?.name, 128); const title = text(item?.title, 200); if (!item || !name || !title || typeof item.description !== 'string' || item.description.length > 2000 || !['makelore', 'pi', 'skill'].includes(String(item.source))) return null; const skillId = item.skillId === undefined ? undefined : text(item.skillId, 128); if (item.skillId !== undefined && !skillId) return null; commands.push({ name, title, description: item.description, source: item.source as RuntimeContextDetailsV1['commands'][number]['source'], ...(skillId ? { skillId } : {}), }); } return { schema: 'runtime-context.v1', skills, commands }; } function dataServiceContext(value: unknown): DataServiceErrorContext | undefined | null { if (value === undefined) return undefined; const source = record(value); if (!source || Object.keys(source).length > 8) return null; const context: DataServiceErrorContext = {}; for (const [key, item] of Object.entries(source)) { if (!DATA_SERVICE_CONTEXT_KEYS.has(key)) return null; if (key === 'resource') { if (typeof item !== 'string' || !DATA_SERVICE_CONTEXT_RESOURCES.has(item)) return null; context[key] = item; } else if (Number.isSafeInteger(item) && (item as number) >= 0) { context[key] = item as number; } else { return null; } } return Object.keys(context).length > 0 ? context : undefined; } function dataServiceDetails(value: Record): DataServiceToolDetailsV1 | null { if (!DATA_SERVICE_PI_TOOL_NAMES.includes(value.operation as DataServicePiToolName)) return null; if (typeof value.success !== 'boolean' || !Number.isSafeInteger(value.status) || (value.status as number) < 100 || (value.status as number) > 599 || (value.code !== null && text(value.code, 128) === null) || (value.error !== null && text(value.error, 2_000) === null) || typeof value.retryable !== 'boolean' || !Object.prototype.hasOwnProperty.call(value, 'data')) return null; if (value.success) { if ((value.status as number) < 200 || (value.status as number) > 299 || value.code !== null || value.error !== null || value.retryable !== false) return null; } else if ((value.status as number) < 400 || value.code === null || value.error === null) { return null; } const retryAfter = value.retry_after_seconds; if (retryAfter !== undefined && (!Number.isSafeInteger(retryAfter) || (retryAfter as number) < 0 || (retryAfter as number) > 86_400)) { return null; } const context = dataServiceContext(value.context); if (context === null) return null; const data = value.data === null ? null : record(value.data); if (value.data !== null && !data) return null; return { schema: 'data-service.v1', operation: value.operation as DataServicePiToolName, success: value.success, status: value.status as number, code: value.code as string | null, error: value.error as string | null, retryable: value.retryable, ...(retryAfter === undefined ? {} : { retry_after_seconds: retryAfter as number }), ...(context === undefined ? {} : { context }), data: data as DataServiceToolData | null, }; } export function productToolDetails(value: unknown): Exclude | null { const details = record(value); if (!details) return null; if (details.schema === 'changed-file.v1') { const paths = relativePaths(details.paths); return paths ? { schema: 'changed-file.v1', paths } satisfies ChangedFileDetailsV1 : null; } if (details.schema === 'task-state.v1') return taskDetails(details); if (details.schema === 'agent-browser.v1') return browserDetails(details); if (details.schema === 'game-assets.v1') return gameAssetDetails(details); if (details.schema === 'runtime-context.v1') return runtimeContextDetails(details); if (details.schema === 'data-service.v1') return dataServiceDetails(details); return null; } export function productToolDetailsOfResult(value: unknown) { return productToolDetails(record(value)?.details); } export function isProductToolName(value: string): boolean { return PRODUCT_TOOL_NAMES.has(value); }