blob: e51afa724c1cae780822d829504fd51bdb547417 [file]
import {
WEB_SEARCH_MAX_LIMIT,
WEB_SEARCH_PROVIDERS,
type WebSearchErrorReason,
type WebSearchResponse,
type WebSearchResultRow,
} from '@maka/core';
import {
requireEncodedByteLimit,
requireExactRecord,
requireRecord,
requireShapedRecord,
requireUtf8String,
} from './codec.js';
import { invalidProtocolFrame } from './errors.js';
import { defineOperation } from './operation-spec.js';
const WEB_SEARCH_QUERY_MAX_BYTES = 800;
const WEB_SEARCH_SECRET_MAX_BYTES = 16 * 1024;
const WEB_SEARCH_RESULT_MAX_BYTES = 32 * 1024;
const WEB_SEARCH_TEXT_MAX_BYTES = 4 * 1024;
const WEB_SEARCH_ERROR_REASONS: readonly WebSearchErrorReason[] = [
'invalid_query',
'incognito_active',
'not_configured',
'invalid_credentials',
'rate_limited',
'network_error',
'timeout',
'unsupported_provider',
'experimental_disabled',
];
const ERRORS = [
'host_not_ready',
'host_draining',
'operation_unavailable',
'invalid_request',
'internal_failure',
] as const;
export type WebSearchExecuteInput =
| {
readonly kind: 'query';
readonly query: string;
readonly limit: number;
readonly apiKey?: string;
}
| {
readonly kind: 'test';
readonly provider: 'tavily';
readonly apiKey?: string;
};
export type WebSearchExecuteResult = WebSearchResponse;
export const WEB_SEARCH_OPERATION_SPECS = {
'web-search.execute': defineOperation<
WebSearchExecuteInput,
WebSearchExecuteResult,
(typeof ERRORS)[number]
>({
mode: 'command',
availability: 'ready',
errors: ERRORS,
decodeInput: decodeWebSearchExecuteInput,
decodeOutput: decodeWebSearchExecuteResult,
}),
} as const;
function decodeWebSearchExecuteInput(value: unknown): WebSearchExecuteInput {
const record = requireRecord(value, 'Web Search execute input');
if (record.kind === 'query') {
const exact = requireShapedRecord(
record,
'Web Search query input',
['kind', 'query', 'limit'],
['apiKey'],
);
if (
!Number.isSafeInteger(exact.limit) ||
(exact.limit as number) < 1 ||
(exact.limit as number) > WEB_SEARCH_MAX_LIMIT
) {
throw invalidProtocolFrame('Invalid Web Search result limit');
}
return {
kind: 'query',
query: requireUtf8String(exact.query, 'Web Search query', WEB_SEARCH_QUERY_MAX_BYTES),
limit: exact.limit as number,
...(exact.apiKey === undefined
? {}
: {
apiKey: requireUtf8String(
exact.apiKey,
'Web Search credential override',
WEB_SEARCH_SECRET_MAX_BYTES,
),
}),
};
}
if (record.kind === 'test') {
const exact = requireShapedRecord(
record,
'Web Search test input',
['kind', 'provider'],
['apiKey'],
);
if (exact.provider !== 'tavily') {
throw invalidProtocolFrame('Invalid Web Search test provider');
}
return {
kind: 'test',
provider: exact.provider,
...(exact.apiKey === undefined
? {}
: {
apiKey: requireUtf8String(
exact.apiKey,
'Web Search credential override',
WEB_SEARCH_SECRET_MAX_BYTES,
),
}),
};
}
throw invalidProtocolFrame('Invalid Web Search execute kind');
}
function decodeWebSearchExecuteResult(value: unknown): WebSearchExecuteResult {
const record = requireRecord(value, 'Web Search execute result');
if (record.ok === false) {
const exact = requireExactRecord(record, 'Web Search error result', [
'ok',
'reason',
'message',
]);
if (
typeof exact.reason !== 'string' ||
!WEB_SEARCH_ERROR_REASONS.includes(exact.reason as WebSearchErrorReason)
) {
throw invalidProtocolFrame('Invalid Web Search error reason');
}
const result: WebSearchExecuteResult = {
ok: false,
reason: exact.reason as WebSearchErrorReason,
message: requireUtf8String(
exact.message,
'Web Search error message',
WEB_SEARCH_TEXT_MAX_BYTES,
),
};
requireEncodedByteLimit(result, 'Web Search result', WEB_SEARCH_RESULT_MAX_BYTES);
return result;
}
const exact = requireShapedRecord(
record,
'Web Search success result',
['ok', 'results'],
['provider'],
);
if (
exact.ok !== true ||
!Array.isArray(exact.results) ||
exact.results.length > WEB_SEARCH_MAX_LIMIT
) {
throw invalidProtocolFrame('Invalid Web Search success result');
}
if (
exact.provider !== undefined &&
!WEB_SEARCH_PROVIDERS.includes(exact.provider as (typeof WEB_SEARCH_PROVIDERS)[number])
) {
throw invalidProtocolFrame('Invalid Web Search result provider');
}
const results = exact.results.map((row) => decodeWebSearchRow(row));
const result: WebSearchExecuteResult = {
ok: true,
...(exact.provider === undefined
? {}
: { provider: exact.provider as (typeof WEB_SEARCH_PROVIDERS)[number] }),
results,
};
requireEncodedByteLimit(result, 'Web Search result', WEB_SEARCH_RESULT_MAX_BYTES);
return result;
}
function decodeWebSearchRow(value: unknown): WebSearchResultRow {
const row = requireExactRecord(value, 'Web Search result row', [
'provider',
'title',
'url',
'snippet',
'source',
]);
if (!WEB_SEARCH_PROVIDERS.includes(row.provider as (typeof WEB_SEARCH_PROVIDERS)[number])) {
throw invalidProtocolFrame('Invalid Web Search row provider');
}
return {
provider: row.provider as (typeof WEB_SEARCH_PROVIDERS)[number],
title: requireUtf8String(row.title, 'Web Search result title', WEB_SEARCH_TEXT_MAX_BYTES),
url: requireUtf8String(row.url, 'Web Search result URL', WEB_SEARCH_TEXT_MAX_BYTES),
snippet:
typeof row.snippet === 'string' &&
Buffer.byteLength(row.snippet, 'utf8') <= WEB_SEARCH_TEXT_MAX_BYTES
? row.snippet
: (() => {
throw invalidProtocolFrame('Invalid Web Search result snippet');
})(),
source: requireUtf8String(row.source, 'Web Search result source', WEB_SEARCH_TEXT_MAX_BYTES),
};
}