@dudko.dev/agent-web

中文文档
0.0.33发布于 2026/11/09MITTypeScriptESM + CJS构建来源已验证近 30 天下载 2790

Headless, configurable in-browser LLM agent on the Vercel AI SDK: multi-provider bring-your-own-key (OpenAI, Anthropic, Google, xAI, DeepSeek, gateway, openai-compatible) AND local WebGPU/WebLLM, native + prompted tool-calling, structured output, an encry

原始使用文档
文档目录13 节
  1. Install
  2. CDN / standalone bundle (no build step)
  3. Quick start — cloud model, bring-your-own-key
  4. Quick start — local WebGPU model (no key, offline)
  5. Simple generation & tool-calling (no agent loop)
  6. Optional: MCP tools over HTTP
  7. OAuth 2.1 + Dynamic Client Registration
  8. CORS: what the server must send
  9. Optional: WebMCP — a page's tools, both ways
  10. Thinking, limits, consent, skills, subagents
  11. Configuration (highlights)
  12. Docs
  13. License

@dudko.dev/agent-web

Headless, universal, configurable in-browser LLM agent built on the Vercel AI SDK. One package drives any model from the browser:

  • ☁️ Cloud providers, bring-your-own-key — OpenAI, Anthropic, Google, xAI, DeepSeek, any OpenAI-compatible server, and the Vercel AI Gateway.
  • 🖥️ Local WebGPU models — via WebLLM (@browser-ai/web-llm); no server, no key, data never leaves the device.
  • 🔌 Any AI SDK LanguageModel — e.g. Chrome/Edge built-in AI.

It gives you simple text generation, streaming, native tool-calling, and structured output, plus a high-level plan → execute → replan → synthesize agent — with host-defined tools, optional MCP, system prompts, IndexedDB storage, and an encrypted token vault. UI-agnostic: it streams typed events; you render them however you like.

The agent loop also has thinking (portable reasoning levels or exact budgets), token limits (input / output / thinking / total per run), context compaction (automatic and agent.compact()), prompt caching, skills (SKILL.md bundles), tool consent modes with an autopilot switch, tool search for catalogues of hundreds of MCP tools, subagents in Web Workers, image / PDF / file / URL input (with a clear error for models that can't take them), a virtual file system in IndexedDB with fs_* tools, usage counted live across a session (agent.usage), steering a run while it goes (RunControl), resuming an interrupted run, egress you switch on and bound (the model's web_fetch), and learning — the agent writes its own lessons down as skills — see docs/capabilities.md.

npm npm NpmLicense GitHub last commit GitHub release

Browser sibling of the Node package @dudko.dev/agent.

Using React? @dudko.dev/agent-web-react wraps this core in a useAgent hook, an <AgentProvider> context and drop-in UI components — try the live demo (source).

Install

npm install @dudko.dev/agent-web ai zod

Then add only the providers you use (all optional peers, dynamically imported):

# cloud, pick what you need
npm install @ai-sdk/openai        # or @ai-sdk/anthropic, @ai-sdk/google, @ai-sdk/xai, @ai-sdk/deepseek, @ai-sdk/openai-compatible
# local WebGPU models
npm install @browser-ai/web-llm @mlc-ai/web-llm
# optional: HTTP MCP
npm install @modelcontextprotocol/sdk

CDN / standalone bundle (no build step)

