@ai-agent-forge/plugin-search 0.0.0-stage → 0.88.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +56 -2
  2. package/agent-forge.json +19 -0
  3. package/dist/cache.d.ts +103 -0
  4. package/dist/cache.d.ts.map +1 -0
  5. package/dist/cache.js +666 -0
  6. package/dist/cache.js.map +1 -0
  7. package/dist/capability.d.ts +22 -0
  8. package/dist/capability.d.ts.map +1 -0
  9. package/dist/capability.js +213 -0
  10. package/dist/capability.js.map +1 -0
  11. package/dist/entry.d.ts +3 -0
  12. package/dist/entry.d.ts.map +1 -0
  13. package/dist/entry.js +3 -0
  14. package/dist/entry.js.map +1 -0
  15. package/dist/extract.d.ts +18 -0
  16. package/dist/extract.d.ts.map +1 -0
  17. package/dist/extract.js +120 -0
  18. package/dist/extract.js.map +1 -0
  19. package/dist/http.d.ts +65 -0
  20. package/dist/http.d.ts.map +1 -0
  21. package/dist/http.js +307 -0
  22. package/dist/http.js.map +1 -0
  23. package/dist/index.d.ts +5 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +5 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/limits.d.ts +39 -0
  28. package/dist/limits.d.ts.map +1 -0
  29. package/dist/limits.js +43 -0
  30. package/dist/limits.js.map +1 -0
  31. package/dist/model-call.d.ts +10 -0
  32. package/dist/model-call.d.ts.map +1 -0
  33. package/dist/model-call.js +46 -0
  34. package/dist/model-call.js.map +1 -0
  35. package/dist/model-search.d.ts +14 -0
  36. package/dist/model-search.d.ts.map +1 -0
  37. package/dist/model-search.js +85 -0
  38. package/dist/model-search.js.map +1 -0
  39. package/dist/summarize.d.ts +40 -0
  40. package/dist/summarize.d.ts.map +1 -0
  41. package/dist/summarize.js +148 -0
  42. package/dist/summarize.js.map +1 -0
  43. package/dist/text.d.ts +38 -0
  44. package/dist/text.d.ts.map +1 -0
  45. package/dist/text.js +135 -0
  46. package/dist/text.js.map +1 -0
  47. package/dist/types.d.ts +38 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +2 -0
  50. package/dist/types.js.map +1 -0
  51. package/dist/url-search.d.ts +36 -0
  52. package/dist/url-search.d.ts.map +1 -0
  53. package/dist/url-search.js +335 -0
  54. package/dist/url-search.js.map +1 -0
  55. package/package.json +57 -5
  56. package/plugin.json +12 -0
