@agent-native/core 0.121.1 → 0.121.2
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/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/docs/content/getting-started.mdx +477 -115
- package/corpus/core/package.json +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/docs/content/getting-started.mdx +477 -115
- package/package.json +1 -1
|
@@ -10,13 +10,18 @@ share the same [actions](/docs/actions), SQL data, and application state. Start
|
|
|
10
10
|
with chat so users can talk to the agent immediately, then add the app surfaces
|
|
11
11
|
your workflow earns.
|
|
12
12
|
|
|
13
|
+
The quickest way in is the **Chat template**: a minimal app that gives you a
|
|
14
|
+
working AI chat interface, durable threads, auth, and an `actions/` directory
|
|
15
|
+
ready to extend. It's the foundation most Agent-Native apps grow from.
|
|
16
|
+
|
|
13
17
|
The first useful path is:
|
|
14
18
|
|
|
15
19
|
1. Create a chat app.
|
|
16
|
-
2.
|
|
17
|
-
3.
|
|
18
|
-
4.
|
|
19
|
-
5.
|
|
20
|
+
2. Connect an AI engine so the agent can respond.
|
|
21
|
+
3. Add one action.
|
|
22
|
+
4. Render the action result inline in chat.
|
|
23
|
+
5. Persist data in SQL.
|
|
24
|
+
6. Add a page the agent can open when visual inspection is better than another
|
|
20
25
|
paragraph in the transcript.
|
|
21
26
|
|
|
22
27
|
Want a complete domain app instead? Clone a rich template such as
|
|
@@ -29,7 +34,7 @@ Want a complete domain app instead? Clone a rich template such as
|
|
|
29
34
|
|
|
30
35
|
You'll need [Node.js 22+](https://nodejs.org) and [pnpm](https://pnpm.io).
|
|
31
36
|
|
|
32
|
-
|
|
37
|
+
Open a terminal, go to the directory where you want your Agent Native app, and run:
|
|
33
38
|
|
|
34
39
|
```bash
|
|
35
40
|
npx @agent-native/core@latest create my-app --template chat
|
|
@@ -42,103 +47,212 @@ This gives you durable chat threads, auth, live sync, an `actions/` directory,
|
|
|
42
47
|
standard `view-screen` and `navigate` actions, and a small React app you can
|
|
43
48
|
extend.
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
The dev server starts and opens `http://localhost:8080` in your browser. You may
|
|
51
|
+
notice a few startup log lines like `NitroViteError: Vite environment "nitro" is
|
|
52
|
+
unavailable`. These are a normal race condition during the initial boot and
|
|
53
|
+
resolve on their own. The app is ready once **VITE ready** displays in the
|
|
54
|
+
terminal output.
|
|
55
|
+
|
|
56
|
+
When the app starts, you might find yourself on the login screen rather than in the app. In this case, just sign up with a made up login to satisfy the login screen.
|
|
57
|
+
For local development, no email verification is actually required.
|
|
58
|
+
|
|
59
|
+
### If you want a different type of app
|
|
60
|
+
|
|
61
|
+
This guide uses the Chat template because it is the shortest path
|
|
62
|
+
to an agentic application: the agent can act on day one, and you have a real app
|
|
63
|
+
surface to grow from. However, if you want to create a different kind of app, run `create` with no flags for the CLI picker for domain templates and advanced app shapes:
|
|
47
64
|
|
|
48
65
|
```bash
|
|
49
66
|
npx @agent-native/core@latest create my-app
|
|
50
67
|
```
|
|
51
68
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
69
|
+
## 2. Connect an AI engine {#connect-ai}
|
|
70
|
+
|
|
71
|
+
The agent chat can't respond until you connect an AI engine. After you're logged in, click the **Connect AI** button.
|
|
72
|
+
|
|
73
|
+
You have two options:
|
|
74
|
+
|
|
75
|
+
**Option A: Connect Builder.** Click **Connect Builder.io**, choose the Builder Space, and click the **Authorize** button. You don't have to manually provide any keys.
|
|
76
|
+
|
|
77
|
+
**Option B: Add your own keys.** Enter your Anthropic or OpenAI API key. You can get an
|
|
78
|
+
Anthropic key at [console.anthropic.com](https://console.anthropic.com).
|
|
79
|
+
|
|
80
|
+
Alternatively, create a `.env` file in the `my-app/` directory (the same
|
|
81
|
+
directory that contains `package.json`) before running `pnpm dev`:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
echo "ANTHROPIC_API_KEY=sk-ant-..." >> .env
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Restart the dev server. Once an AI engine is connected, the Setup panel
|
|
88
|
+
hides itself and the agent is ready to chat.
|
|
89
|
+
|
|
90
|
+
> **Blank screen?** Create a `.env` file in your `my-app/` directory with
|
|
91
|
+
> `ANTHROPIC_API_KEY=sk-ant-...`, then restart `pnpm dev`. The in-app Setup
|
|
92
|
+
> panel only appears once the app has loaded, so a missing key that prevents
|
|
93
|
+
> the app from rendering needs to be fixed via the environment variable rather than
|
|
94
|
+
> the UI.
|
|
95
|
+
|
|
96
|
+
## 3. Add an action {#add-an-action}
|
|
55
97
|
|
|
56
|
-
|
|
98
|
+
An action is a typed operation that both your agent and your UI can call. It's
|
|
99
|
+
how the agent does things in your app. Actions live in the `actions/` directory
|
|
100
|
+
and can be triggered from chat, from React components, from the CLI, or on a
|
|
101
|
+
schedule. You define them once and call them from anywhere.
|
|
57
102
|
|
|
58
|
-
|
|
59
|
-
starter `hello` action with the first real operation in your domain. This example
|
|
60
|
-
analyzes form responses and returns a validated shape for a custom chat chart:
|
|
103
|
+
### Try the starter action
|
|
61
104
|
|
|
62
|
-
|
|
105
|
+
The Chat template includes a `hello` action at `actions/hello.ts`:
|
|
106
|
+
|
|
107
|
+
```ts filename="actions/hello.ts"
|
|
63
108
|
import { defineAction } from "@agent-native/core/action";
|
|
64
109
|
import { z } from "zod";
|
|
65
110
|
|
|
66
|
-
|
|
111
|
+
export default defineAction({
|
|
112
|
+
description: "Return a friendly greeting.",
|
|
113
|
+
schema: z.object({
|
|
114
|
+
name: z.string().default("world").describe("Name to greet"),
|
|
115
|
+
}),
|
|
116
|
+
http: { method: "GET" },
|
|
117
|
+
run: async ({ name }) => {
|
|
118
|
+
return { message: `Hello, ${name}!` };
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Run it from the terminal (inside your `my-app/` directory):
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
pnpm action hello --name Alice
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Or open your app at `http://localhost:8080` and ask the agent in the chat there:
|
|
130
|
+
|
|
131
|
+
> Use the hello action with the name Alice.
|
|
132
|
+
|
|
133
|
+
### Add your own action
|
|
134
|
+
|
|
135
|
+
Replace the starter action with the first real operation in your domain. This example
|
|
136
|
+
counts words, sentences, and paragraphs in any text you pass it. It computes
|
|
137
|
+
everything locally, so there's nothing to configure and no external service to connect.
|
|
138
|
+
|
|
139
|
+
Create a new file called `analyze-text.ts` in your `actions/` directory:
|
|
140
|
+
|
|
141
|
+
```ts filename="actions/analyze-text.ts"
|
|
142
|
+
import { defineAction } from "@agent-native/core/action";
|
|
143
|
+
import { z } from "zod";
|
|
144
|
+
|
|
145
|
+
const textStatsSchema = z.object({
|
|
67
146
|
title: z.string(),
|
|
68
|
-
points: z.array(z.object({
|
|
147
|
+
points: z.array(z.object({ label: z.string(), value: z.number() })),
|
|
69
148
|
});
|
|
70
149
|
|
|
71
150
|
export default defineAction({
|
|
72
|
-
description: "
|
|
151
|
+
description: "Count words, sentences, and paragraphs in a block of text.",
|
|
73
152
|
schema: z.object({
|
|
74
|
-
|
|
153
|
+
text: z
|
|
154
|
+
.string()
|
|
155
|
+
.default(
|
|
156
|
+
"The quick brown fox jumps over the lazy dog. Pack my box with five dozen liquor jugs.",
|
|
157
|
+
),
|
|
75
158
|
}),
|
|
76
|
-
outputSchema:
|
|
159
|
+
outputSchema: textStatsSchema,
|
|
77
160
|
chatUI: {
|
|
78
|
-
renderer: "
|
|
79
|
-
title: "
|
|
161
|
+
renderer: "text.stats-chart",
|
|
162
|
+
title: "Text stats",
|
|
80
163
|
},
|
|
81
164
|
readOnly: true,
|
|
82
|
-
run: async ({
|
|
83
|
-
title:
|
|
165
|
+
run: async ({ text }) => ({
|
|
166
|
+
title: "Text statistics",
|
|
84
167
|
points: [
|
|
85
|
-
{
|
|
86
|
-
{
|
|
87
|
-
{
|
|
88
|
-
|
|
168
|
+
{ label: "Characters", value: text.length },
|
|
169
|
+
{ label: "Words", value: text.split(/\s+/).filter(Boolean).length },
|
|
170
|
+
{
|
|
171
|
+
label: "Sentences",
|
|
172
|
+
value: text.split(/[.!?]+/).filter(Boolean).length,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
label: "Paragraphs",
|
|
176
|
+
value: text.split(/\n\n+/).filter(Boolean).length,
|
|
177
|
+
},
|
|
89
178
|
],
|
|
90
179
|
}),
|
|
91
180
|
});
|
|
92
181
|
```
|
|
93
182
|
|
|
94
|
-
Try it
|
|
183
|
+
Try it from the terminal:
|
|
95
184
|
|
|
96
185
|
```bash
|
|
97
|
-
pnpm action analyze-
|
|
186
|
+
pnpm action analyze-text --text "Hello world. How are you today?"
|
|
98
187
|
```
|
|
99
188
|
|
|
100
|
-
|
|
189
|
+
Or open your app at `http://localhost:8080` and ask the agent in the chat there:
|
|
190
|
+
|
|
191
|
+
> Run the analyze-text action on "Hello world. How are you today?"
|
|
192
|
+
|
|
193
|
+
#### Define once, call from anywhere
|
|
194
|
+
|
|
195
|
+
This action is now reachable from chat, React hooks, CLI, HTTP, MCP, A2A,
|
|
196
|
+
scheduled jobs, and webhooks.
|
|
197
|
+
|
|
198
|
+
TIP: Any time you want the agent to call a specific action without ambiguity, phrasing it as "Run the `<action-name>` action" is most reliable. Natural-language prompts work well once the agent has enough context about your app's domain. For a brand-new app with no data or context yet, explicit is safer.
|
|
101
199
|
|
|
102
|
-
|
|
200
|
+
## 4. Render the result inline {#render-inline}
|
|
103
201
|
|
|
104
|
-
|
|
105
|
-
|
|
202
|
+
When the agent runs `analyze-text`, it returns structured data: a title and an
|
|
203
|
+
array of counts. By default the agent will describe that data in prose: "The
|
|
204
|
+
text has 9 words, 2 sentences..." and so on. That works, but you can
|
|
205
|
+
also render the result as a real UI component (a bar chart, a table, a card)
|
|
206
|
+
directly inside the chat transcript, right where the agent responded.
|
|
106
207
|
|
|
107
|
-
|
|
208
|
+
This is what `chatUI.renderer` in the action does. It's a label that says "when
|
|
209
|
+
this action's result appears in chat, hand it to this React component instead of
|
|
210
|
+
summarizing it in text." The component receives the validated action output as
|
|
211
|
+
props and renders whatever you want.
|
|
108
212
|
|
|
109
|
-
|
|
110
|
-
`
|
|
111
|
-
into prose. Register a React component for that exact renderer id, for example
|
|
112
|
-
by importing `app/chat-renderers.tsx` once from `app/root.tsx`:
|
|
213
|
+
In the next step, you'll create `app/chat-renderers.tsx`, but first, add one import line
|
|
214
|
+
to `app/root.tsx` so it runs on startup:
|
|
113
215
|
|
|
114
|
-
```tsx
|
|
216
|
+
```ts filename="app/root.tsx"
|
|
217
|
+
import "./chat-renderers";
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Add it alongside your other imports at the top of the file. That's the only
|
|
221
|
+
change to `root.tsx`. The import just ensures the file runs and registers the
|
|
222
|
+
renderer. Now create the renderer file:
|
|
223
|
+
|
|
224
|
+
```tsx filename="app/chat-renderers.tsx"
|
|
115
225
|
import {
|
|
116
226
|
registerActionChatRenderer,
|
|
117
227
|
type ToolRendererProps,
|
|
118
228
|
} from "@agent-native/core/client/chat";
|
|
119
229
|
|
|
120
|
-
type
|
|
230
|
+
type TextStatsResult = {
|
|
121
231
|
title: string;
|
|
122
|
-
points: Array<{
|
|
232
|
+
points: Array<{ label: string; value: number }>;
|
|
123
233
|
};
|
|
124
234
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
235
|
+
const MAX_BAR_PX = 80;
|
|
236
|
+
|
|
237
|
+
function TextStatsChart({ context }: ToolRendererProps) {
|
|
238
|
+
const result = context.resultJson as TextStatsResult;
|
|
239
|
+
const max = Math.max(...result.points.map((point) => point.value), 1);
|
|
128
240
|
return (
|
|
129
241
|
<section className="rounded-lg border bg-card p-4">
|
|
130
242
|
<h3 className="text-sm font-medium">{result.title}</h3>
|
|
131
|
-
<div className="mt-4 flex
|
|
243
|
+
<div className="mt-4 flex items-end gap-2">
|
|
132
244
|
{result.points.map((point) => (
|
|
133
245
|
<div
|
|
134
|
-
key={point.
|
|
246
|
+
key={point.label}
|
|
135
247
|
className="flex flex-1 flex-col items-center gap-2"
|
|
136
248
|
>
|
|
137
249
|
<div
|
|
138
|
-
className="w-full rounded-t bg-
|
|
139
|
-
style={{
|
|
250
|
+
className="w-full rounded-t bg-blue-500"
|
|
251
|
+
style={{
|
|
252
|
+
height: `${Math.max(Math.round((point.value / max) * MAX_BAR_PX), 2)}px`,
|
|
253
|
+
}}
|
|
140
254
|
/>
|
|
141
|
-
<span className="text-xs text-muted-foreground">{point.
|
|
255
|
+
<span className="text-xs text-muted-foreground">{point.label}</span>
|
|
142
256
|
</div>
|
|
143
257
|
))}
|
|
144
258
|
</div>
|
|
@@ -147,20 +261,21 @@ function ResponseChart({ context }: ToolRendererProps) {
|
|
|
147
261
|
}
|
|
148
262
|
|
|
149
263
|
registerActionChatRenderer({
|
|
150
|
-
id: "
|
|
151
|
-
renderer: "
|
|
152
|
-
Component:
|
|
264
|
+
id: "text.stats-chart",
|
|
265
|
+
renderer: "text.stats-chart",
|
|
266
|
+
Component: TextStatsChart,
|
|
153
267
|
});
|
|
154
268
|
```
|
|
155
269
|
|
|
156
|
-
|
|
157
|
-
|
|
270
|
+
Once the renderer is registered, the agent's response looks like this. Instead
|
|
271
|
+
of a paragraph of text, your React component renders directly inside the chat
|
|
272
|
+
transcript:
|
|
158
273
|
|
|
159
274
|
<WireframeBlock id="doc-block-inline-result-wireframe">
|
|
160
275
|
<Screen
|
|
161
276
|
surface="desktop"
|
|
162
277
|
html={
|
|
163
|
-
"<div style='min-height:340px;box-sizing:border-box;padding:24px;display:flex;justify-content:center;align-items:center;background:var(--wf-bg)'><div style='width:min(
|
|
278
|
+
"<div style='min-height:340px;box-sizing:border-box;padding:24px;display:flex;justify-content:center;align-items:center;background:var(--wf-bg)'><div style='width:min(640px,100%);display:flex;flex-direction:column;gap:14px'><div class='wf-card' data-rough style='align-self:flex-end;max-width:70%;padding:12px 14px'><strong>User</strong><p style='margin:6px 0 0'>Run the analyze-text action on \"Hello world. How are you today?\"</p></div><div class='wf-card' data-rough style='align-self:flex-start;width:min(480px,100%);padding:14px'><strong>Agent</strong><p class='wf-muted' style='margin:6px 0 12px'>Rendered with text.stats-chart.</p><section class='wf-card' data-rough style='padding:14px'><h3 style='margin:0 0 12px;font-size:14px'>Text statistics</h3><div data-rough='line:bottom' style='height:104px;display:flex;align-items:end;gap:8px;border-bottom:1.4px solid var(--wf-line);padding-bottom:4px'><div style='flex:1;display:flex;flex-direction:column;align-items:center;gap:6px'><div data-rough style='height:80px;width:100%;background:color-mix(in srgb, var(--wf-accent) 36%, transparent);border:1.4px solid var(--wf-accent);border-radius:8px 8px 3px 3px'></div><span class='wf-muted'>Characters</span></div><div style='flex:1;display:flex;flex-direction:column;align-items:center;gap:6px'><div data-rough style='height:18px;width:100%;background:color-mix(in srgb, var(--wf-accent) 30%, transparent);border:1.4px solid var(--wf-accent);border-radius:8px 8px 3px 3px'></div><span class='wf-muted'>Words</span></div><div style='flex:1;display:flex;flex-direction:column;align-items:center;gap:6px'><div data-rough style='height:2px;width:100%;background:color-mix(in srgb, var(--wf-accent) 24%, transparent);border:1.4px solid var(--wf-accent);border-radius:8px 8px 3px 3px'></div><span class='wf-muted'>Sentences</span></div><div style='flex:1;display:flex;flex-direction:column;align-items:center;gap:6px'><div data-rough style='height:2px;width:100%;background:color-mix(in srgb, var(--wf-accent) 24%, transparent);border:1.4px solid var(--wf-accent);border-radius:8px 8px 3px 3px'></div><span class='wf-muted'>Paragraphs</span></div></div></section></div></div></div>"
|
|
164
279
|
}
|
|
165
280
|
/>
|
|
166
281
|
</WireframeBlock>
|
|
@@ -179,69 +294,279 @@ summary/chart/table cards. See [Native Chat UI](/docs/native-chat-ui). For
|
|
|
179
294
|
temporary controls the agent creates at runtime, see
|
|
180
295
|
[Generative UI](/docs/generative-ui).
|
|
181
296
|
|
|
182
|
-
##
|
|
297
|
+
## 5. Persist data in SQL {#persist-data}
|
|
298
|
+
|
|
299
|
+
Right now, every time the agent runs `analyze-text` the result appears in chat
|
|
300
|
+
and then disappears. There's nothing to look back at, nothing the agent can
|
|
301
|
+
reference later, and no way to build a page around the data. Persisting to SQL
|
|
302
|
+
fixes that: the agent writes results to a table, and both the agent and your UI
|
|
303
|
+
can read them back at any time.
|
|
304
|
+
|
|
305
|
+
Agent-Native apps have a SQL database available by default: SQLite locally,
|
|
306
|
+
and your configured provider (Postgres, Turso/libSQL, Cloudflare D1) in
|
|
307
|
+
production.
|
|
308
|
+
|
|
309
|
+
### Wire up the database plugin
|
|
310
|
+
|
|
311
|
+
The Chat template doesn't include a database plugin by default. Create
|
|
312
|
+
`server/plugins/db.ts` to initialize it. This is what runs migrations and
|
|
313
|
+
makes the database available to your actions:
|
|
314
|
+
|
|
315
|
+
```ts filename="server/plugins/db.ts"
|
|
316
|
+
import { runMigrations } from "@agent-native/core/db";
|
|
317
|
+
|
|
318
|
+
export default runMigrations(
|
|
319
|
+
[
|
|
320
|
+
{
|
|
321
|
+
version: 1,
|
|
322
|
+
sql: `CREATE TABLE IF NOT EXISTS text_analyses (
|
|
323
|
+
id TEXT PRIMARY KEY,
|
|
324
|
+
input TEXT NOT NULL,
|
|
325
|
+
char_count INTEGER NOT NULL,
|
|
326
|
+
word_count INTEGER NOT NULL,
|
|
327
|
+
sentence_count INTEGER NOT NULL,
|
|
328
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
329
|
+
)`,
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
{ table: "text_analyses_migrations" },
|
|
333
|
+
);
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Each entry in the array is an additive migration. When you add new columns or
|
|
337
|
+
tables later, append a new version object. Never edit existing ones.
|
|
183
338
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
339
|
+
### Define the schema
|
|
340
|
+
|
|
341
|
+
Create `server/db/schema.ts`. The `server/db/` directory may not exist yet,
|
|
342
|
+
so create it if needed. This file describes your tables using typed helpers so
|
|
343
|
+
your actions get full TypeScript autocomplete:
|
|
187
344
|
|
|
188
345
|
```ts filename="server/db/schema.ts"
|
|
189
346
|
import { integer, now, table, text } from "@agent-native/core/db/schema";
|
|
190
347
|
|
|
191
|
-
export const
|
|
348
|
+
export const textAnalyses = table("text_analyses", {
|
|
192
349
|
id: text("id").primaryKey(),
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
350
|
+
input: text("input").notNull(),
|
|
351
|
+
charCount: integer("char_count").notNull(),
|
|
352
|
+
wordCount: integer("word_count").notNull(),
|
|
353
|
+
sentenceCount: integer("sentence_count").notNull(),
|
|
197
354
|
createdAt: text("created_at").notNull().default(now()),
|
|
198
|
-
updatedAt: text("updated_at").notNull().default(now()),
|
|
199
355
|
});
|
|
200
356
|
```
|
|
201
357
|
|
|
202
|
-
Use the framework schema helpers
|
|
203
|
-
dialect-specific
|
|
204
|
-
same schema
|
|
205
|
-
|
|
358
|
+
Use the framework schema helpers (`table`, `text`, `integer`, `now`) rather than
|
|
359
|
+
`sqliteTable`, `pgTable`, or dialect-specific imports. They pick the configured
|
|
360
|
+
SQL backend automatically, so the same schema runs locally on SQLite and in
|
|
361
|
+
production on any supported provider.
|
|
362
|
+
|
|
363
|
+
After adding both files, restart the dev server so the migration runs:
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
pnpm dev
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Look for these two lines in the terminal output. They confirm the table was created:
|
|
370
|
+
|
|
371
|
+
```
|
|
372
|
+
[db] Applying 1 migration(s) on SQLite/libsql…
|
|
373
|
+
[db] Applied migration v1 (1 statement)
|
|
374
|
+
```
|
|
206
375
|
|
|
207
|
-
|
|
376
|
+
The `NitroViteError` lines, `BETTER_AUTH_SECRET` warning, and
|
|
377
|
+
`SECRETS_ENCRYPTION_KEY` warning that also appear are normal for local dev and
|
|
378
|
+
can be ignored.
|
|
208
379
|
|
|
209
|
-
|
|
210
|
-
- `list-response-insights` reads the rows for the page.
|
|
211
|
-
- `update-response-insight` edits an existing row after the agent learns more.
|
|
212
|
-
- `analyze-responses` can call the same internal helper and return the native
|
|
213
|
-
chat widget for the latest insight.
|
|
380
|
+
### Add actions for the table
|
|
214
381
|
|
|
215
|
-
|
|
216
|
-
|
|
382
|
+
Now create the action files that read and write the table. These go in your
|
|
383
|
+
`actions/` directory, the same place as `hello.ts` and `analyze-text.ts`. You
|
|
384
|
+
create them yourself, one file per operation. The agent and your UI will call
|
|
385
|
+
them the same way they call any other action.
|
|
217
386
|
|
|
218
|
-
|
|
387
|
+
**`actions/save-text-analysis.ts`** writes a result row to the database.
|
|
388
|
+
Call this after running `analyze-text` to make the result durable:
|
|
219
389
|
|
|
220
|
-
|
|
390
|
+
```ts filename="actions/save-text-analysis.ts"
|
|
391
|
+
import { defineAction } from "@agent-native/core/action";
|
|
392
|
+
import { getDbExec } from "@agent-native/core/db";
|
|
393
|
+
import { z } from "zod";
|
|
221
394
|
|
|
222
|
-
|
|
395
|
+
export default defineAction({
|
|
396
|
+
description: "Save a text analysis result to the database.",
|
|
397
|
+
schema: z.object({
|
|
398
|
+
input: z.string(),
|
|
399
|
+
charCount: z.number(),
|
|
400
|
+
wordCount: z.number(),
|
|
401
|
+
sentenceCount: z.number(),
|
|
402
|
+
}),
|
|
403
|
+
run: async ({ input, charCount, wordCount, sentenceCount }) => {
|
|
404
|
+
const id = crypto.randomUUID();
|
|
405
|
+
await getDbExec().execute({
|
|
406
|
+
sql: `INSERT INTO text_analyses (id, input, char_count, word_count, sentence_count)
|
|
407
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
408
|
+
args: [id, input, charCount, wordCount, sentenceCount],
|
|
409
|
+
});
|
|
410
|
+
return { id };
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
**`actions/list-text-analyses.ts`** reads all saved results. The agent can
|
|
416
|
+
call this to summarize past analyses, and your UI can use it to populate a page:
|
|
417
|
+
|
|
418
|
+
```ts filename="actions/list-text-analyses.ts"
|
|
419
|
+
import { defineAction } from "@agent-native/core/action";
|
|
420
|
+
import { getDbExec } from "@agent-native/core/db";
|
|
421
|
+
import { z } from "zod";
|
|
422
|
+
|
|
423
|
+
export default defineAction({
|
|
424
|
+
description: "List all saved text analyses, newest first.",
|
|
425
|
+
schema: z.object({}),
|
|
426
|
+
run: async () => {
|
|
427
|
+
const result = await getDbExec().execute(
|
|
428
|
+
`SELECT id, input, char_count, word_count, sentence_count, created_at
|
|
429
|
+
FROM text_analyses
|
|
430
|
+
ORDER BY created_at DESC`,
|
|
431
|
+
);
|
|
432
|
+
return result.rows;
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
**`actions/delete-text-analysis.ts`** removes a row by id:
|
|
438
|
+
|
|
439
|
+
```ts filename="actions/delete-text-analysis.ts"
|
|
440
|
+
import { defineAction } from "@agent-native/core/action";
|
|
441
|
+
import { getDbExec } from "@agent-native/core/db";
|
|
442
|
+
import { z } from "zod";
|
|
443
|
+
|
|
444
|
+
export default defineAction({
|
|
445
|
+
description: "Delete a saved text analysis by id.",
|
|
446
|
+
schema: z.object({ id: z.string() }),
|
|
447
|
+
run: async ({ id }) => {
|
|
448
|
+
await getDbExec().execute({
|
|
449
|
+
sql: `DELETE FROM text_analyses WHERE id = ?`,
|
|
450
|
+
args: [id],
|
|
451
|
+
});
|
|
452
|
+
return { deleted: id };
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Once these files are saved the dev server picks them up automatically. No
|
|
458
|
+
restart needed. Try listing analyses from the terminal:
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
pnpm action list-text-analyses
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
You should see an empty array. The table exists and the action works; there's
|
|
465
|
+
just nothing saved yet:
|
|
466
|
+
|
|
467
|
+
```
|
|
468
|
+
[]
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
Data is saved to `data/app.db`, a SQLite file in your project directory that
|
|
472
|
+
gets created automatically on first run. In production you'd point
|
|
473
|
+
`DATABASE_URL` at a hosted database instead, but locally this file is all you
|
|
474
|
+
need.
|
|
475
|
+
|
|
476
|
+
To save something, first run `analyze-text` to get the counts:
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
pnpm action analyze-text --text "Hello world"
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
You'll see output like:
|
|
483
|
+
|
|
484
|
+
```
|
|
485
|
+
{
|
|
486
|
+
title: 'Text statistics',
|
|
487
|
+
points: [
|
|
488
|
+
{ label: 'Characters', value: 11 },
|
|
489
|
+
{ label: 'Words', value: 2 },
|
|
490
|
+
{ label: 'Sentences', value: 1 },
|
|
491
|
+
{ label: 'Paragraphs', value: 1 }
|
|
492
|
+
]
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
Then pass those values to `save-text-analysis`:
|
|
497
|
+
|
|
498
|
+
```bash
|
|
499
|
+
pnpm action save-text-analysis \
|
|
500
|
+
--input "Hello world" \
|
|
501
|
+
--charCount 11 \
|
|
502
|
+
--wordCount 2 \
|
|
503
|
+
--sentenceCount 1
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
Now run `list-text-analyses` again and you'll see the saved row:
|
|
507
|
+
|
|
508
|
+
```bash
|
|
509
|
+
pnpm action list-text-analyses
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Or ask the agent in the chat at `http://localhost:8080` to do both steps at once:
|
|
513
|
+
|
|
514
|
+
> Run analyze-text on "Hello world", then save the result.
|
|
515
|
+
|
|
516
|
+
## 6. Add a page the agent can open {#add-a-page}
|
|
517
|
+
|
|
518
|
+
Chat is great for conversational interaction, but some data is better inspected
|
|
519
|
+
in a dedicated UI: a table you can scan, sort, or delete rows from. This step
|
|
520
|
+
adds a React route that displays everything saved in `text_analyses`, using the
|
|
521
|
+
same `list-text-analyses` and `delete-text-analysis` actions you already wrote.
|
|
522
|
+
There's no second data layer. The page is just a view over the same SQL state
|
|
523
|
+
the agent reads and writes.
|
|
524
|
+
|
|
525
|
+
Create the route file at `app/routes/text-analyses.tsx`. Route files in
|
|
526
|
+
`app/routes/` are automatically picked up by the framework. The filename
|
|
527
|
+
becomes the URL path, so this page will be available at
|
|
528
|
+
`http://localhost:8080/text-analyses`.
|
|
529
|
+
|
|
530
|
+
```tsx filename="app/routes/text-analyses.tsx"
|
|
223
531
|
import {
|
|
224
532
|
useActionMutation,
|
|
225
533
|
useActionQuery,
|
|
226
534
|
} from "@agent-native/core/client/hooks";
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
535
|
+
|
|
536
|
+
export default function TextAnalysesRoute() {
|
|
537
|
+
const analyses = useActionQuery("list-text-analyses", {});
|
|
538
|
+
const deleteAnalysis = useActionMutation("delete-text-analysis");
|
|
230
539
|
|
|
231
540
|
return (
|
|
232
|
-
<main className="mx-auto flex max-w-
|
|
541
|
+
<main className="mx-auto flex max-w-3xl flex-col gap-6 p-6">
|
|
233
542
|
<header>
|
|
234
|
-
<h1>
|
|
235
|
-
<p
|
|
543
|
+
<h1 className="text-2xl font-semibold">Text analyses</h1>
|
|
544
|
+
<p className="text-muted-foreground">
|
|
545
|
+
Results saved by the agent or triggered manually.
|
|
546
|
+
</p>
|
|
236
547
|
</header>
|
|
237
|
-
<
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
{
|
|
242
|
-
<article
|
|
243
|
-
|
|
244
|
-
|
|
548
|
+
<section className="flex flex-col gap-3">
|
|
549
|
+
{analyses.data?.length === 0 && (
|
|
550
|
+
<p className="text-muted-foreground">No analyses saved yet.</p>
|
|
551
|
+
)}
|
|
552
|
+
{analyses.data?.map((row: any) => (
|
|
553
|
+
<article
|
|
554
|
+
key={row.id}
|
|
555
|
+
className="flex items-start justify-between rounded-lg border p-4"
|
|
556
|
+
>
|
|
557
|
+
<div className="flex flex-col gap-1">
|
|
558
|
+
<p className="text-sm font-medium">{row.input}</p>
|
|
559
|
+
<p className="text-xs text-muted-foreground">
|
|
560
|
+
{row.word_count} words · {row.char_count} characters ·{" "}
|
|
561
|
+
{row.sentence_count} sentences
|
|
562
|
+
</p>
|
|
563
|
+
</div>
|
|
564
|
+
<button
|
|
565
|
+
className="text-xs text-destructive hover:underline"
|
|
566
|
+
onClick={() => deleteAnalysis.mutate({ id: row.id })}
|
|
567
|
+
>
|
|
568
|
+
Delete
|
|
569
|
+
</button>
|
|
245
570
|
</article>
|
|
246
571
|
))}
|
|
247
572
|
</section>
|
|
@@ -250,29 +575,66 @@ export default function ResponseInsightsRoute() {
|
|
|
250
575
|
}
|
|
251
576
|
```
|
|
252
577
|
|
|
253
|
-
|
|
254
|
-
|
|
578
|
+
`useActionQuery` calls `list-text-analyses` and keeps the result live. If the
|
|
579
|
+
agent saves a new row while the page is open, it appears automatically.
|
|
580
|
+
`useActionMutation` calls `delete-text-analysis` when the user clicks Delete,
|
|
581
|
+
then invalidates the query so the list refreshes.
|
|
582
|
+
|
|
583
|
+
Open `http://localhost:8080/text-analyses` in your browser. If you saved an
|
|
584
|
+
analysis in the previous step you'll see it listed. Then ask the agent in chat:
|
|
585
|
+
|
|
586
|
+
> Open the text analyses page.
|
|
587
|
+
|
|
588
|
+
If you get a 404, try restarting your dev server.
|
|
589
|
+
|
|
590
|
+
The agent calls the `navigate` action (already included in the Chat
|
|
591
|
+
template) to send the browser to `/text-analyses`. This is what it looks like
|
|
592
|
+
with a few saved rows:
|
|
255
593
|
|
|
256
594
|
<WireframeBlock id="doc-block-response-insights-page-wireframe">
|
|
257
595
|
<Screen
|
|
258
596
|
surface="desktop"
|
|
259
597
|
html={
|
|
260
|
-
"<main style='min-height:
|
|
598
|
+
"<main style='min-height:400px;box-sizing:border-box;padding:28px;background:var(--wf-bg)'><div style='max-width:720px;margin:0 auto;display:flex;flex-direction:column;gap:20px'><header><h2 style='margin:0 0 4px;font-size:24px;font-weight:600'>Text analyses</h2><p class='wf-muted' style='margin:0;font-size:14px'>Results saved by the agent or triggered manually.</p></header><section style='display:flex;flex-direction:column;gap:10px'><article class='wf-card' data-rough style='display:flex;align-items:center;justify-content:space-between;padding:14px 16px'><div><p style='margin:0 0 4px;font-size:14px;font-weight:500'>Hello world</p><p class='wf-muted' style='margin:0;font-size:12px'>2 words · 11 characters · 1 sentence</p></div><span class='wf-muted' style='font-size:12px'>Delete</span></article><article class='wf-card' data-rough style='display:flex;align-items:center;justify-content:space-between;padding:14px 16px'><div><p style='margin:0 0 4px;font-size:14px;font-weight:500'>The quick brown fox jumps over the lazy dog.</p><p class='wf-muted' style='margin:0;font-size:12px'>9 words · 44 characters · 1 sentence</p></div><span class='wf-muted' style='font-size:12px'>Delete</span></article><article class='wf-card' data-rough style='display:flex;align-items:center;justify-content:space-between;padding:14px 16px'><div><p style='margin:0 0 4px;font-size:14px;font-weight:500'>Pack my box with five dozen liquor jugs.</p><p class='wf-muted' style='margin:0;font-size:12px'>8 words · 40 characters · 1 sentence</p></div><span class='wf-muted' style='font-size:12px'>Delete</span></article></section></div></main>"
|
|
261
599
|
}
|
|
262
600
|
/>
|
|
263
601
|
</WireframeBlock>
|
|
264
602
|
|
|
265
|
-
##
|
|
603
|
+
## 7. Extend the navigation {#extend-navigation}
|
|
604
|
+
|
|
605
|
+
The sidebar is configured in `app/config/nav.ts`. Open it and add an entry for
|
|
606
|
+
the Text analyses page:
|
|
607
|
+
|
|
608
|
+
```ts filename="app/config/nav.ts"
|
|
609
|
+
export const navConfig = [
|
|
610
|
+
{
|
|
611
|
+
label: "Chat",
|
|
612
|
+
href: "/",
|
|
613
|
+
icon: "chat",
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
label: "Text analyses",
|
|
617
|
+
href: "/text-analyses",
|
|
618
|
+
icon: "list",
|
|
619
|
+
},
|
|
620
|
+
];
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
Save the file. The dev server picks up the change automatically and the sidebar
|
|
624
|
+
updates without a restart.
|
|
625
|
+
|
|
626
|
+
### Agent navigation
|
|
627
|
+
|
|
628
|
+
The sidebar link lets users navigate manually. The agent can also open pages on
|
|
629
|
+
its own using two built-in actions that ship with the Chat template:
|
|
266
630
|
|
|
267
|
-
|
|
268
|
-
|
|
631
|
+
- **`view-screen`** reads the current route and returns a compact summary of
|
|
632
|
+
what the user is looking at.
|
|
633
|
+
- **`navigate`** writes a same-origin path to the browser's history.
|
|
269
634
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
the agent decides the user should inspect the result visually.
|
|
274
|
-
- Action results can include links such as `href: "/response-insights"` so chat
|
|
275
|
-
can offer an explicit "Open response insights" button.
|
|
635
|
+
As you add more pages, keep `navigate` updated so the agent knows what
|
|
636
|
+
destinations exist. Document available paths in `AGENTS.md` so the model can
|
|
637
|
+
reason about them.
|
|
276
638
|
|
|
277
639
|
When the app has both a full-page chat route and an app page, use the shared chat
|
|
278
640
|
handoff helpers described in [Agent Surfaces](/docs/agent-surfaces#rich-chat):
|
|
@@ -295,7 +657,7 @@ my-app/
|
|
|
295
657
|
|
|
296
658
|
## Want a full analytics starting point? {#analytics-starting-point}
|
|
297
659
|
|
|
298
|
-
The
|
|
660
|
+
The text-analyses example above is intentionally small so you can see the
|
|
299
661
|
framework pieces. If you are building a real analytics product, start from
|
|
300
662
|
[Analytics](/docs/template-analytics) instead. It is the robust starting point:
|
|
301
663
|
connect your providers, use the existing dashboards and agent actions, then
|
|
@@ -303,15 +665,15 @@ customize the app from there.
|
|
|
303
665
|
|
|
304
666
|
## What's next {#next}
|
|
305
667
|
|
|
306
|
-
- **[Actions](/docs/actions)
|
|
307
|
-
- **[Native Chat UI](/docs/native-chat-ui)
|
|
668
|
+
- **[Actions](/docs/actions)**: schemas, auth, approvals, hooks, and transport.
|
|
669
|
+
- **[Native Chat UI](/docs/native-chat-ui)**: render action results as tables,
|
|
308
670
|
charts, and typed cards.
|
|
309
|
-
- **[Chat Template](/docs/template-chat)
|
|
671
|
+
- **[Chat Template](/docs/template-chat)**: the minimal chat-first app you just
|
|
310
672
|
created.
|
|
311
|
-
- **[Analytics Template](/docs/template-analytics)
|
|
673
|
+
- **[Analytics Template](/docs/template-analytics)**: a robust analytics app
|
|
312
674
|
starting point; connect providers and customize from there.
|
|
313
|
-
- **[Context Awareness](/docs/context-awareness)
|
|
675
|
+
- **[Context Awareness](/docs/context-awareness)**: `view-screen`, `navigate`,
|
|
314
676
|
route state, and selected objects.
|
|
315
|
-
- **[Agent Surfaces](/docs/agent-surfaces)
|
|
677
|
+
- **[Agent Surfaces](/docs/agent-surfaces)**: chat, inline UI, app pages,
|
|
316
678
|
embedded sidecars, automation, and external agents.
|
|
317
|
-
- **[Deployment](/docs/deployment)
|
|
679
|
+
- **[Deployment](/docs/deployment)**: put your app on your own domain.
|