import type { CodingPluginToolDefinition } from '../../../shared/coding-plugins'; import type { CapabilityBillingReceiptV1 } from '../../../shared/data-service'; import { MarketplaceHostedAdmissionError, MarketplaceHostedAdmissionResolver, } from '../hosted-admission'; import type { MarketplacePackageClientPort, PluginPackageStore } from '../package-store'; import type { AdapterInvocationResult, CodingPluginAdapter, PluginBackendProjection, TrustedCodingCapabilityContext, } from '../registry'; import { WebSearchClientError, type WebSearchClient, type WebSearchRead, } from '../../services/web-search-client'; const PLUGIN_ID = 'makelore.web-search'; const TOOL_NAME = 'makelore_web_search'; const CAPABILITY_ID = 'web-search.search'; const OPERATION = 'search'; const MAX_QUERY_LENGTH = 2_000; const MAX_RETRY_AFTER_SECONDS = 86_400; const SAFE_ERROR_CODES = new Set([ 'authentication_required', 'confirmation_required', 'plugin_backend_invalid', 'plugin_backend_unavailable', 'plugin_backend_response_too_large', 'plugin_billing_unavailable', 'plugin_execution_unavailable', 'plugin_operation_conflict', 'plugin_account_changed', 'plugin_provider_unavailable', 'plugin_receipt_unavailable', 'plugin_release_admission_required', 'plugin_release_unavailable', 'plugin_reservation_expired', 'plugin_reservation_unavailable', 'plugin_runtime_stale', 'token_point_balance_exhausted', 'web_search_provider_rejected', 'web_search_rate_limited', 'web_search_request_invalid', 'web_search_result_invalid', 'web_search_submission_unknown', ]); type Input = Record; type MeteredBilling = Extract; const NOT_STARTED: MeteredBilling = { mode: 'platform_metered', status: 'not_started' }; const RECEIPT_UNAVAILABLE: MeteredBilling = { mode: 'platform_metered', status: 'receipt_unavailable', }; export interface WebSearchPluginAdapterOptions { readonly client: WebSearchClient; readonly marketplace?: MarketplacePackageClientPort; readonly packageStore?: Pick; readonly makeloreVersion?: string; readonly admissionResolver?: MarketplaceHostedAdmissionResolver; } function isRecord(value: unknown): value is Input { return value !== null && typeof value === 'object' && !Array.isArray(value); } function exactInput(value: Input): boolean { const keys = Object.keys(value); return keys.length === 2 && keys.includes('query') && keys.includes('confirmed'); } function validRetryAfter(value: unknown): value is number { return Number.isSafeInteger(value) && (value as number) >= 0 && (value as number) <= MAX_RETRY_AFTER_SECONDS; } function safeErrorMessage(code: string, status: number): string { switch (code) { case 'confirmation_required': return 'Explicit Token Point Web Search confirmation is required'; case 'plugin_input_invalid': return 'Web Search input is invalid'; case 'plugin_release_admission_required': return 'Web Search Release admission is required'; case 'plugin_release_unavailable': return 'Web Search Release is unavailable'; case 'plugin_account_changed': return 'Marketplace account changed while resolving Web Search admission'; case 'plugin_runtime_stale': return 'Web Search worker resources are stale'; case 'plugin_operation_conflict': return 'Web Search operation conflicts with an existing request'; case 'token_point_balance_exhausted': return 'Token Point balance is insufficient'; case 'plugin_provider_unavailable': return 'Web Search Provider is unavailable'; case 'web_search_provider_rejected': return 'Web Search Provider rejected the request'; case 'web_search_rate_limited': return 'Web Search is rate limited; confirm a new search later'; case 'web_search_submission_unknown': return 'Web Search submission status is unknown; do not retry automatically'; case 'plugin_receipt_unavailable': return 'Web Search billing status could not be synchronized; do not retry automatically'; case 'plugin_backend_invalid': return 'Web Search service returned an invalid response'; case 'plugin_backend_response_too_large': return 'Web Search service returned an oversized response'; case 'plugin_billing_unavailable': return 'Web Search billing is unavailable'; case 'authentication_required': return 'Works Square sign-in is required'; default: return status === 401 ? 'Works Square sign-in is required' : 'Web Search service is temporarily unavailable'; } } function safeErrorCode(code: unknown, fallback = 'plugin_backend_unavailable'): string { return typeof code === 'string' && SAFE_ERROR_CODES.has(code) ? code : fallback; } function failure( code: string, error: string, status: number, retryable: boolean, billing: MeteredBilling = NOT_STARTED, retryAfterSeconds?: number, ): AdapterInvocationResult { return { success: false, status, code, error, retryable, ...(validRetryAfter(retryAfterSeconds) ? { retry_after_seconds: retryAfterSeconds } : {}), payload_schema: 'web-search.v1', data: null, billing, }; } function resultData(result: WebSearchRead): { answer: string; sources: readonly { title: string; url: string }[]; searchQueries: readonly string[]; } | null { if (typeof result.answer !== 'string' || result.answer.length === 0 || result.answer.length > 16_000 || !Array.isArray(result.sources) || result.sources.length > 20 || !Array.isArray(result.searchQueries) || result.searchQueries.length > 8) return null; return { answer: result.answer, sources: result.sources, searchQueries: result.searchQueries, }; } function project(result: WebSearchRead): AdapterInvocationResult { if (result.status === 'succeeded' || result.status === 'pending_review') { const data = resultData(result); if (!data) { return failure('plugin_backend_invalid', 'Web Search result is incomplete', 502, false); } return { success: true, status: result.status === 'pending_review' ? 202 : 200, code: null, error: null, retryable: false, payload_schema: 'web-search.v1', data, billing: result.billing, }; } if (result.status === 'submission_unknown') { return failure( safeErrorCode(result.errorCode, 'web_search_submission_unknown'), 'Web Search submission status is unknown; do not retry automatically', 503, false, result.billing, ); } if (result.status === 'failed') { const rateLimited = result.errorCode === 'web_search_rate_limited'; return failure( safeErrorCode(result.errorCode, 'web_search_request_invalid'), rateLimited ? 'Web Search is rate limited; confirm a new search later' : 'Web Search request was rejected', rateLimited ? 429 : 422, false, result.billing, rateLimited ? result.retryAfterSeconds : undefined, ); } return failure( 'plugin_receipt_unavailable', 'Web Search billing status could not be synchronized; do not retry automatically', 503, false, RECEIPT_UNAVAILABLE, ); } function clientFailure(error: unknown): AdapterInvocationResult { if (error instanceof MarketplaceHostedAdmissionError) { const code = safeErrorCode(error.code); return failure(code, safeErrorMessage(code, error.status), error.status, error.retryable); } if (error instanceof WebSearchClientError) { const code = safeErrorCode(error.code); if (code === 'plugin_receipt_unavailable') { return failure(code, safeErrorMessage(code, 503), 503, false, RECEIPT_UNAVAILABLE); } return failure( code, safeErrorMessage(code, error.status), error.status, error.retryable, NOT_STARTED, error.status === 429 ? error.retryAfterSeconds : undefined, ); } return failure( 'plugin_backend_unavailable', 'Web Search service is temporarily unavailable', 503, true, ); } export class WebSearchPluginAdapter implements CodingPluginAdapter { readonly pluginId = PLUGIN_ID; private readonly admissionResolver: MarketplaceHostedAdmissionResolver; constructor(private readonly options: WebSearchPluginAdapterOptions) { if (options.admissionResolver) { this.admissionResolver = options.admissionResolver; } else if (options.marketplace && options.packageStore && options.makeloreVersion) { this.admissionResolver = new MarketplaceHostedAdmissionResolver({ marketplace: options.marketplace, packageStore: options.packageStore, makeloreVersion: options.makeloreVersion, }); } else { throw new TypeError('Web Search adapter requires Marketplace admission dependencies'); } } async inspect(_projectPath: string): Promise { const installed = await this.options.packageStore?.getInstalled(PLUGIN_ID).catch(() => null); return installed ? { status: 'ready' } : { status: 'unconfigured' }; } async invoke( context: TrustedCodingCapabilityContext, tool: CodingPluginToolDefinition, input: unknown, ): Promise { if (tool.name !== TOOL_NAME || tool.capabilityId !== CAPABILITY_ID || tool.operation !== OPERATION) { return failure('plugin_contract_unsupported', 'Web Search operation is unavailable', 503, true); } if (!isRecord(input) || !exactInput(input) || typeof input.query !== 'string' || !input.query.trim() || input.query.trim().length > MAX_QUERY_LENGTH) { return failure('plugin_input_invalid', 'Web Search input is invalid', 422, false); } if (input.confirmed !== true) { return failure( 'confirmation_required', 'Explicit Token Point Web Search confirmation is required', 400, false, ); } let admission: { releaseId: string; releaseAdmissionId: string }; try { admission = await this.admissionResolver.resolve({ pluginId: PLUGIN_ID, workerSnapshot: { requestId: context.requestId, ...(context.pluginReleaseId === undefined ? {} : { pluginReleaseId: context.pluginReleaseId }), }, }); } catch (error) { return clientFailure(error); } try { return project(await this.options.client.search({ releaseId: admission.releaseId, releaseAdmissionId: admission.releaseAdmissionId, projectId: context.durableProjectId, logicalOperationId: context.requestId, query: input.query.trim(), confirmed: true, })); } catch (error) { return clientFailure(error); } } } export function createWebSearchPluginAdapter( options: WebSearchPluginAdapterOptions, ): WebSearchPluginAdapter { return new WebSearchPluginAdapter(options); }