package/dist/http.d.ts ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Bounded HTTP fetch for the search tool (搜索工具设计 §6): http(s) only,
3
+ * manual redirects, per-page timeout/size caps, no cookies or provider
4
+ * credentials. Domain/private-network policy is deliberately out of scope;
5
+ * deployments that need it install a policy plugin before the tool call.
6
+ */
7
+ import type { NetworkRouteConfigV1 } from "@agent-forge/plugin-sdk";
8
+ export interface FetchResponseLike {
9
+ readonly status: number;
10
+ readonly headers: {
11
+ get(name: string): string | null;
12
+ };
13
+ readonly body?: AsyncIterable<Uint8Array> | null;
14
+ text(): Promise<string>;
15
+ }
16
+ export type FetchImpl = (url: string, init: {
17
+ method?: string;
18
+ headers?: Record<string, string>;
19
+ redirect: "manual";
20
+ signal: AbortSignal;
21
+ dispatcher?: unknown;
22
+ }) => Promise<FetchResponseLike>;
23
+ export type PageErrorCodeV1 = "invalid_url" | "invalid_redirect" | "too_many_redirects" | "http_error" | "timeout" | "network_error" | "too_large" | "unsupported_content_type" | "empty_content" | "cancelled";
24
+ export declare class PageError extends Error {
25
+ readonly code: PageErrorCodeV1;
26
+ readonly httpStatus?: number;
27
+ /** Raw bytes consumed before the failure, when the body read reached them. */
28
+ readonly bytesRead?: number;
29
+ constructor(code: PageErrorCodeV1, message: string, httpStatus?: number, bytesRead?: number);
30
+ }
31
+ export interface FetchedPageV1 {
32
+ readonly finalUrl: string;
33
+ readonly title?: string;
34
+ readonly contentType: string;
35
+ readonly text: string;
36
+ readonly truncated: boolean;
37
+ readonly bytesFetched: number;
38
+ }
39
+ export interface FetchPageOptionsV1 {
40
+ readonly url: string;
41
+ readonly maxTextBytes: number;
42
+ readonly maxRawBytes: number;
43
+ readonly timeoutMs: number;
44
+ readonly signal?: AbortSignal;
45
+ readonly network?: NetworkRouteConfigV1;
46
+ readonly fetchImpl?: FetchImpl;
47
+ readonly dispatchers?: DispatcherPool;
48
+ }
49
+ /**
50
+ * Lazily-created undici dispatchers; closed by the caller. Direct routes get an
51
+ * explicit Agent so the host's process-wide env-proxy dispatcher cannot
52
+ * silently re-route a settings `direct` mode or a bypassed host.
53
+ */
54
+ export declare class DispatcherPool {
55
+ private readonly agents;
56
+ private directAgent;
57
+ private readonly network;
58
+ private readonly enabled;
59
+ constructor(network: NetworkRouteConfigV1 | undefined, enabled: boolean);
60
+ get(host: string): unknown | undefined;
61
+ close(): Promise<void>;
62
+ }
63
+ /** Fetch and extract one page; bounded and explicit about every failure. */
64
+ export declare function fetchPage(options: FetchPageOptionsV1): Promise<FetchedPageV1>;
65
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAOpE,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE;QAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACvD,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC;IACjD,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACxB;AAED,MAAM,MAAM,SAAS,GAAG,CACvB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IACL,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB,KACG,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAEhC,MAAM,MAAM,eAAe,GACxB,aAAa,GACb,kBAAkB,GAClB,oBAAoB,GACpB,YAAY,GACZ,SAAS,GACT,eAAe,GACf,WAAW,GACX,0BAA0B,GAC1B,eAAe,GACf,WAAW,CAAC;AAEf,qBAAa,SAAU,SAAQ,KAAK;IACnC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAE5B,YAAY,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAM1F;CACD;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,OAAO,CAAC,EAAE,oBAAoB,CAAC;IACxC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B,QAAQ,CAAC,WAAW,CAAC,EAAE,cAAc,CAAC;CACtC;AAqBD;;;;GAIG;AACH,qBAAa,cAAc;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiC;IACxD,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;IAC3D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAElC,YAAY,OAAO,EAAE,oBAAoB,GAAG,SAAS,EAAE,OAAO,EAAE,OAAO,EAGtE;IAED,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAsBrC;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAO3B;CACD;AA2LD,4EAA4E;AAC5E,wBAAsB,SAAS,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC,CAqDnF","sourcesContent":["/**\n * Bounded HTTP fetch for the search tool (搜索工具设计 §6): http(s) only,\n * manual redirects, per-page timeout/size caps, no cookies or provider\n * credentials. Domain/private-network policy is deliberately out of scope;\n * deployments that need it install a policy plugin before the tool call.\n */\n\nimport type { NetworkRouteConfigV1 } from \"@agent-forge/plugin-sdk\";\nimport { resolveNetworkRouteV1 } from \"@agent-forge/plugin-sdk\";\nimport { Agent, ProxyAgent, fetch as undiciFetch } from \"undici\";\nimport { extractReadablePage } from \"./extract.ts\";\nimport { SEARCH_LIMITS } from \"./limits.ts\";\nimport { byteLength, truncateToBytes } from \"./text.ts\";\n\nexport interface FetchResponseLike {\n\treadonly status: number;\n\treadonly headers: { get(name: string): string | null };\n\treadonly body?: AsyncIterable<Uint8Array> | null;\n\ttext(): Promise<string>;\n}\n\nexport type FetchImpl = (\n\turl: string,\n\tinit: {\n\t\tmethod?: string;\n\t\theaders?: Record<string, string>;\n\t\tredirect: \"manual\";\n\t\tsignal: AbortSignal;\n\t\tdispatcher?: unknown;\n\t},\n) => Promise<FetchResponseLike>;\n\nexport type PageErrorCodeV1 =\n\t| \"invalid_url\"\n\t| \"invalid_redirect\"\n\t| \"too_many_redirects\"\n\t| \"http_error\"\n\t| \"timeout\"\n\t| \"network_error\"\n\t| \"too_large\"\n\t| \"unsupported_content_type\"\n\t| \"empty_content\"\n\t| \"cancelled\";\n\nexport class PageError extends Error {\n\treadonly code: PageErrorCodeV1;\n\treadonly httpStatus?: number;\n\t/** Raw bytes consumed before the failure, when the body read reached them. */\n\treadonly bytesRead?: number;\n\n\tconstructor(code: PageErrorCodeV1, message: string, httpStatus?: number, bytesRead?: number) {\n\t\tsuper(message);\n\t\tthis.name = \"PageError\";\n\t\tthis.code = code;\n\t\tif (httpStatus !== undefined) this.httpStatus = httpStatus;\n\t\tif (bytesRead !== undefined) this.bytesRead = bytesRead;\n\t}\n}\n\nexport interface FetchedPageV1 {\n\treadonly finalUrl: string;\n\treadonly title?: string;\n\treadonly contentType: string;\n\treadonly text: string;\n\treadonly truncated: boolean;\n\treadonly bytesFetched: number;\n}\n\nexport interface FetchPageOptionsV1 {\n\treadonly url: string;\n\treadonly maxTextBytes: number;\n\treadonly maxRawBytes: number;\n\treadonly timeoutMs: number;\n\treadonly signal?: AbortSignal;\n\treadonly network?: NetworkRouteConfigV1;\n\treadonly fetchImpl?: FetchImpl;\n\treadonly dispatchers?: DispatcherPool;\n}\n\nconst ALLOWED_CONTENT_TYPES: ReadonlySet<string> = new Set([\n\t\"text/html\",\n\t\"application/xhtml+xml\",\n\t\"text/plain\",\n\t\"application/json\",\n]);\n\nconst MIN_MEANINGFUL_TEXT_CHARS = 300;\nconst JS_SHELL_RAW_BYTES = 10_000;\nconst JS_SHELL_TEXT_RATIO = 0.01;\n\nconst DEFAULT_HEADERS: Readonly<Record<string, string>> = {\n\t\"user-agent\": \"agent-forge-search/1.0\",\n\taccept: \"text/html, application/xhtml+xml, text/plain, application/json;q=0.9\",\n\t// Keep zstd out of the negotiation: undici advertises it, but Node runtimes\n\t// without zlib zstd decoding fail the whole response stream.\n\t\"accept-encoding\": \"gzip, deflate, br\",\n};\n\n/**\n * Lazily-created undici dispatchers; closed by the caller. Direct routes get an\n * explicit Agent so the host's process-wide env-proxy dispatcher cannot\n * silently re-route a settings `direct` mode or a bypassed host.\n */\nexport class DispatcherPool {\n\tprivate readonly agents = new Map<string, ProxyAgent>();\n\tprivate directAgent: Agent | undefined;\n\tprivate readonly network: NetworkRouteConfigV1 | undefined;\n\tprivate readonly enabled: boolean;\n\n\tconstructor(network: NetworkRouteConfigV1 | undefined, enabled: boolean) {\n\t\tthis.network = network;\n\t\tthis.enabled = enabled;\n\t}\n\n\tget(host: string): unknown | undefined {\n\t\tif (!this.enabled || this.network === undefined) return undefined;\n\t\tconst resolution = resolveNetworkRouteV1(this.network, host);\n\t\tif (resolution.mode !== \"proxy\" || resolution.proxy === undefined) {\n\t\t\tthis.directAgent ??= new Agent();\n\t\t\treturn this.directAgent;\n\t\t}\n\t\tconst key = `${resolution.proxy.url}\\u0000${resolution.proxy.username ?? \"\"}\\u0000${resolution.proxy.password ?? \"\"}`;\n\t\tconst existing = this.agents.get(key);\n\t\tif (existing !== undefined) return existing;\n\t\tconst credentials =\n\t\t\tresolution.proxy.username === undefined && resolution.proxy.password === undefined\n\t\t\t\t? undefined\n\t\t\t\t: `${resolution.proxy.username ?? \"\"}:${resolution.proxy.password ?? \"\"}`;\n\t\tconst agent = new ProxyAgent({\n\t\t\turi: resolution.proxy.url,\n\t\t\t...(credentials === undefined\n\t\t\t\t? {}\n\t\t\t\t: { token: `Basic ${Buffer.from(credentials, \"utf8\").toString(\"base64\")}` }),\n\t\t});\n\t\tthis.agents.set(key, agent);\n\t\treturn agent;\n\t}\n\n\tasync close(): Promise<void> {\n\t\tawait Promise.all([...this.agents.values()].map((agent) => agent.close()));\n\t\tthis.agents.clear();\n\t\tif (this.directAgent !== undefined) {\n\t\t\tawait this.directAgent.close();\n\t\t\tthis.directAgent = undefined;\n\t\t}\n\t}\n}\n\nconst defaultFetch: FetchImpl = (url, init) =>\n\tundiciFetch(url, init as Parameters<typeof undiciFetch>[1]) as unknown as Promise<FetchResponseLike>;\n\nfunction parseHttpUrl(raw: string): URL {\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(raw);\n\t} catch {\n\t\tthrow new PageError(\"invalid_url\", `unparseable URL: ${JSON.stringify(raw)}`);\n\t}\n\tif (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n\t\tthrow new PageError(\"invalid_url\", `only http(s) URLs are supported: ${JSON.stringify(raw)}`);\n\t}\n\treturn url;\n}\n\nasync function readBodyCapped(response: FetchResponseLike, maxBytes: number): Promise<string> {\n\tif (response.body === undefined || response.body === null) {\n\t\tconst text = await response.text();\n\t\tconst size = byteLength(text);\n\t\tif (size > maxBytes) {\n\t\t\tthrow new PageError(\"too_large\", `response body exceeds the ${maxBytes} byte limit`, undefined, size);\n\t\t}\n\t\treturn text;\n\t}\n\tconst chunks: Uint8Array[] = [];\n\tlet total = 0;\n\ttry {\n\t\tfor await (const chunk of response.body) {\n\t\t\ttotal += chunk.byteLength;\n\t\t\tif (total > maxBytes) {\n\t\t\t\tthrow new PageError(\"too_large\", `response body exceeds the ${maxBytes} byte limit`, undefined, total);\n\t\t\t}\n\t\t\tchunks.push(chunk);\n\t\t}\n\t} catch (error) {\n\t\tif (error instanceof PageError) throw error;\n\t\tthrow new PageError(\"network_error\", error instanceof Error ? error.message : String(error), undefined, total);\n\t}\n\treturn Buffer.concat(chunks).toString(\"utf8\");\n}\n\nfunction contentTypeOf(response: FetchResponseLike): string {\n\tconst header = response.headers.get(\"content-type\") ?? \"\";\n\treturn header.split(\";\")[0]?.trim().toLowerCase() ?? \"\";\n}\n\ninterface FetchOnceResultV1 {\n\treadonly finalUrl: string;\n\treadonly contentType: string;\n\treadonly bodyText: string;\n\treadonly bytesFetched: number;\n}\n\nasync function fetchOnce(options: FetchPageOptionsV1, deadlineAt: number): Promise<FetchOnceResultV1> {\n\tconst fetchImpl = options.fetchImpl ?? defaultFetch;\n\tconst start = parseHttpUrl(options.url);\n\tlet current = start;\n\tfor (let hop = 0; hop <= SEARCH_LIMITS.maxRedirects; hop += 1) {\n\t\tconst remaining = deadlineAt - Date.now();\n\t\tif (remaining <= 0) {\n\t\t\tthrow new PageError(\"timeout\", `request timed out after ${options.timeoutMs}ms`);\n\t\t}\n\t\tconst controller = new AbortController();\n\t\tconst timer = setTimeout(() => controller.abort(), remaining);\n\t\tconst abortFromCaller = (): void => controller.abort();\n\t\toptions.signal?.addEventListener(\"abort\", abortFromCaller, { once: true });\n\t\ttry {\n\t\t\tconst headers: Record<string, string> = { ...DEFAULT_HEADERS };\n\t\t\tconst dispatcher = options.dispatchers?.get(current.hostname);\n\t\t\tlet response: FetchResponseLike;\n\t\t\ttry {\n\t\t\t\tresponse = await fetchImpl(current.toString(), {\n\t\t\t\t\tmethod: \"GET\",\n\t\t\t\t\theaders,\n\t\t\t\t\tredirect: \"manual\",\n\t\t\t\t\tsignal: controller.signal,\n\t\t\t\t\t...(dispatcher === undefined ? {} : { dispatcher }),\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tconst bytesRead = error instanceof PageError ? error.bytesRead : undefined;\n\t\t\t\tif (options.signal?.aborted === true) {\n\t\t\t\t\tthrow new PageError(\"cancelled\", \"request cancelled by caller\", undefined, bytesRead);\n\t\t\t\t}\n\t\t\t\tif (controller.signal.aborted) {\n\t\t\t\t\tthrow new PageError(\"timeout\", `request timed out after ${options.timeoutMs}ms`, undefined, bytesRead);\n\t\t\t\t}\n\t\t\t\tthrow new PageError(\n\t\t\t\t\t\"network_error\",\n\t\t\t\t\t`request failed: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t\tundefined,\n\t\t\t\t\tbytesRead,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (response.status >= 300 && response.status < 400) {\n\t\t\t\tconst location = response.headers.get(\"location\");\n\t\t\t\tif (location === null || location.trim() === \"\") {\n\t\t\t\t\tthrow new PageError(\"invalid_redirect\", `redirect ${response.status} without Location`);\n\t\t\t\t}\n\t\t\t\tif (hop === SEARCH_LIMITS.maxRedirects) {\n\t\t\t\t\tthrow new PageError(\"too_many_redirects\", `more than ${SEARCH_LIMITS.maxRedirects} redirects`);\n\t\t\t\t}\n\t\t\t\tlet next: URL;\n\t\t\t\ttry {\n\t\t\t\t\tnext = new URL(location, current);\n\t\t\t\t} catch {\n\t\t\t\t\tthrow new PageError(\"invalid_redirect\", `redirect target is not a URL: ${JSON.stringify(location)}`);\n\t\t\t\t}\n\t\t\t\tif (next.protocol !== \"http:\" && next.protocol !== \"https:\") {\n\t\t\t\t\tthrow new PageError(\"invalid_redirect\", `redirect target must be http(s): ${next.toString()}`);\n\t\t\t\t}\n\t\t\t\tcurrent = next;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (response.status < 200 || response.status >= 300) {\n\t\t\t\tthrow new PageError(\"http_error\", `HTTP ${response.status}`, response.status);\n\t\t\t}\n\n\t\t\tconst contentType = contentTypeOf(response);\n\t\t\tif (!ALLOWED_CONTENT_TYPES.has(contentType)) {\n\t\t\t\tthrow new PageError(\n\t\t\t\t\t\"unsupported_content_type\",\n\t\t\t\t\t`content type ${contentType === \"\" ? \"(missing)\" : contentType} is not supported`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst declaredLength = response.headers.get(\"content-length\");\n\t\t\tif (\n\t\t\t\tdeclaredLength !== null &&\n\t\t\t\tNumber.isInteger(Number(declaredLength)) &&\n\t\t\t\tNumber(declaredLength) > options.maxRawBytes\n\t\t\t) {\n\t\t\t\tthrow new PageError(\"too_large\", `response declares ${declaredLength} bytes`);\n\t\t\t}\n\t\t\tlet bodyText: string;\n\t\t\ttry {\n\t\t\t\tbodyText = await readBodyCapped(response, options.maxRawBytes);\n\t\t\t} catch (error) {\n\t\t\t\tconst bytesRead = error instanceof PageError ? error.bytesRead : undefined;\n\t\t\t\tif (options.signal?.aborted === true) {\n\t\t\t\t\tthrow new PageError(\"cancelled\", \"request cancelled by caller\", undefined, bytesRead);\n\t\t\t\t}\n\t\t\t\tif (controller.signal.aborted) {\n\t\t\t\t\tthrow new PageError(\"timeout\", `request timed out after ${options.timeoutMs}ms`, undefined, bytesRead);\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tfinalUrl: current.toString(),\n\t\t\t\tcontentType,\n\t\t\t\tbodyText,\n\t\t\t\tbytesFetched: byteLength(bodyText),\n\t\t\t};\n\t\t} finally {\n\t\t\tclearTimeout(timer);\n\t\t\toptions.signal?.removeEventListener(\"abort\", abortFromCaller);\n\t\t}\n\t}\n\tthrow new PageError(\"too_many_redirects\", `more than ${SEARCH_LIMITS.maxRedirects} redirects`);\n}\n\nfunction extractPage(contentType: string, bodyText: string): { title?: string; text: string } {\n\tif (contentType === \"text/html\" || contentType === \"application/xhtml+xml\") {\n\t\tconst page = extractReadablePage(bodyText);\n\t\tif (page === undefined || page.text.trim() === \"\") {\n\t\t\tthrow new PageError(\"empty_content\", \"no readable text extracted; the page may require JavaScript\");\n\t\t}\n\t\treturn { ...(page.title === undefined ? {} : { title: page.title }), text: page.text };\n\t}\n\tconst text = bodyText.trim();\n\tif (text === \"\") throw new PageError(\"empty_content\", \"empty response body\");\n\treturn { text };\n}\n\nfunction isRetryable(error: unknown): boolean {\n\tif (!(error instanceof PageError)) return false;\n\tif (error.code === \"network_error\") return true;\n\treturn (\n\t\terror.code === \"http_error\" &&\n\t\terror.httpStatus !== undefined &&\n\t\t(error.httpStatus === 429 || error.httpStatus >= 500)\n\t);\n}\n\n/** Fetch and extract one page; bounded and explicit about every failure. */\nexport async function fetchPage(options: FetchPageOptionsV1): Promise<FetchedPageV1> {\n\tif (options.signal?.aborted === true) {\n\t\tthrow new PageError(\"cancelled\", \"request cancelled by caller\");\n\t}\n\tconst deadlineAt = Date.now() + options.timeoutMs;\n\tlet bytesRead = 0;\n\tlet lastError: unknown;\n\tfor (let attempt = 0; attempt < 2; attempt += 1) {\n\t\ttry {\n\t\t\tconst remainingRawBytes = Math.max(0, options.maxRawBytes - bytesRead);\n\t\t\tconst result = await fetchOnce({ ...options, maxRawBytes: remainingRawBytes }, deadlineAt);\n\t\t\tlet extracted: { title?: string; text: string };\n\t\t\ttry {\n\t\t\t\textracted = extractPage(result.contentType, result.bodyText);\n\t\t\t} catch (error) {\n\t\t\t\tif (error instanceof PageError) {\n\t\t\t\t\tthrow new PageError(error.code, error.message, error.httpStatus, result.bytesFetched);\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tconst meaningfulChars = extracted.text.trim().length;\n\t\t\tif (\n\t\t\t\tmeaningfulChars < MIN_MEANINGFUL_TEXT_CHARS &&\n\t\t\t\tresult.bytesFetched > JS_SHELL_RAW_BYTES &&\n\t\t\t\tmeaningfulChars / result.bytesFetched < JS_SHELL_TEXT_RATIO\n\t\t\t) {\n\t\t\t\tthrow new PageError(\n\t\t\t\t\t\"empty_content\",\n\t\t\t\t\t\"no meaningful text extracted; the page may require JavaScript or client-side rendering\",\n\t\t\t\t\tundefined,\n\t\t\t\t\tresult.bytesFetched,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst bounded = truncateToBytes(extracted.text, options.maxTextBytes);\n\t\t\treturn {\n\t\t\t\tfinalUrl: result.finalUrl,\n\t\t\t\t...(extracted.title === undefined ? {} : { title: extracted.title }),\n\t\t\t\tcontentType: result.contentType,\n\t\t\t\ttext: bounded.text,\n\t\t\t\ttruncated: bounded.truncated,\n\t\t\t\tbytesFetched: result.bytesFetched + bytesRead,\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tif (error instanceof PageError && error.bytesRead !== undefined) bytesRead += error.bytesRead;\n\t\t\tlastError = error;\n\t\t\tif (attempt === 0 && isRetryable(error) && options.maxRawBytes > bytesRead) continue;\n\t\t\tif (error instanceof PageError) {\n\t\t\t\tthrow new PageError(error.code, error.message, error.httpStatus, bytesRead);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\tthrow lastError instanceof Error ? lastError : new PageError(\"network_error\", String(lastError));\n}\n"]}
package/dist/http.js ADDED
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Bounded HTTP fetch for the search tool (搜索工具设计 §6): http(s) only,
3
+ * manual redirects, per-page timeout/size caps, no cookies or provider
4
+ * credentials. Domain/private-network policy is deliberately out of scope;
5
+ * deployments that need it install a policy plugin before the tool call.
6
+ */
7
+ import { resolveNetworkRouteV1 } from "@agent-forge/plugin-sdk";
8
+ import { Agent, ProxyAgent, fetch as undiciFetch } from "undici";
9
+ import { extractReadablePage } from "./extract.js";
10
+ import { SEARCH_LIMITS } from "./limits.js";
11
+ import { byteLength, truncateToBytes } from "./text.js";
12
+ export class PageError extends Error {
13
+ code;
14
+ httpStatus;
15
+ /** Raw bytes consumed before the failure, when the body read reached them. */
16
+ bytesRead;
17
+ constructor(code, message, httpStatus, bytesRead) {
18
+ super(message);
19
+ this.name = "PageError";
20
+ this.code = code;
21
+ if (httpStatus !== undefined)
22
+ this.httpStatus = httpStatus;
23
+ if (bytesRead !== undefined)
24
+ this.bytesRead = bytesRead;
25
+ }
26
+ }
27
+ const ALLOWED_CONTENT_TYPES = new Set([
28
+ "text/html",
29
+ "application/xhtml+xml",
30
+ "text/plain",
31
+ "application/json",
32
+ ]);
33
+ const MIN_MEANINGFUL_TEXT_CHARS = 300;
34
+ const JS_SHELL_RAW_BYTES = 10_000;
35
+ const JS_SHELL_TEXT_RATIO = 0.01;
36
+ const DEFAULT_HEADERS = {
37
+ "user-agent": "agent-forge-search/1.0",
38
+ accept: "text/html, application/xhtml+xml, text/plain, application/json;q=0.9",
39
+ // Keep zstd out of the negotiation: undici advertises it, but Node runtimes
40
+ // without zlib zstd decoding fail the whole response stream.
41
+ "accept-encoding": "gzip, deflate, br",
42
+ };
43
+ /**
44
+ * Lazily-created undici dispatchers; closed by the caller. Direct routes get an
45
+ * explicit Agent so the host's process-wide env-proxy dispatcher cannot
46
+ * silently re-route a settings `direct` mode or a bypassed host.
47
+ */
48
+ export class DispatcherPool {
49
+ agents = new Map();
50
+ directAgent;
51
+ network;
52
+ enabled;
53
+ constructor(network, enabled) {
54
+ this.network = network;
55
+ this.enabled = enabled;
56
+ }
57
+ get(host) {
58
+ if (!this.enabled || this.network === undefined)
59
+ return undefined;
60
+ const resolution = resolveNetworkRouteV1(this.network, host);
61
+ if (resolution.mode !== "proxy" || resolution.proxy === undefined) {
62
+ this.directAgent ??= new Agent();
63
+ return this.directAgent;
64
+ }
65
+ const key = `${resolution.proxy.url}\u0000${resolution.proxy.username ?? ""}\u0000${resolution.proxy.password ?? ""}`;
66
+ const existing = this.agents.get(key);
67
+ if (existing !== undefined)
68
+ return existing;
69
+ const credentials = resolution.proxy.username === undefined && resolution.proxy.password === undefined
70
+ ? undefined
71
+ : `${resolution.proxy.username ?? ""}:${resolution.proxy.password ?? ""}`;
72
+ const agent = new ProxyAgent({
73
+ uri: resolution.proxy.url,
74
+ ...(credentials === undefined
75
+ ? {}
76
+ : { token: `Basic ${Buffer.from(credentials, "utf8").toString("base64")}` }),
77
+ });
78
+ this.agents.set(key, agent);
79
+ return agent;
80
+ }
81
+ async close() {
82
+ await Promise.all([...this.agents.values()].map((agent) => agent.close()));
83
+ this.agents.clear();
84
+ if (this.directAgent !== undefined) {
85
+ await this.directAgent.close();
86
+ this.directAgent = undefined;
87
+ }
88
+ }
89
+ }
90
+ const defaultFetch = (url, init) => undiciFetch(url, init);
91
+ function parseHttpUrl(raw) {
92
+ let url;
93
+ try {
94
+ url = new URL(raw);
95
+ }
96
+ catch {
97
+ throw new PageError("invalid_url", `unparseable URL: ${JSON.stringify(raw)}`);
98
+ }
99
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
100
+ throw new PageError("invalid_url", `only http(s) URLs are supported: ${JSON.stringify(raw)}`);
101
+ }
102
+ return url;
103
+ }
104
+ async function readBodyCapped(response, maxBytes) {
105
+ if (response.body === undefined || response.body === null) {
106
+ const text = await response.text();
107
+ const size = byteLength(text);
108
+ if (size > maxBytes) {
109
+ throw new PageError("too_large", `response body exceeds the ${maxBytes} byte limit`, undefined, size);
110
+ }
111
+ return text;
112
+ }
113
+ const chunks = [];
114
+ let total = 0;
115
+ try {
116
+ for await (const chunk of response.body) {
117
+ total += chunk.byteLength;
118
+ if (total > maxBytes) {
119
+ throw new PageError("too_large", `response body exceeds the ${maxBytes} byte limit`, undefined, total);
120
+ }
121
+ chunks.push(chunk);
122
+ }
123
+ }
124
+ catch (error) {
125
+ if (error instanceof PageError)
126
+ throw error;
127
+ throw new PageError("network_error", error instanceof Error ? error.message : String(error), undefined, total);
128
+ }
129
+ return Buffer.concat(chunks).toString("utf8");
130
+ }
131
+ function contentTypeOf(response) {
132
+ const header = response.headers.get("content-type") ?? "";
133
+ return header.split(";")[0]?.trim().toLowerCase() ?? "";
134
+ }
135
+ async function fetchOnce(options, deadlineAt) {
136
+ const fetchImpl = options.fetchImpl ?? defaultFetch;
137
+ const start = parseHttpUrl(options.url);
138
+ let current = start;
139
+ for (let hop = 0; hop <= SEARCH_LIMITS.maxRedirects; hop += 1) {
140
+ const remaining = deadlineAt - Date.now();
141
+ if (remaining <= 0) {
142
+ throw new PageError("timeout", `request timed out after ${options.timeoutMs}ms`);
143
+ }
144
+ const controller = new AbortController();
145
+ const timer = setTimeout(() => controller.abort(), remaining);
146
+ const abortFromCaller = () => controller.abort();
147
+ options.signal?.addEventListener("abort", abortFromCaller, { once: true });
148
+ try {
149
+ const headers = { ...DEFAULT_HEADERS };
150
+ const dispatcher = options.dispatchers?.get(current.hostname);
151
+ let response;
152
+ try {
153
+ response = await fetchImpl(current.toString(), {
154
+ method: "GET",
155
+ headers,
156
+ redirect: "manual",
157
+ signal: controller.signal,
158
+ ...(dispatcher === undefined ? {} : { dispatcher }),
159
+ });
160
+ }
161
+ catch (error) {
162
+ const bytesRead = error instanceof PageError ? error.bytesRead : undefined;
163
+ if (options.signal?.aborted === true) {
164
+ throw new PageError("cancelled", "request cancelled by caller", undefined, bytesRead);
165
+ }
166
+ if (controller.signal.aborted) {
167
+ throw new PageError("timeout", `request timed out after ${options.timeoutMs}ms`, undefined, bytesRead);
168
+ }
169
+ throw new PageError("network_error", `request failed: ${error instanceof Error ? error.message : String(error)}`, undefined, bytesRead);
170
+ }
171
+ if (response.status >= 300 && response.status < 400) {
172
+ const location = response.headers.get("location");
173
+ if (location === null || location.trim() === "") {
174
+ throw new PageError("invalid_redirect", `redirect ${response.status} without Location`);
175
+ }
176
+ if (hop === SEARCH_LIMITS.maxRedirects) {
177
+ throw new PageError("too_many_redirects", `more than ${SEARCH_LIMITS.maxRedirects} redirects`);
178
+ }
179
+ let next;
180
+ try {
181
+ next = new URL(location, current);
182
+ }
183
+ catch {
184
+ throw new PageError("invalid_redirect", `redirect target is not a URL: ${JSON.stringify(location)}`);
185
+ }
186
+ if (next.protocol !== "http:" && next.protocol !== "https:") {
187
+ throw new PageError("invalid_redirect", `redirect target must be http(s): ${next.toString()}`);
188
+ }
189
+ current = next;
190
+ continue;
191
+ }
192
+ if (response.status < 200 || response.status >= 300) {
193
+ throw new PageError("http_error", `HTTP ${response.status}`, response.status);
194
+ }
195
+ const contentType = contentTypeOf(response);
196
+ if (!ALLOWED_CONTENT_TYPES.has(contentType)) {
197
+ throw new PageError("unsupported_content_type", `content type ${contentType === "" ? "(missing)" : contentType} is not supported`);
198
+ }
199
+ const declaredLength = response.headers.get("content-length");
200
+ if (declaredLength !== null &&
201
+ Number.isInteger(Number(declaredLength)) &&
202
+ Number(declaredLength) > options.maxRawBytes) {
203
+ throw new PageError("too_large", `response declares ${declaredLength} bytes`);
204
+ }
205
+ let bodyText;
206
+ try {
207
+ bodyText = await readBodyCapped(response, options.maxRawBytes);
208
+ }
209
+ catch (error) {
210
+ const bytesRead = error instanceof PageError ? error.bytesRead : undefined;
211
+ if (options.signal?.aborted === true) {
212
+ throw new PageError("cancelled", "request cancelled by caller", undefined, bytesRead);
213
+ }
214
+ if (controller.signal.aborted) {
215
+ throw new PageError("timeout", `request timed out after ${options.timeoutMs}ms`, undefined, bytesRead);
216
+ }
217
+ throw error;
218
+ }
219
+ return {
220
+ finalUrl: current.toString(),
221
+ contentType,
222
+ bodyText,
223
+ bytesFetched: byteLength(bodyText),
224
+ };
225
+ }
226
+ finally {
227
+ clearTimeout(timer);
228
+ options.signal?.removeEventListener("abort", abortFromCaller);
229
+ }
230
+ }
231
+ throw new PageError("too_many_redirects", `more than ${SEARCH_LIMITS.maxRedirects} redirects`);
232
+ }
233
+ function extractPage(contentType, bodyText) {
234
+ if (contentType === "text/html" || contentType === "application/xhtml+xml") {
235
+ const page = extractReadablePage(bodyText);
236
+ if (page === undefined || page.text.trim() === "") {
237
+ throw new PageError("empty_content", "no readable text extracted; the page may require JavaScript");
238
+ }
239
+ return { ...(page.title === undefined ? {} : { title: page.title }), text: page.text };
240
+ }
241
+ const text = bodyText.trim();
242
+ if (text === "")
243
+ throw new PageError("empty_content", "empty response body");
244
+ return { text };
245
+ }
246
+ function isRetryable(error) {
247
+ if (!(error instanceof PageError))
248
+ return false;
249
+ if (error.code === "network_error")
250
+ return true;
251
+ return (error.code === "http_error" &&
252
+ error.httpStatus !== undefined &&
253
+ (error.httpStatus === 429 || error.httpStatus >= 500));
254
+ }
255
+ /** Fetch and extract one page; bounded and explicit about every failure. */
256
+ export async function fetchPage(options) {
257
+ if (options.signal?.aborted === true) {
258
+ throw new PageError("cancelled", "request cancelled by caller");
259
+ }
260
+ const deadlineAt = Date.now() + options.timeoutMs;
261
+ let bytesRead = 0;
262
+ let lastError;
263
+ for (let attempt = 0; attempt < 2; attempt += 1) {
264
+ try {
265
+ const remainingRawBytes = Math.max(0, options.maxRawBytes - bytesRead);
266
+ const result = await fetchOnce({ ...options, maxRawBytes: remainingRawBytes }, deadlineAt);
267
+ let extracted;
268
+ try {
269
+ extracted = extractPage(result.contentType, result.bodyText);
270
+ }
271
+ catch (error) {
272
+ if (error instanceof PageError) {
273
+ throw new PageError(error.code, error.message, error.httpStatus, result.bytesFetched);
274
+ }
275
+ throw error;
276
+ }
277
+ const meaningfulChars = extracted.text.trim().length;
278
+ if (meaningfulChars < MIN_MEANINGFUL_TEXT_CHARS &&
279
+ result.bytesFetched > JS_SHELL_RAW_BYTES &&
280
+ meaningfulChars / result.bytesFetched < JS_SHELL_TEXT_RATIO) {
281
+ throw new PageError("empty_content", "no meaningful text extracted; the page may require JavaScript or client-side rendering", undefined, result.bytesFetched);
282
+ }
283
+ const bounded = truncateToBytes(extracted.text, options.maxTextBytes);
284
+ return {
285
+ finalUrl: result.finalUrl,
286
+ ...(extracted.title === undefined ? {} : { title: extracted.title }),
287
+ contentType: result.contentType,
288
+ text: bounded.text,
289
+ truncated: bounded.truncated,
290
+ bytesFetched: result.bytesFetched + bytesRead,
291
+ };
292
+ }
293
+ catch (error) {
294
+ if (error instanceof PageError && error.bytesRead !== undefined)
295
+ bytesRead += error.bytesRead;
296
+ lastError = error;
297
+ if (attempt === 0 && isRetryable(error) && options.maxRawBytes > bytesRead)
298
+ continue;
299
+ if (error instanceof PageError) {
300
+ throw new PageError(error.code, error.message, error.httpStatus, bytesRead);
301
+ }
302
+ throw error;
303
+ }
304
+ }
305
+ throw lastError instanceof Error ? lastError : new PageError("network_error", String(lastError));
306
+ }
307
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Bounded HTTP fetch for the search tool (搜索工具设计 §6): http(s) only,\n * manual redirects, per-page timeout/size caps, no cookies or provider\n * credentials. Domain/private-network policy is deliberately out of scope;\n * deployments that need it install a policy plugin before the tool call.\n */\n\nimport type { NetworkRouteConfigV1 } from \"@agent-forge/plugin-sdk\";\nimport { resolveNetworkRouteV1 } from \"@agent-forge/plugin-sdk\";\nimport { Agent, ProxyAgent, fetch as undiciFetch } from \"undici\";\nimport { extractReadablePage } from \"./extract.ts\";\nimport { SEARCH_LIMITS } from \"./limits.ts\";\nimport { byteLength, truncateToBytes } from \"./text.ts\";\n\nexport interface FetchResponseLike {\n\treadonly status: number;\n\treadonly headers: { get(name: string): string | null };\n\treadonly body?: AsyncIterable<Uint8Array> | null;\n\ttext(): Promise<string>;\n}\n\nexport type FetchImpl = (\n\turl: string,\n\tinit: {\n\t\tmethod?: string;\n\t\theaders?: Record<string, string>;\n\t\tredirect: \"manual\";\n\t\tsignal: AbortSignal;\n\t\tdispatcher?: unknown;\n\t},\n) => Promise<FetchResponseLike>;\n\nexport type PageErrorCodeV1 =\n\t| \"invalid_url\"\n\t| \"invalid_redirect\"\n\t| \"too_many_redirects\"\n\t| \"http_error\"\n\t| \"timeout\"\n\t| \"network_error\"\n\t| \"too_large\"\n\t| \"unsupported_content_type\"\n\t| \"empty_content\"\n\t| \"cancelled\";\n\nexport class PageError extends Error {\n\treadonly code: PageErrorCodeV1;\n\treadonly httpStatus?: number;\n\t/** Raw bytes consumed before the failure, when the body read reached them. */\n\treadonly bytesRead?: number;\n\n\tconstructor(code: PageErrorCodeV1, message: string, httpStatus?: number, bytesRead?: number) {\n\t\tsuper(message);\n\t\tthis.name = \"PageError\";\n\t\tthis.code = code;\n\t\tif (httpStatus !== undefined) this.httpStatus = httpStatus;\n\t\tif (bytesRead !== undefined) this.bytesRead = bytesRead;\n\t}\n}\n\nexport interface FetchedPageV1 {\n\treadonly finalUrl: string;\n\treadonly title?: string;\n\treadonly contentType: string;\n\treadonly text: string;\n\treadonly truncated: boolean;\n\treadonly bytesFetched: number;\n}\n\nexport interface FetchPageOptionsV1 {\n\treadonly url: string;\n\treadonly maxTextBytes: number;\n\treadonly maxRawBytes: number;\n\treadonly timeoutMs: number;\n\treadonly signal?: AbortSignal;\n\treadonly network?: NetworkRouteConfigV1;\n\treadonly fetchImpl?: FetchImpl;\n\treadonly dispatchers?: DispatcherPool;\n}\n\nconst ALLOWED_CONTENT_TYPES: ReadonlySet<string> = new Set([\n\t\"text/html\",\n\t\"application/xhtml+xml\",\n\t\"text/plain\",\n\t\"application/json\",\n]);\n\nconst MIN_MEANINGFUL_TEXT_CHARS = 300;\nconst JS_SHELL_RAW_BYTES = 10_000;\nconst JS_SHELL_TEXT_RATIO = 0.01;\n\nconst DEFAULT_HEADERS: Readonly<Record<string, string>> = {\n\t\"user-agent\": \"agent-forge-search/1.0\",\n\taccept: \"text/html, application/xhtml+xml, text/plain, application/json;q=0.9\",\n\t// Keep zstd out of the negotiation: undici advertises it, but Node runtimes\n\t// without zlib zstd decoding fail the whole response stream.\n\t\"accept-encoding\": \"gzip, deflate, br\",\n};\n\n/**\n * Lazily-created undici dispatchers; closed by the caller. Direct routes get an\n * explicit Agent so the host's process-wide env-proxy dispatcher cannot\n * silently re-route a settings `direct` mode or a bypassed host.\n */\nexport class DispatcherPool {\n\tprivate readonly agents = new Map<string, ProxyAgent>();\n\tprivate directAgent: Agent | undefined;\n\tprivate readonly network: NetworkRouteConfigV1 | undefined;\n\tprivate readonly enabled: boolean;\n\n\tconstructor(network: NetworkRouteConfigV1 | undefined, enabled: boolean) {\n\t\tthis.network = network;\n\t\tthis.enabled = enabled;\n\t}\n\n\tget(host: string): unknown | undefined {\n\t\tif (!this.enabled || this.network === undefined) return undefined;\n\t\tconst resolution = resolveNetworkRouteV1(this.network, host);\n\t\tif (resolution.mode !== \"proxy\" || resolution.proxy === undefined) {\n\t\t\tthis.directAgent ??= new Agent();\n\t\t\treturn this.directAgent;\n\t\t}\n\t\tconst key = `${resolution.proxy.url}\\u0000${resolution.proxy.username ?? \"\"}\\u0000${resolution.proxy.password ?? \"\"}`;\n\t\tconst existing = this.agents.get(key);\n\t\tif (existing !== undefined) return existing;\n\t\tconst credentials =\n\t\t\tresolution.proxy.username === undefined && resolution.proxy.password === undefined\n\t\t\t\t? undefined\n\t\t\t\t: `${resolution.proxy.username ?? \"\"}:${resolution.proxy.password ?? \"\"}`;\n\t\tconst agent = new ProxyAgent({\n\t\t\turi: resolution.proxy.url,\n\t\t\t...(credentials === undefined\n\t\t\t\t? {}\n\t\t\t\t: { token: `Basic ${Buffer.from(credentials, \"utf8\").toString(\"base64\")}` }),\n\t\t});\n\t\tthis.agents.set(key, agent);\n\t\treturn agent;\n\t}\n\n\tasync close(): Promise<void> {\n\t\tawait Promise.all([...this.agents.values()].map((agent) => agent.close()));\n\t\tthis.agents.clear();\n\t\tif (this.directAgent !== undefined) {\n\t\t\tawait this.directAgent.close();\n\t\t\tthis.directAgent = undefined;\n\t\t}\n\t}\n}\n\nconst defaultFetch: FetchImpl = (url, init) =>\n\tundiciFetch(url, init as Parameters<typeof undiciFetch>[1]) as unknown as Promise<FetchResponseLike>;\n\nfunction parseHttpUrl(raw: string): URL {\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(raw);\n\t} catch {\n\t\tthrow new PageError(\"invalid_url\", `unparseable URL: ${JSON.stringify(raw)}`);\n\t}\n\tif (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n\t\tthrow new PageError(\"invalid_url\", `only http(s) URLs are supported: ${JSON.stringify(raw)}`);\n\t}\n\treturn url;\n}\n\nasync function readBodyCapped(response: FetchResponseLike, maxBytes: number): Promise<string> {\n\tif (response.body === undefined || response.body === null) {\n\t\tconst text = await response.text();\n\t\tconst size = byteLength(text);\n\t\tif (size > maxBytes) {\n\t\t\tthrow new PageError(\"too_large\", `response body exceeds the ${maxBytes} byte limit`, undefined, size);\n\t\t}\n\t\treturn text;\n\t}\n\tconst chunks: Uint8Array[] = [];\n\tlet total = 0;\n\ttry {\n\t\tfor await (const chunk of response.body) {\n\t\t\ttotal += chunk.byteLength;\n\t\t\tif (total > maxBytes) {\n\t\t\t\tthrow new PageError(\"too_large\", `response body exceeds the ${maxBytes} byte limit`, undefined, total);\n\t\t\t}\n\t\t\tchunks.push(chunk);\n\t\t}\n\t} catch (error) {\n\t\tif (error instanceof PageError) throw error;\n\t\tthrow new PageError(\"network_error\", error instanceof Error ? error.message : String(error), undefined, total);\n\t}\n\treturn Buffer.concat(chunks).toString(\"utf8\");\n}\n\nfunction contentTypeOf(response: FetchResponseLike): string {\n\tconst header = response.headers.get(\"content-type\") ?? \"\";\n\treturn header.split(\";\")[0]?.trim().toLowerCase() ?? \"\";\n}\n\ninterface FetchOnceResultV1 {\n\treadonly finalUrl: string;\n\treadonly contentType: string;\n\treadonly bodyText: string;\n\treadonly bytesFetched: number;\n}\n\nasync function fetchOnce(options: FetchPageOptionsV1, deadlineAt: number): Promise<FetchOnceResultV1> {\n\tconst fetchImpl = options.fetchImpl ?? defaultFetch;\n\tconst start = parseHttpUrl(options.url);\n\tlet current = start;\n\tfor (let hop = 0; hop <= SEARCH_LIMITS.maxRedirects; hop += 1) {\n\t\tconst remaining = deadlineAt - Date.now();\n\t\tif (remaining <= 0) {\n\t\t\tthrow new PageError(\"timeout\", `request timed out after ${options.timeoutMs}ms`);\n\t\t}\n\t\tconst controller = new AbortController();\n\t\tconst timer = setTimeout(() => controller.abort(), remaining);\n\t\tconst abortFromCaller = (): void => controller.abort();\n\t\toptions.signal?.addEventListener(\"abort\", abortFromCaller, { once: true });\n\t\ttry {\n\t\t\tconst headers: Record<string, string> = { ...DEFAULT_HEADERS };\n\t\t\tconst dispatcher = options.dispatchers?.get(current.hostname);\n\t\t\tlet response: FetchResponseLike;\n\t\t\ttry {\n\t\t\t\tresponse = await fetchImpl(current.toString(), {\n\t\t\t\t\tmethod: \"GET\",\n\t\t\t\t\theaders,\n\t\t\t\t\tredirect: \"manual\",\n\t\t\t\t\tsignal: controller.signal,\n\t\t\t\t\t...(dispatcher === undefined ? {} : { dispatcher }),\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tconst bytesRead = error instanceof PageError ? error.bytesRead : undefined;\n\t\t\t\tif (options.signal?.aborted === true) {\n\t\t\t\t\tthrow new PageError(\"cancelled\", \"request cancelled by caller\", undefined, bytesRead);\n\t\t\t\t}\n\t\t\t\tif (controller.signal.aborted) {\n\t\t\t\t\tthrow new PageError(\"timeout\", `request timed out after ${options.timeoutMs}ms`, undefined, bytesRead);\n\t\t\t\t}\n\t\t\t\tthrow new PageError(\n\t\t\t\t\t\"network_error\",\n\t\t\t\t\t`request failed: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t\tundefined,\n\t\t\t\t\tbytesRead,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (response.status >= 300 && response.status < 400) {\n\t\t\t\tconst location = response.headers.get(\"location\");\n\t\t\t\tif (location === null || location.trim() === \"\") {\n\t\t\t\t\tthrow new PageError(\"invalid_redirect\", `redirect ${response.status} without Location`);\n\t\t\t\t}\n\t\t\t\tif (hop === SEARCH_LIMITS.maxRedirects) {\n\t\t\t\t\tthrow new PageError(\"too_many_redirects\", `more than ${SEARCH_LIMITS.maxRedirects} redirects`);\n\t\t\t\t}\n\t\t\t\tlet next: URL;\n\t\t\t\ttry {\n\t\t\t\t\tnext = new URL(location, current);\n\t\t\t\t} catch {\n\t\t\t\t\tthrow new PageError(\"invalid_redirect\", `redirect target is not a URL: ${JSON.stringify(location)}`);\n\t\t\t\t}\n\t\t\t\tif (next.protocol !== \"http:\" && next.protocol !== \"https:\") {\n\t\t\t\t\tthrow new PageError(\"invalid_redirect\", `redirect target must be http(s): ${next.toString()}`);\n\t\t\t\t}\n\t\t\t\tcurrent = next;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (response.status < 200 || response.status >= 300) {\n\t\t\t\tthrow new PageError(\"http_error\", `HTTP ${response.status}`, response.status);\n\t\t\t}\n\n\t\t\tconst contentType = contentTypeOf(response);\n\t\t\tif (!ALLOWED_CONTENT_TYPES.has(contentType)) {\n\t\t\t\tthrow new PageError(\n\t\t\t\t\t\"unsupported_content_type\",\n\t\t\t\t\t`content type ${contentType === \"\" ? \"(missing)\" : contentType} is not supported`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst declaredLength = response.headers.get(\"content-length\");\n\t\t\tif (\n\t\t\t\tdeclaredLength !== null &&\n\t\t\t\tNumber.isInteger(Number(declaredLength)) &&\n\t\t\t\tNumber(declaredLength) > options.maxRawBytes\n\t\t\t) {\n\t\t\t\tthrow new PageError(\"too_large\", `response declares ${declaredLength} bytes`);\n\t\t\t}\n\t\t\tlet bodyText: string;\n\t\t\ttry {\n\t\t\t\tbodyText = await readBodyCapped(response, options.maxRawBytes);\n\t\t\t} catch (error) {\n\t\t\t\tconst bytesRead = error instanceof PageError ? error.bytesRead : undefined;\n\t\t\t\tif (options.signal?.aborted === true) {\n\t\t\t\t\tthrow new PageError(\"cancelled\", \"request cancelled by caller\", undefined, bytesRead);\n\t\t\t\t}\n\t\t\t\tif (controller.signal.aborted) {\n\t\t\t\t\tthrow new PageError(\"timeout\", `request timed out after ${options.timeoutMs}ms`, undefined, bytesRead);\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tfinalUrl: current.toString(),\n\t\t\t\tcontentType,\n\t\t\t\tbodyText,\n\t\t\t\tbytesFetched: byteLength(bodyText),\n\t\t\t};\n\t\t} finally {\n\t\t\tclearTimeout(timer);\n\t\t\toptions.signal?.removeEventListener(\"abort\", abortFromCaller);\n\t\t}\n\t}\n\tthrow new PageError(\"too_many_redirects\", `more than ${SEARCH_LIMITS.maxRedirects} redirects`);\n}\n\nfunction extractPage(contentType: string, bodyText: string): { title?: string; text: string } {\n\tif (contentType === \"text/html\" || contentType === \"application/xhtml+xml\") {\n\t\tconst page = extractReadablePage(bodyText);\n\t\tif (page === undefined || page.text.trim() === \"\") {\n\t\t\tthrow new PageError(\"empty_content\", \"no readable text extracted; the page may require JavaScript\");\n\t\t}\n\t\treturn { ...(page.title === undefined ? {} : { title: page.title }), text: page.text };\n\t}\n\tconst text = bodyText.trim();\n\tif (text === \"\") throw new PageError(\"empty_content\", \"empty response body\");\n\treturn { text };\n}\n\nfunction isRetryable(error: unknown): boolean {\n\tif (!(error instanceof PageError)) return false;\n\tif (error.code === \"network_error\") return true;\n\treturn (\n\t\terror.code === \"http_error\" &&\n\t\terror.httpStatus !== undefined &&\n\t\t(error.httpStatus === 429 || error.httpStatus >= 500)\n\t);\n}\n\n/** Fetch and extract one page; bounded and explicit about every failure. */\nexport async function fetchPage(options: FetchPageOptionsV1): Promise<FetchedPageV1> {\n\tif (options.signal?.aborted === true) {\n\t\tthrow new PageError(\"cancelled\", \"request cancelled by caller\");\n\t}\n\tconst deadlineAt = Date.now() + options.timeoutMs;\n\tlet bytesRead = 0;\n\tlet lastError: unknown;\n\tfor (let attempt = 0; attempt < 2; attempt += 1) {\n\t\ttry {\n\t\t\tconst remainingRawBytes = Math.max(0, options.maxRawBytes - bytesRead);\n\t\t\tconst result = await fetchOnce({ ...options, maxRawBytes: remainingRawBytes }, deadlineAt);\n\t\t\tlet extracted: { title?: string; text: string };\n\t\t\ttry {\n\t\t\t\textracted = extractPage(result.contentType, result.bodyText);\n\t\t\t} catch (error) {\n\t\t\t\tif (error instanceof PageError) {\n\t\t\t\t\tthrow new PageError(error.code, error.message, error.httpStatus, result.bytesFetched);\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tconst meaningfulChars = extracted.text.trim().length;\n\t\t\tif (\n\t\t\t\tmeaningfulChars < MIN_MEANINGFUL_TEXT_CHARS &&\n\t\t\t\tresult.bytesFetched > JS_SHELL_RAW_BYTES &&\n\t\t\t\tmeaningfulChars / result.bytesFetched < JS_SHELL_TEXT_RATIO\n\t\t\t) {\n\t\t\t\tthrow new PageError(\n\t\t\t\t\t\"empty_content\",\n\t\t\t\t\t\"no meaningful text extracted; the page may require JavaScript or client-side rendering\",\n\t\t\t\t\tundefined,\n\t\t\t\t\tresult.bytesFetched,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst bounded = truncateToBytes(extracted.text, options.maxTextBytes);\n\t\t\treturn {\n\t\t\t\tfinalUrl: result.finalUrl,\n\t\t\t\t...(extracted.title === undefined ? {} : { title: extracted.title }),\n\t\t\t\tcontentType: result.contentType,\n\t\t\t\ttext: bounded.text,\n\t\t\t\ttruncated: bounded.truncated,\n\t\t\t\tbytesFetched: result.bytesFetched + bytesRead,\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tif (error instanceof PageError && error.bytesRead !== undefined) bytesRead += error.bytesRead;\n\t\t\tlastError = error;\n\t\t\tif (attempt === 0 && isRetryable(error) && options.maxRawBytes > bytesRead) continue;\n\t\t\tif (error instanceof PageError) {\n\t\t\t\tthrow new PageError(error.code, error.message, error.httpStatus, bytesRead);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\tthrow lastError instanceof Error ? lastError : new PageError(\"network_error\", String(lastError));\n}\n"]}
@@ -0,0 +1,5 @@
1
+ export { PageCache, SummaryCache } from "./cache.ts";
2
+ export { createSearchCapabilityFactoryV1, type SearchPluginOptionsV1 } from "./capability.ts";
3
+ export { extractReadablePage, selectPassages } from "./extract.ts";
4
+ export { SEARCH_LIMITS } from "./limits.ts";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,+BAA+B,EAAE,KAAK,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAC9F,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC","sourcesContent":["export { PageCache, SummaryCache } from \"./cache.ts\";\nexport { createSearchCapabilityFactoryV1, type SearchPluginOptionsV1 } from \"./capability.ts\";\nexport { extractReadablePage, selectPassages } from \"./extract.ts\";\nexport { SEARCH_LIMITS } from \"./limits.ts\";\n"]}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { PageCache, SummaryCache } from "./cache.js";
2
+ export { createSearchCapabilityFactoryV1 } from "./capability.js";
3
+ export { extractReadablePage, selectPassages } from "./extract.js";
4
+ export { SEARCH_LIMITS } from "./limits.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,+BAA+B,EAA8B,MAAM,iBAAiB,CAAC;AAC9F,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC","sourcesContent":["export { PageCache, SummaryCache } from \"./cache.ts\";\nexport { createSearchCapabilityFactoryV1, type SearchPluginOptionsV1 } from \"./capability.ts\";\nexport { extractReadablePage, selectPassages } from \"./extract.ts\";\nexport { SEARCH_LIMITS } from \"./limits.ts\";\n"]}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Search tool resource ceilings (搜索工具设计 §8). These are resource bounds,
3
+ * not content policy: the model chooses URLs and no domain/scope restrictions
4
+ * are applied here.
5
+ *
6
+ * The page/extraction limits are coupled to the 5 MiB disk-cache entry size:
7
+ * a cached page is at most 5 MiB, so a fresh extraction keeps up to 4 MiB of
8
+ * text and reads at most 8 MiB of raw response bytes.
9
+ */
10
+ export declare const SEARCH_LIMITS: Readonly<{
11
+ maxUrls: 32;
12
+ maxUrlLength: 2048;
13
+ maxQueryLength: 2000;
14
+ maxRawBytesPerPage: number;
15
+ maxTotalRawBytes: number;
16
+ maxExtractedBytesPerPage: number;
17
+ maxSummaryInputBytes: number;
18
+ maxSummaryOutputBytes: number;
19
+ maxSummaryOutputTokens: 8192;
20
+ maxPassagesPerPage: 24;
21
+ fetchConcurrency: 8;
22
+ urlTimeoutMs: 20000;
23
+ totalFetchTimeoutMs: 120000;
24
+ modelTimeoutMs: 120000;
25
+ maxRedirects: 5;
26
+ pageCacheBytes: number;
27
+ pageCacheEntries: 5000;
28
+ cacheMaxEntryBytes: number;
29
+ cacheBaseTtlMs: number;
30
+ cacheTtlTiersMs: readonly number[];
31
+ cacheMetaFlushMs: 5000;
32
+ cachePruneIntervalMs: number;
33
+ cacheWriteQueueEntries: 8;
34
+ cacheWriteQueueBytes: number;
35
+ summaryCacheEntries: 32;
36
+ summaryCacheTtlMs: number;
37
+ }>;
38
+ export type SearchMode = "model" | "url";
39
+ //# sourceMappingURL=limits.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"limits.d.ts","sourceRoot":"","sources":["../src/limits.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgCxB,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,KAAK,CAAC","sourcesContent":["/**\n * Search tool resource ceilings (搜索工具设计 §8). These are resource bounds,\n * not content policy: the model chooses URLs and no domain/scope restrictions\n * are applied here.\n *\n * The page/extraction limits are coupled to the 5 MiB disk-cache entry size:\n * a cached page is at most 5 MiB, so a fresh extraction keeps up to 4 MiB of\n * text and reads at most 8 MiB of raw response bytes.\n */\nexport const SEARCH_LIMITS = Object.freeze({\n\tmaxUrls: 32,\n\tmaxUrlLength: 2048,\n\tmaxQueryLength: 2000,\n\tmaxRawBytesPerPage: 8 * 1024 * 1024,\n\tmaxTotalRawBytes: 128 * 1024 * 1024,\n\tmaxExtractedBytesPerPage: 4 * 1024 * 1024,\n\tmaxSummaryInputBytes: 512 * 1024,\n\tmaxSummaryOutputBytes: 16 * 1024,\n\tmaxSummaryOutputTokens: 8192,\n\tmaxPassagesPerPage: 24,\n\tfetchConcurrency: 8,\n\turlTimeoutMs: 20_000,\n\ttotalFetchTimeoutMs: 120_000,\n\tmodelTimeoutMs: 120_000,\n\tmaxRedirects: 5,\n\tpageCacheBytes: 2 * 1024 * 1024 * 1024,\n\tpageCacheEntries: 5000,\n\tcacheMaxEntryBytes: 5 * 1024 * 1024,\n\tcacheBaseTtlMs: 14 * 24 * 60 * 60 * 1000,\n\tcacheTtlTiersMs: Object.freeze([\n\t\t30 * 24 * 60 * 60 * 1000,\n\t\t60 * 24 * 60 * 60 * 1000,\n\t\t120 * 24 * 60 * 60 * 1000,\n\t\t365 * 24 * 60 * 60 * 1000,\n\t]),\n\tcacheMetaFlushMs: 5_000,\n\tcachePruneIntervalMs: 5 * 60 * 1000,\n\tcacheWriteQueueEntries: 8,\n\tcacheWriteQueueBytes: 32 * 1024 * 1024,\n\tsummaryCacheEntries: 32,\n\tsummaryCacheTtlMs: 10 * 60 * 1000,\n});\n\nexport type SearchMode = \"model\" | \"url\";\n"]}
package/dist/limits.js ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Search tool resource ceilings (搜索工具设计 §8). These are resource bounds,
3
+ * not content policy: the model chooses URLs and no domain/scope restrictions
4
+ * are applied here.
5
+ *
6
+ * The page/extraction limits are coupled to the 5 MiB disk-cache entry size:
7
+ * a cached page is at most 5 MiB, so a fresh extraction keeps up to 4 MiB of
8
+ * text and reads at most 8 MiB of raw response bytes.
9
+ */
10
+ export const SEARCH_LIMITS = Object.freeze({
11
+ maxUrls: 32,
12
+ maxUrlLength: 2048,
13
+ maxQueryLength: 2000,
14
+ maxRawBytesPerPage: 8 * 1024 * 1024,
15
+ maxTotalRawBytes: 128 * 1024 * 1024,
16
+ maxExtractedBytesPerPage: 4 * 1024 * 1024,
17
+ maxSummaryInputBytes: 512 * 1024,
18
+ maxSummaryOutputBytes: 16 * 1024,
19
+ maxSummaryOutputTokens: 8192,
20
+ maxPassagesPerPage: 24,
21
+ fetchConcurrency: 8,
22
+ urlTimeoutMs: 20_000,
23
+ totalFetchTimeoutMs: 120_000,
24
+ modelTimeoutMs: 120_000,
25
+ maxRedirects: 5,
26
+ pageCacheBytes: 2 * 1024 * 1024 * 1024,
27
+ pageCacheEntries: 5000,
28
+ cacheMaxEntryBytes: 5 * 1024 * 1024,
29
+ cacheBaseTtlMs: 14 * 24 * 60 * 60 * 1000,
30
+ cacheTtlTiersMs: Object.freeze([
31
+ 30 * 24 * 60 * 60 * 1000,
32
+ 60 * 24 * 60 * 60 * 1000,
33
+ 120 * 24 * 60 * 60 * 1000,
34
+ 365 * 24 * 60 * 60 * 1000,
35
+ ]),
36
+ cacheMetaFlushMs: 5_000,
37
+ cachePruneIntervalMs: 5 * 60 * 1000,
38
+ cacheWriteQueueEntries: 8,
39
+ cacheWriteQueueBytes: 32 * 1024 * 1024,
40
+ summaryCacheEntries: 32,
41
+ summaryCacheTtlMs: 10 * 60 * 1000,
42
+ });
43
+ //# sourceMappingURL=limits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"limits.js","sourceRoot":"","sources":["../src/limits.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC;IAC1C,OAAO,EAAE,EAAE;IACX,YAAY,EAAE,IAAI;IAClB,cAAc,EAAE,IAAI;IACpB,kBAAkB,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI;IACnC,gBAAgB,EAAE,GAAG,GAAG,IAAI,GAAG,IAAI;IACnC,wBAAwB,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI;IACzC,oBAAoB,EAAE,GAAG,GAAG,IAAI;IAChC,qBAAqB,EAAE,EAAE,GAAG,IAAI;IAChC,sBAAsB,EAAE,IAAI;IAC5B,kBAAkB,EAAE,EAAE;IACtB,gBAAgB,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM;IACpB,mBAAmB,EAAE,OAAO;IAC5B,cAAc,EAAE,OAAO;IACvB,YAAY,EAAE,CAAC;IACf,cAAc,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI;IACtC,gBAAgB,EAAE,IAAI;IACtB,kBAAkB,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI;IACnC,cAAc,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI;IACxC,eAAe,EAAE,MAAM,CAAC,MAAM,CAAC;QAC9B,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI;QACxB,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI;QACxB,GAAG,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI;QACzB,GAAG,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI;KACzB,CAAC;IACF,gBAAgB,EAAE,KAAK;IACvB,oBAAoB,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI;IACnC,sBAAsB,EAAE,CAAC;IACzB,oBAAoB,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI;IACtC,mBAAmB,EAAE,EAAE;IACvB,iBAAiB,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;CACjC,CAAC,CAAC","sourcesContent":["/**\n * Search tool resource ceilings (搜索工具设计 §8). These are resource bounds,\n * not content policy: the model chooses URLs and no domain/scope restrictions\n * are applied here.\n *\n * The page/extraction limits are coupled to the 5 MiB disk-cache entry size:\n * a cached page is at most 5 MiB, so a fresh extraction keeps up to 4 MiB of\n * text and reads at most 8 MiB of raw response bytes.\n */\nexport const SEARCH_LIMITS = Object.freeze({\n\tmaxUrls: 32,\n\tmaxUrlLength: 2048,\n\tmaxQueryLength: 2000,\n\tmaxRawBytesPerPage: 8 * 1024 * 1024,\n\tmaxTotalRawBytes: 128 * 1024 * 1024,\n\tmaxExtractedBytesPerPage: 4 * 1024 * 1024,\n\tmaxSummaryInputBytes: 512 * 1024,\n\tmaxSummaryOutputBytes: 16 * 1024,\n\tmaxSummaryOutputTokens: 8192,\n\tmaxPassagesPerPage: 24,\n\tfetchConcurrency: 8,\n\turlTimeoutMs: 20_000,\n\ttotalFetchTimeoutMs: 120_000,\n\tmodelTimeoutMs: 120_000,\n\tmaxRedirects: 5,\n\tpageCacheBytes: 2 * 1024 * 1024 * 1024,\n\tpageCacheEntries: 5000,\n\tcacheMaxEntryBytes: 5 * 1024 * 1024,\n\tcacheBaseTtlMs: 14 * 24 * 60 * 60 * 1000,\n\tcacheTtlTiersMs: Object.freeze([\n\t\t30 * 24 * 60 * 60 * 1000,\n\t\t60 * 24 * 60 * 60 * 1000,\n\t\t120 * 24 * 60 * 60 * 1000,\n\t\t365 * 24 * 60 * 60 * 1000,\n\t]),\n\tcacheMetaFlushMs: 5_000,\n\tcachePruneIntervalMs: 5 * 60 * 1000,\n\tcacheWriteQueueEntries: 8,\n\tcacheWriteQueueBytes: 32 * 1024 * 1024,\n\tsummaryCacheEntries: 32,\n\tsummaryCacheTtlMs: 10 * 60 * 1000,\n});\n\nexport type SearchMode = \"model\" | \"url\";\n"]}
@@ -0,0 +1,10 @@
1
+ export interface ModelCallSignalV1 {
2
+ readonly signal: AbortSignal;
3
+ timedOut(): boolean;
4
+ dispose(): void;
5
+ }
6
+ /** Combine a caller signal with the model-call deadline without leaking timers. */
7
+ export declare function createModelCallSignal(caller: AbortSignal | undefined): ModelCallSignalV1;
8
+ /** Combine the per-call signal with the overall fetch budget. */
9
+ export declare function createFetchBudgetSignal(caller: AbortSignal | undefined): ModelCallSignalV1;
10
+ //# sourceMappingURL=model-call.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-call.d.ts","sourceRoot":"","sources":["../src/model-call.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,IAAI,OAAO,CAAC;IACpB,OAAO,IAAI,IAAI,CAAC;CAChB;AAED,mFAAmF;AACnF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,iBAAiB,CAkBxF;AAED,iEAAiE;AACjE,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,iBAAiB,CAkB1F","sourcesContent":["import { SEARCH_LIMITS } from \"./limits.ts\";\n\nexport interface ModelCallSignalV1 {\n\treadonly signal: AbortSignal;\n\ttimedOut(): boolean;\n\tdispose(): void;\n}\n\n/** Combine a caller signal with the model-call deadline without leaking timers. */\nexport function createModelCallSignal(caller: AbortSignal | undefined): ModelCallSignalV1 {\n\tconst controller = new AbortController();\n\tlet timedOut = false;\n\tconst timer = setTimeout(() => {\n\t\ttimedOut = true;\n\t\tcontroller.abort();\n\t}, SEARCH_LIMITS.modelTimeoutMs);\n\tconst abortFromCaller = (): void => controller.abort();\n\tif (caller?.aborted === true) controller.abort();\n\telse caller?.addEventListener(\"abort\", abortFromCaller, { once: true });\n\treturn {\n\t\tsignal: controller.signal,\n\t\ttimedOut: () => timedOut,\n\t\tdispose: () => {\n\t\t\tclearTimeout(timer);\n\t\t\tcaller?.removeEventListener(\"abort\", abortFromCaller);\n\t\t},\n\t};\n}\n\n/** Combine the per-call signal with the overall fetch budget. */\nexport function createFetchBudgetSignal(caller: AbortSignal | undefined): ModelCallSignalV1 {\n\tconst controller = new AbortController();\n\tlet timedOut = false;\n\tconst timer = setTimeout(() => {\n\t\ttimedOut = true;\n\t\tcontroller.abort();\n\t}, SEARCH_LIMITS.totalFetchTimeoutMs);\n\tconst abortFromCaller = (): void => controller.abort();\n\tif (caller?.aborted === true) controller.abort();\n\telse caller?.addEventListener(\"abort\", abortFromCaller, { once: true });\n\treturn {\n\t\tsignal: controller.signal,\n\t\ttimedOut: () => timedOut,\n\t\tdispose: () => {\n\t\t\tclearTimeout(timer);\n\t\t\tcaller?.removeEventListener(\"abort\", abortFromCaller);\n\t\t},\n\t};\n}\n"]}