@dudko.dev/agent-web-react

中文文档
0.0.24发布于 2026/11/14MITTypeScriptESM + CJS零依赖构建来源已验证近 30 天下载 2000

React bindings for @dudko.dev/agent-web: a headless useAgent hook, an AgentProvider context, and optional pre-styled components (chat panel, plan/step view, BYOK key form, WebLLM load bar) that connect the in-browser LLM agent to any React site. UI you ca

原始使用文档
文档目录15 节
  1. Install
  2. Quick start — a drop-in chat panel
  3. The useAgent hook (headless)
  4. Local WebGPU model (no key, offline)
  5. Any on-device runtime — useLocalModel
  6. Models in a bundler (Vite, Next, CRA)
  7. MCP hooks live in their own subpath
  8. The useMcp hook — connect a remote MCP server
  9. Several MCP servers — useMcpServers
  10. Components
  11. Build your own UI
  12. How it fits together
  13. Browser tests
  14. Demo & deployment
  15. License

@dudko.dev/agent-web-react

React bindings for @dudko.dev/agent-web — the headless, universal in-browser LLM agent. Drop the agent into any React app with a single hook and (optionally) a set of pre-styled components:

  • 🪝 useAgent — build the agent from a config, stream its typed events into a ready-to-render state (plan, steps, tool calls, streamed answer, token usage, model-load progress, chat transcript), and get run / stop / reset / reload.
  • 💬 <AgentChat> — a drop-in chat: every tool call with its cost, tokens by kind per answer and in total, thoughts, subagents, consent prompts; a composer with "+" / Ctrl+V / drag & drop attachments (images, PDFs, files), "/" commands, run timer, agents, model + thinking chip, the "⚡ Auto" consent chip and speech to text; saved chats in IndexedDB and a workspace (virtual file system) panel. Everything is a prop: show/hide, labels (i18n), render props, slots, swappable components, theme — see docs/chat.md.
  • 🗣️ Steering and learning — type to the agent while it works (send, stopSubagent; queued / delivered notes under the step that got them), and watch it keep a notebook: lessons it wrote down as skills (learned, <LearnedSkills>, forgetSkill) and the pages it fetched — see docs/chat.md.
  • 🧩 Building blocks — <AgentComposer>, <MessageList>, <StepList>, <ToolApprovalPrompt>, <ApprovalModeSwitch>, <ChatHistoryList>, <FilesPanel>, <LearnedSkills>, <ContextMeter>, <PlanView>, <ModelLoadBar>, <UsageBadge>, <ApiKeyForm>; hooks useChatHistory, useVirtualFiles, useSpeechToText; MCP hooks useMcp / useMcpServers live in @dudko.dev/agent-web-react/mcp.
  • 🔑 useCredentials — store BYOK API keys encrypted at rest (WebCrypto + IndexedDB).
  • 🖥️ useWebLLMModel / useLocalModel — load an on-device model (WebLLM, the browser's built-in model, transformers.js, or your own runtime) with download progress, switching and unloading.
  • 🎛️ Headless-first — the event→UI logic is a pure, exported reducer (agentStateReducer); the components are optional sugar on top.

npm npm downloads license GitHub last commit

▶︎ Live demo — three agents: one edits a sticky-notes board, one drives any number of MCP servers (plus a 185-tool mock catalogue to show tool search), and one plays chess against you — your move triggers it, analyst subagents run in Web Workers. Cloud BYOK or local WebGPU, all in your browser. Settings turn on tool consent, thinking, token and step limits, compaction, skills - the agent's own notebook (it writes down what it figured out, in the workspace, across reloads) and web access limited to hosts you list. Type to the agent while it works. Source in demo/.

Install

npm install @dudko.dev/agent-web-react @dudko.dev/agent-web react react-dom

Then add only the model providers you use (optional peers of the core, 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

Import the optional stylesheet once (skip it if you style the components yourself):

import '@dudko.dev/agent-web-react/styles.css'

Quick start — a drop-in chat panel

<AgentProvider> builds one agent and shares it; <AgentChat> renders it.

import { useMemo } from 'react'
import {
  AgentProvider,
  AgentChat,
  useCredentials,
  defineTool,
} from '@dudko.dev/agent-web-react'
import { z } from 'zod'
import '@dudko.dev/agent-web-react/styles.css'

export function Assistant() {
  const credentials = useCredentials() // encrypted vault (WebCrypto + IndexedDB)

  // Your app's actions, as tools. Keep the object referentially stable.
  const tools = useMemo(
    () => ({
      add_text: defineTool({
        description: 'Add a text block to the page.',
        inputSchema: z.object({ text: z.string() }),
        execute: async ({ text }) => addTextBlock(text), // your code
      }),
    }),
    [],
  )

  return (
    <AgentProvider
      config={{
        model: { providerType: 'google', model: 'gemini-3.5-flash', credentialRef: 'google' },
        credentials: credentials.store,
        tools,
        describeState: () => serializeMyState(), // optional grounding
      }}
    >
      <AgentChat title="Assistant" style={{ height: 520 }} />
    </AgentProvider>
  )
}

Using a bundler (Vite, Next, CRA)? A provider spec like the one above asks the core to import('@ai-sdk/google') at runtime — but browser bundlers can't resolve that bare, @vite-ignored import, so it fails with “Provider package … is not installed” (and WebLLM fails with “webLLM is not a function”). Build the model yourself and pass it directly instead — see Models in a bundler. It's a few extra lines and works in every bundler.

Store the user's key once (encrypted at rest), e.g. from a settings form:

const credentials = useCredentials()
await credentials.setKey('google', userProvidedKey)
// or drop in <ApiKeyForm credentials={credentials} credentialRef="google" />

The useAgent hook (headless)

useAgent is the whole library in one hook — use it directly if you want your own UI:

import { useAgent } from '@dudko.dev/agent-web-react'

function Custom() {
  const agent = useAgent({
    // In a bundler, pass a model you built (see “Models in a bundler”); a
    // provider spec like this one only resolves where dynamic imports do.
    model: { providerType: 'openai', model: 'gpt-5.4-mini', credentialRef: 'openai' },
    credentials,
    tools,
  })

  return (
    <>
      <button disabled={!agent.isReady} onClick={() => agent.run('Add a totals row')}>
        Run
      </button>
      {agent.isRunning && <button onClick={agent.stop}>Stop</button>}

      {/* Everything below is live, derived from the event stream: */}
      {agent.plan && <p>{agent.plan.thought}</p>}
      {agent.steps.map((s) => (
        <div key={s.id}>
          {s.index}/{s.total} · {s.step.description} · {s.status}
          {s.toolCalls.map((c, i) => (
            <code key={i}>{c.name}</code>
          ))}
        </div>
      ))}
      <p>{agent.finalText}</p>
      <small>{agent.usage.totalTokens} tokens</small>
    </>
  )
}

useAgent(config, options) returns the full AgentUiState plus:

FieldDescription
run(goal, { images, files, attachments, label })Start a run (with attachments); resolves with the RunResult
stop()Abort the in-flight run
agentthe core Agent itself (or undefined until built): agent.usage / agent.sessionUsage() (tokens spent, counted live; core ≥ 0.0.32), agent.listLearnedSkills(), agent.capabilities
forgetSkill(name)delete one of the agent's own lessons from its notebook
sendUndelivered() / discardUndelivered()send, or drop, what a stopped run never showed the agent
send(text), stopSubagent(id)Say something to the running agent (shown to the model at its next boundary; the replanner re-checks the plan) / stop one subagent. queued, notes and undelivered show where a message is — see docs/chat.md
reset()Clear the whole conversation (and the agent's memory of it)
reload()Rebuild the agent (e.g. after storing a new key)
statusidle | initializing | ready | running | error
isReady / isRunningconvenience booleans
messagestranscript; each assistant message keeps its steps, tool calls, thoughts, approvals, subagents, usage and duration
plan / stepsthe live plan and per-step tool calls (with timing)
finalText / finalReasoningthe streamed answer and its thoughts
usage / totalUsagetokens of this run / of the conversation, by kind
pendingApprovals, approve(id, { remember }), deny(id)tool consent
approvalMode / setApprovalMode(mode)the autopilot switch, live
compact()summarise the stored transcript now
sessionId / loadChat(chat)the memory session; switch conversations
subagents, skills, discoveredTools, compactions, budgetthe rest of the run, folded
learned, learnedNow, fetchesthe agent's notebook (LessonView[], survives reset), what it wrote this run, and the web_fetch requests this run made (FetchView[])
queued, notes, undelivered, replansteering: messages waiting, shown (and where), left over; the last replan decision (byUser when a message caused it)
error, stopped, events, seqthe run's error text, whether it was stopped, the raw event log and its counter
modelLoadWebLLM download progress, when loading

Options: deps (rebuild the agent when these change — e.g. on a model switch), onEvent (tap the raw event stream), autoStart, maxEvents, approvals (route consent requests into the hook; default true).

Keep tools and describeState referentially stable (useMemo / useCallback); pass a changed deps array to rebuild the agent on a model/provider switch.

Local WebGPU model (no key, offline)

import { useAgent, useWebLLMModel, ModelLoadBar } from '@dudko.dev/agent-web-react'
import { webLLM } from '@browser-ai/web-llm' // your app's optional peer

// Build WebLLM in your own code so the bundler includes it (see note below).
const createLocalModel = (id: string, opts?: object) => Promise.resolve(webLLM(id, opts as never))

function LocalAgent() {
  const local = useWebLLMModel('Qwen2.5-1.5B-Instruct-q4f16_1-MLC', { create: createLocalModel })
  const agent = useAgent(
    { model: local.model!, tools },
    { deps: [local.model] }, // build once the model is loaded
  )

  if (!local.ready) {
    return local.loading ? (
      <ModelLoadBar load={{ progress: local.progress, text: local.text }} />
    ) : (
      <button disabled={!local.supported} onClick={local.load}>
        Load local model
      </button>
    )
  }
  return <AgentChat controller={agent} />
}

load() eagerly downloads the weights (so the progress bar fills during load, not silently on the first message) and ready flips only once the model can actually answer. The create option is what makes it work under a bundler — see below.

Change the modelId and everything the hook reports — model, ready, loading, progress, error — is about the new one, which is not loaded yet. The previous model stays in memory (loadedModelId; switching back is instant) until the new one is loaded — that frees it first, so two models never share the GPU — or until unload().

Context window. WebLLM loads its models with a 4096-token window, far below what Qwen3 or Llama 3.x were trained on. Pass contextWindowTokens to load with more (it costs KV-cache VRAM, not a new download); your create factory receives it and applies it with the core's withWebLLMContextWindow (see the demo's providers.ts). local.contextWindow is the window the model was loaded with, and the agent fits its runs into it on its own — compaction, tool lists and tool results are sized from it.

Vision. Phi-3.5-vision-instruct-q4f16_1-MLC takes images (paste, drop or attach them) — a multimodal model that never sends them anywhere.

Any on-device runtime — useLocalModel

useWebLLMModel is useLocalModel with the WebLLM engine. Describe another runtime as an engine — create, and optionally warmUp (download now, with progress), unload, supported, contextWindowOf — and the hook gives the same load / unload / progress / switching:

import { useLocalModel, createWebLLMEngine, type LocalModelEngine } from '@dudko.dev/agent-web-react'

const builtIn: LocalModelEngine = {
  create: async () => (await import('@browser-ai/core')).browserAI('text', {
    expectedInputs: [{ type: 'text' }, { type: 'image' }],
  }),
  warmUp: async (m, { onProgress }) => {
    await m.createSessionWithProgress((p) => onProgress({ progress: p, text: 'Downloading' }))
  },
  supported: () => 'LanguageModel' in globalThis, // Chrome's Prompt API
  contextWindowOf: (m) => m.getContextWindow(),
}
const webllm = createWebLLMEngine({ create: createLocalModel })

// One hook, any runtime: switching frees the previous model with its own engine.
const local = useLocalModel(option.model, option.builtIn ? builtIn : webllm, {
  contextWindowTokens: option.contextWindow,
})

The demo runs WebLLM, Chrome's Gemini Nano and transformers.js this way — see demo/src/local-engines.ts.

Models in a bundler (Vite, Next, CRA)

The core builds cloud providers with import('@ai-sdk/<provider>') and WebLLM with import('@browser-ai/web-llm'). Those are bare, @vite-ignored dynamic imports — great for Node/SSR/import-map setups, but a browser bundler either leaves them unresolvable at runtime (cloud → “Provider package … is not installed”) or stubs them to an empty module (WebLLM → “webLLM is not a function”).

The fix is the same for both: build the model in your own code (a static import your bundler can see) and hand the agent a direct LanguageModel.

Cloud — construct the provider from the vault-stored key:

import { useEffect, useState } from 'react'
import { createGoogleGenerativeAI } from '@ai-sdk/google'
import type { LanguageModel } from 'ai'
import { useAgent, useCredentials } from '@dudko.dev/agent-web-react'

function CloudAgent() {
  const credentials = useCredentials()
  const [model, setModel] = useState<LanguageModel>()

  useEffect(() => {
    credentials.store.getApiKey('google').then((key) => {
      setModel(key ? createGoogleGenerativeAI({ apiKey: key })('gemini-3.5-flash') : undefined)
    })
  }, [credentials.store, credentials.version])

  const agent = useAgent({ model: model!, credentials: credentials.store, tools }, { deps: [model] })
  // …render <AgentChat controller={agent} /> once model is set
}

Anthropic needs headers: { 'anthropic-dangerous-direct-browser-access': 'true' } passed to createAnthropic({ … }) for direct browser calls.

Local — pass a statically-imported webLLM factory via useWebLLMModel's create option (see the WebLLM example above).

The demo/ app does exactly this — see demo/src/providers.ts.

MCP hooks live in their own subpath

import { useMcp, useMcpServers } from '@dudko.dev/agent-web-react/mcp'

The MCP hooks are not exported from @dudko.dev/agent-web-react. They load the core's @dudko.dev/agent-web/mcp, which imports @modelcontextprotocol/sdk — an optional peer, so npm does not install it for you. A bundler cannot honour "optional": it resolves even a dynamic import() at build time, so a host that merely imported useAgent from the main entry used to fail webpack/CRA with Module not found: @modelcontextprotocol/sdk/… while never touching MCP. Behind a subpath the decision is yours:

  • Don't use MCP → import only from the main entry. Nothing in your build mentions the SDK; install nothing extra.
  • Use MCP → import from …/mcp and npm install @modelcontextprotocol/sdk next to the core (it is bundled into an async chunk, loaded when you connect).

Migrating from ≤ 0.0.18. useMcp, useMcpServers and their helpers (readCallbackParams, claimOAuthCallback, stripOAuthParams, describeMcpResult, isUnauthorizedError) and types (McpStatus, McpAuthMode, McpServerView, …) moved: change the import path, nothing else.

The useMcp hook — connect a remote MCP server

Let the user name their own MCP server at runtime and hand its tools to the agent. Auth is either a static header or the full OAuth 2.1 flow with dynamic client registration — no client ID to configure, and the access token is refreshed for you when it expires mid-run.

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

const mcp = useMcp({ clientName: 'my-app' })

await mcp.connect({ url, oauth: true })       // or { url, headers: { Authorization } }

// The server wants a sign-in: send the user off from a real click.
{mcp.status === 'needs-authorization' && <button onClick={mcp.authorize}>Authorize</button>}

// Then just merge the tools into any agent config.
const config = { model, tools: { ...myTools, ...mcp.tools } }

useMcp returns { status, tools, catalog, error, authorizationUrl, oauthSupported, completingAuthorization, connect, disconnect, authorize, forgetAuthorization, checkOAuthSupport }. status is idle | connecting | connected | needs-authorization | error.

The OAuth round-trip navigates away from your app, so the hook persists what it needs and, on the way back, finishes the code exchange, strips ?code=… from the address bar and reconnects before rendering — completingAuthorization covers that window.

Two notes:

  • The connector lives in the core's optional @dudko.dev/agent-web/mcp subpath and is loaded with a dynamic import, so the SDK stays out of your initial bundle until connect runs. Install @modelcontextprotocol/sdk alongside the core — importing these hooks is what puts it in your build.
  • oauthSupported is false on cores older than @dudko.dev/[email protected], which introduced BrowserOAuthProvider; header auth still works there. The peer floor is >=0.0.30 — the first core with the learning notebook (forgetSkill, listLearnedSkills) on top of the virtual file system, attachments, consent, tool search, subagents, steering and context editing this package's chat builds on. agent.usage / agent.sessionUsage() and the window-derived output caps need >=0.0.32.

Several MCP servers — useMcpServers

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

const mcp = useMcpServers({ clientName: 'my-app' }) // the list persists; reconnects on load
await mcp.add({ name: 'docs', url, auth: 'oauth' }) // 'none' | 'bearer' (+ token) | 'oauth'
mcp.servers  // [{ name, url, status, catalog, error, … }]
const config = { model, tools: { ...myTools, ...mcp.tools } } // "<name>__<tool>"

Servers connect in parallel, each with its own deadline; paginated tool lists are read to the end; tools a server marks read-only skip the "ask before changes" prompt. With hundreds of tools the core switches to tool search (a compact catalogue + find_tools) above toolSearchThreshold — see the core's capabilities doc.

Components

All components are optional and styled by styles.css (class-prefixed awr-, themeable via --awr-* custom properties, light + dark, or theme). Each accepts a className and labels; data components take plain props so you can use them standalone. The full guide: docs/chat.md.

ComponentPurpose
<AgentChat>Full panel: transcript with per-answer activity + composer + optional history and files panels. Reads a controller prop or the <AgentProvider> context.
<AgentComposer>The input and its toolbar: attachments, commands, timer, agents, model/thinking, consent, mic, send/stop.
<MessageList>The transcript; each answer with its steps, tool calls, thoughts, usage.
<StepList> / <ToolCallRow>Execution steps / one tool call with its cost and images.
<ToolApprovalPrompt>Consent cards (Allow once / Always / Deny).
<ApprovalModeSwitch>Segmented consent-mode switch.
<ChatHistoryList>Saved chats (from useChatHistory).
<FilesPanel>A VirtualFileSystem, live: preview, download, delete, upload.
<LearnedSkills>The agent's notebook (learning): its lessons, where each came from, forget.
<ContextMeter>Tokens by kind, budget bar, last compaction, Compact.
<PlanView>A plan's reasoning + step list.
<ModelLoadBar>WebLLM download/init progress.
<UsageBadge>Compact token readout.
<ApiKeyForm>BYOK key entry that writes to the encrypted vault.
<Composer>The minimal textarea + send/stop (kept for existing apps).

Build your own UI

The components are a thin layer over the exported, pure reducer. Use it directly if you'd rather render everything yourself:

import { agentStateReducer, createInitialAgentState } from '@dudko.dev/agent-web-react'

let state = createInitialAgentState()
state = agentStateReducer(state, { type: 'event', event }) // fold each AgentEvent

The state is made of plain, exported view types you can render with your own components: StepView (a step and its ToolCallViews), ApprovalView, SubagentView, UserNoteView / QueuedMessageView (steering), LessonView / FetchView (the notebook and the pages fetched), BudgetView, CompactionView, AttachmentView, ChatMessage (a transcript entry that keeps its own steps, usage and duration). Outside <AgentChat>, useAgentContext() returns the controller of the enclosing <AgentProvider> (it throws outside one; useOptionalAgentContext() returns undefined instead), and useLabels() the labels in effect.

How it fits together

@dudko.dev/agent-web (peer)     →  the headless agent + providers + vault
@dudko.dev/agent-web-react      →  useAgent / <AgentProvider> / components
your app                        →  tools, credentials, and where the panel goes

The React package re-exports the core primitives you usually need (createAgent, defineTool, VaultCredentialStore, createWebLLMModel, isWebGPUAvailable, and the key types), so a React app can import everything from one place.

Direct browser calls & CORS: not every provider allows direct BYOK calls from a browser origin. Google (Gemini), openai-compatible and the gateway are the reliable direct paths; Anthropic works (a required header is injected); OpenAI/xAI/DeepSeek usually need a proxy. See the core's providers doc. Ship only the user's own key to the browser — shared/app keys belong behind a proxy or the gateway.

Browser tests

npm run test:e2e builds the demo and drives it in Chromium:

  • MCP: connecting to one and to several servers, the full OAuth redirect round-trip (authorize → come back with a code → connected), reconnecting with the stored tokens after a reload, and an unreachable server;
  • the agent, against a scripted Gemini endpoint (e2e/gemini.ts, the real @ai-sdk/google provider underneath): tool calls and tokens by kind in the transcript, a chat saved and restored after a reload, an image attached (shown, sent as a file part, saved in the workspace), a file dropped anywhere on the page, consent in "Ask" mode, slash commands, the chess move that triggers the agent, and Russian labels; the agent's notebook (a lesson written, kept in the workspace across a reload, seen by the planner, forgotten on request) and web access (off by default; an allowed page is read, a metadata address is refused). The MCP and authorization servers it talks to are started on loopback by e2e/servers.ts, CORS headers included — so the test also pins the deployment requirement that the challenge header be exposed.

These cover what unit tests structurally cannot: effect ordering across a real render, a real navigation away and back, and IndexedDB.

Demo & deployment

The demo/ app (Vite + React) is deployed to GitHub Pages by .github/workflows/deploy-demo.yml on every push to main. To run it locally:

npm install && npm run build      # build the library into dist/
cd demo && npm install && npm run dev

The demo aliases @dudko.dev/agent-web-react to the built ../dist, so rebuild the library (npm run build) after changing its source.

License

MIT © Siarhei Dudko

版本与发布记录

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

版本发布日期距今
0.0.24latest2026/11/14今天
0.0.232026/11/095 天前
0.0.222026/11/095 天前
0.0.212026/11/095 天前
0.0.202026/11/095 天前
0.0.192026/11/095 天前

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

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

React bindings for @dudko.dev/agent-web: a headless useAgent hook, an AgentProvider context, and optional pre-styled components (chat panel, plan/step view, BYOK key form, WebLLM load bar) that connect the in-browser LLM agent to any React site. UI you ca

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

在项目目录运行 npm install @dudko.dev/agent-web-react,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(>=18)、react-dom(>=18)、@dudko.dev/agent-web(>=0.0.30)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

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

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

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

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

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

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

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

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

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

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

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

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