feat(coding): add model tools and device packages
This commit is contained in:
@@ -1,300 +0,0 @@
|
||||
import type { CodingPluginToolDefinition } from '../../../shared/coding-plugins';
|
||||
import type { CapabilityBillingReceiptV1 } from '../../../shared/data-service';
|
||||
import {
|
||||
type BundledHostedRelease,
|
||||
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<string, unknown>;
|
||||
type MeteredBilling = Extract<CapabilityBillingReceiptV1, { mode: 'platform_metered' }>;
|
||||
|
||||
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<PluginPackageStore, 'getInstalled' | 'getInstalledRelease'>;
|
||||
readonly makeloreVersion?: string;
|
||||
readonly bundledReleases?: Readonly<Record<string, BundledHostedRelease>>;
|
||||
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,
|
||||
bundledReleases: options.bundledReleases,
|
||||
});
|
||||
} else {
|
||||
throw new TypeError('Web Search adapter requires Marketplace admission dependencies');
|
||||
}
|
||||
}
|
||||
|
||||
async inspect(_projectPath: string): Promise<PluginBackendProjection> {
|
||||
if (this.options.bundledReleases?.[PLUGIN_ID]) return { status: 'ready' };
|
||||
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<AdapterInvocationResult> {
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user