2026 is the year AI stops explaining what to do and starts doing it.
Users don't want walkthroughs. They want outcomes.
"Add a high-priority task to review the Q1 budget."
"Mark all completed tasks as archived."
"Cancel my subscription."
Not a help article. Just... done.
Today we're open-sourcing InAppAI: React components for building AI agents that actually operate your application.
npm install @inappai/react
This post breaks down the three core concepts that make it work: Agents, Context, and Tools.
The Three Core Concepts
InAppAI is built around a simple mental model:
| Concept | What it does |
|---|---|
| Agent | The AI assistant configured for your app |
| Context | Real-time app state the agent can see |
| Tools | Actions the agent can execute |
Here's how they fit together:
User: "Add a task to buy groceries"
↓
┌─────────────┐
│ AGENT │ ← Understands intent
└─────────────┘
↓
┌─────────────┐
│ CONTEXT │ ← Sees current todos: [{id: 1, text: "Call mom"}]
└─────────────┘
↓
┌─────────────┐
│ TOOLS │ ← Calls addTodo({ text: "Buy groceries" })
└─────────────┘
↓
UI updates with new todo
Let's look at each one.
1. Agents
An Agent is a server-side AI configuration identified by an agentId. Each agent has its own:
- AI provider (OpenAI, Anthropic, or Google)
- Model settings (temperature, max tokens)
- Knowledge base configuration
- Rate limits and caching rules
Your React component connects to an agent like this:
import { InAppAI } from '@inappai/react';
function App() {
const [messages, setMessages] = useState<Message[]>([]);
return (
<InAppAI
agentId="your-agent-id"
messages={messages}
onMessagesChange={setMessages}
/>
);
}
The agentId is the only required prop. Everything else—provider selection, failover logic, rate limiting—is handled server-side. You can switch from GPT-4 to Claude without changing your frontend code.
Why server-side?
- API keys never touch the client
- Change models without redeploying
- Add rate limits per user/tenant
- Enable knowledge base RAG without client complexity
2. Context
Context is the bridge between your UI state and the agent's understanding. Without context, the agent is blind to what's happening in your app.
Pass context as an object or a function that returns fresh state:
<InAppAI
agentId="your-agent-id"
messages={messages}
onMessagesChange={setMessages}
// Static context
context={{ userId: "123", plan: "pro" }}
// Or dynamic context (recommended)
context={() => ({
todos: todos.map(t => ({
id: t.id,
text: t.text,
completed: t.completed,
priority: t.priority,
})),
stats: {
total: todos.length,
active: todos.filter(t => !t.completed).length,
completed: todos.filter(t => t.completed).length,
},
})}
/>
Why use a function?
When the agent executes a tool (like adding a todo), the component automatically calls your context function again to get fresh state. This lets the agent see the results of its actions:
1. User: "Add a task to buy milk"
2. Agent calls addTodo tool
3. Tool handler: setTodos([...todos, newTodo])
4. Component calls context() again ← Fresh state
5. Agent sees updated todo list
6. Agent responds: "Done! I've added 'Buy milk' to your list."
Without fresh context, the agent would still see the old state and couldn't confirm what changed.
What to include in context
Include anything the agent needs to make decisions:
context={() => ({
// Current page/view
currentView: 'dashboard',
// User info
user: {
name: user.name,
role: user.role,
permissions: user.permissions,
},
// Relevant data
selectedItems: selectedIds,
filters: activeFilters,
// Application state
isLoading: false,
lastError: null,
})}
Keep it lean. Don't dump your entire Redux store—just what's relevant for the agent's decisions.
3. Tools
Tools are functions the agent can call to take action in your app. Each tool has:
-
name: Identifier the agent uses to call it -
description: Helps the agent decide when to use it -
parameters: JSON Schema defining expected inputs -
handler: The function that actually runs (client-side)
Here's a complete example:
import { Tool } from '@inappai/react';
const todoTools: Tool[] = [
{
name: 'addTodo',
description: 'Create a new todo item when the user wants to add a task',
parameters: {
type: 'object',
properties: {
text: {
type: 'string',
description: 'The task description',
},
priority: {
type: 'string',
enum: ['low', 'medium', 'high'],
description: 'Task priority level. Default to medium if not specified.',
},
},
required: ['text'],
},
handler: async ({ text, priority = 'medium' }) => {
const newTodo = {
id: crypto.randomUUID(),
text,
completed: false,
priority,
};
setTodos(prev => [...prev, newTodo]);
return { success: true, todo: newTodo };
},
},
{
name: 'completeTodo',
description: 'Mark a todo as completed by its ID or by matching text',
parameters: {
type: 'object',
properties: {
identifier: {
type: 'string',
description: 'The todo ID or a keyword from the task text',
},
},
required: ['identifier'],
},
handler: async ({ identifier }) => {
const todo = todos.find(
t => t.id === identifier ||
t.text.toLowerCase().includes(identifier.toLowerCase())
);
if (!todo) {
return { success: false, error: 'Todo not found' };
}
setTodos(prev =>
prev.map(t => t.id === todo.id ? { ...t, completed: true } : t)
);
return { success: true, completed: todo.text };
},
},
{
name: 'deleteTodo',
description: 'Remove a todo from the list permanently',
parameters: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'The todo ID to delete',
},
},
required: ['id'],
},
handler: async ({ id }) => {
const todo = todos.find(t => t.id === id);
if (!todo) {
return { success: false, error: 'Todo not found' };
}
setTodos(prev => prev.filter(t => t.id !== id));
return { success: true, deleted: todo.text };
},
},
];
How tool execution works
- User sends message → "Add a high-priority task to review the budget"
-
Agent analyzes intent → Decides to call
addTodo -
Backend returns tool call →
{ name: "addTodo", arguments: { text: "Review the budget", priority: "high" } } - Handler executes → Your React state updates
-
Results sent back → Agent sees
{ success: true, todo: {...} } - Agent responds → "I've added 'Review the budget' as a high-priority task."
The handler runs in your React component, so it has full access to state setters, hooks, and any other client-side logic.
Tool design tips
Be specific with descriptions. The agent uses descriptions to decide which tool to call:
// ❌ Vague
description: 'Handles todos'
// ✅ Specific
description: 'Create a new todo item when the user wants to add a task, reminder, or action item to their list'
Return useful information. The agent uses return values to formulate its response:
// ❌ Minimal
return { success: true };
// ✅ Informative
return {
success: true,
todo: newTodo,
message: `Created "${newTodo.text}" with ${newTodo.priority} priority`
};
Handle errors gracefully. Return error info instead of throwing:
handler: async ({ id }) => {
const item = items.find(i => i.id === id);
if (!item) {
return { success: false, error: `Item with ID ${id} not found` };
}
// ... proceed
}
Putting It All Together
Here's a complete example combining all three concepts:
import { useState } from 'react';
import { InAppAI, Tool, Message } from '@inappai/react';
interface Todo {
id: string;
text: string;
completed: boolean;
priority: 'low' | 'medium' | 'high';
}
export function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([]);
const [messages, setMessages] = useState<Message[]>([]);
// Define tools with access to state
const tools: Tool[] = [
{
name: 'addTodo',
description: 'Create a new todo item',
parameters: {
type: 'object',
properties: {
text: { type: 'string', description: 'Task description' },
priority: {
type: 'string',
enum: ['low', 'medium', 'high'],
description: 'Priority level'
},
},
required: ['text'],
},
handler: async ({ text, priority = 'medium' }) => {
const newTodo: Todo = {
id: crypto.randomUUID(),
text,
completed: false,
priority,
};
setTodos(prev => [...prev, newTodo]);
return { success: true, todo: newTodo };
},
},
{
name: 'listTodos',
description: 'Get all todos, optionally filtered by status',
parameters: {
type: 'object',
properties: {
filter: {
type: 'string',
enum: ['all', 'active', 'completed'],
description: 'Filter by completion status',
},
},
},
handler: async ({ filter = 'all' }) => {
let filtered = todos;
if (filter === 'active') filtered = todos.filter(t => !t.completed);
if (filter === 'completed') filtered = todos.filter(t => t.completed);
return { todos: filtered, count: filtered.length };
},
},
// ... more tools
];
return (
<div className="app">
{/* Your existing UI */}
<TodoList todos={todos} />
{/* AI Agent */}
<InAppAI
agentId="your-agent-id"
messages={messages}
onMessagesChange={setMessages}
tools={tools}
context={() => ({
todos: todos.map(({ id, text, completed, priority }) => ({
id, text, completed, priority,
})),
stats: {
total: todos.length,
active: todos.filter(t => !t.completed).length,
completed: todos.filter(t => t.completed).length,
},
})}
displayMode="popup"
theme="light"
/>
</div>
);
}
Now users can say:
- "Add three tasks: buy milk, call dentist, and finish report"
- "What tasks do I have left?"
- "Mark the dentist task as done"
- "Delete all completed tasks"
The agent understands intent, sees your current state, and executes the right tools.
What's Next
This is just the foundation. InAppAI also supports:
- Knowledge Base RAG — Ground responses in your docs
- Multiple display modes — Popup, sidebar, inline, fullscreen
- Theme customization — 7 built-in themes or bring your own
- Tool Registry — Manage tools across components with namespaces
We'll cover these in future posts.
Get Started
Try the demo: react-demo.inappai.com
Read the docs: inappai.com/docs
Star on GitHub: github.com/inappai/react
InAppAI is MIT-licensed. Inspect the code, fork it, extend it, or contribute back.
Questions? Drop a comment below or open an issue on GitHub.
Top comments (0)