# web-ai-sdk — full package documentation > Concatenated content of every packages/*/README.md, in the order published. Generated at build time; the READMEs in the GitHub repo are the source of truth. > Source: https://github.com/obetomuniz/web-ai-sdk --- # @web-ai-sdk/all One-install meta-package for every `@web-ai-sdk/*` building block: `prompt`, `summarizer`, `translator`, `detector`, and `webmcp`. Pulls each scoped package in as a regular dependency so consumers don't have to track them individually. **Docs:** · **All packages & links:** ## Status Each underlying scoped package is independently supported in Chrome / Edge with the corresponding Built-in AI flag enabled. See the per-package READMEs for browser support details: - [`@web-ai-sdk/prompt`](https://github.com/obetomuniz/web-ai-sdk/tree/main/packages/prompt) - [`@web-ai-sdk/summarizer`](https://github.com/obetomuniz/web-ai-sdk/tree/main/packages/summarizer) - [`@web-ai-sdk/translator`](https://github.com/obetomuniz/web-ai-sdk/tree/main/packages/translator) - [`@web-ai-sdk/detector`](https://github.com/obetomuniz/web-ai-sdk/tree/main/packages/detector) - [`@web-ai-sdk/webmcp`](https://github.com/obetomuniz/web-ai-sdk/tree/main/packages/webmcp) ## Install ```sh pnpm add @web-ai-sdk/all # or: npm i @web-ai-sdk/all / bun add @web-ai-sdk/all ``` `react` is a peer dependency only when you import any `/react` subpath. ## Two equivalent import shapes ### Subpath imports (recommended for production) Tree-shakes cleanly; the bundler only pulls in the building blocks you actually use. ```ts import { ask, createSession } from "@web-ai-sdk/all/prompt"; import { summarize } from "@web-ai-sdk/all/summarizer"; import { translate } from "@web-ai-sdk/all/translator"; import { detect } from "@web-ai-sdk/all/detector"; import { registerTool, defineTool } from "@web-ai-sdk/all/webmcp"; ``` ```tsx import { usePrompt, useSession } from "@web-ai-sdk/all/prompt/react"; import { useSummarizer } from "@web-ai-sdk/all/summarizer/react"; ``` ### Namespaced root (handy for prototyping) ```ts import { prompt, summarizer, translator, detector, webmcp } from "@web-ai-sdk/all"; await prompt.ask({ input: "Hello" }); await summarizer.summarize({ language: "en", article: document.body }); ``` The root entry namespaces each scoped package because several exports (e.g. `checkAvailability`, `defaultCacheKey`, `createSessionStorageCache`) appear in more than one package and would collide on a flat re-export. ## Why a meta-package? The scoped packages are deliberately small lifecycle wrappers; a meta-package just spares consumers from tracking five separate installs and version pins. The aggregator is a thin re-export shell with no behaviour of its own; all logic, tests, and version history live in the scoped packages. ## Versioning The aggregator and the five scoped packages release together via a Changesets `fixed` group: every release ships all six at the same version. Pin a single number, get the whole suite. ## License [MIT](./LICENSE). --- # @web-ai-sdk/prompt Building block for the Web's Built-in [Prompt API](https://developer.chrome.com/docs/extensions/ai/prompt-api) (`LanguageModel`). One-shot `ask()` for embeds and widgets, plus a thin `createSession()` primitive (and React `useSession`) for chat-shaped apps that need independent per-conversation sessions and delta-shaped streaming. The wrapper smooths cross-browser quirks (delta-vs-cumulative chunks, output sanitization, abort wiring); UI state and conversation history are the consumer's concern. **Docs:** · **React:** [`usePrompt`](https://web-ai-sdk.dev/docs/react/use-prompt/) · [`useSession`](https://web-ai-sdk.dev/docs/react/use-session/) ## Status Prompt API ships stable in Chrome 148+ — no flag required. Chrome 138–147 still works with `chrome://flags/#prompt-api-for-gemini-nano` enabled. On Edge it remains a developer preview in Canary/Dev 138+ behind `edge://flags/#prompt-api-for-phi-mini`, with Phi-4-mini's stricter safety pipeline often refusing output (see [Browser support](https://web-ai-sdk.dev/browser-support)). On any other browser this library is a no-op for the React hook (it stays in `"unavailable"`). The vanilla `ask()` throws `PromptUnavailableError` so callers can branch explicitly. ## Install ```sh pnpm add @web-ai-sdk/prompt # or: npm i @web-ai-sdk/prompt / bun add @web-ai-sdk/prompt ``` The React adapter ships as a subpath export, with no extra install. `react` is a peer dependency only when you import the `/react` entry. ## Vanilla TypeScript / DOM ### One-shot — `ask()` ```ts import { ask } from "@web-ai-sdk/prompt"; const result = await ask({ input: "Summarize this in one sentence: WebMCP lets web pages expose tools to agents.", systemPrompt: "You are concise. Reply with a single sentence.", temperature: 0.2, onUpdate: (text) => console.log("partial", text), // cumulative buffer }); console.log(result.response, result.cached); ``` `ask()` shares a warm `LanguageModel` instance across same-shape callers so the cold start is paid once per persona. That's right for embeds, widgets, ask-and-display flows. It's the wrong shape for chat: two callers with the same mode would share one instance, so conversation history cross-bleeds and `abort()` on one caller kills the other. ### Chat — `createSession()` ```ts import { createSession } from "@web-ai-sdk/prompt"; const session = createSession({ systemPrompt: "You are a helpful assistant.", temperature: 0.7, }); // Streaming, yields DELTA chunks (not cumulative buffers): for await (const delta of session.sendStreaming("Tell me about WebMCP.")) { process.stdout.write(delta); } // Or one-shot per turn: const text = await session.send("And what about the Prompt API?"); // Tear down explicitly when the conversation ends. session.destroy(); ``` Every `createSession()` call returns an independent `LanguageModelInstance` with its own history, system prompt, sampling, and lifecycle — `abort()` / `destroy()` on one session never touch another. Concurrent `send` / `sendStreaming` calls on the **same** session are NOT queued — the underlying `LanguageModel` is sequential per instance and will reject the overlapping call with `InvalidStateError`. Either `await` the previous send or call `session.abort()` before issuing a new turn. Multi-turn conversation context is tracked by the native instance itself; UI message lists are your data model. **Concurrency note.** Each session is an independent `LanguageModel` instance: independent history, system prompt, sampling, and lifecycle. The underlying on-device model is single-instance, so the browser currently schedules `sendStreaming` calls across sessions FIFO. Overlapping sends do not interleave token-by-token in Chrome 148 / Edge 138 — the second send waits for the first to drain. This is a constraint of the runtime, not of the API; code written against `createSession()` becomes faster automatically if a future release exposes parallel inference. ## React ### One-shot — `usePrompt` ```tsx import { usePrompt } from "@web-ai-sdk/prompt/react"; export function AskBox() { const { status, response, error, ask, abort } = usePrompt({ systemPrompt: "You are a helpful assistant. Be concise.", temperature: 0.7, }); if (status === "unavailable") return null; return (
{ e.preventDefault(); const input = new FormData(e.currentTarget).get("q") as string; if (input) ask(input); }} > {response &&

{response}

} {error && {error.message}}
); } ``` State machine: `idle | loading | streaming | done | unavailable`. `ask(input)` triggers a request, cancels any in-flight one, and updates `response` as chunks stream. ### Chat — `useSession` ```tsx import { useSession } from "@web-ai-sdk/prompt/react"; import { useState } from "react"; export function Chat({ persona }: { persona: string }) { const { status, session } = useSession({ systemPrompt: persona }); const [response, setResponse] = useState(""); if (status === "unavailable" || !session) return null; const send = async (text: string) => { setResponse(""); let buffer = ""; for await (const delta of session.sendStreaming(text)) { buffer += delta; setResponse(buffer); } }; return (
{ e.preventDefault(); send("Hello"); }}>

{response}

); } ``` `useSession` is lifecycle-only: it creates the session on mount, destroys it on unmount, and recreates it when any primitive option changes. It deliberately does **not** track `response` / `history` / streaming status — that's your UI state, you own it. Each `useSession()` call owns its own underlying `LanguageModelInstance`, so component state and `abort()` / `destroy()` stay scoped to the owning component. Token-level interleaving across sessions is browser-defined (see the Concurrency note above) — N mounted components in Chrome 148 / Edge 138 still drain through one underlying model FIFO. ## API ### `ask(options): Promise` ```ts interface AskOptions { input: string; systemPrompt?: string; temperature?: number; topK?: number; language?: string; // BCP-47 hint, folded into expectedInputs/Outputs supportedLanguages?: readonly string[]; // default ["en"] expectedInputs?: LanguageModelExpectedInput[]; // advanced passthrough expectedOutputs?: LanguageModelExpectedOutput[]; // advanced passthrough createOptions?: Partial; responseConstraint?: object; // JSON Schema for structured output cache?: ResponseCache; cacheKey?: string; onUpdate?: (text: string) => void; // CUMULATIVE buffer signal?: AbortSignal; } interface AskResult { response: string | null; cached: boolean; } ``` `onUpdate` receives the cumulative text so far, not deltas. For delta-shaped streaming use `createSession().sendStreaming()`. If `systemPrompt` is passed alongside `createOptions.initialPrompts`, the SDK emits a one-shot `console.warn` because `initialPrompts` overrides the synthesized system prompt and the persona is silently lost. ### `createSession(options?): Session` ```ts interface CreateSessionOptions { systemPrompt?: string; temperature?: number; topK?: number; language?: string; supportedLanguages?: readonly string[]; expectedInputs?: LanguageModelExpectedInput[]; expectedOutputs?: LanguageModelExpectedOutput[]; // Pass `initialPrompts` here to seed multi-turn context. createOptions?: Partial; } interface Session { readonly destroyed: boolean; send(input: string, options?: SessionSendOptions): Promise; sendStreaming(input: string, options?: SessionSendOptions): AsyncIterable; abort(): void; destroy(): void; } ``` `Session.sendStreaming()` yields **deltas** (each chunk is the new text since the last yield, never cumulative). The wrapper does no extra bookkeeping: no history tracking, no concurrent-send queue, no usage telemetry. Always destroy sessions you no longer need. ### `useSession(options?): UseSessionReturn` ```ts interface UseSessionReturn { status: "loading" | "ready" | "unavailable"; error: Error | null; session: Session | null; // null until status === "ready" } ``` Lifecycle-only: feature detection + create + destroy on unmount + recreate when any primitive option (`systemPrompt`, `temperature`, `topK`, `language`) changes. Object options (`expectedInputs`, `createOptions`) participate by reference; memoize them or accept the recreate cost. UI state is your concern — iterate `session.sendStreaming()` and accumulate text into your own component state. ### `isPromptAvailable(): boolean` Feature-detect helper. ### `checkAvailability(opts?): Promise` Forwards to `LanguageModel.availability()`. Returns `null` if the global is missing or the call throws. ### `createSessionStorageCache({ storage?, prefix? }): ResponseCache` Optional cache backend. Pass it to `ask({ cache })` to enable response caching, with an optional custom `storage` (e.g. `localStorage`, an in-memory polyfill). ### Cache controls ```ts import { clearSessions, // drop every cached one-shot session clearSession, // drop one cached session by create-options configurePromptCache, // change the LRU cap (default 8) } from "@web-ai-sdk/prompt"; ``` The internal session cache is LRU-bounded (default 8) and only memoizes sessions created by `ask()`; `createSession()` is never cached. ### Lower-level helpers (advanced) `getLanguageModelApi`, `getOrCreateLanguageModel`, `defaultCacheKey`; exported so you can compose your own pipeline. ## Caching Two layers, same as `@web-ai-sdk/summarizer`: - **Session cache** (internal, in-memory, on by default for `ask()` only): a bounded LRU of `LanguageModel` instances keyed by stringified create-options. Cold-start ≈ 1-3s; warm calls are sub-second. `createSession()` bypasses this cache entirely. - **Result cache** (opt-in): pass a `cache` (anything matching `{ get, set }`) to memoize final responses by `(input, systemPrompt, temperature, topK)`. Omit it for a fresh model call every time. ```ts // Off by default; every call hits the model. ask({ input: "hi" }); // Opt in for sessionStorage-backed caching. ask({ input: "hi", cache: createSessionStorageCache() }); // Or roll your own. ask({ input: "hi", cache: myMap, cacheKey: "greeting" }); ``` ## Errors and unavailability The vanilla `ask()` throws `PromptUnavailableError` when the API is missing or reports `availability: "unavailable"`. The React hook absorbs this and returns `status: "unavailable"` instead. `createSession()` returns a `Session` synchronously even if the underlying `create()` rejects; the error surfaces on the first `send` / `sendStreaming`. `AbortSignal` is supported on every surface. Aborting mid-stream resolves cleanly; the result cache is not written for aborted runs. ## License MIT © Beto Muniz --- # @web-ai-sdk/webmcp Building block for the W3C [WebMCP](https://webmachinelearning.github.io/webmcp/) API exposed at `navigator.modelContext`. An ergonomic, framework-agnostic adapter over the native browser API, with safe register/unregister cleanup and a feature-detected no-op fallback for non-supporting browsers. **Docs:** · **React:** [`useWebMCP`](https://web-ai-sdk.dev/docs/react/use-web-mcp/) ## Status WebMCP shipped as an early preview in Chrome 146+ behind `chrome://flags/#enable-webmcp-testing`; a public [origin trial](https://developer.chrome.com/docs/ai/webmcp) opens in Chrome 149. Edge added support in 147+ behind the matching `edge://flags/` toggle. On any browser that doesn't expose `navigator.modelContext`, this library is a no-op. Your app stays callable, and no tools get registered. ## Install ```sh pnpm add @web-ai-sdk/webmcp # or: npm i @web-ai-sdk/webmcp / bun add @web-ai-sdk/webmcp ``` React adapter is shipped as a subpath export, with no extra install. `react` is a peer dependency only when you import the `/react` entry. ## Vanilla TypeScript / DOM ```ts import { registerTools } from "@web-ai-sdk/webmcp"; const cleanup = registerTools([ { name: "list_blog_posts", description: "List published blog posts.", readOnly: true, execute: async () => { const res = await fetch("/api/posts.json"); return { results: await res.json() }; }, }, { name: "send_contact_email", description: "Send a contact email on behalf of the visitor. Confirm the body with the user before invoking.", destructive: true, inputSchema: { type: "object", properties: { name: { type: "string", minLength: 1 }, email: { type: "string", format: "email" }, subject: { type: "string", minLength: 1 }, message: { type: "string", minLength: 1 }, }, required: ["name", "email", "subject", "message"], }, execute: async (input) => { const res = await fetch("/api/send-email", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(input), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return { ok: true }; }, }, ]); // later, e.g. on page teardown cleanup(); ``` `registerTool(tool)` registers a single tool and returns the cleanup. `registerTools([...])` registers many and returns one cleanup that disposes all of them. Re-registering a tool with the same name is safe; the previous registration is dropped first. ## React ```tsx import { useWebMCP, type Tool } from "@web-ai-sdk/webmcp/react"; import { useMemo } from "react"; const TOOLS: Tool[] = [ { name: "list_blog_posts", description: "List published blog posts.", readOnly: true, execute: async () => { const res = await fetch("/api/posts.json"); return { results: await res.json() }; }, }, ]; export function WebMCP() { // Stable reference: keep tools outside the component or wrap in useMemo, // otherwise the hook will unregister/re-register on every render. const tools = useMemo(() => TOOLS, []); useWebMCP(tools); return null; } ``` The hook registers on mount, unregisters on unmount, and re-registers when the array reference changes. ## API ### `registerTool(tool): () => void` Register a single tool. Returns a cleanup function. No-op on unsupported browsers. ### `registerTools(tools): () => void` Register many tools at once. Returns a single cleanup that unregisters all of them. ### `isWebMCPAvailable(): boolean` Feature-detect helper. ### `getModelContext(): ModelContext | undefined` Escape hatch: the raw `navigator.modelContext` if present, for cases the wrapper doesn't cover (e.g. `requestUserInteraction`). ### `Tool` ```ts interface Tool { name: string; description: string; inputSchema?: object; // JSON Schema readOnly?: boolean; // shorthand for annotations.readOnlyHint destructive?: boolean; // shorthand for annotations.destructiveHint annotations?: ToolAnnotations; // raw passthrough, merged on top execute: (input: TInput) => Promise | TOutput; } ``` `description` is consumed by the agent host (Cursor / Claude / Chrome agent / etc.). Write it as an instruction to an LLM about when to call the tool. ### `defineTool({...}): Tool` — typed schema adapter (Standard Schema) ```ts import { defineTool } from "@web-ai-sdk/webmcp"; import { z } from "zod"; // or valibot, arktype, effect, … const sendEmail = defineTool({ name: "send_contact_email", description: "Send a contact email on behalf of the visitor.", destructive: true, // Standard Schema (https://standardschema.dev): used to narrow execute's // input type. Validation at runtime is opt-in via `validate: true`. input: z.object({ name: z.string().min(1), email: z.string().email(), subject: z.string().min(1), message: z.string().min(1), }), // The host still wants raw JSON Schema for tool dispatch; pass it explicitly. // Standard Schema does not emit JSON Schema, so we don't bridge between // them — keeping both lets you choose your validator without coupling. inputSchema: { type: "object", properties: { name: { type: "string", minLength: 1 }, email: { type: "string", format: "email" }, subject: { type: "string", minLength: 1 }, message: { type: "string", minLength: 1 }, }, required: ["name", "email", "subject", "message"], }, async execute({ name, email, subject, message }) { // `name`, `email`, etc. are typed from the Zod schema. const res = await fetch("/api/send-email", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, email, subject, message }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return { ok: true }; }, }); // `sendEmail` is a plain Tool and can be passed to registerTool / registerTools / useWebMCP. ``` `defineTool` accepts any [Standard Schema](https://standardschema.dev) V1 validator (Zod 3.24+, Valibot, ArkType, Effect, …) — no SDK dependency on any specific library. The returned object is a plain `Tool`, so it composes with the rest of the API unchanged. **Validation:** off by default (`validate: false`). Most WebMCP hosts validate against `inputSchema` themselves; running the Standard Schema validator on top is opt-in via `validate: true`, which throws `ToolValidationError` on bad input. With `validate: false` the schema is type-only. ## Safety Mark mutating tools `destructive: true`. The host (browser, agent) is responsible for gating destructive tools on explicit user approval; `@web-ai-sdk/webmcp` only forwards the annotation. For sensitive operations also defend server-side (origin allowlist, rate limit, validation). ## Troubleshooting - **Inspector / agent doesn't see the tools.** `navigator.modelContext` is per-Window. Tools registered inside an `