@agent-native/core 0.210.0-nightly-20261019153056 → 0.210.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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agent/action-error-details.d.ts +26 -0
  3. package/dist/agent/action-error-details.js +99 -0
  4. package/dist/agent/execution-fence.d.ts +9 -2
  5. package/dist/agent/execution-fence.js +42 -12
  6. package/dist/agent/production-agent.js +9 -0
  7. package/dist/agent/run-manager.js +14 -14
  8. package/dist/agent/run-store.js +8 -6
  9. package/dist/agent/tool-error-redaction.d.ts +8 -1
  10. package/dist/agent/tool-error-redaction.js +70 -7
  11. package/dist/collab/awareness.d.ts +2 -2
  12. package/dist/collab/routes.d.ts +1 -1
  13. package/dist/collab/struct-routes.d.ts +1 -1
  14. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  15. package/dist/mcp/actions/create-org-service-token.js +6 -3
  16. package/dist/mcp/actions/set-service-principal-lifecycle.js +1 -0
  17. package/dist/mcp/actions/set-service-principal-policy.js +1 -0
  18. package/dist/mcp/direct-action-call.js +39 -10
  19. package/dist/notifications/routes.d.ts +3 -3
  20. package/dist/observability/routes.d.ts +3 -3
  21. package/dist/onboarding/workspace-services.js +1 -1
  22. package/dist/progress/routes.d.ts +1 -1
  23. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  24. package/dist/secrets/routes.d.ts +6 -6
  25. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  26. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  27. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  28. package/dist/templates/chat/.agents/skills/actions/SKILL.md +2 -2
  29. package/dist/templates/chat/netlify.toml +0 -1
  30. package/dist/templates/default/.agents/skills/actions/SKILL.md +2 -2
  31. package/dist/templates/factory/.agents/skills/review-prs/SKILL.md +44 -21
  32. package/dist/templates/headless/.agents/skills/actions/SKILL.md +2 -2
  33. package/dist/templates/workspace-core/.agents/skills/actions/SKILL.md +2 -2
  34. package/docs/content/actions-defining.mdx +2 -2
  35. package/docs/content/template-chat-agent.mdx +88 -0
  36. package/docs/content/template-chat-developers.mdx +586 -104
  37. package/docs/content/template-chat-features.mdx +158 -0
  38. package/docs/content/template-chat-integrations.mdx +66 -0
  39. package/docs/content/template-chat.mdx +36 -166
  40. package/docs/content/template-clips-developers.mdx +1 -1
  41. package/docs/content/template-clips-integrations.mdx +50 -5
  42. package/package.json +4 -4
  43. package/src/templates/chat/.agents/skills/actions/SKILL.md +2 -2
  44. package/src/templates/chat/netlify.toml +0 -1
  45. package/src/templates/default/.agents/skills/actions/SKILL.md +2 -2
  46. package/src/templates/factory/.agents/skills/review-prs/SKILL.md +44 -21
  47. package/src/templates/headless/.agents/skills/actions/SKILL.md +2 -2
  48. package/src/templates/workspace-core/.agents/skills/actions/SKILL.md +2 -2
  49. package/docs/content/template-chat-first-edits.mdx +0 -158
@@ -1,14 +1,13 @@
1
1
  ---
2
2
  title: "Chat: Developer Guide"
3
- description: "Quick start, the action inventory, core tables, auth, and the Database/Observability/Extensions admin pages that ship by default with the Chat template."
3
+ description: "Quick start, the action inventory, the data model, an action walked through, the routes reference, and how to add your first feature to the Chat template."
4
4
  ---
5
5
 
6
6
  # Developer Guide
7
7
 
8
- This page is for developers running or extending the Chat template: the
9
- quick start, every built-in action, the core tables it runs on, how auth is
10
- configured, and the admin pages — Database, Observability, and Extensions —
11
- that ship by default whether or not you ever open them. See
8
+ This page is for developers running or extending the Chat template. It covers
9
+ the quick start, every built-in action, the data model, one action walked
10
+ through, the routes, and how to add your first feature. See
12
11
  [Chat](/docs/template-chat) for what the app does.
13
12
 
14
13
  ## Quick start {#quick-start}
