export type ToolBatchItem = { itemKey: string; payload: Record; inputHash?: string | null; }; export type ToolBatchRequest = { runId: string; orgId: string; toolId: string; operation: string; provider: string; items: ToolBatchItem[]; waterfallId?: string | null; stageId?: string | null; fieldName?: string | null; mapName?: string | null; chunkIndex?: number | null; userProvidedRateLimitKey?: string | null; providerBatchSize: number; }; export type ToolBatchItemResult = { itemKey: string; result: unknown; cached?: boolean; }; export type ToolBatchResult = { runId: string; toolId: string; operation: string; provider: string; batchCount: number; itemCount: number; results: ToolBatchItemResult[]; }; export type ToolBatchExecutorTransport = { executeProviderBatch(input: { request: ToolBatchRequest; batchIndex: number; idempotencyKeys: string[]; rateLimitKey: string; items: ToolBatchItem[]; }): Promise; }; export type ToolBatchExecutor = { executeToolBatch(request: ToolBatchRequest): Promise; }; export function createToolBatchExecutor( transport: ToolBatchExecutorTransport, ): ToolBatchExecutor { return { async executeToolBatch(request) { const providerBatchSize = Math.max( 1, Math.floor(request.providerBatchSize), ); const batches = chunkToolBatchItems(request.items, providerBatchSize); const results: ToolBatchItemResult[] = []; for (let batchIndex = 0; batchIndex < batches.length; batchIndex += 1) { const items = batches[batchIndex]!; results.push( ...(await transport.executeProviderBatch({ request, batchIndex, items, rateLimitKey: buildToolBatchRateLimitKey(request), idempotencyKeys: items.map((item) => buildToolBatchIdempotencyKey(request, item), ), })), ); } return { runId: request.runId, toolId: request.toolId, operation: request.operation, provider: request.provider, batchCount: batches.length, itemCount: request.items.length, results, }; }, }; } export function buildToolBatchIdempotencyKey( request: ToolBatchRequest, item: ToolBatchItem, ): string { return [ request.runId, request.mapName ?? '', request.chunkIndex ?? '', item.itemKey, request.fieldName ?? '', request.waterfallId ?? '', request.stageId ?? '', item.inputHash ?? stableToolBatchHash(item.payload), ].join(':'); } export function buildToolBatchRateLimitKey(request: ToolBatchRequest): string { return [ request.orgId, request.provider, request.operation, request.userProvidedRateLimitKey ?? '', ].join(':'); } function chunkToolBatchItems( items: readonly ToolBatchItem[], size: number, ): ToolBatchItem[][] { const chunks: ToolBatchItem[][] = []; for (let index = 0; index < items.length; index += size) { chunks.push(items.slice(index, index + size)); } return chunks; } function stableToolBatchHash(value: unknown): string { const text = stableStringify(value); let hash = 2166136261; for (let index = 0; index < text.length; index += 1) { hash ^= text.charCodeAt(index); hash = Math.imul(hash, 16777619); } return (hash >>> 0).toString(36); } function stableStringify(value: unknown): string { if (value === null || typeof value !== 'object') { return JSON.stringify(value); } if (Array.isArray(value)) { return `[${value.map(stableStringify).join(',')}]`; } const record = value as Record; return `{${Object.keys(record) .sort() .map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`) .join(',')}}`; }