agents 0.31.0 → 0.32.0
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.
- package/dist/{agent-routing-wS1TimZ_.d.ts → agent-routing-vVMA7m8Y.d.ts} +6 -6
- package/dist/agent-routing.d.ts +1 -1
- package/dist/agent-tool-types.d.ts +1 -1
- package/dist/{agent-tools-Bamxxb6Y.d.ts → agent-tools-gGXXkf1z.d.ts} +2 -2
- package/dist/agent-tools.d.ts +1 -1
- package/dist/agent-tools.js +0 -1
- package/dist/agent-tools.js.map +1 -1
- package/dist/browser/ai.js +1 -1
- package/dist/browser/tools/ai-sdk.d.ts +66 -1
- package/dist/browser/tools/ai-sdk.js +2 -2
- package/dist/browser/tools/pi.js +2 -2
- package/dist/browser/tools/tanstack-ai.d.ts +2 -2
- package/dist/browser/tools/tanstack-ai.js +2 -2
- package/dist/browser/tools/tanstack-ai.js.map +1 -1
- package/dist/{browser-tool-lGnvEBLJ.js → browser-tool-DRan7Lk6.js} +2 -2
- package/dist/{browser-tool-lGnvEBLJ.js.map → browser-tool-DRan7Lk6.js.map} +1 -1
- package/dist/catalog-BpOejOsE.js +239 -0
- package/dist/catalog-BpOejOsE.js.map +1 -0
- package/dist/catalog-BpiVV1OF.d.ts +173 -0
- package/dist/chat/index.d.ts +2 -2
- package/dist/chat-sdk/index.d.ts +1 -1
- package/dist/chat-sdk/index.js +1 -1
- package/dist/{client-wn4-yqGz.d.ts → client-eSfgYvd3.d.ts} +2 -2
- package/dist/client.d.ts +1 -1
- package/dist/{cloudflare-Dzvc7V2N.js → cloudflare-CUUm_9q9.js} +26 -4
- package/dist/cloudflare-CUUm_9q9.js.map +1 -0
- package/dist/{do-oauth-client-provider-DfsUcpc3.d.ts → do-oauth-client-provider-DtNqv7oj.d.ts} +26 -2
- package/dist/{index-YSKgfgg9.d.ts → emitter-BJZtCVjQ.d.ts} +6 -140
- package/dist/emitter-DPOVcCln.js +15 -0
- package/dist/emitter-DPOVcCln.js.map +1 -0
- package/dist/experimental/channels/web/index.js +1 -1
- package/dist/harness/container/index.js +0 -1
- package/dist/harness/container/index.js.map +1 -1
- package/dist/harness/container/runtime/index.js +2 -2
- package/dist/harness/opencode/index.js +0 -1
- package/dist/harness/opencode/index.js.map +1 -1
- package/dist/harness/pi/index.d.ts +11 -4
- package/dist/harness/pi/index.js +43 -141
- package/dist/harness/pi/index.js.map +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.js +2 -2
- package/dist/markdown-BZ-VLtA1.js +86 -0
- package/dist/markdown-BZ-VLtA1.js.map +1 -0
- package/dist/mcp/client/do-oauth-client-provider.d.ts +4 -2
- package/dist/mcp/client/do-oauth-client-provider.js +15 -1
- package/dist/mcp/client/do-oauth-client-provider.js.map +1 -1
- package/dist/mcp/client/index.d.ts +1 -1
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/models/pi-ai/index.js +129 -2
- package/dist/models/pi-ai/index.js.map +1 -1
- package/dist/observability/ai/index.js +1 -2
- package/dist/observability/ai/index.js.map +1 -1
- package/dist/observability/index.d.ts +170 -9
- package/dist/observability/index.js +386 -11
- package/dist/observability/index.js.map +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/resource-B59OV2a2.js +325 -0
- package/dist/resource-B59OV2a2.js.map +1 -0
- package/dist/routing/index.d.ts +1 -1
- package/dist/script-CFDLTk5b.d.ts +91 -0
- package/dist/script-workspace-BpdAYq1R.d.ts +41 -0
- package/dist/serializable.d.ts +1 -1
- package/dist/skill-DRBcIBjC.d.ts +221 -0
- package/dist/skills/index.d.ts +275 -36
- package/dist/skills/index.js +265 -1534
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/runner.d.ts +37 -0
- package/dist/skills/runner.js +912 -0
- package/dist/skills/runner.js.map +1 -0
- package/dist/skills/tools/ai-sdk.d.ts +36 -0
- package/dist/skills/tools/ai-sdk.js +94 -0
- package/dist/skills/tools/ai-sdk.js.map +1 -0
- package/dist/source-9RQzlEhM.d.ts +112 -0
- package/dist/{src-BGolXG_H.js → src-DoGNJ4Ud.js} +4 -5
- package/dist/src-DoGNJ4Ud.js.map +1 -0
- package/dist/sub-routing.d.ts +1 -1
- package/dist/tool-CxKsvxjm.js.map +1 -1
- package/dist/tool-DkT_DJBm.js.map +1 -1
- package/dist/{tool-helpers-BodWi4gd.js → tool-helpers-BI7ifO2S.js} +1 -2
- package/dist/{tool-helpers-BodWi4gd.js.map → tool-helpers-BI7ifO2S.js.map} +1 -1
- package/dist/vite.d.ts +2 -2
- package/dist/vite.js +10 -258
- package/dist/vite.js.map +1 -1
- package/dist/voice/index.d.ts +1 -1
- package/dist/websockets/index.js +1 -1
- package/dist/{websockets-CyoeosdO.js → websockets-C6SJtWCJ.js} +1 -2
- package/dist/{websockets-CyoeosdO.js.map → websockets-C6SJtWCJ.js.map} +1 -1
- package/dist/workflow-types.d.ts +1 -1
- package/dist/workflows.d.ts +1 -1
- package/dist/workflows.js +0 -1
- package/dist/workflows.js.map +1 -1
- package/docs/harnesses/pi.md +9 -3
- package/docs/mcp-client.md +23 -1
- package/docs/mcp-servers.md +2 -2
- package/docs/observability.md +5 -3
- package/package.json +24 -15
- package/dist/ai-sdk-hW3vntnk.d.ts +0 -72
- package/dist/cloudflare-Dzvc7V2N.js.map +0 -1
- package/dist/index-iv9FTXX9.d.ts +0 -311
- package/dist/skills/compile.d.ts +0 -40
- package/dist/skills/compile.js +0 -65
- package/dist/skills/compile.js.map +0 -1
- package/dist/src-BGolXG_H.js.map +0 -1
- package/skills-module.d.ts +0 -22
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-DkT_DJBm.js","names":[],"sources":["../src/web/search/contract.ts","../src/web/search/source.ts","../src/web/search/tool.ts"],"sourcesContent":["/**\n * The `web_search` tool contract: its name, what it tells the model, what the\n * model passes in, what comes back, and how results are rendered for the\n * model. Shared by every executor of the tool — the harness-executed\n * adapters in `agents/websearch/*` today, and a gateway-executed variant in\n * `agents/models/*` once AI Gateway server tools ship — so a `web_search` call\n * looks the same in a transcript whoever ran it.\n */\n\n/**\n * The tool's name, as the model calls it and as it appears in transcripts.\n * `web_search` is the name models know from Anthropic's and OpenAI's built-in\n * search tools.\n */\nexport const WEB_SEARCH_TOOL_NAME = \"web_search\";\n\n/** The default number of results when the model doesn't ask for a count. */\nexport const DEFAULT_WEB_SEARCH_LIMIT = 5;\n\n/** The most results one search can return; the API rejects more. */\nexport const MAX_WEB_SEARCH_LIMIT = 10;\n\n/** The longest query the API accepts, in characters. */\nexport const MAX_WEB_SEARCH_QUERY_LENGTH = 1024;\n\n/**\n * Per-result description length in the model's view, in characters. Provider\n * descriptions run to ~8,000 characters (Ceramic returns page text), which at\n * ten results is 80 KB of context per search. The full text stays in the\n * host-side output.\n */\nexport const DEFAULT_WEB_SEARCH_DESCRIPTION_CHARS = 600;\n\n/*\n * These are type aliases, not interfaces: pi stores tool details as strict\n * JSON, and only aliases satisfy its `JsonValue` index signature.\n */\n\n/** What the model says when it wants a search. */\nexport type WebSearchToolInput = {\n /** The search query. */\n query: string;\n /** How many results to return, 1–10. Defaults to the host's `limit`. */\n limit?: number;\n};\n\n/**\n * One search result. `url` and `title` are always present; the rest only\n * when the provider returns them (Exa adds `imageUrl` and\n * `lastModifiedDate`, Linkup adds `faviconUrl`, Ceramic adds neither).\n */\nexport type WebSearchResult = {\n url: string;\n title: string;\n /** A snippet or extract of the page. Length and style vary by provider. */\n description?: string;\n /** ISO 8601 timestamp of the page's last modification. */\n lastModifiedDate?: string;\n imageUrl?: string;\n faviconUrl?: string;\n};\n\n/** The Web Search API's response: results plus request metadata. */\nexport type WebSearchResponse = {\n items: WebSearchResult[];\n metadata: {\n /** The query as the provider saw it. */\n query: string;\n /** AI Gateway request id, for the gateway log. */\n requestId: string;\n /** Provider latency in milliseconds. */\n latencyMs: number;\n };\n};\n\n/**\n * What one `web_search` run produces for the host: the full API response and\n * which provider served it. The model sees {@link renderWebSearchResults}\n * of this.\n */\nexport type WebSearchToolOutput = WebSearchResponse & {\n /** The provider that ran the search, when the source knows. */\n provider?: string;\n};\n\nexport const WEB_SEARCH_TOOL_DESCRIPTION = [\n \"Search the public web. Returns up to `limit` results, each with a URL, a title, and a short description of the page.\",\n \"Use it for current events, facts you're unsure of, documentation, and anything that may have changed since your training.\",\n \"There is no next page: for more or different results, search again with a rephrased query.\",\n \"Results are discovery only: to read a page, fetch its URL with `web_fetch` if it is available.\"\n].join(\" \");\n\nexport interface RenderWebSearchResultsOptions {\n /**\n * Trim each description to this many characters in the model's view.\n * Defaults to {@link DEFAULT_WEB_SEARCH_DESCRIPTION_CHARS}; `Infinity`\n * disables trimming.\n */\n maxDescriptionChars?: number;\n}\n\n/**\n * Render search results as the text the model reads: one numbered block per\n * result, descriptions trimmed and whitespace collapsed.\n */\nexport function renderWebSearchResults(\n response: WebSearchResponse,\n options: RenderWebSearchResultsOptions = {}\n): string {\n const maxChars =\n options.maxDescriptionChars ?? DEFAULT_WEB_SEARCH_DESCRIPTION_CHARS;\n if (response.items.length === 0) {\n return `No results for \"${response.metadata.query}\". Try a broader or rephrased query.`;\n }\n const blocks = response.items.map((item, index) => {\n const lines = [`${index + 1}. ${item.title.trim() || item.url}`, item.url];\n const description = compactDescription(item.description, maxChars);\n if (description) lines.push(description);\n if (item.lastModifiedDate) lines.push(`Modified: ${item.lastModifiedDate}`);\n return lines.join(\"\\n\");\n });\n return [\n `${response.items.length} result${response.items.length === 1 ? \"\" : \"s\"} for \"${response.metadata.query}\":`,\n \"\",\n blocks.join(\"\\n\\n\")\n ].join(\"\\n\");\n}\n\nfunction compactDescription(\n description: string | undefined,\n maxChars: number\n): string | undefined {\n if (!description) return undefined;\n const compact = description.replace(/\\s+/g, \" \").trim();\n if (!compact) return undefined;\n if (compact.length <= maxChars) return compact;\n const cut = compact.slice(0, Math.max(0, maxChars - 1));\n // Back off to the last word boundary, unless that loses too much.\n const space = cut.lastIndexOf(\" \");\n const end = space >= cut.length - WORD_BOUNDARY_BACKOFF ? space : cut.length;\n return `${cut.slice(0, end).trimEnd()}…`;\n}\n\n/** How far truncation backs off to end on a whole word. */\nconst WORD_BOUNDARY_BACKOFF = 20;\n","/**\n * Where searches run. A {@link WebSearchSource} turns a query into a\n * {@link WebSearchResponse}; the tool core is written against it, so the\n * same tool works over the Workers AI binding, the HTTP API, or a fake in\n * tests.\n */\nimport { DEFAULT_GATEWAY_ID } from \"../../models/core/settings\";\nimport { abortable } from \"../abortable\";\nimport {\n DEFAULT_WEB_SEARCH_LIMIT,\n MAX_WEB_SEARCH_LIMIT,\n MAX_WEB_SEARCH_QUERY_LENGTH,\n type WebSearchResponse,\n type WebSearchResult\n} from \"./contract\";\n\n/**\n * Search providers behind the Cloudflare Web Search API. `ceramic` is the\n * platform default and the cheapest; `exa` and `linkup` cost more and, in our\n * testing, ranked announcement-style queries noticeably better. Pricing and\n * data-retention terms are in the Web Search docs.\n */\nexport type WebSearchProvider = \"ceramic\" | \"exa\" | \"linkup\";\n\n/** One search request as a source receives it. */\nexport interface WebSearchRequest {\n /** 1–1024 characters. */\n query: string;\n /** 1–10. Defaults to 5. */\n limit?: number;\n}\n\n/** Per-call options a source receives alongside the request. */\nexport interface WebSearchCallOptions {\n /** Abort the search. A source should reject with the signal's reason. */\n signal?: AbortSignal;\n}\n\n/**\n * Runs searches. The tool core calls `search` and renders what comes back.\n * An object rather than a bare function so a wrapper (caching, logging)\n * keeps `provider` by spreading the source it wraps.\n */\nexport interface WebSearchSource {\n search(\n request: WebSearchRequest,\n options?: WebSearchCallOptions\n ): Promise<WebSearchResponse>;\n /**\n * The provider this source searches with, when known. Informational:\n * recorded on the tool's host output.\n */\n readonly provider?: WebSearchProvider;\n}\n\n/** Options shared by the AI-binding and HTTP sources. */\nexport interface WebSearchGatewayOptions {\n /** AI Gateway id. Defaults to `\"default\"`, which Cloudflare creates on first use. */\n gateway?: string;\n /**\n * Which provider runs the search. Host-chosen; the model never picks.\n * Defaults to the platform default (`ceramic`).\n */\n provider?: WebSearchProvider;\n /**\n * The BYOK key alias on the gateway to bill the provider with. When set\n * and no such key exists the request fails rather than falling back to\n * AI Gateway credits. When omitted, a key under the `default` alias is\n * used if one exists, otherwise credits.\n */\n byokAlias?: string;\n}\n\n/** A search over the Workers AI binding (`env.AI.websearch`). */\nexport interface AIWebSearchOptions extends WebSearchGatewayOptions {\n /** The Workers AI binding. Requires `\"ai\": { \"binding\": \"AI\" }` in wrangler.jsonc. */\n binding: Ai;\n}\n\n/**\n * Search through the Workers AI binding. The Worker's own account and the\n * named gateway are billed.\n */\nexport function createAIWebSearch(\n options: AIWebSearchOptions\n): WebSearchSource {\n validateGatewayOptions(options);\n const binding = options.binding;\n const search: WebSearchSource[\"search\"] = async (input, call = {}) => {\n const request = validateRequest(input);\n if (typeof binding.websearch !== \"function\") {\n throw new WebSearchError(\n \"This Workers runtime has no env.AI.websearch(). Web search needs workerd 1.20260924.1 or later; see https://github.com/cloudflare/agents/blob/main/docs/agents/search-the-web.md.\",\n {\n status: 501,\n code: \"web_search_unsupported_runtime\",\n retryable: false\n }\n );\n }\n // Do not start a billed search for a call that is already cancelled.\n call.signal?.throwIfAborted();\n // The binding takes no signal, so an abort stops waiting for it.\n const response = await abortable(\n binding.websearch({\n gatewayId: options.gateway ?? DEFAULT_GATEWAY_ID,\n query: request.query,\n limit: request.limit,\n provider: options.provider,\n byokAlias: options.byokAlias\n }),\n call.signal\n );\n return readResponse(response, request.query);\n };\n return { search, provider: options.provider };\n}\n\n/** A search over the HTTP API, from any runtime with `fetch`. */\nexport interface HTTPWebSearchOptions extends WebSearchGatewayOptions {\n /** The Cloudflare account the gateway belongs to. */\n accountId: string;\n /** An API token with `Workers AI: Read` and `AI Gateway: Read` on that account. */\n apiToken: string;\n /** Override for tests. Defaults to the global `fetch`. */\n fetch?: typeof fetch;\n /** API origin. Defaults to `https://api.cloudflare.com`. */\n baseUrl?: string;\n}\n\n/**\n * Search through `POST /accounts/{account_id}/ai/websearch`. Use this\n * outside Workers, or to search through a gateway in another account.\n */\nexport function createHTTPWebSearch(\n options: HTTPWebSearchOptions\n): WebSearchSource {\n validateGatewayOptions(options);\n const doFetch = options.fetch ?? fetch;\n const baseUrl = (options.baseUrl ?? \"https://api.cloudflare.com\").replace(\n /\\/+$/,\n \"\"\n );\n const url = `${baseUrl}/client/v4/accounts/${encodeURIComponent(options.accountId)}/ai/websearch/`;\n const search: WebSearchSource[\"search\"] = async (input, call = {}) => {\n const request = validateRequest(input);\n const response = await doFetch(url, {\n method: \"POST\",\n signal: call.signal,\n headers: {\n Authorization: `Bearer ${options.apiToken}`,\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify({\n query: request.query,\n limit: request.limit,\n provider: options.provider,\n byokAlias: options.byokAlias,\n options: { gateway: { id: options.gateway ?? DEFAULT_GATEWAY_ID } }\n })\n });\n return readResponse(response, request.query);\n };\n return { search, provider: options.provider };\n}\n\n/** The API's pattern for `byokAlias`. */\nconst BYOK_ALIAS_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;\n\n/** Host configuration errors are programming errors: throw when building. */\nfunction validateGatewayOptions(options: WebSearchGatewayOptions): void {\n if (\n options.byokAlias !== undefined &&\n !BYOK_ALIAS_PATTERN.test(options.byokAlias)\n ) {\n throw new RangeError(\n `byokAlias must be 1–64 letters, digits, \"_\" or \"-\"; got ${JSON.stringify(options.byokAlias)}.`\n );\n }\n}\n\n/**\n * Error codes a {@link WebSearchError} carries. The `web_search_*` codes the\n * API returns pass through as-is; failures without one get a code from the\n * HTTP status, so every error has a code to branch on.\n */\nexport type WebSearchErrorCode =\n /** No AI Gateway credits and no provider key for the search. */\n | \"web_search_payment_required\"\n /** `byokAlias` names a key the gateway does not have. */\n | \"web_search_byok_not_configured\"\n /** The query, limit, or provider was rejected. */\n | \"invalid_web_search_input\"\n /** The gateway does not exist or is not set up. */\n | \"web_search_gateway_not_configured\"\n /** The API token or binding is not allowed to search. */\n | \"web_search_unauthorized\"\n /** Too many searches; retry later. */\n | \"web_search_rate_limited\"\n /** The search took longer than the tool's `timeoutMs`. */\n | \"web_search_timeout\"\n /** The API or the provider failed. */\n | \"web_search_unavailable\"\n /** The Workers runtime has no `env.AI.websearch()`. */\n | \"web_search_unsupported_runtime\"\n /** A code the API added after this release. */\n | (string & {});\n\n/** Why a search failed, with what the API said. */\nexport class WebSearchError extends Error {\n override readonly name = \"WebSearchError\";\n readonly status: number;\n readonly code: WebSearchErrorCode;\n /** Whether retrying the same search might succeed. */\n readonly retryable: boolean;\n /** AI Gateway's id for the failed request, for the gateway log. */\n readonly requestId?: string;\n /** The Cloudflare API's numeric error code, when it gave one. */\n readonly apiCode?: number;\n\n constructor(\n message: string,\n details: {\n status: number;\n code: WebSearchErrorCode;\n retryable?: boolean;\n requestId?: string;\n apiCode?: number;\n cause?: unknown;\n }\n ) {\n super(message, { cause: details.cause });\n this.status = details.status;\n this.code = details.code;\n this.retryable = details.retryable ?? isRetryableStatus(details.status);\n this.requestId = details.requestId;\n this.apiCode = details.apiCode;\n }\n}\n\n/** Check a request against the API's limits and fill in the default limit. */\nfunction validateRequest(\n request: WebSearchRequest\n): Required<WebSearchRequest> {\n const query = request.query.trim();\n const limit = request.limit ?? DEFAULT_WEB_SEARCH_LIMIT;\n if (query.length === 0) {\n throw new WebSearchError(\"Query must not be empty.\", {\n status: 400,\n code: \"invalid_web_search_input\"\n });\n }\n if (query.length > MAX_WEB_SEARCH_QUERY_LENGTH) {\n throw new WebSearchError(\n `Query must be at most ${MAX_WEB_SEARCH_QUERY_LENGTH} characters.`,\n { status: 400, code: \"invalid_web_search_input\" }\n );\n }\n if (!Number.isInteger(limit) || limit < 1 || limit > MAX_WEB_SEARCH_LIMIT) {\n throw new WebSearchError(\n `Limit must be an integer from 1 to ${MAX_WEB_SEARCH_LIMIT}.`,\n { status: 400, code: \"invalid_web_search_input\" }\n );\n }\n return { query, limit };\n}\n\nasync function readResponse(\n response: Response,\n query: string\n): Promise<WebSearchResponse> {\n const text = await response.text();\n let body: unknown;\n try {\n body = JSON.parse(text);\n } catch {\n body = undefined;\n }\n if (!response.ok) throw toWebSearchError(response.status, body, text);\n const result = toWebSearchResponse(body, query);\n if (!result) {\n // A success status with a body that is not a search response is an\n // upstream fault, likely transient, not a reason to stop searching.\n throw new WebSearchError(\n `Web search returned HTTP ${response.status} without search results${text ? `: ${text.slice(0, 200)}` : \"\"}.`,\n {\n status: response.status,\n code: \"web_search_unavailable\",\n retryable: true\n }\n );\n }\n return result;\n}\n\n/**\n * Read a 200 body into a {@link WebSearchResponse}, tolerating what a beta\n * API and three providers might send: items without a URL are dropped, a\n * missing title falls back to the URL, unknown or mistyped fields are left\n * out, and missing metadata gets defaults. Only a body with no `items`\n * array is rejected.\n */\nfunction toWebSearchResponse(\n body: unknown,\n query: string\n): WebSearchResponse | undefined {\n if (!isRecord(body) || !Array.isArray(body.items)) return undefined;\n const items: WebSearchResult[] = [];\n for (const item of body.items as unknown[]) {\n if (!isRecord(item)) continue;\n const url = asString(item.url)?.trim();\n if (!url) continue;\n const result: WebSearchResult = {\n url,\n title: asString(item.title)?.trim() || url\n };\n for (const key of OPTIONAL_RESULT_FIELDS) {\n const value = asString(item[key]);\n if (value !== undefined) result[key] = value;\n }\n items.push(result);\n }\n const metadata = isRecord(body.metadata) ? body.metadata : {};\n return {\n items,\n metadata: {\n query: asString(metadata.query) ?? query,\n requestId: asString(metadata.requestId) ?? \"\",\n latencyMs: typeof metadata.latencyMs === \"number\" ? metadata.latencyMs : 0\n }\n };\n}\n\nconst OPTIONAL_RESULT_FIELDS = [\n \"description\",\n \"lastModifiedDate\",\n \"imageUrl\",\n \"faviconUrl\"\n] as const satisfies readonly (keyof WebSearchResult)[];\n\n/**\n * The API fails in three shapes. Gateway-native:\n * `{ ok: false, error: { category, code, status, retryable, gatewayRequestId } }`.\n * Request validation, the Cloudflare envelope with issue details:\n * `{ success: false, errors: [{ code: 7000, message }], messages: [{ message, path }] }`.\n * Gateway configuration (`AiGatewayError`):\n * `{ success: false, error: [{ code: 2001, message }], message, description }`.\n */\nfunction toWebSearchError(\n status: number,\n body: unknown,\n text: string\n): WebSearchError {\n if (isRecord(body)) {\n const error = body.error;\n if (isRecord(error) && typeof error.code === \"string\") {\n const errorStatus =\n typeof error.status === \"number\" ? error.status : status;\n return new WebSearchError(describeCode(error.code, errorStatus), {\n status: errorStatus,\n code: error.code,\n retryable:\n typeof error.retryable === \"boolean\" ? error.retryable : undefined,\n requestId: asString(error.gatewayRequestId)\n });\n }\n const envelope: unknown[] = Array.isArray(body.errors)\n ? body.errors\n : Array.isArray(error)\n ? error\n : [];\n const entries = envelope.filter(isRecord);\n const issues: unknown[] = Array.isArray(body.messages) ? body.messages : [];\n const message =\n [\n ...entries.map((entry) => asString(entry.message)),\n ...issues.map(describeIssue)\n ]\n .filter((m): m is string => Boolean(m))\n .join(\"; \") || asString(body.message);\n if (message) {\n const apiCode = entries\n .map((entry) => entry.code)\n .find((code): code is number => typeof code === \"number\");\n return new WebSearchError(message, {\n status,\n code: codeForApiError(apiCode, status),\n apiCode\n });\n }\n }\n return new WebSearchError(\n `Web search failed with HTTP ${status}${text ? `: ${text.slice(0, 200)}` : \"\"}.`,\n { status, code: codeForStatus(status) }\n );\n}\n\nfunction codeForApiError(\n apiCode: number | undefined,\n status: number\n): WebSearchErrorCode {\n switch (apiCode) {\n case 7000:\n return \"invalid_web_search_input\";\n case 2001:\n return \"web_search_gateway_not_configured\";\n default:\n return codeForStatus(status);\n }\n}\n\nfunction codeForStatus(status: number): WebSearchErrorCode {\n if (status === 400) return \"invalid_web_search_input\";\n if (status === 401 || status === 403) return \"web_search_unauthorized\";\n if (status === 402) return \"web_search_payment_required\";\n if (status === 429) return \"web_search_rate_limited\";\n return \"web_search_unavailable\";\n}\n\nfunction isRetryableStatus(status: number): boolean {\n return status === 429 || status >= 500;\n}\n\nfunction describeCode(code: string, status: number): string {\n switch (code) {\n case \"web_search_payment_required\":\n return \"Web search is unavailable: the AI Gateway has no credits and no provider key for this search. Top up AI Gateway credits or store a BYOK key for the provider.\";\n case \"web_search_byok_not_configured\":\n return \"Web search is unavailable: the requested BYOK key alias is not configured on the gateway.\";\n case \"invalid_web_search_input\":\n return \"The API rejected the request: check the query (1–1024 characters), limit (1–10), and provider (ceramic, exa, or linkup).\";\n default:\n return `Web search failed (${code}, HTTP ${status}).`;\n }\n}\n\nfunction describeIssue(issue: unknown): string | undefined {\n if (!isRecord(issue)) return undefined;\n const message = asString(issue.message);\n if (!message) return undefined;\n const path = Array.isArray(issue.path) ? issue.path.join(\".\") : \"\";\n return path ? `${path}: ${message}` : message;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction asString(value: unknown): string | undefined {\n return typeof value === \"string\" ? value : undefined;\n}\n","/**\n * The harness-neutral core of the `web_search` tool, shared by the pi\n * (`agents/websearch/pi`), AI SDK (`agents/websearch/ai-sdk`), and TanStack\n * AI (`agents/websearch/tanstack-ai`) adapters. Internal — not an entry point.\n */\nimport {\n DEFAULT_WEB_SEARCH_LIMIT,\n MAX_WEB_SEARCH_LIMIT,\n WEB_SEARCH_TOOL_DESCRIPTION,\n WEB_SEARCH_TOOL_NAME,\n renderWebSearchResults,\n type WebSearchResponse,\n type WebSearchToolInput,\n type WebSearchToolOutput\n} from \"./contract\";\nimport { abortable } from \"../abortable\";\nimport {\n WebSearchError,\n createAIWebSearch,\n type AIWebSearchOptions,\n type WebSearchSource\n} from \"./source\";\n\n/** Search through the Workers AI binding; the tool builds the source. */\nexport type WebSearchToolBindingOptions = AIWebSearchOptions & {\n source?: never;\n};\n\n/**\n * Search through a source you built. Gateway, provider, and billing belong\n * to the source, so the binding options are not accepted here.\n */\nexport type WebSearchToolSourceOptions = {\n /** Where searches run, instead of the Workers AI binding. */\n source: WebSearchSource;\n binding?: never;\n gateway?: never;\n provider?: never;\n byokAlias?: never;\n};\n\n/**\n * Options every `web_search` tool adapter accepts. Invalid values throw a\n * `RangeError` when the tool is created.\n */\nexport type WebSearchToolOptions = (\n | WebSearchToolBindingOptions\n | WebSearchToolSourceOptions\n) & {\n /**\n * Results per search when the model doesn't ask for a count, and the most\n * it gets when it asks for more. An integer from 1 to 10; defaults to 5.\n */\n limit?: number;\n /** Replaces the default tool description. */\n description?: string;\n /**\n * Per-result description length in the model's view. A positive integer;\n * defaults to 600 characters. `Infinity` passes descriptions through whole.\n */\n maxDescriptionChars?: number;\n /**\n * Give up on a search after this many milliseconds, as a retryable\n * `web_search_timeout` failure. A positive integer; defaults to 30 seconds.\n */\n timeoutMs?: number;\n};\n\n/** The default for {@link WebSearchToolOptions.timeoutMs}. */\nexport const DEFAULT_WEB_SEARCH_TIMEOUT_MS = 30_000;\n\n/**\n * What one run returns to the adapter: the host output, and the model's\n * text. On failure, `error` is the source's error, with the API's detail for\n * the host, and `text` is what the model should read instead.\n */\nexport type WebSearchToolRun =\n | { ok: true; output: WebSearchToolOutput; text: string }\n | { ok: false; error: WebSearchError; text: string };\n\nexport interface WebSearchToolCore {\n name: typeof WEB_SEARCH_TOOL_NAME;\n description: string;\n /** The host's `limit`: the default and the cap for the model's `limit`. */\n limit: number;\n /** Render a search's output as the text the model reads. */\n render(output: WebSearchResponse): string;\n /**\n * Run one search. Aborting `signal` rejects with its reason rather than\n * producing a failed run, so the harness sees a cancelled call.\n */\n run(\n input: WebSearchToolInput,\n options?: { signal?: AbortSignal }\n ): Promise<WebSearchToolRun>;\n}\n\nexport function createWebSearchToolCore(\n options: WebSearchToolOptions\n): WebSearchToolCore {\n const limit = options.limit ?? DEFAULT_WEB_SEARCH_LIMIT;\n const timeoutMs = options.timeoutMs ?? DEFAULT_WEB_SEARCH_TIMEOUT_MS;\n const { maxDescriptionChars } = options;\n if (!Number.isInteger(limit) || limit < 1 || limit > MAX_WEB_SEARCH_LIMIT) {\n throw new RangeError(\n `limit must be an integer from 1 to ${MAX_WEB_SEARCH_LIMIT}; got ${limit}.`\n );\n }\n if (\n maxDescriptionChars !== undefined &&\n maxDescriptionChars !== Infinity &&\n !(Number.isInteger(maxDescriptionChars) && maxDescriptionChars > 0)\n ) {\n throw new RangeError(\n `maxDescriptionChars must be a positive integer or Infinity; got ${maxDescriptionChars}.`\n );\n }\n if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {\n throw new RangeError(\n `timeoutMs must be a positive integer; got ${timeoutMs}.`\n );\n }\n const source =\n options.source === undefined ? createAIWebSearch(options) : options.source;\n const provider = source.provider;\n const render = (output: WebSearchResponse) =>\n renderWebSearchResults(output, { maxDescriptionChars });\n\n return {\n name: WEB_SEARCH_TOOL_NAME,\n description: options.description ?? WEB_SEARCH_TOOL_DESCRIPTION,\n limit,\n render,\n async run(input, { signal } = {}) {\n signal?.throwIfAborted();\n const request = {\n query: input.query,\n limit: Math.min(clampLimit(input.limit ?? limit), limit)\n };\n const timeout = AbortSignal.timeout(timeoutMs);\n const searchSignal = signal\n ? AbortSignal.any([signal, timeout])\n : timeout;\n try {\n // Race the source too: a custom source may ignore the signal, and\n // the deadline must still end the call.\n const response = await abortable(\n source.search(request, { signal: searchSignal }),\n searchSignal\n );\n const output: WebSearchToolOutput = provider\n ? { ...response, provider }\n : response;\n return {\n ok: true,\n output,\n text: render(response)\n };\n } catch (cause) {\n if (signal?.aborted) throw signal.reason;\n const error = toToolError(cause, timeout.aborted, timeoutMs);\n return { ok: false, error, text: describeFailureForModel(error) };\n }\n }\n };\n}\n\n/**\n * The error a throwing adapter (AI SDK, TanStack AI) raises for a failed\n * run. Those frameworks show the model the error's `message`, so it carries\n * the model's text; the source's error, with the API's detail, is `cause`.\n */\nexport function toolFailure(\n run: Extract<WebSearchToolRun, { ok: false }>\n): WebSearchError {\n const { error } = run;\n return new WebSearchError(run.text, {\n status: error.status,\n code: error.code,\n retryable: error.retryable,\n requestId: error.requestId,\n apiCode: error.apiCode,\n cause: error\n });\n}\n\nfunction toToolError(\n cause: unknown,\n timedOut: boolean,\n timeoutMs: number\n): WebSearchError {\n if (timedOut) {\n return new WebSearchError(`Web search timed out after ${timeoutMs} ms.`, {\n status: 504,\n code: \"web_search_timeout\",\n retryable: true,\n cause\n });\n }\n if (cause instanceof WebSearchError) return cause;\n return new WebSearchError(\n cause instanceof Error ? cause.message : String(cause),\n { status: 500, code: \"web_search_unavailable\", retryable: true, cause }\n );\n}\n\n/**\n * What the model reads when a search fails: whether to retry, change the\n * query, or carry on without search. The operator's detail (credits, keys,\n * gateway setup) stays on the error for the host.\n */\nfunction describeFailureForModel(error: WebSearchError): string {\n if (error.code === \"invalid_web_search_input\") {\n return `Web search rejected the query (${error.message.replace(/\\.$/, \"\")}). Fix the query and try again.`;\n }\n if (error.retryable) {\n return `Web search failed temporarily (${error.code}). You may retry once.`;\n }\n return `Web search is unavailable here (${error.code}). Do not retry; answer without it and say that web search was unavailable.`;\n}\n\n/** The model's `limit`, made an integer from 1 to 10. */\nfunction clampLimit(value: number): number {\n if (!Number.isFinite(value)) return DEFAULT_WEB_SEARCH_LIMIT;\n return Math.min(MAX_WEB_SEARCH_LIMIT, Math.max(1, Math.trunc(value)));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tool-DkT_DJBm.js","names":[],"sources":["../src/web/search/contract.ts","../src/web/search/source.ts","../src/web/search/tool.ts"],"sourcesContent":["/**\n * The `web_search` tool contract: its name, what it tells the model, what the\n * model passes in, what comes back, and how results are rendered for the\n * model. Shared by every executor of the tool — the harness-executed\n * adapters in `agents/websearch/*` today, and a gateway-executed variant in\n * `agents/models/*` once AI Gateway server tools ship — so a `web_search` call\n * looks the same in a transcript whoever ran it.\n */\n\n/**\n * The tool's name, as the model calls it and as it appears in transcripts.\n * `web_search` is the name models know from Anthropic's and OpenAI's built-in\n * search tools.\n */\nexport const WEB_SEARCH_TOOL_NAME = \"web_search\";\n\n/** The default number of results when the model doesn't ask for a count. */\nexport const DEFAULT_WEB_SEARCH_LIMIT = 5;\n\n/** The most results one search can return; the API rejects more. */\nexport const MAX_WEB_SEARCH_LIMIT = 10;\n\n/** The longest query the API accepts, in characters. */\nexport const MAX_WEB_SEARCH_QUERY_LENGTH = 1024;\n\n/**\n * Per-result description length in the model's view, in characters. Provider\n * descriptions run to ~8,000 characters (Ceramic returns page text), which at\n * ten results is 80 KB of context per search. The full text stays in the\n * host-side output.\n */\nexport const DEFAULT_WEB_SEARCH_DESCRIPTION_CHARS = 600;\n\n/*\n * These are type aliases, not interfaces: pi stores tool details as strict\n * JSON, and only aliases satisfy its `JsonValue` index signature.\n */\n\n/** What the model says when it wants a search. */\nexport type WebSearchToolInput = {\n /** The search query. */\n query: string;\n /** How many results to return, 1–10. Defaults to the host's `limit`. */\n limit?: number;\n};\n\n/**\n * One search result. `url` and `title` are always present; the rest only\n * when the provider returns them (Exa adds `imageUrl` and\n * `lastModifiedDate`, Linkup adds `faviconUrl`, Ceramic adds neither).\n */\nexport type WebSearchResult = {\n url: string;\n title: string;\n /** A snippet or extract of the page. Length and style vary by provider. */\n description?: string;\n /** ISO 8601 timestamp of the page's last modification. */\n lastModifiedDate?: string;\n imageUrl?: string;\n faviconUrl?: string;\n};\n\n/** The Web Search API's response: results plus request metadata. */\nexport type WebSearchResponse = {\n items: WebSearchResult[];\n metadata: {\n /** The query as the provider saw it. */\n query: string;\n /** AI Gateway request id, for the gateway log. */\n requestId: string;\n /** Provider latency in milliseconds. */\n latencyMs: number;\n };\n};\n\n/**\n * What one `web_search` run produces for the host: the full API response and\n * which provider served it. The model sees {@link renderWebSearchResults}\n * of this.\n */\nexport type WebSearchToolOutput = WebSearchResponse & {\n /** The provider that ran the search, when the source knows. */\n provider?: string;\n};\n\nexport const WEB_SEARCH_TOOL_DESCRIPTION = [\n \"Search the public web. Returns up to `limit` results, each with a URL, a title, and a short description of the page.\",\n \"Use it for current events, facts you're unsure of, documentation, and anything that may have changed since your training.\",\n \"There is no next page: for more or different results, search again with a rephrased query.\",\n \"Results are discovery only: to read a page, fetch its URL with `web_fetch` if it is available.\"\n].join(\" \");\n\nexport interface RenderWebSearchResultsOptions {\n /**\n * Trim each description to this many characters in the model's view.\n * Defaults to {@link DEFAULT_WEB_SEARCH_DESCRIPTION_CHARS}; `Infinity`\n * disables trimming.\n */\n maxDescriptionChars?: number;\n}\n\n/**\n * Render search results as the text the model reads: one numbered block per\n * result, descriptions trimmed and whitespace collapsed.\n */\nexport function renderWebSearchResults(\n response: WebSearchResponse,\n options: RenderWebSearchResultsOptions = {}\n): string {\n const maxChars =\n options.maxDescriptionChars ?? DEFAULT_WEB_SEARCH_DESCRIPTION_CHARS;\n if (response.items.length === 0) {\n return `No results for \"${response.metadata.query}\". Try a broader or rephrased query.`;\n }\n const blocks = response.items.map((item, index) => {\n const lines = [`${index + 1}. ${item.title.trim() || item.url}`, item.url];\n const description = compactDescription(item.description, maxChars);\n if (description) lines.push(description);\n if (item.lastModifiedDate) lines.push(`Modified: ${item.lastModifiedDate}`);\n return lines.join(\"\\n\");\n });\n return [\n `${response.items.length} result${response.items.length === 1 ? \"\" : \"s\"} for \"${response.metadata.query}\":`,\n \"\",\n blocks.join(\"\\n\\n\")\n ].join(\"\\n\");\n}\n\nfunction compactDescription(\n description: string | undefined,\n maxChars: number\n): string | undefined {\n if (!description) return undefined;\n const compact = description.replace(/\\s+/g, \" \").trim();\n if (!compact) return undefined;\n if (compact.length <= maxChars) return compact;\n const cut = compact.slice(0, Math.max(0, maxChars - 1));\n // Back off to the last word boundary, unless that loses too much.\n const space = cut.lastIndexOf(\" \");\n const end = space >= cut.length - WORD_BOUNDARY_BACKOFF ? space : cut.length;\n return `${cut.slice(0, end).trimEnd()}…`;\n}\n\n/** How far truncation backs off to end on a whole word. */\nconst WORD_BOUNDARY_BACKOFF = 20;\n","/**\n * Where searches run. A {@link WebSearchSource} turns a query into a\n * {@link WebSearchResponse}; the tool core is written against it, so the\n * same tool works over the Workers AI binding, the HTTP API, or a fake in\n * tests.\n */\nimport { DEFAULT_GATEWAY_ID } from \"../../models/core/settings\";\nimport { abortable } from \"../abortable\";\nimport {\n DEFAULT_WEB_SEARCH_LIMIT,\n MAX_WEB_SEARCH_LIMIT,\n MAX_WEB_SEARCH_QUERY_LENGTH,\n type WebSearchResponse,\n type WebSearchResult\n} from \"./contract\";\n\n/**\n * Search providers behind the Cloudflare Web Search API. `ceramic` is the\n * platform default and the cheapest; `exa` and `linkup` cost more and, in our\n * testing, ranked announcement-style queries noticeably better. Pricing and\n * data-retention terms are in the Web Search docs.\n */\nexport type WebSearchProvider = \"ceramic\" | \"exa\" | \"linkup\";\n\n/** One search request as a source receives it. */\nexport interface WebSearchRequest {\n /** 1–1024 characters. */\n query: string;\n /** 1–10. Defaults to 5. */\n limit?: number;\n}\n\n/** Per-call options a source receives alongside the request. */\nexport interface WebSearchCallOptions {\n /** Abort the search. A source should reject with the signal's reason. */\n signal?: AbortSignal;\n}\n\n/**\n * Runs searches. The tool core calls `search` and renders what comes back.\n * An object rather than a bare function so a wrapper (caching, logging)\n * keeps `provider` by spreading the source it wraps.\n */\nexport interface WebSearchSource {\n search(\n request: WebSearchRequest,\n options?: WebSearchCallOptions\n ): Promise<WebSearchResponse>;\n /**\n * The provider this source searches with, when known. Informational:\n * recorded on the tool's host output.\n */\n readonly provider?: WebSearchProvider;\n}\n\n/** Options shared by the AI-binding and HTTP sources. */\nexport interface WebSearchGatewayOptions {\n /** AI Gateway id. Defaults to `\"default\"`, which Cloudflare creates on first use. */\n gateway?: string;\n /**\n * Which provider runs the search. Host-chosen; the model never picks.\n * Defaults to the platform default (`ceramic`).\n */\n provider?: WebSearchProvider;\n /**\n * The BYOK key alias on the gateway to bill the provider with. When set\n * and no such key exists the request fails rather than falling back to\n * AI Gateway credits. When omitted, a key under the `default` alias is\n * used if one exists, otherwise credits.\n */\n byokAlias?: string;\n}\n\n/** A search over the Workers AI binding (`env.AI.websearch`). */\nexport interface AIWebSearchOptions extends WebSearchGatewayOptions {\n /** The Workers AI binding. Requires `\"ai\": { \"binding\": \"AI\" }` in wrangler.jsonc. */\n binding: Ai;\n}\n\n/**\n * Search through the Workers AI binding. The Worker's own account and the\n * named gateway are billed.\n */\nexport function createAIWebSearch(\n options: AIWebSearchOptions\n): WebSearchSource {\n validateGatewayOptions(options);\n const binding = options.binding;\n const search: WebSearchSource[\"search\"] = async (input, call = {}) => {\n const request = validateRequest(input);\n if (typeof binding.websearch !== \"function\") {\n throw new WebSearchError(\n \"This Workers runtime has no env.AI.websearch(). Web search needs workerd 1.20260924.1 or later; see https://github.com/cloudflare/agents/blob/main/docs/agents/search-the-web.md.\",\n {\n status: 501,\n code: \"web_search_unsupported_runtime\",\n retryable: false\n }\n );\n }\n // Do not start a billed search for a call that is already cancelled.\n call.signal?.throwIfAborted();\n // The binding takes no signal, so an abort stops waiting for it.\n const response = await abortable(\n binding.websearch({\n gatewayId: options.gateway ?? DEFAULT_GATEWAY_ID,\n query: request.query,\n limit: request.limit,\n provider: options.provider,\n byokAlias: options.byokAlias\n }),\n call.signal\n );\n return readResponse(response, request.query);\n };\n return { search, provider: options.provider };\n}\n\n/** A search over the HTTP API, from any runtime with `fetch`. */\nexport interface HTTPWebSearchOptions extends WebSearchGatewayOptions {\n /** The Cloudflare account the gateway belongs to. */\n accountId: string;\n /** An API token with `Workers AI: Read` and `AI Gateway: Read` on that account. */\n apiToken: string;\n /** Override for tests. Defaults to the global `fetch`. */\n fetch?: typeof fetch;\n /** API origin. Defaults to `https://api.cloudflare.com`. */\n baseUrl?: string;\n}\n\n/**\n * Search through `POST /accounts/{account_id}/ai/websearch`. Use this\n * outside Workers, or to search through a gateway in another account.\n */\nexport function createHTTPWebSearch(\n options: HTTPWebSearchOptions\n): WebSearchSource {\n validateGatewayOptions(options);\n const doFetch = options.fetch ?? fetch;\n const baseUrl = (options.baseUrl ?? \"https://api.cloudflare.com\").replace(\n /\\/+$/,\n \"\"\n );\n const url = `${baseUrl}/client/v4/accounts/${encodeURIComponent(options.accountId)}/ai/websearch/`;\n const search: WebSearchSource[\"search\"] = async (input, call = {}) => {\n const request = validateRequest(input);\n const response = await doFetch(url, {\n method: \"POST\",\n signal: call.signal,\n headers: {\n Authorization: `Bearer ${options.apiToken}`,\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify({\n query: request.query,\n limit: request.limit,\n provider: options.provider,\n byokAlias: options.byokAlias,\n options: { gateway: { id: options.gateway ?? DEFAULT_GATEWAY_ID } }\n })\n });\n return readResponse(response, request.query);\n };\n return { search, provider: options.provider };\n}\n\n/** The API's pattern for `byokAlias`. */\nconst BYOK_ALIAS_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;\n\n/** Host configuration errors are programming errors: throw when building. */\nfunction validateGatewayOptions(options: WebSearchGatewayOptions): void {\n if (\n options.byokAlias !== undefined &&\n !BYOK_ALIAS_PATTERN.test(options.byokAlias)\n ) {\n throw new RangeError(\n `byokAlias must be 1–64 letters, digits, \"_\" or \"-\"; got ${JSON.stringify(options.byokAlias)}.`\n );\n }\n}\n\n/**\n * Error codes a {@link WebSearchError} carries. The `web_search_*` codes the\n * API returns pass through as-is; failures without one get a code from the\n * HTTP status, so every error has a code to branch on.\n */\nexport type WebSearchErrorCode =\n /** No AI Gateway credits and no provider key for the search. */\n | \"web_search_payment_required\"\n /** `byokAlias` names a key the gateway does not have. */\n | \"web_search_byok_not_configured\"\n /** The query, limit, or provider was rejected. */\n | \"invalid_web_search_input\"\n /** The gateway does not exist or is not set up. */\n | \"web_search_gateway_not_configured\"\n /** The API token or binding is not allowed to search. */\n | \"web_search_unauthorized\"\n /** Too many searches; retry later. */\n | \"web_search_rate_limited\"\n /** The search took longer than the tool's `timeoutMs`. */\n | \"web_search_timeout\"\n /** The API or the provider failed. */\n | \"web_search_unavailable\"\n /** The Workers runtime has no `env.AI.websearch()`. */\n | \"web_search_unsupported_runtime\"\n /** A code the API added after this release. */\n | (string & {});\n\n/** Why a search failed, with what the API said. */\nexport class WebSearchError extends Error {\n override readonly name = \"WebSearchError\";\n readonly status: number;\n readonly code: WebSearchErrorCode;\n /** Whether retrying the same search might succeed. */\n readonly retryable: boolean;\n /** AI Gateway's id for the failed request, for the gateway log. */\n readonly requestId?: string;\n /** The Cloudflare API's numeric error code, when it gave one. */\n readonly apiCode?: number;\n\n constructor(\n message: string,\n details: {\n status: number;\n code: WebSearchErrorCode;\n retryable?: boolean;\n requestId?: string;\n apiCode?: number;\n cause?: unknown;\n }\n ) {\n super(message, { cause: details.cause });\n this.status = details.status;\n this.code = details.code;\n this.retryable = details.retryable ?? isRetryableStatus(details.status);\n this.requestId = details.requestId;\n this.apiCode = details.apiCode;\n }\n}\n\n/** Check a request against the API's limits and fill in the default limit. */\nfunction validateRequest(\n request: WebSearchRequest\n): Required<WebSearchRequest> {\n const query = request.query.trim();\n const limit = request.limit ?? DEFAULT_WEB_SEARCH_LIMIT;\n if (query.length === 0) {\n throw new WebSearchError(\"Query must not be empty.\", {\n status: 400,\n code: \"invalid_web_search_input\"\n });\n }\n if (query.length > MAX_WEB_SEARCH_QUERY_LENGTH) {\n throw new WebSearchError(\n `Query must be at most ${MAX_WEB_SEARCH_QUERY_LENGTH} characters.`,\n { status: 400, code: \"invalid_web_search_input\" }\n );\n }\n if (!Number.isInteger(limit) || limit < 1 || limit > MAX_WEB_SEARCH_LIMIT) {\n throw new WebSearchError(\n `Limit must be an integer from 1 to ${MAX_WEB_SEARCH_LIMIT}.`,\n { status: 400, code: \"invalid_web_search_input\" }\n );\n }\n return { query, limit };\n}\n\nasync function readResponse(\n response: Response,\n query: string\n): Promise<WebSearchResponse> {\n const text = await response.text();\n let body: unknown;\n try {\n body = JSON.parse(text);\n } catch {\n body = undefined;\n }\n if (!response.ok) throw toWebSearchError(response.status, body, text);\n const result = toWebSearchResponse(body, query);\n if (!result) {\n // A success status with a body that is not a search response is an\n // upstream fault, likely transient, not a reason to stop searching.\n throw new WebSearchError(\n `Web search returned HTTP ${response.status} without search results${text ? `: ${text.slice(0, 200)}` : \"\"}.`,\n {\n status: response.status,\n code: \"web_search_unavailable\",\n retryable: true\n }\n );\n }\n return result;\n}\n\n/**\n * Read a 200 body into a {@link WebSearchResponse}, tolerating what a beta\n * API and three providers might send: items without a URL are dropped, a\n * missing title falls back to the URL, unknown or mistyped fields are left\n * out, and missing metadata gets defaults. Only a body with no `items`\n * array is rejected.\n */\nfunction toWebSearchResponse(\n body: unknown,\n query: string\n): WebSearchResponse | undefined {\n if (!isRecord(body) || !Array.isArray(body.items)) return undefined;\n const items: WebSearchResult[] = [];\n for (const item of body.items as unknown[]) {\n if (!isRecord(item)) continue;\n const url = asString(item.url)?.trim();\n if (!url) continue;\n const result: WebSearchResult = {\n url,\n title: asString(item.title)?.trim() || url\n };\n for (const key of OPTIONAL_RESULT_FIELDS) {\n const value = asString(item[key]);\n if (value !== undefined) result[key] = value;\n }\n items.push(result);\n }\n const metadata = isRecord(body.metadata) ? body.metadata : {};\n return {\n items,\n metadata: {\n query: asString(metadata.query) ?? query,\n requestId: asString(metadata.requestId) ?? \"\",\n latencyMs: typeof metadata.latencyMs === \"number\" ? metadata.latencyMs : 0\n }\n };\n}\n\nconst OPTIONAL_RESULT_FIELDS = [\n \"description\",\n \"lastModifiedDate\",\n \"imageUrl\",\n \"faviconUrl\"\n] as const satisfies readonly (keyof WebSearchResult)[];\n\n/**\n * The API fails in three shapes. Gateway-native:\n * `{ ok: false, error: { category, code, status, retryable, gatewayRequestId } }`.\n * Request validation, the Cloudflare envelope with issue details:\n * `{ success: false, errors: [{ code: 7000, message }], messages: [{ message, path }] }`.\n * Gateway configuration (`AiGatewayError`):\n * `{ success: false, error: [{ code: 2001, message }], message, description }`.\n */\nfunction toWebSearchError(\n status: number,\n body: unknown,\n text: string\n): WebSearchError {\n if (isRecord(body)) {\n const error = body.error;\n if (isRecord(error) && typeof error.code === \"string\") {\n const errorStatus =\n typeof error.status === \"number\" ? error.status : status;\n return new WebSearchError(describeCode(error.code, errorStatus), {\n status: errorStatus,\n code: error.code,\n retryable:\n typeof error.retryable === \"boolean\" ? error.retryable : undefined,\n requestId: asString(error.gatewayRequestId)\n });\n }\n const envelope: unknown[] = Array.isArray(body.errors)\n ? body.errors\n : Array.isArray(error)\n ? error\n : [];\n const entries = envelope.filter(isRecord);\n const issues: unknown[] = Array.isArray(body.messages) ? body.messages : [];\n const message =\n [\n ...entries.map((entry) => asString(entry.message)),\n ...issues.map(describeIssue)\n ]\n .filter((m): m is string => Boolean(m))\n .join(\"; \") || asString(body.message);\n if (message) {\n const apiCode = entries\n .map((entry) => entry.code)\n .find((code): code is number => typeof code === \"number\");\n return new WebSearchError(message, {\n status,\n code: codeForApiError(apiCode, status),\n apiCode\n });\n }\n }\n return new WebSearchError(\n `Web search failed with HTTP ${status}${text ? `: ${text.slice(0, 200)}` : \"\"}.`,\n { status, code: codeForStatus(status) }\n );\n}\n\nfunction codeForApiError(\n apiCode: number | undefined,\n status: number\n): WebSearchErrorCode {\n switch (apiCode) {\n case 7000:\n return \"invalid_web_search_input\";\n case 2001:\n return \"web_search_gateway_not_configured\";\n default:\n return codeForStatus(status);\n }\n}\n\nfunction codeForStatus(status: number): WebSearchErrorCode {\n if (status === 400) return \"invalid_web_search_input\";\n if (status === 401 || status === 403) return \"web_search_unauthorized\";\n if (status === 402) return \"web_search_payment_required\";\n if (status === 429) return \"web_search_rate_limited\";\n return \"web_search_unavailable\";\n}\n\nfunction isRetryableStatus(status: number): boolean {\n return status === 429 || status >= 500;\n}\n\nfunction describeCode(code: string, status: number): string {\n switch (code) {\n case \"web_search_payment_required\":\n return \"Web search is unavailable: the AI Gateway has no credits and no provider key for this search. Top up AI Gateway credits or store a BYOK key for the provider.\";\n case \"web_search_byok_not_configured\":\n return \"Web search is unavailable: the requested BYOK key alias is not configured on the gateway.\";\n case \"invalid_web_search_input\":\n return \"The API rejected the request: check the query (1–1024 characters), limit (1–10), and provider (ceramic, exa, or linkup).\";\n default:\n return `Web search failed (${code}, HTTP ${status}).`;\n }\n}\n\nfunction describeIssue(issue: unknown): string | undefined {\n if (!isRecord(issue)) return undefined;\n const message = asString(issue.message);\n if (!message) return undefined;\n const path = Array.isArray(issue.path) ? issue.path.join(\".\") : \"\";\n return path ? `${path}: ${message}` : message;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction asString(value: unknown): string | undefined {\n return typeof value === \"string\" ? value : undefined;\n}\n","/**\n * The harness-neutral core of the `web_search` tool, shared by the pi\n * (`agents/websearch/pi`), AI SDK (`agents/websearch/ai-sdk`), and TanStack\n * AI (`agents/websearch/tanstack-ai`) adapters. Internal — not an entry point.\n */\nimport {\n DEFAULT_WEB_SEARCH_LIMIT,\n MAX_WEB_SEARCH_LIMIT,\n WEB_SEARCH_TOOL_DESCRIPTION,\n WEB_SEARCH_TOOL_NAME,\n renderWebSearchResults,\n type WebSearchResponse,\n type WebSearchToolInput,\n type WebSearchToolOutput\n} from \"./contract\";\nimport { abortable } from \"../abortable\";\nimport {\n WebSearchError,\n createAIWebSearch,\n type AIWebSearchOptions,\n type WebSearchSource\n} from \"./source\";\n\n/** Search through the Workers AI binding; the tool builds the source. */\nexport type WebSearchToolBindingOptions = AIWebSearchOptions & {\n source?: never;\n};\n\n/**\n * Search through a source you built. Gateway, provider, and billing belong\n * to the source, so the binding options are not accepted here.\n */\nexport type WebSearchToolSourceOptions = {\n /** Where searches run, instead of the Workers AI binding. */\n source: WebSearchSource;\n binding?: never;\n gateway?: never;\n provider?: never;\n byokAlias?: never;\n};\n\n/**\n * Options every `web_search` tool adapter accepts. Invalid values throw a\n * `RangeError` when the tool is created.\n */\nexport type WebSearchToolOptions = (\n | WebSearchToolBindingOptions\n | WebSearchToolSourceOptions\n) & {\n /**\n * Results per search when the model doesn't ask for a count, and the most\n * it gets when it asks for more. An integer from 1 to 10; defaults to 5.\n */\n limit?: number;\n /** Replaces the default tool description. */\n description?: string;\n /**\n * Per-result description length in the model's view. A positive integer;\n * defaults to 600 characters. `Infinity` passes descriptions through whole.\n */\n maxDescriptionChars?: number;\n /**\n * Give up on a search after this many milliseconds, as a retryable\n * `web_search_timeout` failure. A positive integer; defaults to 30 seconds.\n */\n timeoutMs?: number;\n};\n\n/** The default for {@link WebSearchToolOptions.timeoutMs}. */\nexport const DEFAULT_WEB_SEARCH_TIMEOUT_MS = 30_000;\n\n/**\n * What one run returns to the adapter: the host output, and the model's\n * text. On failure, `error` is the source's error, with the API's detail for\n * the host, and `text` is what the model should read instead.\n */\nexport type WebSearchToolRun =\n | { ok: true; output: WebSearchToolOutput; text: string }\n | { ok: false; error: WebSearchError; text: string };\n\nexport interface WebSearchToolCore {\n name: typeof WEB_SEARCH_TOOL_NAME;\n description: string;\n /** The host's `limit`: the default and the cap for the model's `limit`. */\n limit: number;\n /** Render a search's output as the text the model reads. */\n render(output: WebSearchResponse): string;\n /**\n * Run one search. Aborting `signal` rejects with its reason rather than\n * producing a failed run, so the harness sees a cancelled call.\n */\n run(\n input: WebSearchToolInput,\n options?: { signal?: AbortSignal }\n ): Promise<WebSearchToolRun>;\n}\n\nexport function createWebSearchToolCore(\n options: WebSearchToolOptions\n): WebSearchToolCore {\n const limit = options.limit ?? DEFAULT_WEB_SEARCH_LIMIT;\n const timeoutMs = options.timeoutMs ?? DEFAULT_WEB_SEARCH_TIMEOUT_MS;\n const { maxDescriptionChars } = options;\n if (!Number.isInteger(limit) || limit < 1 || limit > MAX_WEB_SEARCH_LIMIT) {\n throw new RangeError(\n `limit must be an integer from 1 to ${MAX_WEB_SEARCH_LIMIT}; got ${limit}.`\n );\n }\n if (\n maxDescriptionChars !== undefined &&\n maxDescriptionChars !== Infinity &&\n !(Number.isInteger(maxDescriptionChars) && maxDescriptionChars > 0)\n ) {\n throw new RangeError(\n `maxDescriptionChars must be a positive integer or Infinity; got ${maxDescriptionChars}.`\n );\n }\n if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {\n throw new RangeError(\n `timeoutMs must be a positive integer; got ${timeoutMs}.`\n );\n }\n const source =\n options.source === undefined ? createAIWebSearch(options) : options.source;\n const provider = source.provider;\n const render = (output: WebSearchResponse) =>\n renderWebSearchResults(output, { maxDescriptionChars });\n\n return {\n name: WEB_SEARCH_TOOL_NAME,\n description: options.description ?? WEB_SEARCH_TOOL_DESCRIPTION,\n limit,\n render,\n async run(input, { signal }: { signal?: AbortSignal } = {}) {\n signal?.throwIfAborted();\n const request = {\n query: input.query,\n limit: Math.min(clampLimit(input.limit ?? limit), limit)\n };\n const timeout = AbortSignal.timeout(timeoutMs);\n const searchSignal = signal\n ? AbortSignal.any([signal, timeout])\n : timeout;\n try {\n // Race the source too: a custom source may ignore the signal, and\n // the deadline must still end the call.\n const response = await abortable(\n source.search(request, { signal: searchSignal }),\n searchSignal\n );\n const output: WebSearchToolOutput = provider\n ? { ...response, provider }\n : response;\n return {\n ok: true,\n output,\n text: render(response)\n };\n } catch (cause) {\n if (signal?.aborted) throw signal.reason;\n const error = toToolError(cause, timeout.aborted, timeoutMs);\n return { ok: false, error, text: describeFailureForModel(error) };\n }\n }\n };\n}\n\n/**\n * The error a throwing adapter (AI SDK, TanStack AI) raises for a failed\n * run. Those frameworks show the model the error's `message`, so it carries\n * the model's text; the source's error, with the API's detail, is `cause`.\n */\nexport function toolFailure(\n run: Extract<WebSearchToolRun, { ok: false }>\n): WebSearchError {\n const { error } = run;\n return new WebSearchError(run.text, {\n status: error.status,\n code: error.code,\n retryable: error.retryable,\n requestId: error.requestId,\n apiCode: error.apiCode,\n cause: error\n });\n}\n\nfunction toToolError(\n cause: unknown,\n timedOut: boolean,\n timeoutMs: number\n): WebSearchError {\n if (timedOut) {\n return new WebSearchError(`Web search timed out after ${timeoutMs} ms.`, {\n status: 504,\n code: \"web_search_timeout\",\n retryable: true,\n cause\n });\n }\n if (cause instanceof WebSearchError) return cause;\n return new WebSearchError(\n cause instanceof Error ? cause.message : String(cause),\n { status: 500, code: \"web_search_unavailable\", retryable: true, cause }\n );\n}\n\n/**\n * What the model reads when a search fails: whether to retry, change the\n * query, or carry on without search. The operator's detail (credits, keys,\n * gateway setup) stays on the error for the host.\n */\nfunction describeFailureForModel(error: WebSearchError): string {\n if (error.code === \"invalid_web_search_input\") {\n return `Web search rejected the query (${error.message.replace(/\\.$/, \"\")}). Fix the query and try again.`;\n }\n if (error.retryable) {\n return `Web search failed temporarily (${error.code}). You may retry once.`;\n }\n return `Web search is unavailable here (${error.code}). Do not retry; answer without it and say that web search was unavailable.`;\n}\n\n/** The model's `limit`, made an integer from 1 to 10. */\nfunction clampLimit(value: number): number {\n if (!Number.isFinite(value)) return DEFAULT_WEB_SEARCH_LIMIT;\n return Math.min(MAX_WEB_SEARCH_LIMIT, Math.max(1, Math.trunc(value)));\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAcA,MAAa,uBAAuB;;AAGpC,MAAa,2BAA2B;;AAGxC,MAAa,uBAAuB;;AAGpC,MAAa,8BAA8B;;;;;;;AAQ3C,MAAa,uCAAuC;AAsDpD,MAAa,8BAA8B;CACzC;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,GAAG;;;;;AAeV,SAAgB,uBACd,UACA,UAAyC,CAAC,GAClC;CACR,MAAM,WACJ,QAAQ,uBAAA;CACV,IAAI,SAAS,MAAM,WAAW,GAC5B,OAAO,mBAAmB,SAAS,SAAS,MAAM;CAEpD,MAAM,SAAS,SAAS,MAAM,KAAK,MAAM,UAAU;EACjD,MAAM,QAAQ,CAAC,GAAG,QAAQ,EAAE,IAAI,KAAK,MAAM,KAAK,KAAK,KAAK,OAAO,KAAK,GAAG;EACzE,MAAM,cAAc,mBAAmB,KAAK,aAAa,QAAQ;EACjE,IAAI,aAAa,MAAM,KAAK,WAAW;EACvC,IAAI,KAAK,kBAAkB,MAAM,KAAK,aAAa,KAAK,kBAAkB;EAC1E,OAAO,MAAM,KAAK,IAAI;CACxB,CAAC;CACD,OAAO;EACL,GAAG,SAAS,MAAM,OAAO,SAAS,SAAS,MAAM,WAAW,IAAI,KAAK,IAAI,QAAQ,SAAS,SAAS,MAAM;EACzG;EACA,OAAO,KAAK,MAAM;CACpB,CAAC,CAAC,KAAK,IAAI;AACb;AAEA,SAAS,mBACP,aACA,UACoB;CACpB,IAAI,CAAC,aAAa,OAAO,KAAA;CACzB,MAAM,UAAU,YAAY,QAAQ,QAAQ,GAAG,CAAC,CAAC,KAAK;CACtD,IAAI,CAAC,SAAS,OAAO,KAAA;CACrB,IAAI,QAAQ,UAAU,UAAU,OAAO;CACvC,MAAM,MAAM,QAAQ,MAAM,GAAG,KAAK,IAAI,GAAG,WAAW,CAAC,CAAC;CAEtD,MAAM,QAAQ,IAAI,YAAY,GAAG;CACjC,MAAM,MAAM,SAAS,IAAI,SAAS,wBAAwB,QAAQ,IAAI;CACtE,OAAO,GAAG,IAAI,MAAM,GAAG,GAAG,CAAC,CAAC,QAAQ,EAAE;AACxC;;AAGA,MAAM,wBAAwB;;;;;;;;;;;;;AC7D9B,SAAgB,kBACd,SACiB;CACjB,uBAAuB,OAAO;CAC9B,MAAM,UAAU,QAAQ;CACxB,MAAM,SAAoC,OAAO,OAAO,OAAO,CAAC,MAAM;EACpE,MAAM,UAAU,gBAAgB,KAAK;EACrC,IAAI,OAAO,QAAQ,cAAc,YAC/B,MAAM,IAAI,eACR,qLACA;GACE,QAAQ;GACR,MAAM;GACN,WAAW;EACb,CACF;EAGF,KAAK,QAAQ,eAAe;EAY5B,OAAO,aAAa,MAVG,UACrB,QAAQ,UAAU;GAChB,WAAW,QAAQ,WAAA;GACnB,OAAO,QAAQ;GACf,OAAO,QAAQ;GACf,UAAU,QAAQ;GAClB,WAAW,QAAQ;EACrB,CAAC,GACD,KAAK,MACP,GAC8B,QAAQ,KAAK;CAC7C;CACA,OAAO;EAAE;EAAQ,UAAU,QAAQ;CAAS;AAC9C;;;;;AAkBA,SAAgB,oBACd,SACiB;CACjB,uBAAuB,OAAO;CAC9B,MAAM,UAAU,QAAQ,SAAS;CAKjC,MAAM,MAAM,IAJK,QAAQ,WAAW,6BAAA,CAA8B,QAChE,QACA,EAEmB,EAAE,sBAAsB,mBAAmB,QAAQ,SAAS,EAAE;CACnF,MAAM,SAAoC,OAAO,OAAO,OAAO,CAAC,MAAM;EACpE,MAAM,UAAU,gBAAgB,KAAK;EAgBrC,OAAO,aAAa,MAfG,QAAQ,KAAK;GAClC,QAAQ;GACR,QAAQ,KAAK;GACb,SAAS;IACP,eAAe,UAAU,QAAQ;IACjC,gBAAgB;GAClB;GACA,MAAM,KAAK,UAAU;IACnB,OAAO,QAAQ;IACf,OAAO,QAAQ;IACf,UAAU,QAAQ;IAClB,WAAW,QAAQ;IACnB,SAAS,EAAE,SAAS,EAAE,IAAI,QAAQ,WAAA,UAA8B,EAAE;GACpE,CAAC;EACH,CAAC,GAC6B,QAAQ,KAAK;CAC7C;CACA,OAAO;EAAE;EAAQ,UAAU,QAAQ;CAAS;AAC9C;;AAGA,MAAM,qBAAqB;;AAG3B,SAAS,uBAAuB,SAAwC;CACtE,IACE,QAAQ,cAAc,KAAA,KACtB,CAAC,mBAAmB,KAAK,QAAQ,SAAS,GAE1C,MAAM,IAAI,WACR,2DAA2D,KAAK,UAAU,QAAQ,SAAS,EAAE,EAC/F;AAEJ;;AA8BA,IAAa,iBAAb,cAAoC,MAAM;CAWxC,YACE,SACA,SAQA;EACA,MAAM,SAAS,EAAE,OAAO,QAAQ,MAAM,CAAC;EArBzC,KAAkB,OAAO;EAsBvB,KAAK,SAAS,QAAQ;EACtB,KAAK,OAAO,QAAQ;EACpB,KAAK,YAAY,QAAQ,aAAa,kBAAkB,QAAQ,MAAM;EACtE,KAAK,YAAY,QAAQ;EACzB,KAAK,UAAU,QAAQ;CACzB;AACF;;AAGA,SAAS,gBACP,SAC4B;CAC5B,MAAM,QAAQ,QAAQ,MAAM,KAAK;CACjC,MAAM,QAAQ,QAAQ,SAAA;CACtB,IAAI,MAAM,WAAW,GACnB,MAAM,IAAI,eAAe,4BAA4B;EACnD,QAAQ;EACR,MAAM;CACR,CAAC;CAEH,IAAI,MAAM,SAAA,MACR,MAAM,IAAI,eACR,yBAAyB,4BAA4B,eACrD;EAAE,QAAQ;EAAK,MAAM;CAA2B,CAClD;CAEF,IAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,KAAK,QAAA,IAC3C,MAAM,IAAI,eACR,0CACA;EAAE,QAAQ;EAAK,MAAM;CAA2B,CAClD;CAEF,OAAO;EAAE;EAAO;CAAM;AACxB;AAEA,eAAe,aACb,UACA,OAC4B;CAC5B,MAAM,OAAO,MAAM,SAAS,KAAK;CACjC,IAAI;CACJ,IAAI;EACF,OAAO,KAAK,MAAM,IAAI;CACxB,QAAQ;EACN,OAAO,KAAA;CACT;CACA,IAAI,CAAC,SAAS,IAAI,MAAM,iBAAiB,SAAS,QAAQ,MAAM,IAAI;CACpE,MAAM,SAAS,oBAAoB,MAAM,KAAK;CAC9C,IAAI,CAAC,QAGH,MAAM,IAAI,eACR,4BAA4B,SAAS,OAAO,yBAAyB,OAAO,KAAK,KAAK,MAAM,GAAG,GAAG,MAAM,GAAG,IAC3G;EACE,QAAQ,SAAS;EACjB,MAAM;EACN,WAAW;CACb,CACF;CAEF,OAAO;AACT;;;;;;;;AASA,SAAS,oBACP,MACA,OAC+B;CAC/B,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC,MAAM,QAAQ,KAAK,KAAK,GAAG,OAAO,KAAA;CAC1D,MAAM,QAA2B,CAAC;CAClC,KAAK,MAAM,QAAQ,KAAK,OAAoB;EAC1C,IAAI,CAAC,SAAS,IAAI,GAAG;EACrB,MAAM,MAAM,SAAS,KAAK,GAAG,CAAC,EAAE,KAAK;EACrC,IAAI,CAAC,KAAK;EACV,MAAM,SAA0B;GAC9B;GACA,OAAO,SAAS,KAAK,KAAK,CAAC,EAAE,KAAK,KAAK;EACzC;EACA,KAAK,MAAM,OAAO,wBAAwB;GACxC,MAAM,QAAQ,SAAS,KAAK,IAAI;GAChC,IAAI,UAAU,KAAA,GAAW,OAAO,OAAO;EACzC;EACA,MAAM,KAAK,MAAM;CACnB;CACA,MAAM,WAAW,SAAS,KAAK,QAAQ,IAAI,KAAK,WAAW,CAAC;CAC5D,OAAO;EACL;EACA,UAAU;GACR,OAAO,SAAS,SAAS,KAAK,KAAK;GACnC,WAAW,SAAS,SAAS,SAAS,KAAK;GAC3C,WAAW,OAAO,SAAS,cAAc,WAAW,SAAS,YAAY;EAC3E;CACF;AACF;AAEA,MAAM,yBAAyB;CAC7B;CACA;CACA;CACA;AACF;;;;;;;;;AAUA,SAAS,iBACP,QACA,MACA,MACgB;CAChB,IAAI,SAAS,IAAI,GAAG;EAClB,MAAM,QAAQ,KAAK;EACnB,IAAI,SAAS,KAAK,KAAK,OAAO,MAAM,SAAS,UAAU;GACrD,MAAM,cACJ,OAAO,MAAM,WAAW,WAAW,MAAM,SAAS;GACpD,OAAO,IAAI,eAAe,aAAa,MAAM,MAAM,WAAW,GAAG;IAC/D,QAAQ;IACR,MAAM,MAAM;IACZ,WACE,OAAO,MAAM,cAAc,YAAY,MAAM,YAAY,KAAA;IAC3D,WAAW,SAAS,MAAM,gBAAgB;GAC5C,CAAC;EACH;EAMA,MAAM,WALsB,MAAM,QAAQ,KAAK,MAAM,IACjD,KAAK,SACL,MAAM,QAAQ,KAAK,IACjB,QACA,CAAC,EAAA,CACkB,OAAO,QAAQ;EACxC,MAAM,SAAoB,MAAM,QAAQ,KAAK,QAAQ,IAAI,KAAK,WAAW,CAAC;EAC1E,MAAM,UACJ,CACE,GAAG,QAAQ,KAAK,UAAU,SAAS,MAAM,OAAO,CAAC,GACjD,GAAG,OAAO,IAAI,aAAa,CAC7B,CAAC,CACE,QAAQ,MAAmB,QAAQ,CAAC,CAAC,CAAC,CACtC,KAAK,IAAI,KAAK,SAAS,KAAK,OAAO;EACxC,IAAI,SAAS;GACX,MAAM,UAAU,QACb,KAAK,UAAU,MAAM,IAAI,CAAC,CAC1B,MAAM,SAAyB,OAAO,SAAS,QAAQ;GAC1D,OAAO,IAAI,eAAe,SAAS;IACjC;IACA,MAAM,gBAAgB,SAAS,MAAM;IACrC;GACF,CAAC;EACH;CACF;CACA,OAAO,IAAI,eACT,+BAA+B,SAAS,OAAO,KAAK,KAAK,MAAM,GAAG,GAAG,MAAM,GAAG,IAC9E;EAAE;EAAQ,MAAM,cAAc,MAAM;CAAE,CACxC;AACF;AAEA,SAAS,gBACP,SACA,QACoB;CACpB,QAAQ,SAAR;EACE,KAAK,KACH,OAAO;EACT,KAAK,MACH,OAAO;EACT,SACE,OAAO,cAAc,MAAM;CAC/B;AACF;AAEA,SAAS,cAAc,QAAoC;CACzD,IAAI,WAAW,KAAK,OAAO;CAC3B,IAAI,WAAW,OAAO,WAAW,KAAK,OAAO;CAC7C,IAAI,WAAW,KAAK,OAAO;CAC3B,IAAI,WAAW,KAAK,OAAO;CAC3B,OAAO;AACT;AAEA,SAAS,kBAAkB,QAAyB;CAClD,OAAO,WAAW,OAAO,UAAU;AACrC;AAEA,SAAS,aAAa,MAAc,QAAwB;CAC1D,QAAQ,MAAR;EACE,KAAK,+BACH,OAAO;EACT,KAAK,kCACH,OAAO;EACT,KAAK,4BACH,OAAO;EACT,SACE,OAAO,sBAAsB,KAAK,SAAS,OAAO;CACtD;AACF;AAEA,SAAS,cAAc,OAAoC;CACzD,IAAI,CAAC,SAAS,KAAK,GAAG,OAAO,KAAA;CAC7B,MAAM,UAAU,SAAS,MAAM,OAAO;CACtC,IAAI,CAAC,SAAS,OAAO,KAAA;CACrB,MAAM,OAAO,MAAM,QAAQ,MAAM,IAAI,IAAI,MAAM,KAAK,KAAK,GAAG,IAAI;CAChE,OAAO,OAAO,GAAG,KAAK,IAAI,YAAY;AACxC;AAEA,SAAS,SAAS,OAAkD;CAClE,OAAO,OAAO,UAAU,YAAY,UAAU;AAChD;AAEA,SAAS,SAAS,OAAoC;CACpD,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAA;AAC7C;;;;;;;;;AC7XA,MAAa,gCAAgC;AA4B7C,SAAgB,wBACd,SACmB;CACnB,MAAM,QAAQ,QAAQ,SAAA;CACtB,MAAM,YAAY,QAAQ,aAAA;CAC1B,MAAM,EAAE,wBAAwB;CAChC,IAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,KAAK,QAAA,IAC3C,MAAM,IAAI,WACR,8CAAmE,MAAM,EAC3E;CAEF,IACE,wBAAwB,KAAA,KACxB,wBAAwB,YACxB,EAAE,OAAO,UAAU,mBAAmB,KAAK,sBAAsB,IAEjE,MAAM,IAAI,WACR,mEAAmE,oBAAoB,EACzF;CAEF,IAAI,CAAC,OAAO,UAAU,SAAS,KAAK,aAAa,GAC/C,MAAM,IAAI,WACR,6CAA6C,UAAU,EACzD;CAEF,MAAM,SACJ,QAAQ,WAAW,KAAA,IAAY,kBAAkB,OAAO,IAAI,QAAQ;CACtE,MAAM,WAAW,OAAO;CACxB,MAAM,UAAU,WACd,uBAAuB,QAAQ,EAAE,oBAAoB,CAAC;CAExD,OAAO;EACL,MAAM;EACN,aAAa,QAAQ,eAAe;EACpC;EACA;EACA,MAAM,IAAI,OAAO,EAAE,WAAqC,CAAC,GAAG;GAC1D,QAAQ,eAAe;GACvB,MAAM,UAAU;IACd,OAAO,MAAM;IACb,OAAO,KAAK,IAAI,WAAW,MAAM,SAAS,KAAK,GAAG,KAAK;GACzD;GACA,MAAM,UAAU,YAAY,QAAQ,SAAS;GAC7C,MAAM,eAAe,SACjB,YAAY,IAAI,CAAC,QAAQ,OAAO,CAAC,IACjC;GACJ,IAAI;IAGF,MAAM,WAAW,MAAM,UACrB,OAAO,OAAO,SAAS,EAAE,QAAQ,aAAa,CAAC,GAC/C,YACF;IAIA,OAAO;KACL,IAAI;KACJ,QALkC,WAChC;MAAE,GAAG;MAAU;KAAS,IACxB;KAIF,MAAM,OAAO,QAAQ;IACvB;GACF,SAAS,OAAO;IACd,IAAI,QAAQ,SAAS,MAAM,OAAO;IAClC,MAAM,QAAQ,YAAY,OAAO,QAAQ,SAAS,SAAS;IAC3D,OAAO;KAAE,IAAI;KAAO;KAAO,MAAM,wBAAwB,KAAK;IAAE;GAClE;EACF;CACF;AACF;;;;;;AAOA,SAAgB,YACd,KACgB;CAChB,MAAM,EAAE,UAAU;CAClB,OAAO,IAAI,eAAe,IAAI,MAAM;EAClC,QAAQ,MAAM;EACd,MAAM,MAAM;EACZ,WAAW,MAAM;EACjB,WAAW,MAAM;EACjB,SAAS,MAAM;EACf,OAAO;CACT,CAAC;AACH;AAEA,SAAS,YACP,OACA,UACA,WACgB;CAChB,IAAI,UACF,OAAO,IAAI,eAAe,8BAA8B,UAAU,OAAO;EACvE,QAAQ;EACR,MAAM;EACN,WAAW;EACX;CACF,CAAC;CAEH,IAAI,iBAAiB,gBAAgB,OAAO;CAC5C,OAAO,IAAI,eACT,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GACrD;EAAE,QAAQ;EAAK,MAAM;EAA0B,WAAW;EAAM;CAAM,CACxE;AACF;;;;;;AAOA,SAAS,wBAAwB,OAA+B;CAC9D,IAAI,MAAM,SAAS,4BACjB,OAAO,kCAAkC,MAAM,QAAQ,QAAQ,OAAO,EAAE,EAAE;CAE5E,IAAI,MAAM,WACR,OAAO,kCAAkC,MAAM,KAAK;CAEtD,OAAO,mCAAmC,MAAM,KAAK;AACvD;;AAGA,SAAS,WAAW,OAAuB;CACzC,IAAI,CAAC,OAAO,SAAS,KAAK,GAAG,OAAA;CAC7B,OAAO,KAAK,IAAA,IAA0B,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,CAAC,CAAC;AACtE"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { t as __DO_NOT_USE_WILL_BREAK__agentContext } from "./current-agent-DhoDkSnH.js";
|
|
2
|
-
import "./internal_context.js";
|
|
3
2
|
import "./session-7CDgVE0z.js";
|
|
4
3
|
import { t as redactBase64Payloads } from "./base64-redaction-C1Uu29t5.js";
|
|
5
4
|
//#region src/browser/truncate.ts
|
|
@@ -253,4 +252,4 @@ function resolveCtx(options) {
|
|
|
253
252
|
//#endregion
|
|
254
253
|
export { truncateResult as a, transformBrowserResult as i, browserScreenshotOutput as n, resolveCtx as r, browserExecuteModelOutput as t };
|
|
255
254
|
|
|
256
|
-
//# sourceMappingURL=tool-helpers-
|
|
255
|
+
//# sourceMappingURL=tool-helpers-BI7ifO2S.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-helpers-BodWi4gd.js","names":["agentContext"],"sources":["../src/browser/truncate.ts","../src/browser/tool-helpers.ts"],"sourcesContent":["/**\n * Result truncation for the browser tools, copied from\n * `@cloudflare/codemode`'s `truncateResult` so `agents/browser` doesn't need\n * codemode. Internal — not an entry point.\n *\n * Structured values are truncated structurally: the output is always valid\n * JSON of the same shape, with the largest values cut first. A truncated\n * string ends with a marker, a truncated array ends with a marker element\n * counting the dropped items, and an object that had to lose entries carries a\n * marker entry naming the omitted keys. Every marker contains\n * `--- TRUNCATED ---` so callers and models can find them.\n */\n\n/** ~4 characters per token is a reasonable cross-model estimate. */\nconst CHARS_PER_TOKEN = 4;\nconst DEFAULT_MAX_TOKENS = 6000;\nconst TRUNCATION_MARKER = \"--- TRUNCATED ---\";\n/**\n * The least a string or container is worth keeping at (see `floorSize`). A\n * value that would get fewer serialized characters than this is dropped in\n * favour of its siblings rather than reduced to a bare marker.\n */\nconst MIN_SLOT = 128;\n\ntype TruncateOptions = {\n /**\n * Maximum characters in the (serialized) output before truncation kicks in.\n * Defaults to `maxTokens * 4`.\n */\n maxChars?: number;\n /** Token budget used to derive the default `maxChars`. Defaults to 6000. */\n maxTokens?: number;\n};\n\nfunction budget(options?: TruncateOptions): {\n maxChars: number;\n maxTokens: number;\n} {\n const maxTokens = options?.maxTokens ?? DEFAULT_MAX_TOKENS;\n const maxChars = options?.maxChars ?? maxTokens * CHARS_PER_TOKEN;\n return { maxChars, maxTokens };\n}\n\n/**\n * Truncate a text response to a character budget, appending a marker that notes\n * the original size so the model knows the output was clipped. Returns the\n * input unchanged when it is within budget.\n */\nfunction truncateResponse(text: string, options?: TruncateOptions): string {\n const { maxChars, maxTokens } = budget(options);\n if (text.length <= maxChars) return text;\n\n const estimatedTokens = Math.ceil(text.length / CHARS_PER_TOKEN);\n return (\n text.slice(0, maxChars) +\n `\\n\\n${TRUNCATION_MARKER}\\nResponse was ~${estimatedTokens.toLocaleString()} tokens ` +\n `(limit: ${maxTokens.toLocaleString()}). Narrow the request to reduce response size.`\n );\n}\n\n/**\n * Truncate a structured result. Strings are truncated directly. Other values\n * pass through unchanged (same reference) when their JSON serialization is\n * within budget. When oversized, the value is shrunk structurally — largest\n * values first, deepest last — so the model gets a bounded value that is still\n * valid JSON of the original shape, with markers where content was cut.\n *\n * Values that can't be serialized (cycles, bigint, `undefined`) are returned\n * unchanged.\n */\nexport function truncateResult(\n value: unknown,\n options?: TruncateOptions\n): unknown {\n if (typeof value === \"string\") return truncateResponse(value, options);\n\n let serialized: string | undefined;\n try {\n serialized = JSON.stringify(value);\n } catch {\n return value;\n }\n if (serialized === undefined) return value;\n\n const { maxChars } = budget(options);\n if (serialized.length <= maxChars) return value;\n\n // Re-parse so `toJSON`, class instances and `undefined` members are seen in\n // their serialized form — the shape the model would receive anyway.\n const shrunk = shrink(JSON.parse(serialized) as Json, maxChars);\n // `shrink` honours its budget for anything a container can hold; only a\n // scalar (a number too long for the budget) can still miss it.\n return size(shrunk) <= maxChars\n ? shrunk\n : truncateResponse(serialized, options);\n}\n\n// ---------------------------------------------------------------------------\n// Structural shrinking\n// ---------------------------------------------------------------------------\n//\n// Every `shrink*` below returns a value whose compact serialization fits the\n// budget it was given, provided the budget can hold an empty container (2\n// chars). Containers share their budget by water-filling: each child is owed\n// at least its floor (`floorSize`), small children keep their full size, and\n// the largest children absorb the cut. A container only drops children when\n// even their floors cannot fit — arrays drop from the tail (order carries\n// meaning), objects drop their largest values first (keys are all meaningful).\n\ntype Json = string | number | boolean | null | Json[] | { [key: string]: Json };\n\n/** Serialized size in characters (compact JSON). */\nfunction size(value: Json): number {\n return JSON.stringify(value).length;\n}\n\n/** Only strings and containers can give up characters; scalars are atomic. */\nfunction shrinkable(value: Json): boolean {\n return typeof value === \"string\" || (typeof value === \"object\" && !!value);\n}\n\n/**\n * The least budget a value is kept at; a scalar must fit whole. The floor\n * never exceeds half the container's own budget, so tiny budgets still keep\n * something rather than nothing.\n */\nfunction floorSize(value: Json, maxChars: number): number {\n const full = size(value);\n return shrinkable(value)\n ? Math.min(full, MIN_SLOT, Math.max(2, Math.floor(maxChars / 2)))\n : full;\n}\n\nfunction shrink(value: Json, maxChars: number): Json {\n if (size(value) <= maxChars) return value;\n if (typeof value === \"string\") return shrinkString(value, maxChars);\n if (Array.isArray(value)) return shrinkArray(value, maxChars);\n if (typeof value === \"object\" && value !== null) {\n return shrinkObject(value, maxChars);\n }\n return value;\n}\n\nfunction shrinkString(value: string, maxChars: number): string {\n const suffix = ` ${TRUNCATION_MARKER} ${value.length.toLocaleString()} chars`;\n let keep = Math.max(0, maxChars - suffix.length - 2);\n let out = value.slice(0, keep) + suffix;\n // Escapes inflate the serialized form; back off until it fits.\n while (keep > 0 && size(out) > maxChars) {\n keep = Math.max(0, keep - (size(out) - maxChars));\n out = value.slice(0, keep) + suffix;\n }\n // No room for a prefix and the size note: keep as much of the marker as fits.\n return size(out) <= maxChars\n ? out\n : TRUNCATION_MARKER.slice(0, Math.max(0, maxChars - 2));\n}\n\n/**\n * Share `available` characters across values by water-filling: values are\n * admitted smallest first, each taking the lesser of its full size and an\n * equal share of what is left. Requires `available >= Σ floorSize(values)`;\n * then every value receives at least its floor.\n */\nfunction allocate(values: Json[], available: number): number[] {\n const sizes = values.map(size);\n const order = sizes.map((_, i) => i).sort((a, b) => sizes[a] - sizes[b]);\n const allocation = new Array<number>(values.length);\n let remaining = available;\n let count = values.length;\n for (const i of order) {\n const alloc = Math.min(sizes[i], Math.floor(remaining / count));\n allocation[i] = alloc;\n remaining -= alloc;\n count--;\n }\n return allocation;\n}\n\nfunction shrinkArray(items: Json[], maxChars: number): Json[] {\n const marker = (dropped: number) =>\n `${TRUNCATION_MARKER} ${dropped.toLocaleString()} more items`;\n const floors = items.map((item) => floorSize(item, maxChars));\n const skeleton = (count: number) => 2 + Math.max(0, count - 1);\n\n // Keep everything when every floor fits.\n let keep = items.length;\n if (floors.reduce((n, f) => n + f, 0) + skeleton(keep) > maxChars) {\n // Otherwise keep the longest prefix whose floors fit beside a tail marker\n // (sized for the largest possible count, so the real one always fits).\n const tail = size(marker(items.length)) + 1;\n let used = skeleton(0) + tail;\n keep = 0;\n while (keep < items.length && used + floors[keep] + 1 <= maxChars) {\n used += floors[keep] + 1;\n keep++;\n }\n }\n\n const kept = items.slice(0, keep);\n const dropped = items.length - keep;\n const tail = dropped > 0 ? size(marker(dropped)) + (keep > 0 ? 1 : 0) : 0;\n const allocation = allocate(kept, maxChars - skeleton(keep) - tail);\n const out = kept.map((item, i) => shrink(item, allocation[i]));\n if (dropped > 0 && size([...out, marker(dropped)]) <= maxChars) {\n out.push(marker(dropped));\n }\n return out;\n}\n\nfunction shrinkObject(\n value: { [key: string]: Json },\n maxChars: number\n): { [key: string]: Json } {\n const entries = Object.entries(value);\n // The marker entry must not shadow a real key.\n let markerKey = TRUNCATION_MARKER;\n while (markerKey in value) markerKey += \" \";\n const keyCost = (key: string) => size(key) + 1;\n const floors = entries.map(([k, v]) => keyCost(k) + floorSize(v, maxChars));\n const floorOf = (chars: number) =>\n Math.min(chars, MIN_SLOT, Math.max(2, Math.floor(maxChars / 2)));\n\n // Drop the largest values first, only until the remaining floors fit. Costs\n // are tracked incrementally so a wide object stays linear in its key count;\n // the marker note's cost is derived arithmetically from the omitted names.\n const byValueSize = entries\n .map((_, i) => i)\n .sort((a, b) => size(entries[b][1]) - size(entries[a][1]));\n const dropped = new Set<number>();\n let present = entries.length;\n let floorsSum = floors.reduce((n, f) => n + f, 0);\n let namesLength = 0;\n const noteLength = () =>\n `${dropped.size.toLocaleString()} keys omitted: `.length +\n namesLength +\n 2 * (dropped.size - 1);\n const cost = () => {\n const marker =\n dropped.size > 0 ? keyCost(markerKey) + floorOf(noteLength() + 2) : 0;\n const count = present + (dropped.size > 0 ? 1 : 0);\n return 2 + Math.max(0, count - 1) + floorsSum + marker;\n };\n for (const i of byValueSize) {\n if (cost() <= maxChars) break;\n dropped.add(i);\n present--;\n floorsSum -= floors[i];\n namesLength += size(entries[i][0]) - 2;\n }\n\n const kept = entries.filter((_, i) => !dropped.has(i));\n const omitted = [...dropped].sort((a, b) => a - b).map((i) => entries[i][0]);\n const note = `${omitted.length.toLocaleString()} keys omitted: ${omitted.join(\", \")}`;\n if (cost() > maxChars) {\n // Not even the marker fits at its floor: keep as much of it as there is\n // room for, or nothing.\n const room = maxChars - 2 - keyCost(markerKey);\n return room >= 2 ? { [markerKey]: shrink(note, room) } : {};\n }\n const all: [string, Json][] = [...kept];\n if (omitted.length > 0) all.push([markerKey, note]);\n const fixed =\n 2 + Math.max(0, all.length - 1) + all.reduce((n, [k]) => n + keyCost(k), 0);\n // The note is shrunk like any other value, so it can never overshoot.\n const allocation = allocate(\n all.map(([, v]) => v),\n maxChars - fixed\n );\n const out: { [key: string]: Json } = {};\n all.forEach(([k, v], i) => {\n out[k] = shrink(v, allocation[i]);\n });\n return out;\n}\n","/**\n * Helpers shared by the browser tools: `createBrowserTools` in `ai.ts`, and\n * the `browserTool` adapters in `tools/`. Internal — not an entry point.\n */\nimport type { JSONValue } from \"ai\";\nimport { DEFAULT_MAX_RESULT_BYTES } from \"./session\";\nimport { truncateResult } from \"./truncate\";\nimport { redactBase64Payloads } from \"../core/base64-redaction\";\nimport { __DO_NOT_USE_WILL_BREAK__agentContext as agentContext } from \"../internal_context\";\n\ninterface BrowserScreenshotOutput {\n type: \"browser_screenshot\";\n mediaType: string;\n data: string;\n}\n\n/** What `Page.captureScreenshot` can produce. */\nconst SCREENSHOT_MEDIA_TYPES = new Set([\n \"image/png\",\n \"image/jpeg\",\n \"image/webp\"\n]);\nconst BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;\n\n/**\n * The screenshot the model's code returned, or `null` if it didn't return\n * one. The model writes this shape itself, so only an image type, base64\n * data, and the session's size limit count: anything else is treated as a\n * plain result and cut down like one, rather than kept whole or sent to the\n * model as an image.\n */\nexport function browserScreenshotOutput(\n value: unknown\n): BrowserScreenshotOutput | null {\n const outer =\n typeof value === \"object\" && value !== null\n ? (value as Record<string, unknown>)\n : null;\n const result =\n outer && typeof outer.result === \"object\" && outer.result !== null\n ? (outer.result as Record<string, unknown>)\n : outer;\n if (\n result?.type !== \"browser_screenshot\" ||\n typeof result.mediaType !== \"string\" ||\n typeof result.data !== \"string\" ||\n !SCREENSHOT_MEDIA_TYPES.has(result.mediaType) ||\n result.data.length > DEFAULT_MAX_RESULT_BYTES ||\n !BASE64.test(result.data)\n ) {\n return null;\n }\n return result as unknown as BrowserScreenshotOutput;\n}\n\nexport function transformBrowserResult(value: unknown): unknown {\n // Keep canonical screenshot output intact for UIMessage persistence and the\n // chat renderer. Other results are redacted before truncation so a nested\n // binary payload cannot become a large serialized preview.\n return browserScreenshotOutput(value)\n ? value\n : truncateResult(redactBase64Payloads(value));\n}\n\n/** What the model sees in place of a browser tool's output. */\nexport type BrowserModelOutput =\n | { type: \"text\"; value: string }\n | { type: \"json\"; value: JSONValue };\n\n/**\n * @param notes Sentences appended when a screenshot collapses the output to\n * text, so notices on the output aren't lost with it.\n */\nexport function browserExecuteModelOutput(\n output: unknown,\n notes: string[] = []\n): BrowserModelOutput {\n const screenshot = browserScreenshotOutput(output);\n if (screenshot) {\n const approximateBytes = Math.floor((screenshot.data.length * 3) / 4);\n return {\n type: \"text\",\n value: [\n `Screenshot captured successfully (${screenshot.mediaType}, approximately ${approximateBytes.toLocaleString()} bytes); the image is kept for the UI and omitted here.`,\n ...notes\n ].join(\" \")\n };\n }\n\n // `calls`, the durable audit log `createBrowserTools` adds, stays on the\n // persisted part and never enters the model's context, and the sandbox\n // `logs` are bounded like a result.\n const modelFacing =\n typeof output === \"object\" && output !== null && !Array.isArray(output)\n ? (({ calls: _calls, ...rest }: { calls?: unknown; logs?: unknown }) => ({\n ...rest,\n ...(Array.isArray(rest.logs)\n ? { logs: truncateResult(rest.logs) }\n : {})\n }))(output)\n : output;\n const redacted = redactBase64Payloads(modelFacing);\n try {\n const serialized = JSON.stringify(redacted);\n return {\n type: \"json\",\n value:\n serialized === undefined ? null : (JSON.parse(serialized) as JSONValue)\n };\n } catch {\n return {\n type: \"text\",\n value:\n \"Browser execution completed, but its result could not be serialized for model context.\"\n };\n }\n}\n\n/**\n * The Durable Object state to build the runtime in: the explicit `ctx` if\n * given, otherwise the current Agent's `ctx` (via `getCurrentAgent()`), so\n * `createBrowserRuntime` can be called from an Agent method without threading\n * `this.ctx` through.\n */\nexport function resolveCtx(options: {\n ctx?: DurableObjectState;\n}): DurableObjectState | undefined {\n if (options.ctx) return options.ctx;\n const agent = agentContext.getStore()?.agent as\n | { ctx?: DurableObjectState }\n | undefined;\n return agent?.ctx;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tool-helpers-BI7ifO2S.js","names":["agentContext"],"sources":["../src/browser/truncate.ts","../src/browser/tool-helpers.ts"],"sourcesContent":["/**\n * Result truncation for the browser tools, copied from\n * `@cloudflare/codemode`'s `truncateResult` so `agents/browser` doesn't need\n * codemode. Internal — not an entry point.\n *\n * Structured values are truncated structurally: the output is always valid\n * JSON of the same shape, with the largest values cut first. A truncated\n * string ends with a marker, a truncated array ends with a marker element\n * counting the dropped items, and an object that had to lose entries carries a\n * marker entry naming the omitted keys. Every marker contains\n * `--- TRUNCATED ---` so callers and models can find them.\n */\n\n/** ~4 characters per token is a reasonable cross-model estimate. */\nconst CHARS_PER_TOKEN = 4;\nconst DEFAULT_MAX_TOKENS = 6000;\nconst TRUNCATION_MARKER = \"--- TRUNCATED ---\";\n/**\n * The least a string or container is worth keeping at (see `floorSize`). A\n * value that would get fewer serialized characters than this is dropped in\n * favour of its siblings rather than reduced to a bare marker.\n */\nconst MIN_SLOT = 128;\n\ntype TruncateOptions = {\n /**\n * Maximum characters in the (serialized) output before truncation kicks in.\n * Defaults to `maxTokens * 4`.\n */\n maxChars?: number;\n /** Token budget used to derive the default `maxChars`. Defaults to 6000. */\n maxTokens?: number;\n};\n\nfunction budget(options?: TruncateOptions): {\n maxChars: number;\n maxTokens: number;\n} {\n const maxTokens = options?.maxTokens ?? DEFAULT_MAX_TOKENS;\n const maxChars = options?.maxChars ?? maxTokens * CHARS_PER_TOKEN;\n return { maxChars, maxTokens };\n}\n\n/**\n * Truncate a text response to a character budget, appending a marker that notes\n * the original size so the model knows the output was clipped. Returns the\n * input unchanged when it is within budget.\n */\nfunction truncateResponse(text: string, options?: TruncateOptions): string {\n const { maxChars, maxTokens } = budget(options);\n if (text.length <= maxChars) return text;\n\n const estimatedTokens = Math.ceil(text.length / CHARS_PER_TOKEN);\n return (\n text.slice(0, maxChars) +\n `\\n\\n${TRUNCATION_MARKER}\\nResponse was ~${estimatedTokens.toLocaleString()} tokens ` +\n `(limit: ${maxTokens.toLocaleString()}). Narrow the request to reduce response size.`\n );\n}\n\n/**\n * Truncate a structured result. Strings are truncated directly. Other values\n * pass through unchanged (same reference) when their JSON serialization is\n * within budget. When oversized, the value is shrunk structurally — largest\n * values first, deepest last — so the model gets a bounded value that is still\n * valid JSON of the original shape, with markers where content was cut.\n *\n * Values that can't be serialized (cycles, bigint, `undefined`) are returned\n * unchanged.\n */\nexport function truncateResult(\n value: unknown,\n options?: TruncateOptions\n): unknown {\n if (typeof value === \"string\") return truncateResponse(value, options);\n\n let serialized: string | undefined;\n try {\n serialized = JSON.stringify(value);\n } catch {\n return value;\n }\n if (serialized === undefined) return value;\n\n const { maxChars } = budget(options);\n if (serialized.length <= maxChars) return value;\n\n // Re-parse so `toJSON`, class instances and `undefined` members are seen in\n // their serialized form — the shape the model would receive anyway.\n const shrunk = shrink(JSON.parse(serialized) as Json, maxChars);\n // `shrink` honours its budget for anything a container can hold; only a\n // scalar (a number too long for the budget) can still miss it.\n return size(shrunk) <= maxChars\n ? shrunk\n : truncateResponse(serialized, options);\n}\n\n// ---------------------------------------------------------------------------\n// Structural shrinking\n// ---------------------------------------------------------------------------\n//\n// Every `shrink*` below returns a value whose compact serialization fits the\n// budget it was given, provided the budget can hold an empty container (2\n// chars). Containers share their budget by water-filling: each child is owed\n// at least its floor (`floorSize`), small children keep their full size, and\n// the largest children absorb the cut. A container only drops children when\n// even their floors cannot fit — arrays drop from the tail (order carries\n// meaning), objects drop their largest values first (keys are all meaningful).\n\ntype Json = string | number | boolean | null | Json[] | { [key: string]: Json };\n\n/** Serialized size in characters (compact JSON). */\nfunction size(value: Json): number {\n return JSON.stringify(value).length;\n}\n\n/** Only strings and containers can give up characters; scalars are atomic. */\nfunction shrinkable(value: Json): boolean {\n return typeof value === \"string\" || (typeof value === \"object\" && !!value);\n}\n\n/**\n * The least budget a value is kept at; a scalar must fit whole. The floor\n * never exceeds half the container's own budget, so tiny budgets still keep\n * something rather than nothing.\n */\nfunction floorSize(value: Json, maxChars: number): number {\n const full = size(value);\n return shrinkable(value)\n ? Math.min(full, MIN_SLOT, Math.max(2, Math.floor(maxChars / 2)))\n : full;\n}\n\nfunction shrink(value: Json, maxChars: number): Json {\n if (size(value) <= maxChars) return value;\n if (typeof value === \"string\") return shrinkString(value, maxChars);\n if (Array.isArray(value)) return shrinkArray(value, maxChars);\n if (typeof value === \"object\" && value !== null) {\n return shrinkObject(value, maxChars);\n }\n return value;\n}\n\nfunction shrinkString(value: string, maxChars: number): string {\n const suffix = ` ${TRUNCATION_MARKER} ${value.length.toLocaleString()} chars`;\n let keep = Math.max(0, maxChars - suffix.length - 2);\n let out = value.slice(0, keep) + suffix;\n // Escapes inflate the serialized form; back off until it fits.\n while (keep > 0 && size(out) > maxChars) {\n keep = Math.max(0, keep - (size(out) - maxChars));\n out = value.slice(0, keep) + suffix;\n }\n // No room for a prefix and the size note: keep as much of the marker as fits.\n return size(out) <= maxChars\n ? out\n : TRUNCATION_MARKER.slice(0, Math.max(0, maxChars - 2));\n}\n\n/**\n * Share `available` characters across values by water-filling: values are\n * admitted smallest first, each taking the lesser of its full size and an\n * equal share of what is left. Requires `available >= Σ floorSize(values)`;\n * then every value receives at least its floor.\n */\nfunction allocate(values: Json[], available: number): number[] {\n const sizes = values.map(size);\n const order = sizes.map((_, i) => i).sort((a, b) => sizes[a] - sizes[b]);\n const allocation = new Array<number>(values.length);\n let remaining = available;\n let count = values.length;\n for (const i of order) {\n const alloc = Math.min(sizes[i], Math.floor(remaining / count));\n allocation[i] = alloc;\n remaining -= alloc;\n count--;\n }\n return allocation;\n}\n\nfunction shrinkArray(items: Json[], maxChars: number): Json[] {\n const marker = (dropped: number) =>\n `${TRUNCATION_MARKER} ${dropped.toLocaleString()} more items`;\n const floors = items.map((item) => floorSize(item, maxChars));\n const skeleton = (count: number) => 2 + Math.max(0, count - 1);\n\n // Keep everything when every floor fits.\n let keep = items.length;\n if (floors.reduce((n, f) => n + f, 0) + skeleton(keep) > maxChars) {\n // Otherwise keep the longest prefix whose floors fit beside a tail marker\n // (sized for the largest possible count, so the real one always fits).\n const tail = size(marker(items.length)) + 1;\n let used = skeleton(0) + tail;\n keep = 0;\n while (keep < items.length && used + floors[keep] + 1 <= maxChars) {\n used += floors[keep] + 1;\n keep++;\n }\n }\n\n const kept = items.slice(0, keep);\n const dropped = items.length - keep;\n const tail = dropped > 0 ? size(marker(dropped)) + (keep > 0 ? 1 : 0) : 0;\n const allocation = allocate(kept, maxChars - skeleton(keep) - tail);\n const out = kept.map((item, i) => shrink(item, allocation[i]));\n if (dropped > 0 && size([...out, marker(dropped)]) <= maxChars) {\n out.push(marker(dropped));\n }\n return out;\n}\n\nfunction shrinkObject(\n value: { [key: string]: Json },\n maxChars: number\n): { [key: string]: Json } {\n const entries = Object.entries(value);\n // The marker entry must not shadow a real key.\n let markerKey = TRUNCATION_MARKER;\n while (markerKey in value) markerKey += \" \";\n const keyCost = (key: string) => size(key) + 1;\n const floors = entries.map(([k, v]) => keyCost(k) + floorSize(v, maxChars));\n const floorOf = (chars: number) =>\n Math.min(chars, MIN_SLOT, Math.max(2, Math.floor(maxChars / 2)));\n\n // Drop the largest values first, only until the remaining floors fit. Costs\n // are tracked incrementally so a wide object stays linear in its key count;\n // the marker note's cost is derived arithmetically from the omitted names.\n const byValueSize = entries\n .map((_, i) => i)\n .sort((a, b) => size(entries[b][1]) - size(entries[a][1]));\n const dropped = new Set<number>();\n let present = entries.length;\n let floorsSum = floors.reduce((n, f) => n + f, 0);\n let namesLength = 0;\n const noteLength = () =>\n `${dropped.size.toLocaleString()} keys omitted: `.length +\n namesLength +\n 2 * (dropped.size - 1);\n const cost = () => {\n const marker =\n dropped.size > 0 ? keyCost(markerKey) + floorOf(noteLength() + 2) : 0;\n const count = present + (dropped.size > 0 ? 1 : 0);\n return 2 + Math.max(0, count - 1) + floorsSum + marker;\n };\n for (const i of byValueSize) {\n if (cost() <= maxChars) break;\n dropped.add(i);\n present--;\n floorsSum -= floors[i];\n namesLength += size(entries[i][0]) - 2;\n }\n\n const kept = entries.filter((_, i) => !dropped.has(i));\n const omitted = [...dropped].sort((a, b) => a - b).map((i) => entries[i][0]);\n const note = `${omitted.length.toLocaleString()} keys omitted: ${omitted.join(\", \")}`;\n if (cost() > maxChars) {\n // Not even the marker fits at its floor: keep as much of it as there is\n // room for, or nothing.\n const room = maxChars - 2 - keyCost(markerKey);\n return room >= 2 ? { [markerKey]: shrink(note, room) } : {};\n }\n const all: [string, Json][] = [...kept];\n if (omitted.length > 0) all.push([markerKey, note]);\n const fixed =\n 2 + Math.max(0, all.length - 1) + all.reduce((n, [k]) => n + keyCost(k), 0);\n // The note is shrunk like any other value, so it can never overshoot.\n const allocation = allocate(\n all.map(([, v]) => v),\n maxChars - fixed\n );\n const out: { [key: string]: Json } = {};\n all.forEach(([k, v], i) => {\n out[k] = shrink(v, allocation[i]);\n });\n return out;\n}\n","/**\n * Helpers shared by the browser tools: `createBrowserTools` in `ai.ts`, and\n * the `browserTool` adapters in `tools/`. Internal — not an entry point.\n */\nimport type { JSONValue } from \"ai\";\nimport { DEFAULT_MAX_RESULT_BYTES } from \"./session\";\nimport { truncateResult } from \"./truncate\";\nimport { redactBase64Payloads } from \"../core/base64-redaction\";\nimport { __DO_NOT_USE_WILL_BREAK__agentContext as agentContext } from \"../internal_context\";\n\ninterface BrowserScreenshotOutput {\n type: \"browser_screenshot\";\n mediaType: string;\n data: string;\n}\n\n/** What `Page.captureScreenshot` can produce. */\nconst SCREENSHOT_MEDIA_TYPES = new Set([\n \"image/png\",\n \"image/jpeg\",\n \"image/webp\"\n]);\nconst BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;\n\n/**\n * The screenshot the model's code returned, or `null` if it didn't return\n * one. The model writes this shape itself, so only an image type, base64\n * data, and the session's size limit count: anything else is treated as a\n * plain result and cut down like one, rather than kept whole or sent to the\n * model as an image.\n */\nexport function browserScreenshotOutput(\n value: unknown\n): BrowserScreenshotOutput | null {\n const outer =\n typeof value === \"object\" && value !== null\n ? (value as Record<string, unknown>)\n : null;\n const result =\n outer && typeof outer.result === \"object\" && outer.result !== null\n ? (outer.result as Record<string, unknown>)\n : outer;\n if (\n result?.type !== \"browser_screenshot\" ||\n typeof result.mediaType !== \"string\" ||\n typeof result.data !== \"string\" ||\n !SCREENSHOT_MEDIA_TYPES.has(result.mediaType) ||\n result.data.length > DEFAULT_MAX_RESULT_BYTES ||\n !BASE64.test(result.data)\n ) {\n return null;\n }\n return result as unknown as BrowserScreenshotOutput;\n}\n\nexport function transformBrowserResult(value: unknown): unknown {\n // Keep canonical screenshot output intact for UIMessage persistence and the\n // chat renderer. Other results are redacted before truncation so a nested\n // binary payload cannot become a large serialized preview.\n return browserScreenshotOutput(value)\n ? value\n : truncateResult(redactBase64Payloads(value));\n}\n\n/** What the model sees in place of a browser tool's output. */\nexport type BrowserModelOutput =\n | { type: \"text\"; value: string }\n | { type: \"json\"; value: JSONValue };\n\n/**\n * @param notes Sentences appended when a screenshot collapses the output to\n * text, so notices on the output aren't lost with it.\n */\nexport function browserExecuteModelOutput(\n output: unknown,\n notes: string[] = []\n): BrowserModelOutput {\n const screenshot = browserScreenshotOutput(output);\n if (screenshot) {\n const approximateBytes = Math.floor((screenshot.data.length * 3) / 4);\n return {\n type: \"text\",\n value: [\n `Screenshot captured successfully (${screenshot.mediaType}, approximately ${approximateBytes.toLocaleString()} bytes); the image is kept for the UI and omitted here.`,\n ...notes\n ].join(\" \")\n };\n }\n\n // `calls`, the durable audit log `createBrowserTools` adds, stays on the\n // persisted part and never enters the model's context, and the sandbox\n // `logs` are bounded like a result.\n const modelFacing =\n typeof output === \"object\" && output !== null && !Array.isArray(output)\n ? (({ calls: _calls, ...rest }: { calls?: unknown; logs?: unknown }) => ({\n ...rest,\n ...(Array.isArray(rest.logs)\n ? { logs: truncateResult(rest.logs) }\n : {})\n }))(output)\n : output;\n const redacted = redactBase64Payloads(modelFacing);\n try {\n const serialized = JSON.stringify(redacted);\n return {\n type: \"json\",\n value:\n serialized === undefined ? null : (JSON.parse(serialized) as JSONValue)\n };\n } catch {\n return {\n type: \"text\",\n value:\n \"Browser execution completed, but its result could not be serialized for model context.\"\n };\n }\n}\n\n/**\n * The Durable Object state to build the runtime in: the explicit `ctx` if\n * given, otherwise the current Agent's `ctx` (via `getCurrentAgent()`), so\n * `createBrowserRuntime` can be called from an Agent method without threading\n * `this.ctx` through.\n */\nexport function resolveCtx(options: {\n ctx?: DurableObjectState;\n}): DurableObjectState | undefined {\n if (options.ctx) return options.ctx;\n const agent = agentContext.getStore()?.agent as\n | { ctx?: DurableObjectState }\n | undefined;\n return agent?.ctx;\n}\n"],"mappings":"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"}
|
package/dist/vite.d.ts
CHANGED
|
@@ -15,8 +15,8 @@ interface AgentsPluginOptions {
|
|
|
15
15
|
* Vite plugin for Agents SDK projects.
|
|
16
16
|
*
|
|
17
17
|
* Handles TC39 decorator transforms (Oxc doesn't support them yet, oxc#9170) so
|
|
18
|
-
* `@callable()` works at runtime,
|
|
19
|
-
*
|
|
18
|
+
* `@callable()` works at runtime, and stubs `turndown` to keep Workers
|
|
19
|
+
* deploys clean. Will grow to cover other
|
|
20
20
|
* Agents-specific build concerns as needed.
|
|
21
21
|
*/
|
|
22
22
|
declare function agents(options?: AgentsPluginOptions): Plugin[];
|
package/dist/vite.js
CHANGED
|
@@ -1,248 +1,5 @@
|
|
|
1
|
-
import { compileSkillScript, isCompilableSkillScript } from "./skills/compile.js";
|
|
2
1
|
import babel from "@rolldown/plugin-babel";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
|
-
import { readFile, readdir, stat } from "node:fs/promises";
|
|
5
|
-
import { basename, join, resolve } from "node:path";
|
|
6
|
-
import { parse } from "yaml";
|
|
7
2
|
//#region src/vite.ts
|
|
8
|
-
const SKILLS_SPECIFIER = "agents:skills";
|
|
9
|
-
const SKILLS_VIRTUAL_PREFIX = "\0agents:skills:";
|
|
10
|
-
const SKILL_RESOURCE_ROOTS = /* @__PURE__ */ new Set([
|
|
11
|
-
"references",
|
|
12
|
-
"scripts",
|
|
13
|
-
"assets",
|
|
14
|
-
"graphics",
|
|
15
|
-
"fonts",
|
|
16
|
-
"templates",
|
|
17
|
-
"rendered-files",
|
|
18
|
-
"illustrations"
|
|
19
|
-
]);
|
|
20
|
-
const SKILL_IGNORED_ROOTS = /* @__PURE__ */ new Set([
|
|
21
|
-
".git",
|
|
22
|
-
".hg",
|
|
23
|
-
".svn",
|
|
24
|
-
".DS_Store",
|
|
25
|
-
".idea",
|
|
26
|
-
".vscode",
|
|
27
|
-
"dist",
|
|
28
|
-
"build",
|
|
29
|
-
"coverage",
|
|
30
|
-
"node_modules"
|
|
31
|
-
]);
|
|
32
|
-
const SPEC_RESOURCE_ROOTS = /* @__PURE__ */ new Set([
|
|
33
|
-
"references",
|
|
34
|
-
"scripts",
|
|
35
|
-
"assets"
|
|
36
|
-
]);
|
|
37
|
-
const SKILL_ASSET_WARN_BYTES = 256 * 1024;
|
|
38
|
-
const SKILL_BUNDLE_WARN_BYTES = 1024 * 1024;
|
|
39
|
-
function formatBytes(bytes) {
|
|
40
|
-
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
41
|
-
if (bytes >= 1024) return `${(bytes / 1024).toFixed(0)}KB`;
|
|
42
|
-
return `${bytes}B`;
|
|
43
|
-
}
|
|
44
|
-
function parseFrontmatter(raw) {
|
|
45
|
-
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
46
|
-
if (!match) return {
|
|
47
|
-
data: {},
|
|
48
|
-
body: raw
|
|
49
|
-
};
|
|
50
|
-
const parsed = parse(match[1] ?? "");
|
|
51
|
-
return {
|
|
52
|
-
data: parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {},
|
|
53
|
-
body: match[2] ?? ""
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
function stringField(value) {
|
|
57
|
-
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
58
|
-
}
|
|
59
|
-
function recordField(value) {
|
|
60
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
61
|
-
}
|
|
62
|
-
function resourceKind(path) {
|
|
63
|
-
if (path.startsWith("references/")) return "reference";
|
|
64
|
-
if (path.startsWith("scripts/")) return "script";
|
|
65
|
-
if (path.startsWith("assets/") || path.startsWith("graphics/") || path.startsWith("fonts/") || path.startsWith("templates/") || path.startsWith("rendered-files/") || path.startsWith("illustrations/")) return "asset";
|
|
66
|
-
return "file";
|
|
67
|
-
}
|
|
68
|
-
const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
69
|
-
".bash",
|
|
70
|
-
".css",
|
|
71
|
-
".csv",
|
|
72
|
-
".html",
|
|
73
|
-
".js",
|
|
74
|
-
".json",
|
|
75
|
-
".jsx",
|
|
76
|
-
".md",
|
|
77
|
-
".mjs",
|
|
78
|
-
".py",
|
|
79
|
-
".sh",
|
|
80
|
-
".svg",
|
|
81
|
-
".ts",
|
|
82
|
-
".tsx",
|
|
83
|
-
".txt",
|
|
84
|
-
".xml",
|
|
85
|
-
".yaml",
|
|
86
|
-
".yml"
|
|
87
|
-
]);
|
|
88
|
-
const MIME_TYPES = /* @__PURE__ */ new Map([
|
|
89
|
-
[".css", "text/css"],
|
|
90
|
-
[".gif", "image/gif"],
|
|
91
|
-
[".html", "text/html"],
|
|
92
|
-
[".jpg", "image/jpeg"],
|
|
93
|
-
[".jpeg", "image/jpeg"],
|
|
94
|
-
[".js", "text/javascript"],
|
|
95
|
-
[".json", "application/json"],
|
|
96
|
-
[".md", "text/markdown"],
|
|
97
|
-
[".mjs", "text/javascript"],
|
|
98
|
-
[".pdf", "application/pdf"],
|
|
99
|
-
[".png", "image/png"],
|
|
100
|
-
[".py", "text/x-python"],
|
|
101
|
-
[".sh", "text/x-shellscript"],
|
|
102
|
-
[".svg", "image/svg+xml"],
|
|
103
|
-
[".ts", "text/typescript"],
|
|
104
|
-
[".tsx", "text/typescript"],
|
|
105
|
-
[".txt", "text/plain"],
|
|
106
|
-
[".webp", "image/webp"],
|
|
107
|
-
[".woff", "font/woff"],
|
|
108
|
-
[".woff2", "font/woff2"],
|
|
109
|
-
[".xml", "application/xml"],
|
|
110
|
-
[".yaml", "application/yaml"],
|
|
111
|
-
[".yml", "application/yaml"]
|
|
112
|
-
]);
|
|
113
|
-
function extensionOf(path) {
|
|
114
|
-
const file = path.split("/").at(-1) ?? path;
|
|
115
|
-
const index = file.lastIndexOf(".");
|
|
116
|
-
return index === -1 ? "" : file.slice(index).toLowerCase();
|
|
117
|
-
}
|
|
118
|
-
function resourceEncoding(path) {
|
|
119
|
-
return TEXT_EXTENSIONS.has(extensionOf(path)) ? "text" : "base64";
|
|
120
|
-
}
|
|
121
|
-
function resourceMimeType(path) {
|
|
122
|
-
return MIME_TYPES.get(extensionOf(path));
|
|
123
|
-
}
|
|
124
|
-
async function collectFiles(root, relativeRoot = "", warn) {
|
|
125
|
-
const entries = await readdir(join(root, relativeRoot), { withFileTypes: true }).catch(() => []);
|
|
126
|
-
const files = [];
|
|
127
|
-
for (const entry of entries) {
|
|
128
|
-
if (SKILL_IGNORED_ROOTS.has(entry.name)) continue;
|
|
129
|
-
const relativePath = relativeRoot ? `${relativeRoot}/${entry.name}` : entry.name;
|
|
130
|
-
const absolutePath = join(root, relativePath);
|
|
131
|
-
if (entry.isDirectory()) {
|
|
132
|
-
const resourceRoot = relativePath.split("/")[0];
|
|
133
|
-
if (!resourceRoot || SKILL_IGNORED_ROOTS.has(resourceRoot)) continue;
|
|
134
|
-
if (!SKILL_RESOURCE_ROOTS.has(resourceRoot)) {
|
|
135
|
-
if (!relativeRoot) warn?.(`Ignoring skill directory "${relativePath}". Bundled skill resources should live under references/, scripts/, assets/, or a known asset root.`);
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
if (!SPEC_RESOURCE_ROOTS.has(resourceRoot) && !relativeRoot) warn?.(`Bundling non-standard skill resource root "${resourceRoot}/". Prefer assets/ for portable Agent Skills when possible.`);
|
|
139
|
-
files.push(...await collectFiles(root, relativePath, warn));
|
|
140
|
-
} else if (entry.isFile() && relativePath !== "SKILL.md") {
|
|
141
|
-
const resourceRoot = relativePath.split("/")[0];
|
|
142
|
-
if (!resourceRoot || SKILL_IGNORED_ROOTS.has(resourceRoot)) continue;
|
|
143
|
-
if (!SKILL_RESOURCE_ROOTS.has(resourceRoot)) {
|
|
144
|
-
warn?.(`Ignoring skill file "${relativePath}". Bundled skill resources should live under references/, scripts/, assets/, or a known asset root.`);
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
const info = await stat(absolutePath);
|
|
148
|
-
files.push({
|
|
149
|
-
path: relativePath,
|
|
150
|
-
absolutePath,
|
|
151
|
-
size: info.size
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
156
|
-
}
|
|
157
|
-
async function collectWatchTargets(root, relativeRoot = "") {
|
|
158
|
-
const directory = join(root, relativeRoot);
|
|
159
|
-
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
160
|
-
const targets = [directory];
|
|
161
|
-
for (const entry of entries) {
|
|
162
|
-
if (SKILL_IGNORED_ROOTS.has(entry.name)) continue;
|
|
163
|
-
const relativePath = relativeRoot ? `${relativeRoot}/${entry.name}` : entry.name;
|
|
164
|
-
const resourceRoot = relativePath.split("/")[0];
|
|
165
|
-
if (!resourceRoot || SKILL_IGNORED_ROOTS.has(resourceRoot)) continue;
|
|
166
|
-
const absolutePath = join(root, relativePath);
|
|
167
|
-
targets.push(absolutePath);
|
|
168
|
-
if (entry.isDirectory()) targets.push(...await collectWatchTargets(root, relativePath));
|
|
169
|
-
}
|
|
170
|
-
return targets;
|
|
171
|
-
}
|
|
172
|
-
async function readSkill(skillDir, warn) {
|
|
173
|
-
const rawContent = await readFile(join(skillDir, "SKILL.md"), "utf8").catch(() => null);
|
|
174
|
-
if (rawContent === null) return null;
|
|
175
|
-
const { data, body } = parseFrontmatter(rawContent);
|
|
176
|
-
const name = stringField(data.name);
|
|
177
|
-
const description = stringField(data.description);
|
|
178
|
-
if (!name || !description) return null;
|
|
179
|
-
const resources = await Promise.all((await collectFiles(skillDir, "", warn)).map(async (file) => {
|
|
180
|
-
const encoding = resourceEncoding(file.path);
|
|
181
|
-
const bytes = await readFile(file.absolutePath);
|
|
182
|
-
const kind = resourceKind(file.path);
|
|
183
|
-
let content = encoding === "base64" ? bytes.toString("base64") : bytes.toString();
|
|
184
|
-
let precompiled = false;
|
|
185
|
-
let size = file.size;
|
|
186
|
-
if (kind === "script" && encoding === "text" && isCompilableSkillScript(file.path)) try {
|
|
187
|
-
content = (await compileSkillScript(file.absolutePath)).content;
|
|
188
|
-
precompiled = true;
|
|
189
|
-
size = Buffer.byteLength(content);
|
|
190
|
-
} catch (error) {
|
|
191
|
-
warn?.(`Failed to compile skill script "${file.path}": ${error instanceof Error ? error.message : String(error)}. Skill scripts must be self-contained, compilable modules to run at runtime.`);
|
|
192
|
-
}
|
|
193
|
-
return {
|
|
194
|
-
path: file.path,
|
|
195
|
-
kind,
|
|
196
|
-
size,
|
|
197
|
-
encoding,
|
|
198
|
-
mimeType: resourceMimeType(file.path),
|
|
199
|
-
content,
|
|
200
|
-
precompiled
|
|
201
|
-
};
|
|
202
|
-
}));
|
|
203
|
-
return {
|
|
204
|
-
name,
|
|
205
|
-
description,
|
|
206
|
-
body,
|
|
207
|
-
rawContent,
|
|
208
|
-
compatibility: stringField(data.compatibility),
|
|
209
|
-
license: stringField(data.license),
|
|
210
|
-
allowedTools: stringField(data["allowed-tools"]),
|
|
211
|
-
metadata: recordField(data.metadata),
|
|
212
|
-
resources
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
async function buildSkillsModule(dir, warn) {
|
|
216
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
217
|
-
const skills = [];
|
|
218
|
-
const seen = /* @__PURE__ */ new Set();
|
|
219
|
-
for (const entry of entries) {
|
|
220
|
-
if (!entry.isDirectory()) continue;
|
|
221
|
-
const skill = await readSkill(join(dir, entry.name), warn);
|
|
222
|
-
if (!skill) continue;
|
|
223
|
-
if (seen.has(skill.name)) {
|
|
224
|
-
warn?.(`Duplicate bundled skill name "${skill.name}" in "${entry.name}/"; keeping the first occurrence and ignoring this one.`);
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
seen.add(skill.name);
|
|
228
|
-
skills.push(skill);
|
|
229
|
-
}
|
|
230
|
-
skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
231
|
-
let totalBytes = 0;
|
|
232
|
-
for (const skill of skills) for (const resource of skill.resources) {
|
|
233
|
-
totalBytes += resource.size;
|
|
234
|
-
if (resource.size > SKILL_ASSET_WARN_BYTES) warn?.(`Bundled skill resource "${skill.name}/${resource.path}" is ${formatBytes(resource.size)}; large assets bloat the Worker bundle (base64, ~1.33x). Prefer an R2-backed source via skills.r2().`);
|
|
235
|
-
}
|
|
236
|
-
if (totalBytes > SKILL_BUNDLE_WARN_BYTES) warn?.(`Bundled skills total ${formatBytes(totalBytes)} of embedded resources; this competes with the Worker bundle-size budget. Consider serving large skills from R2 via skills.r2().`);
|
|
237
|
-
const hash = createHash("sha256");
|
|
238
|
-
hash.update(JSON.stringify(skills));
|
|
239
|
-
const manifest = {
|
|
240
|
-
id: `bundle:${basename(dir)}`,
|
|
241
|
-
fingerprint: hash.digest("hex"),
|
|
242
|
-
skills
|
|
243
|
-
};
|
|
244
|
-
return `const manifest = ${JSON.stringify(manifest)};\nexport default {\n id: manifest.id,\n fingerprint: manifest.fingerprint,\n async list() {\n return manifest.skills.map(({ body, rawContent, resources, ...skill }) => skill);\n },\n async load(name) {\n const skill = manifest.skills.find((entry) => entry.name === name);\n if (!skill) return null;\n return {\n ...skill,\n resources: skill.resources.map(({ content, ...resource }) => resource)\n };\n },\n async readResource(name, path) {\n const skill = manifest.skills.find((entry) => entry.name === name);\n const resource = skill?.resources.find((entry) => entry.path === path);\n return resource ? { ...resource } : null;\n }\n};\n`;
|
|
245
|
-
}
|
|
246
3
|
const TURNDOWN_STUB_ID = "\0agents:turndown-stub";
|
|
247
4
|
const TURNDOWN_STUB_MESSAGE = "The Agents Vite plugin stubbed the 'turndown' package to keep Workers bundles compatible with just-bash. If your app uses turndown directly, configure the plugin with agents({ stubTurndown: false }).";
|
|
248
5
|
function turndownStubPlugin() {
|
|
@@ -269,19 +26,14 @@ export default TurndownService;
|
|
|
269
26
|
}
|
|
270
27
|
};
|
|
271
28
|
}
|
|
272
|
-
|
|
29
|
+
const REMOVED_SKILLS_IMPORT_MESSAGE = "The \"agents:skills\" import has been removed. Skills are now files: store them in a Computer Workspace and return skills.workspace(this.workspace) from getSkills(), or load them with skills.r2(bucket) or skills.manifest({ id, skills }). See https://developers.cloudflare.com/agents/skills/";
|
|
30
|
+
function removedSkillsImportPlugin() {
|
|
273
31
|
return {
|
|
274
|
-
name: "agents-skills-import",
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (
|
|
278
|
-
return
|
|
279
|
-
},
|
|
280
|
-
async load(id) {
|
|
281
|
-
if (!id.startsWith(SKILLS_VIRTUAL_PREFIX)) return null;
|
|
282
|
-
const dir = id.slice(15);
|
|
283
|
-
for (const target of await collectWatchTargets(dir)) this.addWatchFile(target);
|
|
284
|
-
return buildSkillsModule(dir, (message) => this.warn(message));
|
|
32
|
+
name: "agents-removed-skills-import",
|
|
33
|
+
enforce: "pre",
|
|
34
|
+
resolveId(source) {
|
|
35
|
+
if (source === "agents:skills" || source.startsWith("agents:skills/")) this.error(REMOVED_SKILLS_IMPORT_MESSAGE);
|
|
36
|
+
return null;
|
|
285
37
|
}
|
|
286
38
|
};
|
|
287
39
|
}
|
|
@@ -289,15 +41,15 @@ function skillsImportPlugin() {
|
|
|
289
41
|
* Vite plugin for Agents SDK projects.
|
|
290
42
|
*
|
|
291
43
|
* Handles TC39 decorator transforms (Oxc doesn't support them yet, oxc#9170) so
|
|
292
|
-
* `@callable()` works at runtime,
|
|
293
|
-
*
|
|
44
|
+
* `@callable()` works at runtime, and stubs `turndown` to keep Workers
|
|
45
|
+
* deploys clean. Will grow to cover other
|
|
294
46
|
* Agents-specific build concerns as needed.
|
|
295
47
|
*/
|
|
296
48
|
function agents(options = {}) {
|
|
297
49
|
const { stubTurndown = true } = options;
|
|
298
50
|
return [
|
|
299
51
|
...stubTurndown ? [turndownStubPlugin()] : [],
|
|
300
|
-
|
|
52
|
+
removedSkillsImportPlugin(),
|
|
301
53
|
babel({ presets: [{
|
|
302
54
|
preset: () => ({ plugins: [["@babel/plugin-proposal-decorators", { version: "2023-11" }]] }),
|
|
303
55
|
rolldown: { filter: { code: "@" } }
|