@@ -40,138 +39,621 @@ pnpm dev
40
39
  ```
41
40
 
42
41
  The app opens at the local URL printed by `pnpm dev`. If it doesn't open
43
- automatically, visit that URL in your browser; the port may change when 8080 is
42
+ automatically, visit that URL in your browser. The port may change when 8080 is
44
43
  already in use.
45
44
 
46
45
  ### Sign in and connect an LLM
47
46
 
48
- On the Welcome screen, select **Continue as local dev** — no email or
47
+ On the Welcome screen, select **Continue as local dev**. No email or
49
48
  password needed for local development. Then connect an LLM: select
50
- **Use Builder.io** for free credits, or **Custom keys** to add an
51
- Anthropic/OpenAI key or a local Ollama model. Chat ships with no domain data
49
+ **Use Builder.io** for free credits, or **Add your own keys** to use a
50
+ model provider key of your own. Chat ships with no domain data
52
51
  to seed, so once an LLM is connected there's nothing else to set up before
53
52
  you can start chatting.
54
53
 
55
54
  </Steps>
56
55
 
56
+ Chat is built for a browser UI. If you want an agent with no screen at all,
57
+ for scheduled jobs, queues, CLI loops, or external-agent workflows, scaffold
58
+ with `--headless` instead:
59
+
60
+ ```bash
61
+ npx --yes @agent-native/core@latest create my-agent --headless
62
+ ```
63
+
64
+ The key idea stays the same. Actions are the shared surface for chat, UI,
65
+ HTTP, MCP, A2A, and CLI. See [Automation-First Apps](/docs/pure-agent-apps)
66
+ for that path in depth.
67
+
57
68
  ## Action inventory {#actions}
58
69
 
59
70
  Every operation is a TypeScript file in `actions/`, auto-mounted at
60
- `POST /_agent-native/actions/:name`.
71
+ `POST /_agent-native/actions/:name` unless the action sets `http: false`.
72
+
73
+ | Action | What it does |
74
+ | ---------------------- | ------------------------------------------------------------------------ |
75
+ | `hello` | Example action. Returns a greeting. Replace it or add actions beside it. |
76
+ | `view-screen` | Standard context action. Reads the current view back for the agent. |
77
+ | `navigate` | Standard navigation action. Moves the UI to a route. |
78
+ | `provider-api-request` | Calls the Slack Web API through a connected Slack workspace. |
79
+
80
+ ### The chat runtime {#chat-runtime}
81
+
82
+ `server/plugins/agent-chat.ts` is where the chat runtime is configured. It
83
+ does four things:
61
84
 
62
- | Action | What it does |
63
- | ------------- | ----------------------------------------------------------------------- |
64
- | `hello` | Example action; returns a greeting. Replace it or add actions beside it |
65
- | `view-screen` | Standard context action; reads the current view back for the agent |
66
- | `navigate` | Standard navigation action; moves the UI to a route |
85
+ - Registers every action in `actions/` as a callable tool, with
86
+ `loadActionsFromStaticRegistry`.
87
+ - Resolves the current organization for scoping, with `resolveOrgId`.
88
+ - Sets the system prompt.
89
+ - Declares which tools the model sees on its first turn, with
90
+ `initialToolNames`.
67
91
 
68
- `server/plugins/agent-chat.ts` is where the chat runtime itself is
69
- configured: it registers every action in `actions/` as a callable tool
70
- (`loadActionsFromStaticRegistry`), resolves the current org for scoping
71
- (`resolveOrgId`), sets the system prompt, and declares which tools the model
72
- sees on the very first turn (`initialToolNames`) — see [Your First Feature in
73
- Chat](/docs/template-chat-first-edits#first-edits) for what that last part
74
- means when you add a new action.
92
+ ### How actions become tools {#actions-as-tools}
75
93
 
76
- ## Core tables and live sync {#tables}
94
+ Every `defineAction` is a tool by default. Set `agentTool: false` to keep an
95
+ action available to your screens but hidden from the agent.
77
96
 
78
- Chat adds no domain tables of its own (see [What ships](/docs/template-chat#what-ships)). It runs entirely on the framework's core tables: chat threads and messages, `application_state`, `settings`, sessions, resources, and run history.
97
+ ### What the model sees first {#initial-tools}
79
98
 
80
- `useDbSync()` is already wired into the app shell: when the agent (or anyone else) writes to SQL, the client receives the change over SSE, or the 2-second polling fallback, and invalidates the matching React Query caches — no manual refresh, no websocket you have to manage yourself. See [`useDbSync`](/docs/client-data#usedbsync) for the full hook API.
99
+ `INITIAL_TOOL_NAMES` lists `view-screen`, `navigate`, `hello`, and
100
+ `provider-api-request`. These are the only tools sent on the first turn.
101
+ Everything else, including an action you add, stays reachable through
102
+ `tool-search` and loads on demand.
103
+
104
+ Add a name to that list only if you want the action in the model's default
105
+ toolset from the first message.
106
+
107
+ ## Data model {#data-model}
108
+
109
+ Chat defines no tables of its own. There is no `server/db/schema.ts` until you
110
+ add one. The app runs on the framework's core tables for chat threads and
111
+ messages, application state, settings, sessions, resources, and run history.
112
+ Those belong to the framework, so don't edit them. See
113
+ [Database](/docs/server-database) for the connection model and where new
114
+ data should live.
115
+
116
+ `useDbSync()` is already wired into the app shell. When the agent or anyone
117
+ else writes to SQL, the client receives the change over SSE, or the 2-second
118
+ polling fallback, and invalidates the matching React Query caches. See
119
+ [`useDbSync`](/docs/client-data#usedbsync) for the full hook API.
120
+
121
+ Your first table is a new file. See [Add your first feature](#first-feature).
81
122
 
82
123
  ## Auth {#auth}
83
124
 
84
- Auth runs on Better Auth: login, signup, sessions, and organizations, configured in `server/plugins/auth.ts` via `createAuthPlugin`. The same flow runs locally and in production, except that development skips email verification so you can sign up and start building immediately. `createAuthPlugin` also takes the login page's marketing copy — app name, tagline, and feature bullets — as plain config in that same file. See [Authentication](/docs/authentication) for the full auth model (sessions, organizations, protected routes).
85
-
86
- ## Admin surfaces that ship by default {#admin-surfaces}
87
-
88
- Three routes are mounted whether or not your app ever links to them:
89
-
90
- <Table
91
- id="doc-block-chat6"
92
- title="Chat's mounted routes"
93
- columns={["Route", "Renders", "What it's for"]}
94
- rows={[
95
- [
96
- "/",
97
- "ChatRoute (app/routes/_index.tsx)",
98
- "Full-page chat, the default view",
99
- ],
100
- [
101
- "/chat/:threadId",
102
- "same component, re-exported",
103
- "Chat with the open thread synced into the URL",
104
- ],
105
- [
106
- "/agent",
107
- "AgentTabsPage (falls back to a plain chat surface on older core versions)",
108
- "The full Agent page — Context, Files, Connections, Jobs, MCP tabs",
109
- ],
110
- ["/database", "DbAdminPage", "Browse and edit every SQL table directly"],
111
- [
112
- "/observability",
113
- "ObservabilityDashboard",
114
- "Agent traces, cost, evals, and feedback",
115
- ],
116
- [
117
- "/extensions",
118
- "redirects to /settings/extensions",
119
- "Extensions are managed from Settings, not a standalone list page",
120
- ],
121
- [
122
- "/extensions/:id (or /extensions/:id/:slug)",
123
- "ExtensionViewerPage",
124
- "Runs one saved extension mini-app; the optional slug segment is just a friendlier URL for the same page",
125
- ],
126
- [
127
- "/settings",
128
- "SettingsTabsPage",
129
- "Account, Connections, Agent, Organization, and Chat pages, plus Labs and What's new",
130
- ],
131
- [
132
- "/team",
133
- "redirects to /settings/organization",
134
- "Organization membership lives under Settings",
135
- ],
125
+ Auth runs on Better Auth: login, signup, sessions, and organizations. Chat
126
+ configures it in `server/plugins/auth.ts` with `createToolkitAuthPlugin`. The
127
+ file keeps `/` public, so the sign-in landing can load, and sets
128
+ `marketing: false`, so the sign-in page has no marketing copy. To show an app
129
+ name, tagline, and feature bullets there, pass a `marketing` object instead.
130
+ The same flow runs locally and in production, except that development skips
131
+ email verification so you can sign up and start building immediately. See
132
+ [Authentication](/docs/authentication) for the full auth model (sessions,
133
+ organizations, protected routes).
134
+
135
+ ## Routes reference {#routes}
136
+
137
+ The app uses file-system routing (`flatRoutes()` in `app/routes.ts`), so
138
+ adding a route file is enough to mount it.
139
+
140
+ <FileTree
141
+ id="doc-block-chat3"
142
+ title="Chat template routes and components"
143
+ entries={[
144
+ {
145
+ path: "app/routes/_index.tsx",
146
+ note: "/: the shared sign-in page, then on to /home",
147
+ },
148
+ {
149
+ path: "app/routes/home.tsx",
150
+ note: "/home: the Get started page, rendered by app/components/home/HomePage.tsx",
151
+ },
152
+ {
153
+ path: "app/routes/chat._index.tsx",
154
+ note: "/chat: starts a new chat and hands off to /chat/:threadId",
155
+ },
156
+ {
157
+ path: "app/routes/chat.$threadId.tsx",
158
+ note: "/chat/:threadId: renders ChatRouteContent for that thread",
159
+ },
160
+ {
161
+ path: "app/routes/settings.tsx",
162
+ note: "/settings and /settings/*: account, connections, agent, and organization pages",
163
+ },
164
+ {
165
+ path: "app/routes/agent.tsx",
166
+ note: "/agent: redirects into the agent pages in Settings",
167
+ },
168
+ {
169
+ path: "app/routes/team.tsx",
170
+ note: "/team: redirects to /settings/organization",
171
+ },
172
+ {
173
+ path: "app/routes/database.tsx",
174
+ note: "/database: the table browser and editor",
175
+ },
176
+ {
177
+ path: "app/routes/observability.tsx",
178
+ note: "/observability: agent traces, cost, evals, and feedback",
179
+ },
180
+ {
181
+ path: "app/routes/extensions._index.tsx",
182
+ note: "/extensions: redirects to /settings/extensions, which only exists once Extensions are switched on",
183
+ },
184
+ {
185
+ path: "app/routes/extensions.$id.tsx",
186
+ note: "/extensions/:id (and /extensions/:id/:slug): runs one saved extension",
187
+ },
188
+ {
189
+ path: "app/components/chat/ChatRouteContent.tsx",
190
+ note: "the full-page chat surface. Edit the empty state, composer, and message slots here",
191
+ },
192
+ {
193
+ path: "app/components/layout/Sidebar.tsx",
194
+ note: "the sidebar: Home, the thread list, and the organization switcher",
195
+ },
196
+ {
197
+ path: "app/lib/chat-paths.ts",
198
+ note: "path helpers for chat and home URLs",
199
+ },
136
200
  ]}
137
201
  />
138
202
 
139
- **Database** (`/database`) is the same `DbAdminPage` every template gets — a direct table browser/editor over the app's SQL database. See [Database](/docs/server-database) for the connection model behind it.
203
+ **Database** (`/database`) is the same `DbAdminPage` every template gets. It's
204
+ a direct table browser and editor over the app's SQL database. See
205
+ [Database](/docs/server-database) for the connection model behind it.
140
206
 
141
- **Observability** (`/observability`) is the same zero-config `ObservabilityDashboard` every template gets: traces, cost, evals, and feedback for every agent run, computed from data already in your own database. See [Agent Observability](/docs/observability).
207
+ **Observability** (`/observability`) is the same zero-config
208
+ `ObservabilityDashboard` every template gets. It shows traces, cost, evals, and
209
+ feedback for every agent run, computed from data already in your own database.
210
+ See [Agent Observability](/docs/observability).
142
211
 
143
- **Extensions** are sandboxed Alpine.js mini-apps your users can ask the agent to build inside the running app — a custom widget or checklist, no deploy required. Chat mounts the routes that view and run them (`/extensions/:id`) by default, even before you or your users create one; `/extensions` itself just redirects into the Extensions tab of Settings rather than showing its own list. See [Extensions](/docs/extensions) for what they can and cannot do.
212
+ **Extensions** are sandboxed Alpine.js mini-apps. Chat mounts the routes that
213
+ view and run them (`/extensions/:id`) and redirects `/extensions` to
214
+ `/settings/extensions`. Chat leaves Extensions off in two places. The Settings
215
+ page only appears when `app/routes/settings.tsx` passes `extensionTools: true`
216
+ to `useAgentSettingsTabs()`. The agent can only create extensions once
217
+ `frameworkTools.extensions` is turned on in `server/plugins/agent-chat.ts`. See
218
+ [Extensions](/docs/extensions) for what they can and cannot do.
144
219
 
145
220
  ## Customizing it {#customizing}
146
221
 
147
222
  Every part of the app is editable source. Start here:
148
223
 
149
- - `actions/` — add a new file with `defineAction` to expose new capability
150
- to both the agent and the frontend.
151
- - `app/routes/` — the UI. The app uses file-system routing (`flatRoutes()`
152
- in `app/routes.ts`), so adding a route file is enough to mount it.
153
- - `server/db/schema.ts` — add your own tables with Drizzle once you outgrow
154
- the core tables above. Keep the code PostgreSQL-specific so local PGlite and
224
+ - `actions/`: add a new file with `defineAction` to expose a new action to
225
+ both the agent and the frontend.
226
+ - `app/routes/`: the UI. Add a route file to mount a page.
227
+ - `server/db/schema.ts`: add your own tables with Drizzle once you outgrow
228
+ the core tables. Keep the code PostgreSQL-specific so local PGlite and
155
229
  hosted Postgres use the same semantics.
156
- - `AGENTS.md` — agent instructions. Update this when you teach the agent new
157
- capabilities or conventions.
158
- - `.agents/skills/` — detailed patterns the agent follows.
230
+ - `AGENTS.md`: agent instructions. Update this when you teach the agent new
231
+ actions or conventions.
232
+ - `.agents/skills/`: detailed patterns the agent follows.
233
+
234
+ If you add a feature, remember to update all four areas: UI, action, skill or
235
+ `AGENTS.md` entry, and any application state the agent needs to see. That is
236
+ what keeps the agent and the UI in parity.
237
+
238
+ ### The chat route {#chat-route}
239
+
240
+ `app/components/chat/ChatRouteContent.tsx` renders the full-page chat at
241
+ `/chat/<threadId>`. The chat surface is assembled from AgentKit pieces: a
242
+ transport, a thread root, and the chat view. Its copy comes from i18n keys
243
+ rather than literal strings. The real file is longer. This is the part you edit
244
+ most often.
245
+
246
+ <AnnotatedCode
247
+ id="doc-block-chat4"
248
+ title="app/components/chat/ChatRouteContent.tsx (trimmed)"
249
+ filename="app/components/chat/ChatRouteContent.tsx"
250
+ language="tsx"
251
+ code={
252
+ 'import { createAgentNativeAgentKitTransport } from "@agent-native/core/client/agentkit-chat/transport";\nimport { useT } from "@agent-native/core/client/i18n";\nimport { AgentKitChat } from "@agent-native/toolkit/app/agentkit/react/components";\nimport { CoreAgentKitRoot } from "@agent-native/toolkit/app/chat/agentkit-chat/index";\nimport { useState } from "react";\nimport { useNavigate, useParams } from "react-router";\n\nimport { chatThreadPath } from "@/lib/chat-paths";\nimport { TAB_ID } from "@/lib/tab-id";\n\nexport default function ChatRouteContent() {\n const { threadId } = useParams();\n const navigate = useNavigate();\n const t = useT();\n const [transport] = useState(() =>\n createAgentNativeAgentKitTransport({\n browserTabId: TAB_ID,\n surface: "app",\n adapter: { textFormat: "markdown" },\n }),\n );\n if (!threadId) return null;\n\n return (\n <CoreAgentKitRoot\n transport={transport}\n threadId={threadId}\n labels={{ composerPlaceholder: t("chat.composerPlaceholder") }}\n slots={{ emptyState: ChatEmptyState }}\n onThreadForked={(thread) => navigate(chatThreadPath(thread.id))}\n >\n <AgentKitChat\n emptyComposerPlacement="center"\n composerProps={{ requireAgentEngine: true, voiceEnabled: true }}\n />\n </CoreAgentKitRoot>\n );\n}\n\nfunction ChatEmptyState() {\n const t = useT();\n return (\n <div className="agentkit-chat-empty-copy">\n <h1>{t("chat.heroTitle")}</h1>\n </div>\n );\n}'
253
+ }
254
+ annotations={[
255
+ {
256
+ lines: "15-21",
257
+ label: "Transport",
258
+ note: "`createAgentNativeAgentKitTransport` connects the chat view to the app's agent runtime. `TAB_ID` (`app/lib/tab-id.ts`, a random id generated once per browser tab) lets the runtime tell open tabs of the same app apart, so a run started in one tab isn't mistaken for one in another.",
259
+ },
260
+ {
261
+ lines: "25-30",
262
+ label: "Thread root",
263
+ note: "`CoreAgentKitRoot` loads the thread named in `/chat/:threadId` (`chat.$threadId.tsx`). Visiting `/chat` creates a new thread id and hands off here. When a thread is forked, `chatThreadPath` moves the URL to the new thread.",
264
+ },
265
+ {
266
+ lines: "29",
267
+ label: "Slots",
268
+ note: "Slots replace parts of the chat view. The real file also passes `message`, `runFailure`, `connectionRequest`, and `footer` slots for recovery messages, MCP connection cards, and the agent footer.",
269
+ },
270
+ {
271
+ lines: "32-35",
272
+ label: "Centered composer",
273
+ note: '`emptyComposerPlacement="center"` centers the composer until the thread has a message. `requireAgentEngine` shows the Connect AI card until a model is connected.',
274
+ },
275
+ {
276
+ lines: "40-47",
277
+ label: "Empty state",
278
+ note: "The heading above the centered composer. Its copy is `chat.heroTitle` in `app/i18n/en-US.ts`: edit the catalog to change the pitch, or add elements here.",
279
+ },
280
+ ]}
281
+ />
282
+
283
+ ### Use your own agent backend {#own-agent-backend}
284
+
285
+ The template uses the built-in app-agent loop by default. To connect a custom
286
+ backend, swap the chat runtime behind the agent chat plugin instead of
287
+ rewriting the UI. The chat route should stay a thin renderer around the shared
288
+ chat surface, and the backend choice belongs in the server plugin and runtime
289
+ adapter.
290
+
291
+ Use this when your model orchestration already lives elsewhere but you still
292
+ want an app with auth, threads, actions, UI state, and deployable pages. See
293
+ [Native Chat UI](/docs/native-chat-ui#byo-agent-runtimes) for the runtime
294
+ options.
295
+
296
+ ## Add your first feature {#first-feature}
297
+
298
+ Most first features follow the same shape: a table, one or two actions, a
299
+ page, a nav entry, and an `AGENTS.md` update. This section builds a notes
300
+ feature in four parts, with the full code for each file. You can also ask the
301
+ agent to do all of it:
302
+
303
+ > Add a data model for `notes`. A note has an id, title, body, and owner. Render a notes page at `/notes`, add create/list actions, and keep chat able to create notes.
304
+
305
+ ### 1. Add the data layer
306
+
307
+ Chat ships with no `server/db` folder, so all three files here are new.
308
+
309
+ The schema defines the tables. `ownableColumns()` adds the owner, organization,
310
+ and visibility columns, and `createSharesTable()` adds a matching table for
311
+ sharing.
312
+
313
+ ```ts filename="server/db/schema.ts"
314
+ import { sql } from "drizzle-orm";
315
+ import { pgTable, text } from "drizzle-orm/pg-core";
316
+ import {
317
+ createSharesTable,
318
+ ownableColumns,
319
+ } from "@agent-native/core/db/schema";
320
+
321
+ export const notes = pgTable("notes", {
322
+ id: text("id").primaryKey(),
323
+ title: text("title").notNull(),
324
+ body: text("body").notNull(),
325
+ createdAt: text("created_at")
326
+ .notNull()
327
+ .default(sql`now()`),
328
+ ...ownableColumns(),
329
+ });
330
+
331
+ export const noteShares = createSharesTable("note_shares");
332
+ ```
333
+
334
+ The database client gives your actions `getDb()` and registers notes with the
335
+ sharing system. Notes have no detail page, so share links point at the list.
336
+
337
+ ```ts filename="server/db/index.ts"
338
+ import { createGetDb } from "@agent-native/core/db";
339
+ import { registerShareableResource } from "@agent-native/core/sharing";
340
+
341
+ import * as schema from "./schema.js";
342
+
343
+ export const getDb = createGetDb(schema);
344
+ export { schema };
345
+
346
+ registerShareableResource({
347
+ type: "note",
348
+ resourceTable: schema.notes,
349
+ sharesTable: schema.noteShares,
350
+ displayName: "Note",
351
+ titleColumn: "title",
352
+ getResourcePath: () => "/notes",
353
+ getDb,
354
+ });
355
+ ```
356
+
357
+ The migration creates the tables when the app starts. Add new entries for later
358
+ changes and never edit one that has already run.
359
+
360
+ ```ts filename="server/plugins/db.ts"
361
+ import { runMigrations } from "@agent-native/core/db";
362
+
363
+ export default runMigrations(
364
+ [
365
+ {
366
+ version: 1,
367
+ name: "notes",
368
+ sql: `CREATE TABLE IF NOT EXISTS notes (
369
+ id TEXT PRIMARY KEY,
370
+ title TEXT NOT NULL,
371
+ body TEXT NOT NULL,
372
+ created_at TEXT NOT NULL DEFAULT (now()),
373
+ owner_email TEXT NOT NULL DEFAULT 'local@localhost',
374
+ org_id TEXT,
375
+ visibility TEXT NOT NULL DEFAULT 'private'
376
+ );
377
+ CREATE TABLE IF NOT EXISTS note_shares (
378
+ id TEXT PRIMARY KEY,
379
+ resource_id TEXT NOT NULL,
380
+ principal_type TEXT NOT NULL,
381
+ principal_id TEXT NOT NULL,
382
+ role TEXT NOT NULL DEFAULT 'viewer',
383
+ created_by TEXT NOT NULL DEFAULT '',
384
+ created_at TEXT NOT NULL DEFAULT (now()),
385
+ notified_at TEXT
386
+ );
387
+ CREATE INDEX IF NOT EXISTS notes_owner_org_idx ON notes (owner_email, org_id, created_at);
388
+ CREATE INDEX IF NOT EXISTS note_shares_resource_idx ON note_shares (resource_id, principal_type, principal_id)`,
389
+ },
390
+ ],
391
+ { table: "notes_app_migrations" },
392
+ );
393
+ ```
394
+
395
+ ### 2. Add the actions
396
+
397
+ Each action is a file in `actions/`, and the filename becomes the tool name.
398
+ The agent and your page call the same two actions.
399
+
400
+ `create-note` records the signed-in user as the owner. The note is visible to
401
+ the organization when one is active and private to its creator otherwise.
402
+
403
+ ```ts filename="actions/create-note.ts"
404
+ import { defineAction, fail } from "@agent-native/core/action";
405
+ import { z } from "zod";
406
+
407
+ import { getDb, schema } from "../server/db/index.js";
408
+
409
+ export default defineAction({
410
+ description:
411
+ "Create a note with a title and body. It is visible to the organization when one is active, otherwise only to its creator.",
412
+ schema: z.object({
413
+ title: z.string().min(1).describe("Short title for the note"),
414
+ body: z.string().describe("Full text of the note"),
415
+ }),
416
+ run: async (args, ctx) => {
417
+ const ownerEmail =
418
+ ctx?.userEmail ?? fail("Sign in to create notes.", { statusCode: 401 });
419
+ const orgId = ctx?.orgId ?? null;
420
+ const note = {
421
+ id: crypto.randomUUID(),
422
+ title: args.title,
423
+ body: args.body,
424
+ ownerEmail,
425
+ orgId,
426
+ visibility: orgId ? ("org" as const) : ("private" as const),
427
+ };
428
+ await getDb().insert(schema.notes).values(note);
429
+ return { id: note.id, title: note.title };
430
+ },
431
+ });
432
+ ```
433
+
434
+ `list-notes` is read-only, and `accessFilter` limits the results to notes the
435
+ current user can see.
436
+
437
+ ```ts filename="actions/list-notes.ts"
438
+ import { defineAction } from "@agent-native/core/action";
439
+ import { accessFilter } from "@agent-native/core/sharing";
440
+ import { desc } from "drizzle-orm";
441
+ import { z } from "zod";
442
+
443
+ import { getDb, schema } from "../server/db/index.js";
444
+
445
+ export default defineAction({
446
+ description: "List the notes the current user can see, newest first.",
447
+ schema: z.object({}),
448
+ http: { method: "GET" },
449
+ readOnly: true,
450
+ run: async () => {
451
+ const { notes, noteShares } = schema;
452
+ const rows = await getDb()
453
+ .select({
454
+ id: notes.id,
455
+ title: notes.title,
456
+ body: notes.body,
457
+ createdAt: notes.createdAt,
458
+ })
459
+ .from(notes)
460
+ .where(accessFilter(notes, noteShares))
461
+ .orderBy(desc(notes.createdAt))
462
+ .limit(200);
463
+ return { notes: rows };
464
+ },
465
+ });
466
+ ```
467
+
468
+ A new action is callable by chat as soon as it exists. See
469
+ [What the model sees first](#initial-tools) if you want it in the model's
470
+ default toolset.
471
+
472
+ ### 3. Add the page and the sidebar link
473
+
474
+ The app uses file-system routing, so adding this file mounts `/notes`. The page
475
+ calls the same actions as the agent with `useActionQuery()` and
476
+ `useActionMutation()`, and a successful mutation refreshes the list on its own.
477
+
478
+ ```tsx filename="app/routes/notes.tsx"
479
+ import {
480
+ useActionMutation,
481
+ useActionQuery,
482
+ } from "@agent-native/core/client/hooks";
483
+ import { useSetPageTitle } from "@agent-native/toolkit/app-shell";
484
+ import { useState, type FormEvent } from "react";
485
+ import { toast } from "sonner";
486
+
487
+ import { Button } from "@/components/ui/button";
488
+ import { Input } from "@/components/ui/input";
489
+ import { Skeleton } from "@/components/ui/skeleton";
490
+ import { APP_TITLE } from "@/lib/app-config";
491
+
492
+ export function meta() {
493
+ return [{ title: `Notes - ${APP_TITLE}` }];
494
+ }
495
+
496
+ export default function NotesRoute() {
497
+ useSetPageTitle("Notes");
498
+ const { data, isLoading } = useActionQuery("list-notes");
499
+ const createNote = useActionMutation("create-note", {
500
+ onError: (error) => toast.error(error.message),
501
+ });
502
+ const [title, setTitle] = useState("");
503
+ const [body, setBody] = useState("");
504
+
505
+ function handleSubmit(event: FormEvent) {
506
+ event.preventDefault();
507
+ createNote.mutate(
508
+ { title, body },
509
+ {
510
+ onSuccess: () => {
511
+ setTitle("");
512
+ setBody("");
513
+ },
514
+ },
515
+ );
516
+ }
517
+
518
+ return (
519
+ <div className="mx-auto flex h-full max-w-2xl flex-col gap-6 overflow-auto p-6">
520
+ <form onSubmit={handleSubmit} className="flex flex-col gap-2">
521
+ <Input
522
+ value={title}
523
+ onChange={(event) => setTitle(event.target.value)}
524
+ placeholder="Title"
525
+ required
526
+ />
527
+ <Input
528
+ value={body}
529
+ onChange={(event) => setBody(event.target.value)}
530
+ placeholder="Write a note"
531
+ />
532
+ <Button type="submit" disabled={createNote.isPending}>
533
+ Add note
534
+ </Button>
535
+ </form>
536
+ <ul className="flex flex-col gap-2">
537
+ {isLoading && (
538
+ <>
539
+ <Skeleton className="h-16 w-full" />
540
+ <Skeleton className="h-16 w-full" />
541
+ </>
542
+ )}
543
+ {data?.notes.map((note) => (
544
+ <li key={note.id} className="rounded-lg border p-3">
545
+ <p className="font-medium">{note.title}</p>
546
+ <p className="text-sm text-muted-foreground">{note.body}</p>
547
+ </li>
548
+ ))}
549
+ </ul>
550
+ </div>
551
+ );
552
+ }
553
+ ```
554
+
555
+ In `app/components/layout/Sidebar.tsx`, add `IconNotes` to the existing
556
+ `@tabler/icons-react` import. Then add this component below `HomeNavLink`. It
557
+ follows the same pattern, including the collapsed sidebar.
558
+
559
+ ```tsx filename="app/components/layout/Sidebar.tsx"
560
+ function NotesNavLink({ collapsed }: { collapsed: boolean }) {
561
+ const location = useLocation();
562
+ const active = location.pathname.startsWith("/notes");
563
+ const link = (
564
+ <Link
565
+ to="/notes"
566
+ aria-current={active ? "page" : undefined}
567
+ aria-label={collapsed ? "Notes" : undefined}
568
+ className={cn(
569
+ "flex items-center text-sidebar-accent-foreground transition-colors hover:bg-sidebar-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
570
+ active && "bg-sidebar-accent",
571
+ collapsed
572
+ ? "size-10 justify-center rounded-md"
573
+ : "h-10 w-full gap-3 rounded-lg px-3 text-sm font-medium",
574
+ )}
575
+ >
576
+ <IconNotes className="size-4 shrink-0" strokeWidth={1.8} />
577
+ <span className={collapsed ? "sr-only" : "truncate"}>Notes</span>
578
+ </Link>
579
+ );
580
+
581
+ if (collapsed) {
582
+ return (
583
+ <Tooltip>
584
+ <TooltipTrigger asChild>{link}</TooltipTrigger>
585
+ <TooltipContent side="right">Notes</TooltipContent>
586
+ </Tooltip>
587
+ );
588
+ }
589
+
590
+ return <div className="px-2">{link}</div>;
591
+ }
592
+ ```
593
+
594
+ Then render it in the sidebar's `<nav>`, before the thread list:
595
+
596
+ ```diff filename="app/components/layout/Sidebar.tsx"
597
+ <HomeNavLink collapsed={collapsed} />
598
+ +<NotesNavLink collapsed={collapsed} />
599
+ <ChatThreadsSection collapsed={collapsed} />
600
+ ```
601
+
602
+ Chat's own labels come from translation files, so the literal "Notes" is a
603
+ shortcut. If your app is localized, add keys to `app/i18n/en-US.ts` and read
604
+ them with `useT()` instead. See [Internationalization](/docs/internationalization).
605
+
606
+ ### 4. Tell the agent about it
607
+
608
+ `app/hooks/use-navigation-state.ts` reports the current view to the agent and
609
+ maps view names back to paths. Without these two edits, the agent sees the
610
+ Notes page as `chat`, and "open Notes" can't navigate there.
611
+
612
+ ```diff filename="app/hooks/use-navigation-state.ts"
613
+ function viewForPath(pathname: string): string {
614
+ if (isChatPathname(pathname)) return "chat";
615
+ if (pathname === HOME_PATH) return "home";
616
+ + if (pathname.startsWith("/notes")) return "notes";
617
+ ```
618
+
619
+ ```diff filename="app/hooks/use-navigation-state.ts"
620
+ function pathForView(view?: string): string {
621
+ switch (view) {
622
+ case "home":
623
+ return HOME_PATH;
624
+ + case "notes":
625
+ + return "/notes";
626
+ ```
627
+
628
+ Then append a short section to `AGENTS.md` so the agent knows notes exist and
629
+ which actions manage them. Keep it brief, because the runtime prompt cuts the
630
+ file off at 6,000 characters.
631
+
632
+ ```md filename="AGENTS.md"
633
+ ## Notes
634
+
635
+ Notes live at `/notes`. Each note has a title and a body. A note is visible to
636
+ the organization when one is active and private to its creator otherwise.
637
+
638
+ `navigation` reports `view: "notes"` while the page is open.
639
+
640
+ | Action | Purpose |
641
+ | ------------- | ----------------------------------- |
642
+ | `list-notes` | List the notes the user can see |
643
+ | `create-note` | Create a note with a title and body |
644
+ ```
159
645
 
160
- If you add a feature, remember to update all four areas: UI, action, skill
161
- or AGENTS.md entry, and any application state the agent needs to see. That
162
- is what keeps the agent and the UI in parity.
646
+ Restart `pnpm dev` so the new actions and the migration load. Then add a note
647
+ on the **Notes** page, and ask a chat to "Create a note called Groceries with
648
+ milk and eggs". Both notes are in the list.
163
649
 
164
650
  ## What's next
165
651
 
166
- - [**Chat**](/docs/template-chat) — what ships by default and when to pick it
167
- - [**Your First Feature in Chat**](/docs/template-chat-first-edits) — the
168
- starter chat route, annotated, plus a full feature walkthrough
169
- - [**Actions**](/docs/actions-overview) — the action system this template is
170
- built on
171
- - [**Database**](/docs/server-database) — connecting PostgreSQL and the
172
- framework data model
173
- - [**Agent Observability**](/docs/observability) — what the dashboard tracks
174
- and how
175
- - [**Extensions**](/docs/extensions) — building mini-apps inside a template
176
- - [**Authentication**](/docs/authentication) — sessions, organizations, and
177
- protected routes
652
+ - [**Chat**](/docs/template-chat): what ships by default and who it's for
653
+ - [**Features**](/docs/template-chat-features): conversations, the example action, teams, and admin pages
654
+ - [**Actions**](/docs/actions-overview): the action system this template is built on
655
+ - [**Native Chat UI**](/docs/native-chat-ui): chat surface primitives and runtime options
656
+ - [**Database**](/docs/server-database): connecting PostgreSQL and the framework data model
657
+ - [**Agent Observability**](/docs/observability): what the dashboard tracks and how
658
+ - [**Extensions**](/docs/extensions): building mini-apps inside a template
659
+ - [**Authentication**](/docs/authentication): sessions, organizations, and protected routes