react-observer-agent 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 sudo-ezekiel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,250 @@
1
+ ![ShowCase](https://github.com/user-attachments/assets/84c38dc6-7f62-4c37-8d96-3b3eac489140)
2
+
3
+ # react-observer-agent
4
+
5
+ A React library that lets an LLM agent observe your app's state, understand what the user is doing, and execute pre-defined actions, all through a declarative `<AIAgentProvider>` and registered tools, with permission boundaries built in.
6
+
7
+ Docs and live examples: **[reactobserveragent.sudo-ezekiel.com](https://reactobserveragent.sudo-ezekiel.com)**
8
+
9
+ This is an experimental project by a solo developer. I am exploring whether an AI agent can be useful inside a live React app without dumping your whole state into a prompt or letting the model run arbitrary code. It works and it is tested, but it remains a research project rather than a product. See the [disclaimer](#disclaimer).
10
+
11
+ - Zero runtime dependencies (adapters use raw `fetch`, no SDKs)
12
+ - TypeScript, dual ESM/CJS builds with types included
13
+ - React >= 18 (peer dependency)
14
+ - Works with any state manager: Zustand, Redux, vanilla React state
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install react-observer-agent
20
+ ```
21
+
22
+ ## Quick start
23
+
24
+ ```tsx
25
+ import { AIAgentProvider, registerTool, openAIAdapter, useAgent } from 'react-observer-agent';
26
+ import { useStore } from './store';
27
+
28
+ // 1. Register tools: actions the agent is allowed to perform
29
+ const tools = [
30
+ registerTool('goToPage', (args: { path: string }) => navigate(args.path), {
31
+ description: 'Navigate to a page in the app',
32
+ parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
33
+ }),
34
+ registerTool('submitForm', () => handleSubmit(), {
35
+ description: 'Submit the current form',
36
+ confirm: true, // requires user approval before executing
37
+ }),
38
+ ];
39
+
40
+ // 2. Configure the model adapter (route through your backend in production)
41
+ const model = openAIAdapter({
42
+ baseURL: '/api/agent', // your backend proxy holds the real API key
43
+ });
44
+ // Or Claude, same interface: claudeAdapter({ baseURL: '/api/agent' })
45
+
46
+ // 3. Wrap your app with the provider
47
+ export default function App() {
48
+ return (
49
+ <AIAgentProvider
50
+ model={model}
51
+ state={() => {
52
+ const { user, cart } = useStore.getState();
53
+ return { user, cart };
54
+ }}
55
+ tools={tools}
56
+ permissions={{
57
+ canAccess: ['user', 'cart'],
58
+ canExecute: ['goToPage', 'submitForm'],
59
+ stateDescriptions: {
60
+ user: 'Current logged-in user profile',
61
+ cart: 'Shopping cart items and quantities',
62
+ },
63
+ }}
64
+ options={{
65
+ onConfirm: async (call) => window.confirm(`Allow "${call.toolName}"?`),
66
+ }}
67
+ >
68
+ <YourApp />
69
+ </AIAgentProvider>
70
+ );
71
+ }
72
+
73
+ // 4. Interact with the agent from any component
74
+ function ChatPanel() {
75
+ const { send, isProcessing, history } = useAgent();
76
+ // send("What's in my cart?") -> agent reads state, responds with text
77
+ // send("Go to settings") -> agent calls goToPage({ path: '/settings' })
78
+ // send(text, { signal }) -> pass an AbortSignal to cancel mid-flight
79
+ }
80
+ ```
81
+
82
+ The `state` prop takes either a plain object or a getter function:
83
+
84
+ ```tsx
85
+ // Vanilla React state: pass an object, re-renders keep it fresh
86
+ <AIAgentProvider state={{ user, cart }} ... >
87
+
88
+ // External stores (Zustand, Redux): pass a getter
89
+ <AIAgentProvider state={() => useStore.getState()} ... >
90
+ ```
91
+
92
+ One gotcha worth knowing: the getter runs outside React rendering, inside the async agent loop, so it must not call hooks. `state={() => useStore.getState()}` is correct; `state={useStore}` passes the hook itself and throws an invalid hook call the first time the agent reads state.
93
+
94
+ ## The core idea: pull-based state
95
+
96
+ State values are never sent to the model upfront. The model receives a manifest, key names plus descriptions, in the system prompt, and pulls specific values on demand through an internal `__readState` tool:
97
+
98
+ ```
99
+ User: "What's in my cart?"
100
+
101
+ System prompt lists: user, cart, products (with descriptions)
102
+ Agent calls: __readState({ keys: ["cart"] })
103
+ Tool returns: { "cart": [{ "product": "Headphones", "qty": 1 }] }
104
+ Agent answers: "You have Wireless Headphones in your cart."
105
+ ```
106
+
107
+ Two things fall out of this:
108
+
109
+ - **Token cost scales with what the agent actually reads**, not with the size of your state tree.
110
+ - **Unread state never leaves the client.** A key the agent does not ask for is never serialized into a request.
111
+
112
+ `__readState` is invisible to you as a consumer. It never appears in `AgentResponse.toolCalls`, the `onToolCall` callback, or `history`. The rationale and a worked example are in [docs/internals.md](docs/internals.md).
113
+
114
+ ## Security model
115
+
116
+ The library treats the LLM as an untrusted planner inside a capability sandbox.
117
+
118
+ **Allowlists.** `canAccess` (state keys) and `canExecute` (tool names) are whitelists. Anything unlisted does not exist from the agent's point of view.
119
+
120
+ **Two enforcement layers.** Permissions are checked before and after the model call:
121
+
122
+ 1. *Visibility*: the model never sees unlisted keys or tools, so it cannot request what it cannot see.
123
+ 2. *Execution*: names are re-validated after the model responds. A hallucinated or injected tool name is rejected with status `denied`, and `__readState` requests are re-filtered against `canAccess`.
124
+
125
+ **Argument validation.** Tool arguments are checked against the tool's `parameters` JSON Schema before the handler runs, and before the confirmation prompt, so nobody is asked to approve a malformed call. Validation covers a deliberate subset (`type`, `properties`, `required`, `items`, `enum`) and ignores keywords outside it, so a richer schema validates on the parts the library understands instead of failing outright. Handlers should still treat args as untrusted, since unvalidated keywords pass through.
126
+
127
+ **Human confirmation.** Tools registered with `confirm: true` route through your `onConfirm` handler before running. You own the UI: modal, toast, `window.confirm`, anything that resolves a boolean. If no handler is provided, the tool is skipped with status `cancelled`. Confirmation is never silently bypassed. Use it for anything irreversible or user-visible.
128
+
129
+ **Prompt injection.** State often contains user-generated content (reviews, messages, profile fields). Once serialized into the conversation, that content can attempt prompt injection. The permission and confirmation layers are the backstop: an injected instruction can at worst invoke allowlisted tools, and confirmed tools still require a human yes.
130
+
131
+ **API keys.** Passing `apiKey` to an adapter ships the key to the browser, visible in DevTools. That is for local development only. In production, route through your own backend with `baseURL` plus `headers`:
132
+
133
+ ```ts
134
+ const model = openAIAdapter({
135
+ baseURL: '/api/agent',
136
+ headers: { Authorization: `Bearer ${sessionToken}` },
137
+ });
138
+ ```
139
+
140
+ The backend holds the real key, applies auth and rate limits, and forwards to the LLM provider.
141
+
142
+ ## Adapters
143
+
144
+ | Adapter | Status | Defaults |
145
+ |---------|--------|----------|
146
+ | `openAIAdapter` | Built in | OpenAI chat completions; model `gpt-4o`, temperature `0.2` |
147
+ | `claudeAdapter` | Built in | Anthropic Messages API; model `claude-opus-5`, `maxTokens` `16000` |
148
+ | `ollamaAdapter` | Planned | Local models via Ollama |
149
+ | Custom | Supported | Implement `ModelAdapter` and pass it to the provider |
150
+
151
+ Both built-in adapters are raw `fetch`, no SDK dependency. Both require either `apiKey` or `baseURL` and throw at construction with neither. `claudeAdapter` sends no sampling parameters, since current Claude models reject them.
152
+
153
+ ## How `send()` behaves
154
+
155
+ Each `send()` runs a turn loop of at most `options.maxTurns` model round trips (default 5). A few behaviors worth knowing:
156
+
157
+ - **Conversation memory.** The prior LLM transcript is replayed with tool calls and their results intact across `send()` calls, so the agent remembers what it already did. `clearHistory()` resets it.
158
+ - **Cancellation.** `send(message, { signal })` takes an `AbortSignal`. Aborts resolve with `error.code: 'ABORTED'` rather than throwing, and deliberately do not fire `onError`, since a cancel is a caller decision, not a failure.
159
+ - **Turn budget.** When `maxTurns` runs out while the model is still calling tools, `send()` resolves with `error.code: 'MAX_TURNS'` and whatever tool calls accumulated.
160
+ - **Token usage.** `AgentResponse.usage` totals prompt and completion tokens across every model call in the interaction, when the adapter reports them.
161
+
162
+ ## API reference
163
+
164
+ Everything the package exports:
165
+
166
+ | Export | What it is |
167
+ |--------|------------|
168
+ | `AIAgentProvider` | Context provider wiring model, state, tools, and permissions together |
169
+ | `useAgent()` | Hook to interact with the agent from anywhere in the provider tree |
170
+ | `registerTool(name, handler, options?)` | Creates a validated tool definition |
171
+ | `openAIAdapter(config)` | OpenAI chat completions adapter |
172
+ | `claudeAdapter(config)` | Anthropic Messages API adapter |
173
+ | `validateToolNames`, `filterState`, `filterTools`, `validateToolCall` | Building blocks for testing and custom wiring; typical apps never call these |
174
+ | Types | `ModelAdapter`, `AgentResponse`, `ToolDefinition`, and the rest of `src/types.ts` |
175
+
176
+ On `registerTool`: a tool needs a `description` to be shown to the model, and omitting `parameters` substitutes the empty object schema. Names beginning with `__` are reserved for internal tools (`__readState`) and rejected on mount, as are duplicate names.
177
+
178
+ ### `<AIAgentProvider>` props
179
+
180
+ | Prop | Type | Notes |
181
+ |------|------|-------|
182
+ | `model` | `ModelAdapter` | Required |
183
+ | `state` | `object \| (() => object)` | Object for React state, getter for external stores |
184
+ | `tools` | `AnyToolDefinition[]` | From `registerTool`; names must be unique, checked on mount |
185
+ | `permissions` | `PermissionsConfig` | Required, see below |
186
+ | `options` | `AgentOptions` | Optional, see below |
187
+ | `children` | `React.ReactNode` | |
188
+
189
+ ### `PermissionsConfig`
190
+
191
+ | Field | Type | Notes |
192
+ |-------|------|-------|
193
+ | `canAccess` | `string[]` | State keys the agent may read |
194
+ | `canExecute` | `string[]` | Tool names the agent may invoke |
195
+ | `stateDescriptions` | `Record<string, string>` | Optional per-key descriptions for the manifest; missing entries fall back to the key name |
196
+
197
+ ### `AgentOptions`
198
+
199
+ | Field | Type | Notes |
200
+ |-------|------|-------|
201
+ | `debug` | `boolean` | Verbose console logging, prefixed `[react-observer-agent]` (default `false`) |
202
+ | `maxTurns` | `number` | Max LLM round trips per `send()` (default `5`) |
203
+ | `systemPrompt` | `string` | Prepended to the generated state manifest prompt |
204
+ | `onError` | `(error: AgentError) => void` | Called when an interaction fails (except `ABORTED`) |
205
+ | `onToolCall` | `(call: ToolCallEvent) => void` | Observer for every user-tool outcome |
206
+ | `onConfirm` | `(call: PendingToolCall) => Promise<boolean>` | Approval handler for `confirm: true` tools |
207
+
208
+ ### `useAgent()` returns
209
+
210
+ | Field | Type | Notes |
211
+ |-------|------|-------|
212
+ | `send` | `(message, options?) => Promise<AgentResponse>` | `options.signal` cancels; resolves rather than rejects on errors |
213
+ | `isProcessing` | `boolean` | True while an interaction is in flight |
214
+ | `history` | `ConversationEntry[]` | User-facing conversation history for this provider instance |
215
+ | `clearHistory` | `() => void` | Resets history, the LLM transcript, and `lastResponse` |
216
+ | `lastResponse` | `AgentResponse \| null` | Most recent response, including error responses |
217
+
218
+ Tool call statuses in `AgentResponse.toolCalls` and `onToolCall`: `success`, `confirmed`, `cancelled`, `denied`, `error`.
219
+
220
+ The full contracts, including the `ModelAdapter` interface for writing custom adapters, are in [SPEC.md](SPEC.md).
221
+
222
+ ## What's next
223
+
224
+ In rough priority order:
225
+
226
+ 1. `ollamaAdapter` for local models
227
+ 2. Streaming responses
228
+ 3. Deeper argument validation
229
+ 4. Transcript compaction, so long sessions stay under the context window
230
+ 5. Per-tool permission scoping
231
+
232
+ Explicit non-goals for now: DOM awareness and page context mapping, automatic state detection, multi-agent orchestration, persistent memory, built-in rate limiting.
233
+
234
+ ## Docs and examples
235
+
236
+ - [reactobserveragent.sudo-ezekiel.com](https://reactobserveragent.sudo-ezekiel.com): guides, API reference, and live examples you can click through
237
+ - [sudo-ezekiel/react-observer-agent-examples](https://github.com/sudo-ezekiel/react-observer-agent-examples): the source for that site, including a runnable Zustand shopping app that proxies both providers
238
+ - [SPEC.md](SPEC.md): the full technical spec
239
+ - [docs/internals.md](docs/internals.md): pull-based state rationale and execution loop detail
240
+ - [CHANGELOG.md](CHANGELOG.md)
241
+
242
+ ## Disclaimer
243
+
244
+ This is a solo experiment. It is **not production-ready**. It may change, break, or stop at any time.
245
+
246
+ If you are curious about intelligent UIs, you are welcome to explore it, fork it, or reach out. Feedback is appreciated.
247
+
248
+ ## License
249
+
250
+ MIT