@agent-native/core 0.141.4 → 0.141.5
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/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/docs/AGENTS.md +1 -1
- package/docs/content/actions.mdx +1 -1
- package/docs/content/agent-surfaces.mdx +1 -1
- package/docs/content/agents.mdx +9 -0
- package/docs/content/database.mdx +2 -2
- package/docs/content/drop-in-agent.mdx +1 -1
- package/docs/content/notifications.mdx +1 -1
- package/docs/content/routing.mdx +1 -1
- package/docs/content/server-database.mdx +437 -0
- package/docs/content/server-middleware.mdx +151 -0
- package/docs/content/server-overview.mdx +234 -0
- package/docs/content/server-plugins.mdx +147 -0
- package/docs/content/server-routes.mdx +194 -0
- package/package.json +1 -1
- package/docs/content/server.mdx +0 -469
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Routes"
|
|
3
|
+
description: "File-based Nitro server routes, the SSR catch-all, naming conventions, and scoping custom routes to the authenticated user."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Routes
|
|
7
|
+
|
|
8
|
+
Server routing in an agent-native app is split across two surfaces.
|
|
9
|
+
|
|
10
|
+
**Framework routes** at `/_agent-native/*` are managed by the framework. They expose the agent chat loop, sync polling, action endpoints, and other core capabilities. You do not create or modify these routes.
|
|
11
|
+
|
|
12
|
+
**Custom file routes** in `server/routes/` are where you add your own endpoints. Nitro picks them up automatically based on where they sit in the directory tree — no registration or config needed. The filename sets the HTTP method, the directory path becomes the URL path, and each file exports a single handler.
|
|
13
|
+
|
|
14
|
+
## Framework-Mounted Routes {#framework-routes}
|
|
15
|
+
|
|
16
|
+
The framework reserves the `/_agent-native/` namespace for its own endpoints. Never put custom routes here. This table is representative, not exhaustive.
|
|
17
|
+
|
|
18
|
+
| Route prefix | Purpose |
|
|
19
|
+
| -------------------------------- | ------------------------------------------------------------------------------- |
|
|
20
|
+
| `/_agent-native/actions/:name` | Action HTTP endpoints |
|
|
21
|
+
| `/_agent-native/agent-chat` | Agent chat loop |
|
|
22
|
+
| `/_agent-native/poll` | SQL-backed UI sync |
|
|
23
|
+
| `/_agent-native/runs` | Progress primitive. The `RunsTray` polls this; see [Progress](/docs/progress) |
|
|
24
|
+
| `/_agent-native/resources/*` | Workspace resources |
|
|
25
|
+
| `/_agent-native/extensions/*` | Runtime extensions and extension proxy (legacy alias: `/_agent-native/tools/*`) |
|
|
26
|
+
| `/_agent-native/integrations/*` | Messaging/webhook integrations |
|
|
27
|
+
| `/_agent-native/a2a` | Agent-to-agent JSON-RPC |
|
|
28
|
+
| `/mcp` | MCP endpoint |
|
|
29
|
+
| `/_agent-native/onboarding/*` | Setup checklist |
|
|
30
|
+
| `/_agent-native/observability/*` | Traces, feedback, evals, experiments |
|
|
31
|
+
| `/_agent-native/file-upload` | File upload provider endpoint |
|
|
32
|
+
|
|
33
|
+
Custom app routes go in `/api/*` or another path that doesn't collide with `/_agent-native/`.
|
|
34
|
+
|
|
35
|
+
## When to Use a Custom Route {#when-custom-routes}
|
|
36
|
+
|
|
37
|
+
Actions cover the vast majority of app operations. Reach for a file route in `server/routes/` only when you need something an action can't express.
|
|
38
|
+
|
|
39
|
+
<Comparison>
|
|
40
|
+
|
|
41
|
+
### Use a custom route {#use-custom-route}
|
|
42
|
+
|
|
43
|
+
- File uploads or multipart form data
|
|
44
|
+
- Streaming responses or server-sent events (SSE)
|
|
45
|
+
- Inbound webhooks that need raw body verification
|
|
46
|
+
- OAuth callbacks
|
|
47
|
+
- Public unauthenticated pages
|
|
48
|
+
- External REST contracts that require a specific URL path or response shape
|
|
49
|
+
|
|
50
|
+
### Use an action instead {#use-action-instead}
|
|
51
|
+
|
|
52
|
+
- Operations the UI and agent both need
|
|
53
|
+
- Reading or writing app data
|
|
54
|
+
- HTTP-shaped endpoints (use the `http` option in [Actions](/docs/actions))
|
|
55
|
+
- Anything that should be an agent tool, MCP tool, or CLI command
|
|
56
|
+
- Standard JSON request/response APIs
|
|
57
|
+
|
|
58
|
+
</Comparison>
|
|
59
|
+
|
|
60
|
+
### Example: Inbound webhook {#example-webhook}
|
|
61
|
+
|
|
62
|
+
Webhooks are a clear case for a custom route. External services send a raw HTTP body that must be read as bytes before parsing, so you can verify the request signature. Actions receive pre-parsed JSON and cannot access the raw body, which makes signature verification impossible inside one.
|
|
63
|
+
|
|
64
|
+
The route below handles an inbound webhook at `POST /api/webhooks/events`. It reads the raw body, verifies an HMAC signature, then processes the payload.
|
|
65
|
+
|
|
66
|
+
```ts filename="server/routes/api/webhooks/events.post.ts"
|
|
67
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
68
|
+
import { createError, defineEventHandler, getHeader, readRawBody } from "h3";
|
|
69
|
+
|
|
70
|
+
export default defineEventHandler(async (event) => {
|
|
71
|
+
const secret = process.env.WEBHOOK_SECRET;
|
|
72
|
+
const signature = getHeader(event, "x-webhook-signature");
|
|
73
|
+
const rawBody = await readRawBody(event);
|
|
74
|
+
|
|
75
|
+
if (!secret || !signature || !rawBody) {
|
|
76
|
+
throw createError({ statusCode: 400, statusMessage: "Bad Request" });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
|
|
80
|
+
if (!timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
|
|
81
|
+
throw createError({ statusCode: 401, statusMessage: "Invalid signature" });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const payload = JSON.parse(rawBody) as { type: string; data: unknown };
|
|
85
|
+
// handle payload.type ...
|
|
86
|
+
|
|
87
|
+
return { received: true };
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Nitro maps this file to `POST /api/webhooks/events` automatically. The `WEBHOOK_SECRET` environment variable should be set to the signing secret provided by the external service.
|
|
92
|
+
|
|
93
|
+
## File-Based Routes {#file-based-routes}
|
|
94
|
+
|
|
95
|
+
The directory path becomes the URL path, and the filename suffix sets the HTTP method. A file at `server/routes/api/items/[id].get.ts` maps to `GET /api/items/:id` — `items/` comes from the directory, `[id]` becomes the `:id` param, and `.get` sets the method.
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
server/routes/
|
|
99
|
+
api/
|
|
100
|
+
items/
|
|
101
|
+
index.get.ts -> GET /api/items
|
|
102
|
+
index.post.ts -> POST /api/items
|
|
103
|
+
[id].get.ts -> GET /api/items/:id
|
|
104
|
+
[id].patch.ts -> PATCH /api/items/:id
|
|
105
|
+
[id].delete.ts -> DELETE /api/items/:id
|
|
106
|
+
webhooks/
|
|
107
|
+
stripe.post.ts -> POST /api/webhooks/stripe
|
|
108
|
+
[...page].get.ts -> SSR catch-all for public pages
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`defineEventHandler` is the H3 primitive for writing a route. It wraps your handler function and gives it access to the `event` object, which carries the request, response, router params, query string, headers, and body. Returning a value from the handler sends it as the response — a plain object becomes JSON automatically. Throwing a `createError` sends the appropriate HTTP error status.
|
|
112
|
+
|
|
113
|
+
Router params like `[id]` are read from `event` with `getRouterParam`:
|
|
114
|
+
|
|
115
|
+
```ts filename="server/routes/api/items/[id].get.ts"
|
|
116
|
+
import { defineEventHandler, getRouterParam, createError } from "h3";
|
|
117
|
+
|
|
118
|
+
export default defineEventHandler((event) => {
|
|
119
|
+
const id = getRouterParam(event, "id");
|
|
120
|
+
if (!id) throw createError({ statusCode: 400, statusMessage: "Missing id" });
|
|
121
|
+
// fetch and return the item by id
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Naming conventions {#naming-conventions}
|
|
126
|
+
|
|
127
|
+
| File path (relative to `server/routes/`) | HTTP method | URL path |
|
|
128
|
+
| ---------------------------------------- | ----------- | ---------------- |
|
|
129
|
+
| `api/items/index.get.ts` | GET | `/api/items` |
|
|
130
|
+
| `api/items/index.post.ts` | POST | `/api/items` |
|
|
131
|
+
| `api/items/[id].get.ts` | GET | `/api/items/:id` |
|
|
132
|
+
| `api/items/[id].patch.ts` | PATCH | `/api/items/:id` |
|
|
133
|
+
| `api/items/[id].delete.ts` | DELETE | `/api/items/:id` |
|
|
134
|
+
| `api/items/[...slug].get.ts` | GET | `/api/items/*` |
|
|
135
|
+
|
|
136
|
+
## Request Context and Access {#request-context}
|
|
137
|
+
|
|
138
|
+
Actions mounted by the framework automatically run with request context. Custom routes do not. If a custom route reads or writes ownable resources, load the session and wrap the work:
|
|
139
|
+
|
|
140
|
+
<AnnotatedCode
|
|
141
|
+
id="doc-block-server-routes-ctx"
|
|
142
|
+
title="Scoping a custom route to the request user"
|
|
143
|
+
filename="server/routes/api/projects.get.ts"
|
|
144
|
+
language="ts"
|
|
145
|
+
code={
|
|
146
|
+
'import { defineEventHandler, createError } from "h3";\nimport { getSession, runWithRequestContext } from "@agent-native/core/server";\nimport { getDb } from "../../db/index.js";\nimport { accessFilter } from "@agent-native/core/sharing";\nimport * as schema from "../../db/schema";\n\nexport default defineEventHandler(async (event) => {\n const session = await getSession(event);\n if (!session?.email) {\n throw createError({ statusCode: 401, statusMessage: "Unauthorized" });\n }\n\n return runWithRequestContext(\n { userEmail: session.email, orgId: session.orgId },\n async () => {\n const db = getDb();\n return db\n .select()\n .from(schema.projects)\n .where(accessFilter(schema.projects, schema.projectShares));\n },\n );\n});'
|
|
147
|
+
}
|
|
148
|
+
annotations={[
|
|
149
|
+
{
|
|
150
|
+
lines: "8-11",
|
|
151
|
+
label: "Custom routes have no auto-context",
|
|
152
|
+
note: "Unlike actions, a file route must load the session itself and fail closed when there is no authenticated user.",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
lines: "13",
|
|
156
|
+
label: "Establish request context",
|
|
157
|
+
note: "`runWithRequestContext` makes the user/org available to scoping helpers for the duration of the work.",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
lines: "17-20",
|
|
161
|
+
label: "Scope ownable reads",
|
|
162
|
+
note: "`accessFilter` constrains the query to rows the caller may see. Never run an unscoped `db.select().from(ownableTable)` here.",
|
|
163
|
+
},
|
|
164
|
+
]}
|
|
165
|
+
/>
|
|
166
|
+
|
|
167
|
+
`getDb` is created per app via `createGetDb(schema)` in `server/db/index.ts`, so custom routes import it from the template path (`../../db/index.js`), not from `@agent-native/core/db`. See [Database](/docs/server-database#db-client).
|
|
168
|
+
|
|
169
|
+
<Callout tone="warning">
|
|
170
|
+
Do not run unscoped `db.select().from(ownableTable)` in custom routes. Always
|
|
171
|
+
establish request context with `runWithRequestContext` before querying ownable
|
|
172
|
+
data.
|
|
173
|
+
</Callout>
|
|
174
|
+
|
|
175
|
+
## SSR Catch-All {#ssr-catch-all}
|
|
176
|
+
|
|
177
|
+
The `[...page].get.ts` route at the root of `server/routes/` handles server-side rendering for public-facing pages. It uses `createH3SSRHandler` to hand off to the React Router build:
|
|
178
|
+
|
|
179
|
+
```ts filename="server/routes/[...page].get.ts"
|
|
180
|
+
import { createH3SSRHandler } from "@agent-native/core/server/ssr-handler";
|
|
181
|
+
|
|
182
|
+
export default createH3SSRHandler(
|
|
183
|
+
() => import("virtual:react-router/server-build"),
|
|
184
|
+
);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Every SSR response is an impersonal, public shell cached at the CDN. Never read cookies, session state, or auth branches in this handler. Personalization happens client-side after hydration.
|
|
188
|
+
|
|
189
|
+
## What's next {#whats-next}
|
|
190
|
+
|
|
191
|
+
- [**Actions**](/docs/actions): the preferred surface for app operations; reach for a route only when it doesn't fit
|
|
192
|
+
- [**Plugins**](/docs/server-plugins): startup plugins that run before routes are served
|
|
193
|
+
- [**Middleware**](/docs/server-middleware): auth guards that protect all routes including `/api/*`
|
|
194
|
+
- [**Security**](/docs/security#access-guards): `accessFilter`/`assertAccess` and the full data-scoping model
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.141.
|
|
3
|
+
"version": "0.141.5",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
package/docs/content/server.mdx
DELETED
|
@@ -1,469 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: "Server"
|
|
3
|
-
description: "Nitro server routes, plugins, framework-mounted routes, request context, and SQL-backed sync."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Server
|
|
7
|
-
|
|
8
|
-
Agent-native apps use [Nitro](https://nitro.build) for server routes and plugins. Most product behavior should live in [Actions](/docs/actions); custom routes are for protocol surfaces that actions do not fit: uploads, streaming, public pages, webhooks, OAuth callbacks, and provider-specific APIs.
|
|
9
|
-
|
|
10
|
-
<Diagram id="doc-block-1cve8m0" title="What runs on the server" summary="Actions are the default. Custom file routes and framework-mounted routes share the same Nitro app and the same SQL database.">
|
|
11
|
-
|
|
12
|
-
```html
|
|
13
|
-
<div class="diagram-server">
|
|
14
|
-
<div class="diagram-col entry">
|
|
15
|
-
<div class="diagram-node">Browser / UI</div>
|
|
16
|
-
<div class="diagram-node">Agent loop</div>
|
|
17
|
-
<div class="diagram-node">
|
|
18
|
-
External clients<br /><small class="diagram-muted"
|
|
19
|
-
>HTTP · MCP · A2A</small
|
|
20
|
-
>
|
|
21
|
-
</div>
|
|
22
|
-
</div>
|
|
23
|
-
<div class="diagram-arrow diagram-muted" aria-hidden="true">→</div>
|
|
24
|
-
<div class="diagram-panel" data-rough>
|
|
25
|
-
<strong>Nitro server</strong>
|
|
26
|
-
<div class="diagram-row">
|
|
27
|
-
<span class="diagram-pill accent">Actions</span
|
|
28
|
-
><small class="diagram-muted">default surface</small>
|
|
29
|
-
</div>
|
|
30
|
-
<div class="diagram-row">
|
|
31
|
-
<span class="diagram-pill">/_agent-native/*</span
|
|
32
|
-
><small class="diagram-muted">framework routes</small>
|
|
33
|
-
</div>
|
|
34
|
-
<div class="diagram-row">
|
|
35
|
-
<span class="diagram-pill">/api/*</span
|
|
36
|
-
><small class="diagram-muted">custom file routes</small>
|
|
37
|
-
</div>
|
|
38
|
-
<div class="diagram-row">
|
|
39
|
-
<span class="diagram-pill">plugins</span
|
|
40
|
-
><small class="diagram-muted">startup: migrations, jobs</small>
|
|
41
|
-
</div>
|
|
42
|
-
</div>
|
|
43
|
-
<div class="diagram-arrow diagram-muted" aria-hidden="true">→</div>
|
|
44
|
-
<div class="diagram-box" data-rough>
|
|
45
|
-
SQL database<br /><small class="diagram-muted"
|
|
46
|
-
>Drizzle · the coordination point</small
|
|
47
|
-
>
|
|
48
|
-
</div>
|
|
49
|
-
</div>
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
```css
|
|
53
|
-
.diagram-server {
|
|
54
|
-
display: flex;
|
|
55
|
-
align-items: center;
|
|
56
|
-
gap: 14px;
|
|
57
|
-
flex-wrap: wrap;
|
|
58
|
-
}
|
|
59
|
-
.diagram-server .diagram-col {
|
|
60
|
-
display: flex;
|
|
61
|
-
flex-direction: column;
|
|
62
|
-
gap: 10px;
|
|
63
|
-
}
|
|
64
|
-
.diagram-server .diagram-panel {
|
|
65
|
-
display: flex;
|
|
66
|
-
flex-direction: column;
|
|
67
|
-
gap: 8px;
|
|
68
|
-
padding: 14px 16px;
|
|
69
|
-
}
|
|
70
|
-
.diagram-server .diagram-row {
|
|
71
|
-
display: flex;
|
|
72
|
-
align-items: center;
|
|
73
|
-
gap: 8px;
|
|
74
|
-
}
|
|
75
|
-
.diagram-server .diagram-arrow {
|
|
76
|
-
font-size: 22px;
|
|
77
|
-
line-height: 1;
|
|
78
|
-
}
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
</Diagram>
|
|
82
|
-
|
|
83
|
-
## File-Based Routes {#file-based-routes}
|
|
84
|
-
|
|
85
|
-
Routes live in `server/routes/` and Nitro maps filenames to methods and paths:
|
|
86
|
-
|
|
87
|
-
```text
|
|
88
|
-
server/routes/
|
|
89
|
-
api/
|
|
90
|
-
health.get.ts -> GET /api/health
|
|
91
|
-
uploads.post.ts -> POST /api/uploads
|
|
92
|
-
webhooks/
|
|
93
|
-
stripe.post.ts -> POST /api/webhooks/stripe
|
|
94
|
-
[...page].get.ts -> SSR catch-all for public pages
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
Each route exports a `defineEventHandler`:
|
|
98
|
-
|
|
99
|
-
```ts filename="server/routes/api/health.get.ts"
|
|
100
|
-
import { defineEventHandler } from "h3";
|
|
101
|
-
|
|
102
|
-
export default defineEventHandler(() => ({
|
|
103
|
-
ok: true,
|
|
104
|
-
service: "my-template",
|
|
105
|
-
}));
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### Route naming conventions {#route-naming-conventions}
|
|
109
|
-
|
|
110
|
-
| File name pattern | HTTP method | Example path |
|
|
111
|
-
| ------------------ | ----------- | --------------------------- |
|
|
112
|
-
| `index.get.ts` | GET | `/api/items` |
|
|
113
|
-
| `index.post.ts` | POST | `/api/items` |
|
|
114
|
-
| `[id].get.ts` | GET | `/api/items/:id` |
|
|
115
|
-
| `[id].patch.ts` | PATCH | `/api/items/:id` |
|
|
116
|
-
| `[id].delete.ts` | DELETE | `/api/items/:id` |
|
|
117
|
-
| `[...slug].get.ts` | GET | `/api/items/*` or catch-all |
|
|
118
|
-
|
|
119
|
-
## Prefer Actions For App Operations {#actions-first}
|
|
120
|
-
|
|
121
|
-
If the UI and agent both need to do something, define an action instead of a custom API route. Actions automatically become:
|
|
122
|
-
|
|
123
|
-
- Agent tools.
|
|
124
|
-
- Typed frontend hooks.
|
|
125
|
-
- HTTP endpoints under `/_agent-native/actions/:name`.
|
|
126
|
-
- MCP and A2A-callable tools.
|
|
127
|
-
- CLI commands for development.
|
|
128
|
-
|
|
129
|
-
Use custom `/api/*` routes only when you need a route-shaped protocol or binary/streaming behavior. See [Actions](/docs/actions).
|
|
130
|
-
|
|
131
|
-
### API-Only Apps {#api-only-apps}
|
|
132
|
-
|
|
133
|
-
You do not need an Express server just because the app is API-only. An agent-native app already ships with a Nitro/H3 server, and `defineAction()` is the default API surface. Define operations in `actions/`; the framework mounts each action at `/_agent-native/actions/<name>` with schema validation and request context.
|
|
134
|
-
|
|
135
|
-
Use the action `http` option to shape direct HTTP access: actions are `POST` by default, `http: { method: "GET" | "PUT" | "DELETE" }` changes the verb, and `http: false` keeps an action off HTTP. Reach for `server/routes/api/*` only for route-shaped or protocol concerns: file uploads, streaming/SSE, webhooks, OAuth callbacks, public pages, or an external REST shape that cannot be represented cleanly as an action. Do not create `/api/*` routes that mostly wrap or re-export actions.
|
|
136
|
-
|
|
137
|
-
For a read-only status endpoint, keep it as an action:
|
|
138
|
-
|
|
139
|
-
```ts filename="actions/get-status.ts"
|
|
140
|
-
import { defineAction } from "@agent-native/core/action";
|
|
141
|
-
import { z } from "zod";
|
|
142
|
-
|
|
143
|
-
export default defineAction({
|
|
144
|
-
description: "Return API service status",
|
|
145
|
-
schema: z.object({
|
|
146
|
-
service: z.string().optional(),
|
|
147
|
-
}),
|
|
148
|
-
http: { method: "GET" },
|
|
149
|
-
run: async ({ service }) => ({
|
|
150
|
-
ok: true,
|
|
151
|
-
service: service ?? "api",
|
|
152
|
-
checkedAt: new Date().toISOString(),
|
|
153
|
-
}),
|
|
154
|
-
});
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
Call it directly over HTTP:
|
|
158
|
-
|
|
159
|
-
```http
|
|
160
|
-
GET /_agent-native/actions/get-status?service=billing
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
For `POST` and other write actions, send a JSON body to the same action path.
|
|
164
|
-
|
|
165
|
-
## One-Shot Text Completion {#complete-text}
|
|
166
|
-
|
|
167
|
-
Most AI work should go through the agent chat so users can see, steer, and audit
|
|
168
|
-
what happened. For narrow server-side transforms that intentionally do not need
|
|
169
|
-
tools, chat history, or run state, use `completeText()` as an explicit escape
|
|
170
|
-
hatch.
|
|
171
|
-
|
|
172
|
-
```ts filename="actions/classify-message.ts"
|
|
173
|
-
import { defineAction } from "@agent-native/core/action";
|
|
174
|
-
import { completeText } from "@agent-native/core/server";
|
|
175
|
-
import { z } from "zod";
|
|
176
|
-
|
|
177
|
-
export default defineAction({
|
|
178
|
-
description: "Classify a short message",
|
|
179
|
-
schema: z.object({ body: z.string() }),
|
|
180
|
-
run: async ({ body }) => {
|
|
181
|
-
const result = await completeText({
|
|
182
|
-
systemPrompt:
|
|
183
|
-
"Return exactly one label: urgent, follow-up, waiting, or archive.",
|
|
184
|
-
input: body,
|
|
185
|
-
maxOutputTokens: 16,
|
|
186
|
-
temperature: 0,
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
return { label: result.text.trim() };
|
|
190
|
-
},
|
|
191
|
-
});
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
`completeText()` runs through the same configured engine layer as the agent
|
|
195
|
-
chat, including Builder, Anthropic, AI SDK providers, user/app model defaults,
|
|
196
|
-
request-scoped secrets, and engine-normalized errors. It is server-only; do not
|
|
197
|
-
call model providers from client code. If the operation is user-facing, wrap it
|
|
198
|
-
in an action so the UI and agent share the same capability.
|
|
199
|
-
|
|
200
|
-
## Request Context And Access {#request-context}
|
|
201
|
-
|
|
202
|
-
Actions mounted by the framework automatically run with request context. Custom routes do not. If a custom route reads or writes ownable resources, load the session and wrap the work:
|
|
203
|
-
|
|
204
|
-
<AnnotatedCode
|
|
205
|
-
id="doc-block-z4ll"
|
|
206
|
-
title="Scoping a custom route to the request user"
|
|
207
|
-
filename="server/routes/api/projects.get.ts"
|
|
208
|
-
language="ts"
|
|
209
|
-
code={
|
|
210
|
-
'import { defineEventHandler, createError } from "h3";\nimport { getSession, runWithRequestContext } from "@agent-native/core/server";\nimport { getDb } from "../../db/index.js";\nimport { accessFilter } from "@agent-native/core/sharing";\nimport * as schema from "../../db/schema";\n\nexport default defineEventHandler(async (event) => {\n const session = await getSession(event);\n if (!session?.email) {\n throw createError({ statusCode: 401, statusMessage: "Unauthorized" });\n }\n\n return runWithRequestContext(\n { userEmail: session.email, orgId: session.orgId },\n async () => {\n const db = getDb();\n return db\n .select()\n .from(schema.projects)\n .where(accessFilter(schema.projects, schema.projectShares));\n },\n );\n});'
|
|
211
|
-
}
|
|
212
|
-
annotations={[
|
|
213
|
-
{
|
|
214
|
-
lines: "7-10",
|
|
215
|
-
label: "Custom routes have no auto-context",
|
|
216
|
-
note: "Unlike actions, a file route must load the session itself and fail closed when there is no authenticated user.",
|
|
217
|
-
},
|
|
218
|
-
{
|
|
219
|
-
lines: "12-13",
|
|
220
|
-
label: "Establish request context",
|
|
221
|
-
note: "`runWithRequestContext` makes the user/org available to scoping helpers for the duration of the work.",
|
|
222
|
-
},
|
|
223
|
-
{
|
|
224
|
-
lines: "18-19",
|
|
225
|
-
label: "Scope ownable reads",
|
|
226
|
-
note: "`accessFilter` constrains the query to rows the caller may see. Never run an unscoped `db.select().from(ownableTable)` here.",
|
|
227
|
-
},
|
|
228
|
-
]}
|
|
229
|
-
/>
|
|
230
|
-
|
|
231
|
-
`getDb` is created per app via `createGetDb(schema)` in `server/db/index.ts`, so custom routes import it from the template (`../../db/index.js`), not from `@agent-native/core/db`; see [Database — Where the DB Client Lives](/docs/database#db-client). Do not run unscoped `db.select().from(ownableTable)` in custom routes.
|
|
232
|
-
|
|
233
|
-
## Server Plugins {#server-plugins}
|
|
234
|
-
|
|
235
|
-
Plugins live in `server/plugins/` and run at startup. Use them for migrations, provider setup, recurring jobs, integration adapters, and framework plugin configuration.
|
|
236
|
-
|
|
237
|
-
```ts filename="server/plugins/db.ts"
|
|
238
|
-
import { runMigrations } from "@agent-native/core/db";
|
|
239
|
-
|
|
240
|
-
export default runMigrations(
|
|
241
|
-
[
|
|
242
|
-
{
|
|
243
|
-
version: 1,
|
|
244
|
-
sql: `CREATE TABLE IF NOT EXISTS projects (
|
|
245
|
-
id TEXT PRIMARY KEY,
|
|
246
|
-
title TEXT NOT NULL,
|
|
247
|
-
owner_email TEXT NOT NULL,
|
|
248
|
-
org_id TEXT,
|
|
249
|
-
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
250
|
-
)`,
|
|
251
|
-
},
|
|
252
|
-
],
|
|
253
|
-
{ table: "my_app_migrations" },
|
|
254
|
-
);
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
Migrations must be additive. Never put destructive SQL in startup plugins.
|
|
258
|
-
|
|
259
|
-
## Framework-Mounted Routes {#framework-routes}
|
|
260
|
-
|
|
261
|
-
The framework mounts its own routes under `/_agent-native/`. Treat that namespace as reserved. This table is representative, not exhaustive — it lists the routes most templates touch directly.
|
|
262
|
-
|
|
263
|
-
| Route prefix | Purpose |
|
|
264
|
-
| -------------------------------- | ------------------------------------------------------------------------------- |
|
|
265
|
-
| `/_agent-native/actions/:name` | Action HTTP endpoints |
|
|
266
|
-
| `/_agent-native/agent-chat` | Agent chat loop |
|
|
267
|
-
| `/_agent-native/poll` | SQL-backed UI sync |
|
|
268
|
-
| `/_agent-native/runs` | Progress primitive — the `RunsTray` polls this; see [Progress](/docs/progress) |
|
|
269
|
-
| `/_agent-native/resources/*` | Workspace resources |
|
|
270
|
-
| `/_agent-native/extensions/*` | Runtime extensions and extension proxy (legacy alias: `/_agent-native/tools/*`) |
|
|
271
|
-
| `/_agent-native/integrations/*` | Messaging/webhook integrations |
|
|
272
|
-
| `/_agent-native/a2a` | Agent-to-agent JSON-RPC |
|
|
273
|
-
| `/mcp` | MCP endpoint |
|
|
274
|
-
| `/_agent-native/onboarding/*` | Setup checklist |
|
|
275
|
-
| `/_agent-native/observability/*` | Traces, feedback, evals, experiments |
|
|
276
|
-
| `/_agent-native/file-upload` | File upload provider endpoint |
|
|
277
|
-
|
|
278
|
-
Custom app routes should use `/api/*`, public app paths, or provider-specific callback paths that do not collide with `/_agent-native/`.
|
|
279
|
-
|
|
280
|
-
## SQL-Backed Sync {#sync}
|
|
281
|
-
|
|
282
|
-
Agent-native does not rely on filesystem watchers or sticky in-memory state. When actions or framework helpers mutate data, the database sync version increments. The client `useDbSync()` hook polls `/_agent-native/poll` and invalidates React Query caches.
|
|
283
|
-
|
|
284
|
-
This works across serverless and multi-instance deployments because the database is the coordination point. If you write custom mutations outside actions, use framework helpers or emit the appropriate sync invalidation so open UIs refresh.
|
|
285
|
-
|
|
286
|
-
<Diagram id="doc-block-qhkdv8" title="SQL-backed sync loop" summary={"No watchers, no sticky state. A write bumps a version in SQL; every client polls the version and refetches."}>
|
|
287
|
-
|
|
288
|
-
```html
|
|
289
|
-
<div class="diagram-sync">
|
|
290
|
-
<div class="diagram-box" data-rough>
|
|
291
|
-
Action / helper<br /><small class="diagram-muted">mutates data</small>
|
|
292
|
-
</div>
|
|
293
|
-
<div class="diagram-arrow diagram-muted" aria-hidden="true">→</div>
|
|
294
|
-
<div class="diagram-panel" data-rough>
|
|
295
|
-
<strong>SQL database</strong
|
|
296
|
-
><small class="diagram-muted">sync version increments</small>
|
|
297
|
-
</div>
|
|
298
|
-
<div class="diagram-arrow diagram-muted" aria-hidden="true">←</div>
|
|
299
|
-
<div class="diagram-col">
|
|
300
|
-
<div class="diagram-node">
|
|
301
|
-
useDbSync()<br /><small class="diagram-muted"
|
|
302
|
-
>polls /_agent-native/poll</small
|
|
303
|
-
>
|
|
304
|
-
</div>
|
|
305
|
-
<div class="diagram-pill ok">invalidate caches → UI refreshes</div>
|
|
306
|
-
</div>
|
|
307
|
-
</div>
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
```css
|
|
311
|
-
.diagram-sync {
|
|
312
|
-
display: flex;
|
|
313
|
-
align-items: center;
|
|
314
|
-
gap: 14px;
|
|
315
|
-
flex-wrap: wrap;
|
|
316
|
-
}
|
|
317
|
-
.diagram-sync .diagram-col {
|
|
318
|
-
display: flex;
|
|
319
|
-
flex-direction: column;
|
|
320
|
-
gap: 8px;
|
|
321
|
-
align-items: flex-start;
|
|
322
|
-
}
|
|
323
|
-
.diagram-sync .diagram-arrow {
|
|
324
|
-
font-size: 22px;
|
|
325
|
-
line-height: 1;
|
|
326
|
-
}
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
</Diagram>
|
|
330
|
-
|
|
331
|
-
<Endpoint id="doc-block-1s11d2e" title="The poll endpoint" method="GET" path="/_agent-native/poll" summary="Return the current per-source database sync versions so the client can detect changes." auth="Session cookie (request-scoped identity)" responses={[
|
|
332
|
-
{
|
|
333
|
-
"status": "200",
|
|
334
|
-
"description": "Current sync versions keyed by source."
|
|
335
|
-
}
|
|
336
|
-
]}>
|
|
337
|
-
|
|
338
|
-
`useDbSync()` calls this on an interval (and falls back to it when SSE is unavailable). When a returned version is higher than the client's last-seen value, the matching React Query caches are invalidated and refetch.
|
|
339
|
-
|
|
340
|
-
</Endpoint>
|
|
341
|
-
|
|
342
|
-
## Webhooks {#webhooks}
|
|
343
|
-
|
|
344
|
-
Inbound webhooks should verify, persist, and return quickly. Long-running agent work should use the integration queue pattern:
|
|
345
|
-
|
|
346
|
-
1. Verify the platform signature or challenge.
|
|
347
|
-
2. Insert durable work into SQL.
|
|
348
|
-
3. Self-fire a signed processor route.
|
|
349
|
-
4. Return 200 immediately.
|
|
350
|
-
5. Let the fresh processor execution run the agent loop and post the result.
|
|
351
|
-
|
|
352
|
-
<Diagram id="doc-block-1v3q81q" title="Integration queue pattern" summary={"The webhook handler returns in milliseconds; a separate signed execution runs the slow agent work."}>
|
|
353
|
-
|
|
354
|
-
```html
|
|
355
|
-
<div class="diagram-webhook">
|
|
356
|
-
<div class="diagram-box" data-rough>
|
|
357
|
-
Inbound webhook<br /><small class="diagram-muted"
|
|
358
|
-
>Slack · Stripe · email</small
|
|
359
|
-
>
|
|
360
|
-
</div>
|
|
361
|
-
<div class="diagram-arrow diagram-muted" aria-hidden="true">→</div>
|
|
362
|
-
<div class="diagram-panel" data-rough>
|
|
363
|
-
<strong>Handler</strong>
|
|
364
|
-
<div class="diagram-step">
|
|
365
|
-
<span class="diagram-pill">1</span
|
|
366
|
-
><small class="diagram-muted">verify signature</small>
|
|
367
|
-
</div>
|
|
368
|
-
<div class="diagram-step">
|
|
369
|
-
<span class="diagram-pill">2</span
|
|
370
|
-
><small class="diagram-muted">insert work into SQL</small>
|
|
371
|
-
</div>
|
|
372
|
-
<div class="diagram-step">
|
|
373
|
-
<span class="diagram-pill">3</span
|
|
374
|
-
><small class="diagram-muted">self-fire processor</small>
|
|
375
|
-
</div>
|
|
376
|
-
<div class="diagram-step">
|
|
377
|
-
<span class="diagram-pill ok">4</span
|
|
378
|
-
><small class="diagram-muted">return 200 now</small>
|
|
379
|
-
</div>
|
|
380
|
-
</div>
|
|
381
|
-
<div class="diagram-arrow diagram-muted" aria-hidden="true">→</div>
|
|
382
|
-
<div class="diagram-box" data-rough>
|
|
383
|
-
Signed processor<br /><small class="diagram-muted"
|
|
384
|
-
>runs agent loop, posts result</small
|
|
385
|
-
>
|
|
386
|
-
</div>
|
|
387
|
-
</div>
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
```css
|
|
391
|
-
.diagram-webhook {
|
|
392
|
-
display: flex;
|
|
393
|
-
align-items: center;
|
|
394
|
-
gap: 14px;
|
|
395
|
-
flex-wrap: wrap;
|
|
396
|
-
}
|
|
397
|
-
.diagram-webhook .diagram-panel {
|
|
398
|
-
display: flex;
|
|
399
|
-
flex-direction: column;
|
|
400
|
-
gap: 6px;
|
|
401
|
-
padding: 14px 16px;
|
|
402
|
-
}
|
|
403
|
-
.diagram-webhook .diagram-step {
|
|
404
|
-
display: flex;
|
|
405
|
-
align-items: center;
|
|
406
|
-
gap: 8px;
|
|
407
|
-
}
|
|
408
|
-
.diagram-webhook .diagram-arrow {
|
|
409
|
-
font-size: 22px;
|
|
410
|
-
line-height: 1;
|
|
411
|
-
}
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
</Diagram>
|
|
415
|
-
|
|
416
|
-
<Callout tone="warning">
|
|
417
|
-
Do not rely on unawaited promises after returning a response — serverless
|
|
418
|
-
hosts freeze the execution. See [Messaging](/docs/messaging) for the canonical
|
|
419
|
-
integration queue.
|
|
420
|
-
</Callout>
|
|
421
|
-
|
|
422
|
-
## Advanced: Escape Hatches {#advanced-escape-hatches}
|
|
423
|
-
|
|
424
|
-
Most templates never need these. Nitro file routes and the framework's agent
|
|
425
|
-
chat plugin already wire up the app server and the production agent handler.
|
|
426
|
-
Reach for them only when building a custom server integration outside the
|
|
427
|
-
standard template plugin stack.
|
|
428
|
-
|
|
429
|
-
### Programmatic H3 servers {#create-server}
|
|
430
|
-
|
|
431
|
-
For custom packages or tests that need an H3 app directly, `createServer()`
|
|
432
|
-
returns a preconfigured app and router:
|
|
433
|
-
|
|
434
|
-
```ts
|
|
435
|
-
import { createServer } from "@agent-native/core/server";
|
|
436
|
-
import { defineEventHandler } from "h3";
|
|
437
|
-
|
|
438
|
-
const { app, router } = createServer();
|
|
439
|
-
|
|
440
|
-
router.get(
|
|
441
|
-
"/api/health",
|
|
442
|
-
defineEventHandler(() => ({ ok: true })),
|
|
443
|
-
);
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
### Production agent handler {#agent-handler}
|
|
447
|
-
|
|
448
|
-
The framework's agent chat plugin already mounts the production agent handler
|
|
449
|
-
for templates. Only call `createProductionAgentHandler()` directly when building
|
|
450
|
-
a custom server integration outside the standard template plugin stack —
|
|
451
|
-
otherwise customize the agent through `AGENTS.md`, skills, actions, and the
|
|
452
|
-
agent chat plugin.
|
|
453
|
-
|
|
454
|
-
```ts
|
|
455
|
-
import { createProductionAgentHandler } from "@agent-native/core/server";
|
|
456
|
-
|
|
457
|
-
const handler = createProductionAgentHandler({
|
|
458
|
-
scripts,
|
|
459
|
-
systemPrompt: "You are the app agent...",
|
|
460
|
-
});
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
## What's next
|
|
464
|
-
|
|
465
|
-
- [**Actions**](/docs/actions) — the default operation surface; reach for a custom route only when it doesn't fit
|
|
466
|
-
- [**Database**](/docs/database) — the `getDb()`/`schema` pattern custom routes and actions both import
|
|
467
|
-
- [**Security**](/docs/security#access-guards) — `accessFilter`/`assertAccess` and the full data-scoping model
|
|
468
|
-
- [**Messaging**](/docs/messaging) — the canonical inbound-webhook integration queue pattern
|
|
469
|
-
- [**MCP Protocol**](/docs/mcp-protocol) — the `/mcp` endpoint this server mounts automatically
|