Thin React hooks that expose opt-in state and actions to CDP/automation clients via a schema-described registry at window.__AGENT__.
Automation clients — LLM browser agents, Playwright scripts, CDP consumers — interact with SPAs through the DOM. This is:
- Token-inefficient. An agent must parse hundreds of lines of HTML to extract a few values.
- Brittle. CSS selectors and DOM structure change more often than business logic.
- Blind. The agent cannot discover available operations or state shape without scraping.
react-agent-bridge solves this by exposing a structured, schema-described registry at window.__AGENT__. Agents read state and invoke business-logic actions in one call — no DOM scraping, no selector maintenance, no wasted tokens.
| Approach | Token cost | Reliable | Discoverable |
|---|---|---|---|
| DOM scraping (innerText, selectors) | High (parse entire DOM) | Brittle (CSS changes) | No |
window.__AGENT__.state.get('key') |
1 call, ~50 chars | Yes (API contract) | Yes (schemas + list) |
await window.__AGENT__.actions.mutate('name', payload) |
1 call, ~80 chars | Yes (business logic preserved) | Yes (descriptors) |
import { AgentBridgeProvider, useAgentState, useAgentAction } from 'react-agent-bridge';
function Counter() {
const [count, setCount] = useAgentState('counter.value', 0, {
description: 'Current counter value',
});
const { mutate: increment } = useAgentAction(
'counter.increment',
async () => { setCount((c) => c + 1); },
{ description: 'Increment the counter by 1' }
);
return <button onClick={increment}>{count}</button>;
}
function App() {
return (
<AgentBridgeProvider appId="my-app">
<Counter />
</AgentBridgeProvider>
);
}From the browser console or an LLM agent:
// Read state — 1 call, no DOM parsing
window.__AGENT__.state.get('counter.value') // → 0
window.__AGENT__.state.list() // → { 'counter.value': { type: 'number', ... }, ... }
// Invoke action — business logic runs untouched
await window.__AGENT__.actions.invoke('counter.increment')
// → { ok: true, data: null }
// Subscribe to changes
window.__AGENT__.state.subscribe('counter.value', console.log)- Read state with
state.get('path.to.key')or snapshot withstate.get() - Invoke actions via
actions.invoke,actions.mutate,actions.dispatch, oractions.send - Discover available state and actions with
state.list()andactions.list() - Subscribe to state changes with
state.subscribe('key', cb) - Schema-described — JSON Schema descriptors tell agents the shape and valid values
- Disabled by default in production — explicit opt-in required
- Token auth — optional bearer token for action invocations in production
- Allowlist — restrict which state keys and actions are exposed
- No mutation from the agent surface — state reads only; all mutations go through registered actions
useAgentState— drop-in replacement foruseStatewith registry keyuseAgentAction— register invocable actions (TanStack QueryuseMutation-aligned)useAgentReducer— register reducer-managed stateagentAtom+useAgentAtom— Jotai-aligned atomic statecreateAgentStore— Zustand-aligned module-level storeuseAgentStateValue/useSetAgentState— read/write split hooks
npm install react-agent-bridgePeer dependencies: react >= 18, react-dom >= 18.
import { AgentBridgeProvider, useAgentState, useAgentAction } from 'react-agent-bridge';
// 1. Wrap your app
<AgentBridgeProvider appId="my-app">
<YourApp />
</AgentBridgeProvider>
// 2. Replace useState with useAgentState
const [form, setForm] = useAgentState('checkout.form', { email: '' }, {
description: 'Checkout form data',
});
// 3. Register actions
const { mutate: submit } = useAgentAction('checkout.submit', async (input) => {
// your business logic
return api.submit(input);
});| Method | Description |
|---|---|
state.get() |
Full state snapshot |
state.get('path.key') |
Single key value |
state.getState('path.key') |
Alias for get |
state.list() |
All state descriptors |
state.subscribe('key', cb) |
Listen to changes |
| Method | Description |
|---|---|
actions.invoke('name', input, opts?) |
Canonical invocation |
actions.mutate('name', input, opts?) |
TanStack Query alias |
actions.dispatch({ type, payload }, opts?) |
Redux alias |
actions.send({ type, ...payload }, opts?) |
XState alias |
actions.list() |
All action descriptors |
Without react-agent-bridge, an LLM agent wanting to know the current checkout state must:
- Fetch the DOM — thousands of characters of HTML
- Find the relevant element — parse selectors, navigate the tree
- Extract text content — innerText, value attributes
- Parse and interpret — convert strings to structured data
That's 2,000–10,000+ tokens for a single value.
With react-agent-bridge:
window.__AGENT__.state.get('checkout.form')
That's ~50 characters — roughly 10–20 tokens. A 99% reduction.
For action invocation, instead of:
- Find the submit button via selector
- Click it
- Wait for navigation
- Verify by scraping again
You do:
await window.__AGENT__.actions.invoke('checkout.submit', { email, items })
~80 characters — ~20 tokens. And you get a structured ActionResult back.
<AgentBridgeProvider
appId="my-app"
production={{
enabled: true, // enable in production
token: process.env.AGENT_BRIDGE_TOKEN, // optional bearer token
allowlist: {
states: ['checkout.form', 'auth.status'], // exposed state keys
actions: ['checkout.submit'], // invocable actions
},
}}
>See examples/ for runnable scenarios:
| Example | Demonstrates |
|---|---|
basic-usage.tsx |
useAgentState + useAgentAction counter |
checkout-form.tsx |
Form state with schema + action-driven submission |
auth-flow.tsx |
useAgentReducer state machine + login/logout actions |
store-usage.tsx |
createAgentStore for module-level shared state |
production-setup.tsx |
Token auth + allowlist in production |
micro-frontend.tsx |
Namespaced prefixes per sub-app |
MIT