| /** |
| * Derive HostCapabilities and deferred ToolAvailability groups from the shared |
| * tool catalog ∩ host binding (#1099). Hosts still construct MakaTool |
| * instances; this module only projects names and surface metadata. |
| */ |
| |
| import { |
| MAKA_CATALOG_SURFACES, |
| catalogSurfaceById, |
| catalogToolByName, |
| unknownBoundToolNames, |
| type ToolHostId, |
| } from '@maka/core'; |
| import type { HostCapabilities } from './skills-context.js'; |
| import type { ToolGroup } from './tool-availability.js'; |
| import type { MakaTool } from './tool-runtime.js'; |
| |
| export interface ProductToolSurfacePolicy { |
| readonly economy: boolean; |
| readonly disabledSurfaceIds?: Iterable<string>; |
| } |
| |
| export interface NormalizedProductToolSurfacePolicy { |
| readonly economy: boolean; |
| readonly disabledSurfaceIds: readonly string[]; |
| } |
| |
| export interface ProductToolSurfaceIdentity { |
| readonly policy: NormalizedProductToolSurfacePolicy; |
| readonly productToolNames: readonly string[]; |
| } |
| |
| export interface EffectiveProductToolSurface { |
| readonly tools: readonly MakaTool[]; |
| readonly toolNames: ReadonlySet<string>; |
| readonly productToolNames: readonly string[]; |
| readonly hostCapabilities: HostCapabilities; |
| readonly toolAvailability: { |
| readonly economy: boolean; |
| readonly groups: readonly ToolGroup[]; |
| }; |
| readonly boundSurfaceIds: readonly string[]; |
| readonly identity: ProductToolSurfaceIdentity; |
| } |
| |
| interface ReadonlySetOperand<T> { |
| readonly size: number; |
| has(value: T): boolean; |
| keys(): Iterator<T>; |
| } |
| |
| interface ReadonlySetAlgebra<T> { |
| union<U>(other: ReadonlySetOperand<U>): Set<T | U>; |
| intersection<U>(other: ReadonlySetOperand<U>): Set<T & U>; |
| difference<U>(other: ReadonlySetOperand<U>): Set<T>; |
| symmetricDifference<U>(other: ReadonlySetOperand<U>): Set<T | U>; |
| isSubsetOf(other: ReadonlySetOperand<unknown>): boolean; |
| isSupersetOf(other: ReadonlySetOperand<unknown>): boolean; |
| isDisjointFrom(other: ReadonlySetOperand<unknown>): boolean; |
| } |
| |
| function readonlySetSnapshot<T>(values: Iterable<T>): ReadonlySet<T> { |
| const snapshot = new Set(values); |
| const algebra = snapshot as Set<T> & ReadonlySetAlgebra<T>; |
| const view: ReadonlySet<T> = Object.freeze({ |
| get size() { |
| return snapshot.size; |
| }, |
| has: (value: T) => snapshot.has(value), |
| entries: () => snapshot.entries(), |
| keys: () => snapshot.keys(), |
| values: () => snapshot.values(), |
| forEach: (callback: (value: T, value2: T, set: ReadonlySet<T>) => void, thisArg?: unknown) => { |
| snapshot.forEach((value, value2) => callback.call(thisArg, value, value2, view)); |
| }, |
| union: <U>(other: ReadonlySetOperand<U>) => algebra.union(other), |
| intersection: <U>(other: ReadonlySetOperand<U>) => algebra.intersection(other), |
| difference: <U>(other: ReadonlySetOperand<U>) => algebra.difference(other), |
| symmetricDifference: <U>(other: ReadonlySetOperand<U>) => algebra.symmetricDifference(other), |
| isSubsetOf: (other: ReadonlySetOperand<unknown>) => algebra.isSubsetOf(other), |
| isSupersetOf: (other: ReadonlySetOperand<unknown>) => algebra.isSupersetOf(other), |
| isDisjointFrom: (other: ReadonlySetOperand<unknown>) => algebra.isDisjointFrom(other), |
| [Symbol.iterator]: () => snapshot[Symbol.iterator](), |
| [Symbol.toStringTag]: 'Set', |
| }); |
| return view; |
| } |
| |
| export function projectEffectiveProductToolSurface(input: { |
| host: ToolHostId; |
| tools: readonly MakaTool[]; |
| policy: ProductToolSurfacePolicy; |
| }): EffectiveProductToolSurface { |
| const disabledSurfaceIds = [...new Set(input.policy.disabledSurfaceIds ?? [])].sort(); |
| const excludedToolNames = new Set<string>(); |
| for (const surfaceId of disabledSurfaceIds) { |
| const surface = catalogSurfaceById(surfaceId); |
| if (!surface) throw new Error(`Unknown product-tool surface "${surfaceId}"`); |
| for (const name of surface.toolNames) excludedToolNames.add(name); |
| } |
| for (const surface of MAKA_CATALOG_SURFACES) { |
| if (surface.hosts[input.host] === 'supported') continue; |
| for (const name of surface.toolNames) excludedToolNames.add(name); |
| } |
| const tools = input.tools.filter((tool) => !excludedToolNames.has(tool.name)); |
| const boundToolNames = new Set(tools.map((tool) => tool.name)); |
| const toolNames = readonlySetSnapshot(boundToolNames); |
| const productToolNames = [...boundToolNames].filter((name) => catalogToolByName(name)).sort(); |
| const groups = buildDeferredToolGroupsFromCatalog(input.host, boundToolNames).map((group) => |
| Object.freeze({ |
| ...group, |
| toolNames: Object.freeze([...group.toolNames]), |
| }), |
| ); |
| const hostCapabilities = buildHostCapabilitiesFromBinding(boundToolNames); |
| const policy = Object.freeze({ |
| economy: input.policy.economy, |
| disabledSurfaceIds: Object.freeze(disabledSurfaceIds), |
| }); |
| return Object.freeze({ |
| tools: Object.freeze(tools), |
| toolNames, |
| productToolNames: Object.freeze(productToolNames), |
| hostCapabilities, |
| toolAvailability: Object.freeze({ |
| economy: input.policy.economy, |
| groups: Object.freeze(groups), |
| }), |
| boundSurfaceIds: Object.freeze(groups.map((group) => group.id)), |
| identity: Object.freeze({ |
| policy, |
| productToolNames: Object.freeze([...productToolNames]), |
| }), |
| }); |
| } |
| |
| /** Build skill-host capability surface from the tools this process actually bound. */ |
| export function buildHostCapabilitiesFromBinding( |
| boundToolNames: Iterable<string>, |
| ): HostCapabilities { |
| const toolNames = new Set<string>(); |
| const capabilities = new Set<string>(); |
| for (const name of boundToolNames) { |
| toolNames.add(name); |
| const tags = catalogToolByName(name)?.capabilityTags; |
| if (!tags) continue; |
| for (const tag of tags) capabilities.add(tag); |
| } |
| const readonlyToolNames = readonlySetSnapshot(toolNames); |
| if (capabilities.size === 0) return Object.freeze({ toolNames: readonlyToolNames }); |
| return Object.freeze({ |
| toolNames: readonlyToolNames, |
| capabilities: readonlySetSnapshot(capabilities), |
| }); |
| } |
| |
| /** |
| * Deferred `load_tools` groups for a host: catalog surfaces that are supported |
| * on the host, deferred, and have at least one bound member. Unsupported |
| * affinity never appears, even if a name were somehow bound. |
| */ |
| export function buildDeferredToolGroupsFromCatalog( |
| host: ToolHostId, |
| boundToolNames: Iterable<string>, |
| ): ToolGroup[] { |
| const bound = boundToolNames instanceof Set ? boundToolNames : new Set(boundToolNames); |
| const groups: ToolGroup[] = []; |
| for (const surface of MAKA_CATALOG_SURFACES) { |
| if (surface.economy !== 'deferred') continue; |
| if (surface.hosts[host] !== 'supported') continue; |
| const toolNames = surface.toolNames.filter((name) => bound.has(name)); |
| if (toolNames.length === 0) continue; |
| groups.push({ |
| id: surface.id, |
| label: surface.label, |
| description: surface.description, |
| toolNames, |
| }); |
| } |
| return groups; |
| } |
| |
| /** |
| * Product-tool catalog cleanliness for host wiring (#1099 S2). |
| * |
| * MCP tools (`mcp__…`) are external and out of product-catalog scope. Harness / |
| * experiment names may be excluded by the caller before invoking this helper. |
| * Throws when any remaining bound name is missing from the catalog. |
| */ |
| export function assertProductBindingCatalogClean( |
| hostLabel: string, |
| boundToolNames: Iterable<string>, |
| ): void { |
| const productNames = [...boundToolNames].filter((name) => !name.startsWith('mcp__')); |
| const unknown = unknownBoundToolNames(productNames); |
| if (unknown.length === 0) return; |
| throw new Error( |
| `[tool-catalog] ${hostLabel}: bound product tools missing from catalog: ${unknown.join(', ')}`, |
| ); |
| } |