dist/agent-web.js is a self-contained ESM bundle of the core — ai, zod and idb are baked in. It ships in the npm package (so it's on every CDN) and is attached to each GitHub release as agent-web-<version>.min.js:

<script type="module">
  import { createAgent, defineTool } from 'https://cdn.jsdelivr.net/npm/@dudko.dev/agent-web/dist/agent-web.js'
  // or: https://unpkg.com/@dudko.dev/agent-web/dist/agent-web.js
</script>

Bundler users can also opt into it via the @dudko.dev/agent-web/bundle subpath. Optional provider peers stay external even in the bundle; in a bundler-less page, map the ones you use with an import map (the runtime import() respects it) — direct LanguageModels and gateway specs need no import map at all:

<script type="importmap">
  {
    "imports": {
      "@ai-sdk/openai": "https://cdn.jsdelivr.net/npm/@ai-sdk/openai/+esm"
    }
  }
</script>

Quick start — cloud model, bring-your-own-key

Keys are stored encrypted at rest (WebCrypto, IndexedDB) and referenced by id — never inlined into config. See security.

import {
  createAgent,
  defineTool,
  VaultCredentialStore,
} from '@dudko.dev/agent-web'
import { z } from 'zod'

// 1. Store the user's key once (e.g. from a settings form). Encrypted at rest.
const credentials = new VaultCredentialStore()
await credentials.setApiKey('openai', userProvidedKey)

// 2. Tools = your app's actions.
const tools = {
  add_text: defineTool({
    description: 'Add a text block to the page.',
    inputSchema: z.object({ text: z.string(), x: z.number().optional() }),
    execute: async ({ text, x }) => addTextBlock(text, x), // your code
  }),
}

// 3. Create the agent. The key is fetched from the vault at build time.
const agent = await createAgent({
  model: { providerType: 'openai', model: 'gpt-4o-mini', credentialRef: 'openai' },
  credentials,
  tools,
  describeState: () => serializeMyCanvas(), // optional grounding
})

// 4. Run. Every step is a typed event.
const result = await agent.run('Add a centered title and a totals line', {
  onEvent: (e) => {
    if (e.type === 'plan.created') console.log('plan:', e.plan.steps)
    if (e.type === 'step.tool-call') console.log('tool:', e.name, e.input)
    if (e.type === 'final') console.log('done:', e.text)
  },
})
console.log(result.final, `— ${result.applied} changes`)

Direct browser calls & CORS: as of October 2026 Google, OpenAI, xAI and DeepSeek send CORS headers, so a user's own key works straight from the page; Anthropic works too (a required header is injected for you). Kimi, Groq, Mistral and OpenRouter do as well (build their AI SDK models yourself and pass them as model). See docs/providers.md.

Quick start — local WebGPU model (no key, offline)

import { createAgent, createWebLLMModel, isWebGPUAvailable } from '@dudko.dev/agent-web'

if (!isWebGPUAvailable()) throw new Error('WebGPU required for local models')

const model = await createWebLLMModel('Llama-3.2-3B-Instruct-q4f16_1-MLC', {
  initProgressCallback: (r) => console.log('loading', Math.round(r.progress * 100), '%'),
  contextWindowTokens: 8192, // WebLLM loads with 4096 unless told otherwise
})

// Local models default to the robust "prompted" tool-mode automatically, and
// the agent sizes compaction and tool lists from the model's own window.
const agent = await createAgent({ model, tools /* ...same as above */ })
await agent.run('Summarize the current page and add a heading')

A local vision model (Phi-3.5-vision-instruct-q4f16_1-MLC) takes images like a cloud one. See docs/capabilities.md for the window: WebLLM's default is 4096 tokens, raised per model at load.

You can also pass a model by spec: { providerType: 'web-llm', model: '…' }.

Simple generation & tool-calling (no agent loop)

import { generate, stream, generateStructured, runToolLoop } from '@dudko.dev/agent-web'
import { z } from 'zod'

const { text } = await generate(model, { prompt: 'Write a haiku about WebGPU' })

const { textStream } = stream(model, { prompt: 'Explain IndexedDB' })
for await (const chunk of textStream) process.stdout.write(chunk)

const { object } = await generateStructured(
  model,
  z.object({ title: z.string(), tags: z.array(z.string()) }),
  { prompt: 'Suggest a title and tags for this article: …' },
)

// Tool-calling with feedback (native or prompted), normalized result. In both
// modes the model sees its tool results and can keep going, up to `maxSteps`
// rounds (default 4; prompted models get the results as a TOOL RESULTS turn):
const { text: reply, toolCalls } = await runToolLoop(model, {
  mode: 'native',
  prompt: 'Add a title',
  tools,
})

Optional: MCP tools over HTTP

import { connectMcpHttp } from '@dudko.dev/agent-web/mcp'

const mcp = await connectMcpHttp({
  docs: { url: 'https://my-mcp-server.example/mcp', headers: { Authorization: `Bearer ${t}` } },
})
const agent = await createAgent({ model, tools: { ...tools, ...mcp.tools } })
// ... later: await mcp.close()

Browsers can only speak the HTTP (StreamableHTTP) transport — stdio MCP is Node-only. The connector lives in the ./mcp subpath so the MCP SDK never enters your core bundle.

Pass several servers at once — they connect concurrently, each with its own deadline (connectTimeoutMs, default 30 s), every page of a paginated tools/list is read, and tools a server marks readOnlyHint are treated as read-only by the consent gate. A failing server is reported in results; the others still mount. With hundreds of tools the agent switches to tool search automatically (see docs/capabilities.md).

connectMcpHttp returns { tools, catalog, results, refreshServer, close }. tools and catalog are mutated in place by refreshServer(name), so an agent built from them picks up a server's new tool list (react to it via the onToolsChanged option) without being rebuilt. Filtering is plain object work: delete the keys you don't want before merging.

OAuth 2.1 + Dynamic Client Registration

A server behind OAuth needs more than a header: access tokens expire mid-run. Pass an authProvider and the MCP SDK discovers the authorization server (RFC 9728), registers this app dynamically (RFC 7591 — no client_id to pre-provision), runs PKCE, and refreshes the access token on any 401 and retries the request.

import { BrowserOAuthProvider, connectMcpHttp, finishMcpOAuth, readOAuthCallback }
  from '@dudko.dev/agent-web/mcp'

const provider = new BrowserOAuthProvider({
  serverUrl: 'https://my-mcp-server.example/mcp',
  redirectUrl: `${location.origin}/callback`, // must match on the way back
})

// 1. On the callback page: finish the flow before anything else.
const callback = readOAuthCallback()
if (callback) await finishMcpOAuth(provider, callback)

// 2. Connect. `needsAuthorization` means "send the user to authorize".
const mcp = await connectMcpHttp({ docs: { url: provider.serverUrl, authProvider: provider } })
if (mcp.results[0]?.needsAuthorization) {
  location.href = String(provider.authorizationUrl) // a button, a popup — your call
}

Tokens, the dynamic registration and the PKCE verifier are stored in the same encrypted IndexedDB vault as provider API keys (VaultOAuthStorage; swap in MemoryOAuthStorage or your own via storage). redirectToAuthorization never navigates on its own — the SDK can demand authorization from inside a running tool call, and evicting the user then would discard the run. The URL is recorded on provider.authorizationUrl and the app decides when to use it.

CORS: what the server must send

A browser tells JavaScript nothing about why it blocked a request — a CORS rejection and a server that is down both arrive as TypeError: Failed to fetch, with the real reason printed only to the devtools console. So these are worth checking first rather than last:

Access-Control-Allow-Origin:   <your origin>
Access-Control-Allow-Methods:  GET, POST, DELETE, OPTIONS
Access-Control-Allow-Headers:  Content-Type, Authorization, mcp-session-id,
                               MCP-Protocol-Version, Last-Event-ID
Access-Control-Expose-Headers: mcp-session-id, WWW-Authenticate

Each line earns its place:

  • MCP-Protocol-Version in Allow-Headers is the one that bites hardest. The spec has required that header on every request after initialize since 2025-06-18, and CORS lists written before that date omit it. The first request does not carry it, so discovery, the whole OAuth dance and initialize all succeed — and then everything afterwards is blocked. It reads exactly like the consent step failing, and sends you to debug the client.
  • WWW-Authenticate in Expose-Headers, or the browser hides the 401 challenge from JS and OAuth discovery never starts.
  • Last-Event-ID for resuming a dropped SSE stream.
  • The authorization server's /register, metadata and token endpoints need CORS too, OPTIONS included — a browser client calls them directly.
  • If the server uses the MCP SDK's DNS-rebinding protection, its allowedOrigins must contain your origin. Left empty, it answers 403 Origin not allowed to any request that carries an Origin header — which is every request a browser makes.

When a connection does fail this way, connectMcpHttp no longer just repeats the browser's silence: it re-probes the endpoint with a plain request and, if that gets through, says which header is being refused. diagnoseMcpCors(url) is exported so a UI can show the same sentence.

Optional: WebMCP — a page's tools, both ways

WebMCP lets a page register named, typed tools (document.modelContext.registerTool(tool, { signal })) that a browser's agent — Chrome's origin trial, Edge, an extension, an in-page agent — can discover and call. The ./webmcp subpath bridges it and an agent's ToolSet, with no extra dependency and as a no-op where the browser has no WebMCP:

import { connectWebMcp, registerWebMcpTools } from '@dudko.dev/agent-web/webmcp'

// 1. Publish the tools your in-page agent uses, so any agent of the browser can drive them too.
//    One definition, two consumers; withdrawn when the signal aborts or unregister() is called.
const reg = await registerWebMcpTools(tools, {
  prefix: 'myapp_', // WebMCP tool names are shared with everything else on the page
  except: ['debug_dump'],
  consequential: ['clear_page'], // annotated consequentialHint: true
  signal: controller.signal,
})
// readOnly: true tools (defineTool) are annotated readOnlyHint: true

// 2. Use the tools a page has registered as the agent's own.
const web = await connectWebMcp({ onToolsChanged: () => rebuildAgent() })
const agent = await createAgent({ model, tools: { ...tools, ...web?.tools } })

Both generations of the browser API are handled: the current one on document.modelContext (a tool is withdrawn by aborting its signal) and Chrome's early preview on navigator.modelContext (unregisterTool(name)). Results are returned as text content (structured results as JSON), as WebMCP clients read them. Validate strictly inside your execute, loosely in the schema: a rejected call should be a result the calling agent can read, not a stalled tool.

import { createAgent, createSubagentTool, defineSkill } from '@dudko.dev/agent-web'

const agent = await createAgent({
  model,
  tools: { ...tools, ...mcp.tools },
  thinking: 'high', // or { level, budgetTokens }; per stage via stageThinking
  limits: { maxTotalTokens: 200_000, maxReasoningTokens: 20_000 },
  maxToolCalls: 40,
  compaction: { contextWindowTokens: 128_000 }, // auto-compacts history and long runs
  skills: [defineSkill({ name: 'triage', description: 'Triage a bug report', content: '…' })],
  toolApproval: {
    mode: 'ask-writes', // autopilot | ask-writes | ask-all | read-only
    onRequest: async (req) => window.confirm(`Allow ${req.toolName}?`),
  },
})
agent.setToolApprovalMode('autopilot') // flip the switch at any time
await agent.compact() // summarise the stored transcript now

With more than 40 tools (several MCP servers) the executor switches to tool search: it starts each step small and calls find_tools to activate what it needs. Subagents are tools too — createSubagentTool({ config }) runs a child agent in-process, createSubagentTool({ worker, workerConfig }) isolates it in a Web Worker served by serveSubagentWorker(). Add profiles and the parent model chooses, per call, between named variants of the same subagent — another model, thinking level, limits or system prompt. Give persistence a store (new IndexedDBRunStore()) and a run interrupted by a reload or a crash resumes from its last checkpoint with run('', { resumeFromRunId }) — the planner does not run again and no finished step is repeated. createRunControl() steers a run that is going: send() puts a message in front of the model at the next boundary (the replanner re-checks the plan against it), stopSubagent(id) stops one subagent. egress (off by default) gives the model one safe web_fetch tool with allow / deny lists. learning (off by default) lets the agent write down what it figured out as skills of its own — in memory, or in the file system across sessions. Details, defaults and events: docs/capabilities.md.

Configuration (highlights)

OptionDefaultPurpose
model—a LanguageModel or ProviderModelSpec (the executor/default)
planner / synthesizer= modelper-stage model overrides (inherit base provider/creds if partial)
credentials—a CredentialStore for credentialRef keys
tools{}host tools (defineTool)
availableTools / excludedTools—whitelist / blacklist of tool names mounted from tools
toolMode'auto'native | prompted | auto (cloud→native, local→prompted)
toolSelectionStrategy'auto'all | plan-narrowed | search | auto (search above toolSearchThreshold, 40, or once the definitions take ¼ of the window)
toolApprovalautopilotconsent policy: { mode, rules, onRequest, timeoutMs }; agent.setToolApprovalMode()
skills—SKILL.md bundles (defineSkill / parseSkillMarkdown / loadSkillFromUrl)
egressoffwhat the model may fetch (web_fetch): true = any public host over https, or { allow, deny, protocols, allowPrivateNetwork, fetch, ... }; a browser can't pin DNS, so prefer an allow list — see Egress
learningoffthe agent writes down lessons as skills (save_skill / forget_skill): true = this session, { store: new VfsSkillStore(vfs) } = across sessions — see Learning
thinking / stageThinkingprovider defaulttrue, a level ('low'…'xhigh', 'none'), or { level, budgetTokens, includeThoughts }
limits—run caps maxInputTokens / maxOutputTokens / maxReasoningTokens / maxTotalTokens + perCall output caps
maxToolCalls / maxPlanSteps∞ / 8tool calls per run / steps per plan
compactionauto, at 85% of a 128k window (a local model: its own){ auto, contextWindowTokens, thresholdRatio, thresholdTokens, keepRecentTurns, keepRecentSteps, maxToolOutputChars } — judged on the whole context (fixed prompt + transcript + step log), never more than the model's window
promptCachingtruestable system prefixes, Anthropic breakpoints, OpenAI cache key
vision / inputsinferredwhat the model takes as attachments (run(goal, { images, files })); see agent.capabilities
systemPrompt / prompts—prepended to every phase / override any phase's prompt builder
describeState—serialize world state into prompt context
memory—ContextStore (IndexedDBStore / MemoryStore); recent turns are read back into the planner prompt
sessionId—names the stored conversation in memory, and the bucket agent.sessionUsage() counts into
persistence—checkpoint runs (IndexedDBRunStore / MemoryRunStore) so run(goal, { resumeFromRunId }) continues after a reload — see Resuming
maxIterations / maxStepsPerTask / maxRevisions8 / 4 / 2loop caps
chatTimeoutMs120000per-call watchdog
temperatureprovider defaultsampling temperature for every stage
budgets—legacy per-phase output caps; limits.perCall wins, and the defaults are shares of the model's window
clientName—sent as the openai-compatible provider name and the MCP client name
logLevel / logger'warn' / consoleverbosity ('silent' | 'error' | 'warn' | 'info' | 'debug') and the console-like sink the logs go to
replan / synthesizetruetoggle phases
replanAfter'failure'replan trigger: 'failure' | 'always' | (stepResult) => boolean | Promise<boolean>
compressAfterChars12000legacy: summarize old history past this size (when compaction is unset)

Docs

License

MIT © Siarhei Dudko

版本与发布记录

@dudko.dev/agent-web 共发布 30 个版本,首次发布于 2026/07/03;过去 12 个月发布了 30 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.0.33latest2026/11/094 天前
0.0.322026/11/094 天前
0.0.312026/11/094 天前
0.0.302026/11/094 天前
0.0.292026/11/094 天前
0.0.282026/11/094 天前

关于 @dudko.dev/agent-web 的常见问题

@dudko.dev/agent-web 是什么?

Headless, configurable in-browser LLM agent on the Vercel AI SDK: multi-provider bring-your-own-key (OpenAI, Anthropic, Google, xAI, DeepSeek, gateway, openai-compatible) AND local WebGPU/WebLLM, native + prompted tool-calling, structured output, an encry

如何安装 @dudko.dev/agent-web?

在项目目录运行 npm install @dudko.dev/agent-web,或使用 pnpm add / yarn add / bun add。

@dudko.dev/agent-web 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@dudko.dev/agent-web 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@dudko.dev/agent-web 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@dudko.dev/agent-web 可以商用吗?

@dudko.dev/agent-web 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@dudko.dev/agent-web 的下载量有多少?

根据 npm 官方下载统计,@dudko.dev/agent-web 在 2026/10/13 – 2026/11/11 这 30 天内被下载 2,790 次,日均约 93 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@dudko.dev/agent-web 还在维护吗?

最新版本 0.0.33 发布于 2026/11/09(4 天前);至今共发布 30 个版本,其中过去 12 个月发布了 30 个;首次发布于 2026/07/03。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。