@agent-native/core 0.63.2 → 0.63.3

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 (83) hide show
  1. package/dist/styles/blocks.css +25 -3
  2. package/docs/content/a2a-protocol.md +48 -10
  3. package/docs/content/actions.md +35 -16
  4. package/docs/content/agent-mentions.md +25 -32
  5. package/docs/content/agent-surfaces.md +31 -0
  6. package/docs/content/agent-teams.md +17 -14
  7. package/docs/content/agent-web-surfaces.md +24 -15
  8. package/docs/content/authentication.md +21 -0
  9. package/docs/content/automations.md +37 -21
  10. package/docs/content/blueprint-installer.md +7 -0
  11. package/docs/content/cli-adapters.md +7 -0
  12. package/docs/content/client.md +14 -0
  13. package/docs/content/cloneable-saas.md +14 -0
  14. package/docs/content/code-agents-ui.md +27 -0
  15. package/docs/content/components.md +21 -0
  16. package/docs/content/context-awareness.md +33 -48
  17. package/docs/content/creating-templates.md +43 -52
  18. package/docs/content/cross-app-sso.md +41 -0
  19. package/docs/content/database.md +41 -21
  20. package/docs/content/deployment.md +23 -6
  21. package/docs/content/dispatch.md +27 -0
  22. package/docs/content/drop-in-agent.md +26 -27
  23. package/docs/content/durable-resume.md +13 -1
  24. package/docs/content/embedding-sdk.md +14 -0
  25. package/docs/content/evals.md +14 -0
  26. package/docs/content/extensions.md +19 -0
  27. package/docs/content/external-agents.md +38 -1
  28. package/docs/content/faq.md +14 -0
  29. package/docs/content/file-uploads.md +20 -0
  30. package/docs/content/frames.md +14 -0
  31. package/docs/content/getting-started.md +34 -16
  32. package/docs/content/harness-agents.md +14 -0
  33. package/docs/content/human-approval.md +15 -30
  34. package/docs/content/key-concepts.md +37 -0
  35. package/docs/content/local-file-mode.md +26 -19
  36. package/docs/content/mcp-apps.md +36 -1
  37. package/docs/content/mcp-clients.md +49 -0
  38. package/docs/content/mcp-protocol.md +36 -0
  39. package/docs/content/messaging.md +34 -8
  40. package/docs/content/migration-workbench.md +7 -0
  41. package/docs/content/multi-app-workspace.md +29 -16
  42. package/docs/content/multi-tenancy.md +14 -0
  43. package/docs/content/native-chat-ui.md +20 -3
  44. package/docs/content/notifications.md +30 -0
  45. package/docs/content/observability.md +20 -1
  46. package/docs/content/observational-memory.md +14 -0
  47. package/docs/content/onboarding.md +32 -41
  48. package/docs/content/plan-plugin.md +14 -0
  49. package/docs/content/pr-visual-recap.md +14 -0
  50. package/docs/content/processors.md +7 -0
  51. package/docs/content/progress.md +23 -0
  52. package/docs/content/pure-agent-apps.md +7 -0
  53. package/docs/content/real-time-collaboration.md +19 -18
  54. package/docs/content/recurring-jobs.md +22 -19
  55. package/docs/content/routing.md +8 -0
  56. package/docs/content/sandbox-adapters.md +19 -0
  57. package/docs/content/security.md +38 -0
  58. package/docs/content/server.md +47 -25
  59. package/docs/content/sharing.md +50 -0
  60. package/docs/content/skills-guide.md +27 -42
  61. package/docs/content/template-analytics.md +60 -0
  62. package/docs/content/template-assets.md +68 -0
  63. package/docs/content/template-brain.md +83 -0
  64. package/docs/content/template-calendar.md +74 -0
  65. package/docs/content/template-chat.md +19 -0
  66. package/docs/content/template-clips.md +113 -0
  67. package/docs/content/template-content.md +133 -0
  68. package/docs/content/template-design.md +55 -0
  69. package/docs/content/template-dispatch.md +50 -0
  70. package/docs/content/template-forms.md +59 -0
  71. package/docs/content/template-mail.md +62 -0
  72. package/docs/content/template-plan.md +74 -0
  73. package/docs/content/template-slides.md +82 -0
  74. package/docs/content/template-videos.md +62 -0
  75. package/docs/content/tracking.md +21 -0
  76. package/docs/content/using-your-agent.md +14 -0
  77. package/docs/content/voice-input.md +31 -10
  78. package/docs/content/what-is-agent-native.md +25 -13
  79. package/docs/content/workspace-connections.md +49 -0
  80. package/docs/content/workspace-management.md +24 -0
  81. package/docs/content/workspace.md +50 -19
  82. package/docs/content/writing-agent-instructions.md +7 -0
  83. package/package.json +1 -1
@@ -1342,6 +1342,19 @@
1342
1342
  max-width: 100%;
1343
1343
  }
1344
1344
 
1345
+ /* Base padding so primitive text never touches the box edge, even when an author
1346
+ * diagram omits its own padding. The wildcard selectors mirror the theme-color
1347
+ * rules above so every box/card/panel variant authors use is covered. Author CSS
1348
+ * (higher specificity, scoped to the diagram instance) still overrides this when
1349
+ * it sets padding explicitly. */
1350
+ .plan-diagram-frame :is(.diagram-node, .diagram-box, [class*="box"]) {
1351
+ padding: 8px 12px;
1352
+ }
1353
+ .plan-diagram-frame
1354
+ :is(.diagram-card, .diagram-panel, [class*="card"], [class*="panel"]) {
1355
+ padding: 14px 16px;
1356
+ }
1357
+
1345
1358
  .plan-diagram-frame :is(.diagram-node, .diagram-box, .diagram-card) small {
1346
1359
  display: block;
1347
1360
  max-width: 100%;
@@ -1383,15 +1396,24 @@
1383
1396
  }
1384
1397
 
1385
1398
  .plan-diagram-frame[data-style="sketchy"] :is(.diagram-node, .diagram-box) {
1386
- padding-inline: 7px;
1399
+ padding-inline: 12px;
1387
1400
  }
1388
1401
 
1389
- .plan-diagram-frame .diagram-pill {
1402
+ .plan-diagram-frame
1403
+ :is(.diagram-pill, [class*="pill"], [class*="badge"], [class*="chip"]) {
1390
1404
  display: inline-flex;
1391
1405
  align-items: center;
1406
+ justify-content: center;
1407
+ /* A pill hugs its label. `fit-content` is a definite cross size, so a flex
1408
+ * column with the default `align-items: stretch` no longer blows the pill out
1409
+ * to full width (the number chips and short status labels stay pill-shaped).
1410
+ * The wildcard selectors match the theme-color rules so every pill/badge/chip
1411
+ * variant used across the docs diagrams stays pill-shaped, not a full bar. */
1412
+ width: fit-content;
1413
+ max-width: 100%;
1392
1414
  border: 1px solid var(--wf-line);
1393
1415
  border-radius: 999px;
1394
- padding: 2px 8px;
1416
+ padding: 3px 11px;
1395
1417
  color: var(--wf-ink);
1396
1418
  background: var(--wf-paper);
1397
1419
  }
@@ -20,6 +20,13 @@ Key concepts:
20
20
  - **Tasks** — each message creates a task with a lifecycle (submitted, working, completed, failed, canceled)
21
21
  - **JWT bearer auth** — production A2A requires `A2A_SECRET` or an explicit legacy `apiKeyEnv`
22
22
 
23
+ ```an-diagram title="One agent hands work to another" summary="A mail agent discovers the analytics agent's card, sends a JSON-RPC message, and gets a completed task back."
24
+ {
25
+ "html": "<div class=\"diagram-handoff\"><div class=\"diagram-card\"><strong>Mail agent</strong><small class=\"diagram-muted\">needs analytics</small></div><div class=\"diagram-col\"><div class=\"diagram-pill\">GET /.well-known/agent-card.json</div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-pill accent\">POST /_agent-native/a2a<br><small class=\"diagram-muted\">message/send</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&larr;</div><div class=\"diagram-pill ok\">task · completed</div></div><div class=\"diagram-card\" data-rough><strong>Analytics agent</strong><small class=\"diagram-muted\">runs run-query, returns result</small></div></div>",
26
+ "css": ".diagram-handoff{display:flex;align-items:center;gap:16px;flex-wrap:wrap}.diagram-handoff .diagram-col{display:flex;flex-direction:column;align-items:center;gap:6px}.diagram-handoff .diagram-arrow{font-size:20px;line-height:1}"
27
+ }
28
+ ```
29
+
23
30
  ## Server setup {#server-setup}
24
31
 
25
32
  Most templates get A2A through the framework agent chat plugin. If you are mounting it yourself, call `mountA2A()` in a server plugin:
@@ -112,24 +119,55 @@ All methods are called via `POST /_agent-native/a2a` with JSON-RPC 2.0 format:
112
119
  | `tasks/get` | Fetch a task by ID — used to poll an async task to completion | `id` |
113
120
  | `tasks/cancel` | Cancel a running task | `id` |
114
121
 
122
+ ```an-api title="Primary A2A endpoint" summary="All JSON-RPC methods are POSTed here. message/send shown."
123
+ {
124
+ "method": "POST",
125
+ "path": "/_agent-native/a2a",
126
+ "summary": "Send a message and wait for the completed task",
127
+ "description": "JSON-RPC 2.0 endpoint for `message/send`, `message/stream`, `tasks/get`, and `tasks/cancel`. Pass `async: true` to return immediately in `working` state and poll with `tasks/get`.",
128
+ "auth": "JWT bearer signed with A2A_SECRET (or legacy apiKeyEnv static token)",
129
+ "params": [
130
+ { "name": "Authorization", "in": "header", "type": "string", "required": false, "description": "Bearer token. Required in hosted production runtimes; optional in local dev." },
131
+ { "name": "method", "in": "body", "type": "string", "required": true, "description": "One of message/send, message/stream, tasks/get, tasks/cancel." },
132
+ { "name": "params.message", "in": "body", "type": "object", "required": false, "description": "{ role, parts[] } for message/send and message/stream." },
133
+ { "name": "params.async", "in": "body", "type": "boolean", "required": false, "description": "Return immediately in working state and poll via tasks/get. Use on serverless hosts." },
134
+ { "name": "params.id", "in": "body", "type": "string", "required": false, "description": "Task id for tasks/get and tasks/cancel." }
135
+ ],
136
+ "request": {
137
+ "contentType": "application/json",
138
+ "example": "{\n \"jsonrpc\": \"2.0\",\n \"id\": 1,\n \"method\": \"message/send\",\n \"params\": {\n \"message\": {\n \"role\": \"user\",\n \"parts\": [{ \"type\": \"text\", \"text\": \"Show signups by source\" }]\n },\n \"async\": true\n }\n}"
139
+ },
140
+ "responses": [
141
+ { "status": "200", "description": "JSON-RPC result containing the task. With async:true the task returns in working state.", "example": "{\n \"jsonrpc\": \"2.0\",\n \"id\": 1,\n \"result\": { \"id\": \"task_123\", \"status\": { \"state\": \"working\" } }\n}" },
142
+ { "status": "503", "description": "Hosted production runtime with no A2A_SECRET configured — fails closed instead of running unauthenticated." }
143
+ ]
144
+ }
145
+ ```
146
+
115
147
  When `message/send` is called with `async: true`, the JSON-RPC handler enqueues the task and self-fires a POST to an internal `/_agent-native/a2a/_process-task` route so the handler runs in a fresh function execution with its own full timeout. This route is authenticated with an HMAC token bound to the task ID (5-minute lifetime, signed with `A2A_SECRET`). It is mounted before the `/_agent-native/a2a` JSON-RPC route so h3's prefix matching does not swallow it.
116
148
 
149
+ ```an-diagram title="Async task lifecycle on serverless" summary="async:true returns working in milliseconds, then a fresh execution runs the agent loop while the caller polls."
150
+ {
151
+ "html": "<div class=\"diagram-async\"><div class=\"diagram-box\" data-rough>message/send<br><small class=\"diagram-muted\">async: true</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-panel\"><span class=\"diagram-pill\">enqueue task</span><span class=\"diagram-pill warn\">return working</span><small class=\"diagram-muted\">~milliseconds</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&darr;</div><div class=\"diagram-box\" data-rough>self-fire POST /_agent-native/a2a/_process-task<br><small class=\"diagram-muted\">HMAC token · fresh execution · full timeout</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-col\"><div class=\"diagram-pill\">tasks/get (poll)</div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&#8635;</div><div class=\"diagram-pill ok\">completed</div></div></div>",
152
+ "css": ".diagram-async{display:flex;align-items:center;gap:14px;flex-wrap:wrap}.diagram-async .diagram-panel{display:flex;flex-direction:column;align-items:center;gap:6px}.diagram-async .diagram-col{display:flex;flex-direction:column;align-items:center;gap:6px}.diagram-async .diagram-arrow{font-size:20px;line-height:1}",
153
+ "caption": "A recurring sweeper re-claims any task left in flight if the function execution dies mid-run."
154
+ }
155
+ ```
156
+
117
157
  > [!IMPORTANT]
118
158
  > **Serverless Webhook & Gateway Timeouts:**
119
159
  > Hosted environment gateways (such as Netlify, Vercel, or Cloudflare Pages) impose strict execution limits (often 10 to 30 seconds) on public-facing HTTP routes. Because agent loops can take significant time to run queries, fetch context, and execute tools, you **must use `async: true`** when calling A2A endpoints or handling external webhooks. This immediately returns a `working` status to the API gateway, keeping the connection open only for a few milliseconds, while the self-fired `/process-task` POST executes the agent loop in the background. Do not block the primary HTTP request waiting for the agent loop to finish.
120
160
 
121
- Messages contain typed parts:
161
+ Messages contain typed parts — text, structured data, and files can all travel in one message:
122
162
 
123
- ```json
163
+ ```an-annotated-code title="A2A message with typed parts"
124
164
  {
125
- "role": "user",
126
- "parts": [
127
- { "type": "text", "text": "Show signups by source" },
128
- { "type": "data", "data": { "dateRange": "last-30d" } },
129
- {
130
- "type": "file",
131
- "file": { "name": "report.csv", "mimeType": "text/csv", "bytes": "..." }
132
- }
165
+ "language": "json",
166
+ "code": "{\n \"role\": \"user\",\n \"parts\": [\n { \"type\": \"text\", \"text\": \"Show signups by source\" },\n { \"type\": \"data\", \"data\": { \"dateRange\": \"last-30d\" } },\n {\n \"type\": \"file\",\n \"file\": { \"name\": \"report.csv\", \"mimeType\": \"text/csv\", \"bytes\": \"...\" }\n }\n ]\n}",
167
+ "annotations": [
168
+ { "lines": "4", "label": "text part", "note": "Plain natural-language instruction the agent reads." },
169
+ { "lines": "5", "label": "data part", "note": "Structured JSON arguments — e.g. a date range — passed alongside the prompt." },
170
+ { "lines": "6-9", "label": "file part", "note": "Attach a file by name, `mimeType`, and base64 `bytes`." }
133
171
  ]
134
172
  }
135
173
  ```
@@ -19,6 +19,13 @@ One definition, seven consumers. This is rung 3 of the [ladder](/docs/what-is-ag
19
19
  If you are deciding whether to expose an operation headlessly, in chat, in an
20
20
  embedded sidecar, or as a full app screen, see [Agent Surfaces](/docs/agent-surfaces).
21
21
 
22
+ ```an-diagram title="One definition, seven consumers" summary="A single defineAction() fans out to every surface — agent, UI, HTTP, MCP, A2A, and CLI — with one validated schema and one run() body."
23
+ {
24
+ "html": "<div class=\"diagram-fanout\"><div class=\"diagram-panel center\" data-rough><span class=\"diagram-pill accent\">defineAction()</span><small class=\"diagram-muted\">schema + run(), defined once</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-grid\"><div class=\"diagram-node\">Agent tool<br><small class=\"diagram-muted\">JSON Schema in context</small></div><div class=\"diagram-node\">React hooks<br><small class=\"diagram-muted\">useActionQuery/Mutation</small></div><div class=\"diagram-node\">callAction()<br><small class=\"diagram-muted\">imperative client</small></div><div class=\"diagram-node\">HTTP<br><small class=\"diagram-muted\">/_agent-native/actions/:name</small></div><div class=\"diagram-node\">MCP tool<br><small class=\"diagram-muted\">external hosts</small></div><div class=\"diagram-node\">A2A tool<br><small class=\"diagram-muted\">other agent-native apps</small></div><div class=\"diagram-node\">CLI<br><small class=\"diagram-muted\">pnpm action &lt;name&gt;</small></div></div></div>",
25
+ "css": ".diagram-fanout{display:flex;align-items:center;gap:14px;flex-wrap:wrap}.diagram-fanout .center{display:flex;flex-direction:column;align-items:center;gap:4px;padding:14px 16px}.diagram-fanout .diagram-arrow{font-size:22px;line-height:1}.diagram-fanout .diagram-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}"
26
+ }
27
+ ```
28
+
22
29
  If the UI and agent both need to do something, reach for an action — not a custom
23
30
  route. For when a route-shaped protocol _is_ the right call, see [Prefer Actions
24
31
  For App Operations](/docs/server#actions-first).
@@ -65,22 +72,17 @@ around actions, not a required prerequisite for the action itself.
65
72
 
66
73
  ## Defining an action {#defining}
67
74
 
68
- ```ts
69
- // actions/reply-to-email.ts
70
- import { defineAction } from "@agent-native/core/action";
71
- import { z } from "zod";
72
-
73
- export default defineAction({
74
- description: "Reply to an email thread in the user's voice.",
75
- schema: z.object({
76
- emailId: z.string().describe("The id of the email to reply to."),
77
- body: z.string().describe("The reply body, in markdown."),
78
- }),
79
- run: async ({ emailId, body }) => {
80
- await db.insert(replies).values({ emailId, body });
81
- return { ok: true, emailId };
82
- },
83
- });
75
+ ```an-annotated-code title="Anatomy of an action"
76
+ {
77
+ "filename": "actions/reply-to-email.ts",
78
+ "language": "ts",
79
+ "code": "import { defineAction } from \"@agent-native/core/action\";\nimport { z } from \"zod\";\n\nexport default defineAction({\n description: \"Reply to an email thread in the user's voice.\",\n schema: z.object({\n emailId: z.string().describe(\"The id of the email to reply to.\"),\n body: z.string().describe(\"The reply body, in markdown.\"),\n }),\n run: async ({ emailId, body }) => {\n await db.insert(replies).values({ emailId, body });\n return { ok: true, emailId };\n },\n});",
80
+ "annotations": [
81
+ { "lines": "5", "label": "Tool surface", "note": "`description` is what the agent reads to decide when to call this. The per-field `.describe()` calls flow into the JSON Schema too." },
82
+ { "lines": "6-9", "label": "Typed contract", "note": "One schema validates input from **every** surface and is converted to JSON Schema for the model. Invalid inputs never reach `run`." },
83
+ { "lines": "10-13", "label": "One implementation", "note": "The `run` body is the single source of truth — the UI button and the agent tool both execute exactly this." }
84
+ ]
85
+ }
84
86
  ```
85
87
 
86
88
  That's it. The framework auto-discovers every file in `actions/` and mounts them on startup.
@@ -141,6 +143,23 @@ export default defineAction({
141
143
 
142
144
  For a `GET` action, `leadId` is passed as a query param: `/_agent-native/actions/get-lead?leadId=abc`.
143
145
 
146
+ ```an-api title="The auto-mounted action endpoint" method="GET" path="/_agent-native/actions/get-lead"
147
+ {
148
+ "method": "GET",
149
+ "path": "/_agent-native/actions/get-lead",
150
+ "summary": "Every action is mounted here automatically — the filename is the action name.",
151
+ "description": "POST by default; `http: { method: \"GET\" }` makes it a GET. The React hooks and `callAction` always call this path by name, regardless of any `http.path` override.",
152
+ "auth": "Session cookie; frontend calls carry `X-Agent-Native-Frontend: 1`",
153
+ "params": [
154
+ { "name": "leadId", "in": "query", "type": "string", "required": true, "description": "GET args arrive as query params; POST args arrive in the JSON body." }
155
+ ],
156
+ "responses": [
157
+ { "status": "200", "description": "The action's return value as JSON." },
158
+ { "status": "400", "description": "Input failed schema validation before run() fired." }
159
+ ]
160
+ }
161
+ ```
162
+
144
163
  - **`http: { method: "GET" | "POST" | "PUT" | "DELETE" }`** — default `POST`. `GET` actions are auto-marked `readOnly` so successful calls don't trigger a UI poll-refresh.
145
164
  - **`http: { path: "..." }`** — override the mounted URL under `/_agent-native/actions/`. Defaults to the filename. **Path overrides change the URL only for direct HTTP callers** — `useActionQuery`, `useActionMutation`, and `callAction` always call `/_agent-native/actions/<name>` regardless of this override, so overriding the path makes those hooks 404. Use path overrides only for external HTTP callers. Note also that `:param` route segments in the override path are **not** parsed into `run()` args — only query-string params and JSON body fields are.
146
165
  - **`http: false`** — disable the HTTP endpoint entirely. Agent + CLI only.
@@ -29,6 +29,13 @@ There are two agent paths:
29
29
 
30
30
  In both cases, your main agent sees the response and can reference or build on it.
31
31
 
32
+ ```an-diagram title="Where an @-mention routes" summary="The server splits each mention by type: custom agents run locally, connected agents go over A2A — both responses fold back into the main agent's context."
33
+ {
34
+ "html": "<div class=\"diagram-mention\"><div class=\"diagram-node\">@-mention<br><small class=\"diagram-muted\">in the composer</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-panel center\" data-rough><span class=\"diagram-pill accent\">Server resolves</span><small class=\"diagram-muted\">extract refs by type</small></div><div class=\"diagram-col\"><div class=\"row\"><span class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</span><div class=\"diagram-box\">Custom agent<br><small class=\"diagram-muted\">agents/*.md &middot; runs local</small></div></div><div class=\"row\"><span class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</span><div class=\"diagram-box\">Connected agent<br><small class=\"diagram-muted\">A2A peer &middot; remote call</small></div></div></div><div class=\"diagram-arrow diagram-accent\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-box diagram-accent\">&lt;agent-response&gt;<br><small class=\"diagram-muted\">injected into main agent</small></div></div>",
35
+ "css": ".diagram-mention{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.diagram-mention .center{display:flex;flex-direction:column;align-items:center;gap:4px;padding:14px}.diagram-mention .diagram-col{display:flex;flex-direction:column;gap:10px}.diagram-mention .row{display:flex;align-items:center;gap:8px}.diagram-mention .diagram-arrow{font-size:22px;line-height:1}"
36
+ }
37
+ ```
38
+
32
39
  ## How it works {#how-it-works}
33
40
 
34
41
  When a message containing an `@`-mention is sent, the following happens on the server:
@@ -55,6 +62,13 @@ Last week's signups: 1,247 total
55
62
 
56
63
  The main agent can then use this data naturally in its response — for example, incorporating the numbers into an email draft.
57
64
 
65
+ ```an-callout
66
+ {
67
+ "tone": "info",
68
+ "body": "Mentioned-agent output arrives as an `<agent-response agent=\"…\">` block in the **main agent's** context — not as separate chat bubbles. The main agent decides how to weave it into the reply."
69
+ }
70
+ ```
71
+
58
72
  ## Adding agents {#adding-agents}
59
73
 
60
74
  Agents become available for mentioning through several mechanisms:
@@ -105,38 +119,17 @@ Remote A2A agents still use JSON manifests:
105
119
 
106
120
  Templates can register custom mention providers to add domain-specific mentionable items beyond agents and files. A mention provider implements the `MentionProvider` interface:
107
121
 
108
- ```ts
109
- import type { MentionProvider } from "@agent-native/core/server";
110
-
111
- const contactsProvider: MentionProvider = {
112
- id: "contacts",
113
- label: "Contacts",
114
-
115
- // Search for mentionable items
116
- async search(query: string) {
117
- const contacts = await db.query.contacts.findMany({
118
- where: like(contacts.name, `%${query}%`),
119
- limit: 10,
120
- });
121
- return contacts.map((c) => ({
122
- id: c.id,
123
- label: c.name,
124
- description: c.email,
125
- type: "contact",
126
- }));
127
- },
128
-
129
- // Resolve a mention into context for the agent
130
- async resolve(id: string) {
131
- const contact = await db.query.contacts.findFirst({
132
- where: eq(contacts.id, id),
133
- });
134
- return {
135
- type: "context",
136
- text: `Contact: ${contact.name} (${contact.email})`,
137
- };
138
- },
139
- };
122
+ ```an-annotated-code title="A custom MentionProvider"
123
+ {
124
+ "filename": "server/mentions/contacts.ts",
125
+ "language": "ts",
126
+ "code": "import type { MentionProvider } from \"@agent-native/core/server\";\n\nconst contactsProvider: MentionProvider = {\n id: \"contacts\",\n label: \"Contacts\",\n\n // Search for mentionable items\n async search(query: string) {\n const contacts = await db.query.contacts.findMany({\n where: like(contacts.name, `%${query}%`),\n limit: 10,\n });\n return contacts.map((c) => ({\n id: c.id,\n label: c.name,\n description: c.email,\n type: \"contact\",\n }));\n },\n\n // Resolve a mention into context for the agent\n async resolve(id: string) {\n const contact = await db.query.contacts.findFirst({\n where: eq(contacts.id, id),\n });\n return {\n type: \"context\",\n text: `Contact: ${contact.name} (${contact.email})`,\n };\n },\n};",
127
+ "annotations": [
128
+ { "lines": "4-5", "label": "Identity", "note": "`id` namespaces the provider; `label` is the section heading shown in the `@` popover." },
129
+ { "lines": "8-9", "label": "search", "note": "Runs as the user types after `@`. Return up to a handful of matches as `{ id, label, description, type }`." },
130
+ { "lines": "23-24", "label": "resolve", "note": "Called when the message is sent. Turns a picked id into `{ type: \"context\", text }` that is injected into the agent's context." }
131
+ ]
132
+ }
140
133
  ```
141
134
 
142
135
  Register providers in the agent-chat plugin configuration:
@@ -25,6 +25,13 @@ Those are stages, not separate products. A workflow can start as a headless
25
25
  agent with one action, appear in chat as a table or chart, and later become a
26
26
  full screen in an app without changing the operation the agent calls.
27
27
 
28
+ ```an-diagram title="The surface spectrum" summary="One action surface, four product shapes — each adds UI without changing the operation underneath."
29
+ {
30
+ "html": "<div class=\"diagram-spectrum\"><div class=\"diagram-card\"><strong>Headless</strong><small class=\"diagram-muted\">actions, jobs, scripts, other agents</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-card\"><strong>Rich chat</strong><small class=\"diagram-muted\">composer, transcript, tool cards</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-card\"><strong>Embedded sidecar</strong><small class=\"diagram-muted\">agent beside an existing app</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-card accent-card\"><span class=\"diagram-pill accent\">most UI</span><strong>Full application</strong><small class=\"diagram-muted\">durable screens, data, collaboration</small></div></div><div class=\"diagram-base\" data-rough><span class=\"diagram-muted\">same actions · same SQL · same agent loop</span></div>",
31
+ "css": ".diagram-spectrum{display:flex;align-items:stretch;gap:10px;flex-wrap:wrap}.diagram-spectrum .diagram-card{display:flex;flex-direction:column;gap:6px;padding:14px 16px;min-width:150px;flex:1}.diagram-spectrum .diagram-arrow{align-self:center;font-size:22px;line-height:1}.diagram-base{margin-top:12px;padding:10px 14px;text-align:center}"
32
+ }
33
+ ```
34
+
28
35
  ## Headless agent {#headless}
29
36
 
30
37
  Use the headless path when no one needs to stare at a custom app screen while
@@ -103,6 +110,23 @@ One action is then callable as:
103
110
  - **UI** — through `useActionQuery`, `useActionMutation`, or `callAction`
104
111
  - **Agent tool** — from the built-in chat loop
105
112
 
113
+ ```an-api title="Calling an action over HTTP"
114
+ {
115
+ "method": "POST",
116
+ "path": "/_agent-native/actions/summarize-week",
117
+ "summary": "Invoke any action by name over HTTP",
118
+ "description": "Every `defineAction` is auto-mounted at `/_agent-native/actions/<name>`. The JSON body is validated against the action's zod schema before `run` executes.",
119
+ "request": {
120
+ "contentType": "application/json",
121
+ "example": "{ \"formId\": \"form_123\" }"
122
+ },
123
+ "responses": [
124
+ { "status": "200", "description": "The action's return value as JSON", "example": "{ \"formId\": \"form_123\", \"summary\": \"34 submissions, up 18% from last week.\" }" },
125
+ { "status": "400", "description": "Input failed schema validation" }
126
+ ]
127
+ }
128
+ ```
129
+
106
130
  This is not a no-database or stateless mode. The app-agent loop stores sessions,
107
131
  threads, runs, settings, credentials, application state, and share records in
108
132
  SQL. Local development defaults to SQLite; hosted headless apps should use a
@@ -319,6 +343,13 @@ export function AppShell({ children }) {
319
343
  }
320
344
  ```
321
345
 
346
+ ```an-diagram title="How the sidecar bridges to a host app" summary="The plugin mounts Agent-Native routes server-side; the React sidecar streams page context in and host commands out."
347
+ {
348
+ "html": "<div class=\"diagram-sidecar\"><div class=\"diagram-panel\"><strong>Host app</strong><small class=\"diagram-muted\">your existing SaaS</small><div class=\"diagram-node\">getContext()<br><small class=\"diagram-muted\">route · selection</small></div><div class=\"diagram-node\">onNavigate / onRefresh<br><small class=\"diagram-muted\">host commands</small></div></div><div class=\"diagram-col-arrows\"><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&larr;</div></div><div class=\"diagram-panel accent-panel\"><span class=\"diagram-pill accent\">AgentNativeEmbedded</span><small class=\"diagram-muted\">agent + workspace</small><div class=\"diagram-box\" data-rough>Agent-Native routes<br><small class=\"diagram-muted\">mounted by the server plugin</small></div></div></div>",
349
+ "css": ".diagram-sidecar{display:flex;align-items:center;gap:14px;flex-wrap:wrap}.diagram-sidecar .diagram-panel{display:flex;flex-direction:column;gap:8px;padding:14px 16px;min-width:200px}.diagram-sidecar .diagram-col-arrows{display:flex;flex-direction:column;gap:6px}.diagram-sidecar .diagram-arrow{font-size:22px;line-height:1}"
350
+ }
351
+ ```
352
+
322
353
  See [Embedding SDK](/docs/embedding-sdk) for host auth, database isolation,
323
354
  iframe/picker mode, and lower-level bridge APIs.
324
355
 
@@ -20,6 +20,13 @@ Agent Teams runs on the core run-manager: events stream and persist, aborts prop
20
20
 
21
21
  Sub-agent state is persisted in the `application_state` SQL table (under `agent-task:<taskId>`), so tasks survive serverless cold starts and work across multiple processes.
22
22
 
23
+ ```an-diagram title="Orchestrator and specialists" summary="The main chat delegates to sub-agents that run in their own threads and report back as inline chips."
24
+ {
25
+ "html": "<div class=\"at-orc\"><div class=\"diagram-card main\"><span class=\"diagram-pill accent\">Main chat</span><small class=\"diagram-muted\">orchestrator &mdash; reads your request, delegates</small></div><div class=\"at-fan\"><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&darr;</div><div class=\"at-subs\"><div class=\"diagram-box\">Code review<br><small class=\"diagram-muted\">own thread &amp; prompt</small></div><div class=\"diagram-box\">BigQuery analysis<br><small class=\"diagram-muted\">own tools</small></div><div class=\"diagram-box\">Email in voice<br><small class=\"diagram-muted\">own context</small></div></div></div><div class=\"diagram-pill\">each appears inline as a live chip &#8635;</div></div>",
26
+ "css": ".at-orc{display:flex;flex-direction:column;align-items:center;gap:12px}.at-orc .diagram-card{padding:14px 18px;display:flex;flex-direction:column;gap:4px;align-items:center}.at-orc .at-fan{display:flex;flex-direction:column;align-items:center;gap:8px}.at-orc .diagram-arrow{font-size:22px}.at-orc .at-subs{display:flex;gap:12px;flex-wrap:wrap;justify-content:center}.at-orc .diagram-box{text-align:center}"
27
+ }
28
+ ```
29
+
23
30
  ## When to spawn a sub-agent {#when-to-spawn}
24
31
 
25
32
  Spawn when the task:
@@ -78,15 +85,11 @@ Most app code won't call this directly — the framework does it under the hood
78
85
 
79
86
  ## Task lifecycle {#lifecycle}
80
87
 
81
- ```
82
- spawnTask()
83
- ├─ creates a new thread in chat_threads (with the description as the first user message)
84
- ├─ writes agent-task:<taskId> to application_state (status=running)
85
- ├─ emits agent_task_started to the parent stream → chip appears in the UI
86
- ├─ runs the agent loop in the background
87
- │ └─ emits agent_task_step events → chip updates live
88
- ├─ on completion: updates status=completed, writes summary + preview
89
- └─ emits agent_task_done → chip shows final summary
88
+ ```an-diagram title="What spawnTask() does" summary="Each spawn creates a thread, persists state to SQL, and streams chip events through to completion."
89
+ {
90
+ "html": "<div class=\"at-life\"><div class=\"diagram-box\"><code>spawnTask()</code></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&darr;</div><div class=\"diagram-card\"><span class=\"diagram-pill\">create thread</span><small class=\"diagram-muted\">new row in <code>chat_threads</code>, description as first message</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&darr;</div><div class=\"diagram-card\"><span class=\"diagram-pill\">persist state</span><small class=\"diagram-muted\"><code>agent-task:&lt;id&gt;</code> &rarr; <code>application_state</code>, status=running</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&darr;</div><div class=\"diagram-card\"><span class=\"diagram-pill accent\">stream</span><small class=\"diagram-muted\"><code>agent_task_started</code> &rarr; chip appears; <code>agent_task_step</code> &rarr; chip updates live</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&darr;</div><div class=\"diagram-card ok\"><span class=\"diagram-pill ok\">complete</span><small class=\"diagram-muted\">status=completed, write summary + preview, emit <code>agent_task_done</code></small></div></div>",
91
+ "css": ".at-life{display:flex;flex-direction:column;align-items:stretch;gap:6px;max-width:560px}.at-life .diagram-card{display:flex;flex-direction:column;gap:3px;padding:10px 14px}.at-life .diagram-box{align-self:flex-start}.at-life .diagram-arrow{font-size:18px;align-self:center}"
92
+ }
90
93
  ```
91
94
 
92
95
  At any point the parent agent can resume the sub-agent with a follow-up via `sendToTask(taskId, message)`. If the sub-agent errors, `markTaskErrored(taskId, reason)` records the failure and surfaces it to the user.
@@ -135,11 +138,11 @@ Sub-agents can spawn sub-agents, which is a runaway/cost risk: an unbounded chai
135
138
 
136
139
  The top-level chat is depth `0`. A sub-agent it spawns is depth `1`; that sub-agent may spawn once more (depth `2`); a spawn that would create a depth-`3` sub-agent is **refused**. The default cap is **2**.
137
140
 
138
- ```text
139
- depth 0 top-level chat (may spawn)
140
- depth 1 sub-agent (may spawn)
141
- depth 2 sub-agent's sub-agent (at the cap — may NOT spawn)
142
- depth 3 refused
141
+ ```an-diagram title="Delegation depth guard (default cap 2)" summary="Each level may spawn one deeper until the cap; a spawn past it is refused server-side."
142
+ {
143
+ "html": "<div class=\"at-depth\"><div class=\"diagram-card ok\"><span class=\"diagram-pill\">depth 0</span><strong>Top-level chat</strong><small class=\"diagram-muted ok\">may spawn &darr;</small></div><div class=\"diagram-card ok\"><span class=\"diagram-pill\">depth 1</span><strong>Sub-agent</strong><small class=\"diagram-muted ok\">may spawn &darr;</small></div><div class=\"diagram-card warn\"><span class=\"diagram-pill warn\">depth 2</span><strong>Sub-agent's sub-agent</strong><small class=\"diagram-muted\">at the cap &mdash; may NOT spawn</small></div><div class=\"diagram-card\"><span class=\"diagram-pill warn\">depth 3</span><strong>Refused</strong><small class=\"diagram-muted\">server-side error</small></div></div>",
144
+ "css": ".at-depth{display:flex;flex-direction:column;gap:8px}.at-depth .diagram-card{display:flex;flex-direction:column;gap:2px;padding:10px 14px}.at-depth .rung-1,.at-depth .diagram-card:nth-child(2){margin-inline-start:24px}.at-depth .diagram-card:nth-child(3){margin-inline-start:48px}.at-depth .diagram-card:nth-child(4){margin-inline-start:72px}"
145
+ }
143
146
  ```
144
147
 
145
148
  Enforcement is ambient: each sub-agent runs inside an `AsyncLocalStorage` that records its own depth, so any `spawnTask` reached transitively from that run reads its parent's depth and refuses once the cap is hit — even if the `agent-teams` tool was handed to a sub-agent that should not have had it. The decision is exposed as a pure, unit-testable `evaluateSubagentDepth(parentDepth)`. A refused spawn returns a clear error: _"Delegation depth limit reached (max N); cannot spawn another sub-agent."_
@@ -19,6 +19,13 @@ The docs site is the reference implementation. Today it ships:
19
19
 
20
20
  Setting `publicMcp: true` additionally exposes opted-in actions as a public MCP endpoint, allowing external agents to call them directly (see [MCP Protocol](/docs/mcp-protocol)).
21
21
 
22
+ ```an-diagram title="What a public route publishes" summary="A public route fans out into agent-friendly representations. Reading the route is separate from calling tools — tool access stays opt-in."
23
+ {
24
+ "html": "<div class=\"diagram-web\"><div class=\"diagram-box\" data-rough>Public route<br><small class=\"diagram-muted\">derived from route access settings</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-grid\"><span class=\"diagram-pill\">robots.txt</span><span class=\"diagram-pill\">sitemap.xml</span><span class=\"diagram-pill\">llms.txt</span><span class=\"diagram-pill\">.md mirror</span><span class=\"diagram-pill\">JSON-LD</span><span class=\"diagram-pill\">text/markdown</span></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-col gate\"><span class=\"diagram-pill warn\">Tools stay private</span><small class=\"diagram-muted\">publicMcp + publicAgent.expose required</small></div></div>",
25
+ "css": ".diagram-web{display:flex;align-items:center;gap:14px;flex-wrap:wrap}.diagram-web .diagram-arrow{font-size:22px;line-height:1}.diagram-web .diagram-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.diagram-web .gate{display:flex;flex-direction:column;gap:4px;align-items:flex-start}"
26
+ }
27
+ ```
28
+
22
29
  ## Configuration {#config}
23
30
 
24
31
  Add `agentWeb` under the existing workspace app config (in your app's `package.json` under the `agent-native` key — or equivalently `workspace.agentWeb`, `agentWeb`, or `root.agentWeb`). The public route list is still derived from the app's route access settings; `agentWeb` controls how that public surface is represented to agents.
@@ -67,23 +74,25 @@ This keeps mixed apps natural. A forms app can expose a public form page and kee
67
74
 
68
75
  Public page access and public tool access are separate. A route being public only means agents can read that route as HTML, Markdown, sitemap entries, llms entries, and structured data.
69
76
 
77
+ ```an-callout
78
+ {
79
+ "tone": "warning",
80
+ "body": "**A public page is not a public tool.** Making a route crawlable never exposes an action. Tool access requires an explicit `publicAgent.expose` opt-in on the action *and* `publicMcp: true` on the app."
81
+ }
82
+ ```
83
+
70
84
  To expose an action through a public agent protocol, the action must opt in:
71
85
 
72
- ```ts
73
- export default defineAction({
74
- description: "Search published docs",
75
- readOnly: true,
76
- publicAgent: {
77
- expose: true,
78
- readOnly: true,
79
- requiresAuth: false,
80
- isConsequential: false,
81
- title: "Search published docs",
82
- },
83
- run: async (args) => {
84
- // ...
85
- },
86
- });
86
+ ```an-annotated-code title="Opting one safe action onto the public surface"
87
+ {
88
+ "filename": "actions/search-docs.ts",
89
+ "language": "ts",
90
+ "code": "export default defineAction({\n description: \"Search published docs\",\n readOnly: true,\n publicAgent: {\n expose: true,\n readOnly: true,\n requiresAuth: false,\n isConsequential: false,\n title: \"Search published docs\",\n },\n run: async (args) => {\n // ...\n },\n});",
91
+ "annotations": [
92
+ { "lines": "4", "label": "Explicit opt-in", "note": "Without `publicAgent.expose === true`, the action never appears on any public agent surface — no matter how public its routes are." },
93
+ { "lines": "5-7", "label": "Self-describe safety", "note": "Mark it read-only, declare whether it needs auth, and flag whether it is consequential. Public MCP excludes consequential/write actions unless policy explicitly allows them." }
94
+ ]
95
+ }
87
96
  ```
88
97
 
89
98
  `agentWeb.publicMcp` stays `false` by default. When public MCP is enabled, the server should expose only actions with `publicAgent.expose === true`, and should still exclude consequential or write actions unless the action and auth policy explicitly allow them.
@@ -15,6 +15,13 @@ Auth is configured automatically via `autoMountAuth(app)` in the auth server plu
15
15
  - **Remote MCP OAuth:** Standard OAuth 2.1 for MCP hosts such as Claude Code and ChatGPT connectors.
16
16
  - **Custom:** Bring your own auth via `getSession` callback.
17
17
 
18
+ ```an-diagram title="Three ways in, one session" summary="Browser visitors, programmatic MCP clients, and custom providers all resolve to the same AuthSession that downstream scoping reads."
19
+ {
20
+ "html": "<div class=\"auth-modes\"><div class=\"diagram-col\"><div class=\"diagram-card\"><span class=\"diagram-pill accent\">Default</span><strong>Better Auth</strong><small class=\"diagram-muted\">email/password &middot; Google &middot; GitHub</small></div><div class=\"diagram-card\"><span class=\"diagram-pill\">Remote MCP OAuth</span><strong>OAuth 2.1 + PKCE</strong><small class=\"diagram-muted\">Claude Code, ChatGPT connectors</small></div><div class=\"diagram-card\"><span class=\"diagram-pill\">Custom</span><strong>getSession callback</strong><small class=\"diagram-muted\">Clerk &middot; Auth0 &middot; Firebase</small></div></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-panel center\"><span class=\"diagram-pill ok\">AuthSession</span><small class=\"diagram-muted\">email &middot; orgId &middot; orgRole</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-box\">Request context &amp; data scoping</div></div>",
21
+ "css": ".auth-modes{display:flex;align-items:center;gap:14px;flex-wrap:wrap}.auth-modes .diagram-col{display:flex;flex-direction:column;gap:10px}.auth-modes .diagram-card{display:flex;flex-direction:column;gap:4px;padding:10px 12px}.auth-modes .diagram-arrow{font-size:22px;line-height:1}.auth-modes .center{display:flex;flex-direction:column;align-items:center;gap:4px}"
22
+ }
23
+ ```
24
+
18
25
  The browser flow is the same Better Auth flow everywhere — there is **no dev auth bypass**, and `getSession()` never falls back to a `local@localhost` sentinel. What changes between environments is signup friction, not the login wall:
19
26
 
20
27
  | Environment | First-load behavior | Email verification |
@@ -29,6 +36,13 @@ A few flags tune this; full details are in the [Environment Variables](#environm
29
36
  - `AUTH_DISABLED=true` — skip login/signup entirely and run every request as one shared user (local dev / previews / demos only, never production with real users).
30
37
  - `AUTH_MODE=local` — affects only CLI/agent identity (which dev user `pnpm action` runs as); it is **not** a browser login bypass.
31
38
 
39
+ ```an-callout
40
+ {
41
+ "tone": "warning",
42
+ "body": "`AUTH_DISABLED=true` runs **every request as one shared user**. Use it only for local dev, previews, or demos — never in production with real users, where it would expose all data to anyone."
43
+ }
44
+ ```
45
+
32
46
  ## Better Auth (Default) {#better-auth}
33
47
 
34
48
  By default, Better Auth powers authentication. It provides:
@@ -139,6 +153,13 @@ https://mail.agent-native.com/_agent-native/mcp
139
153
 
140
154
  Unauthenticated MCP requests return a `WWW-Authenticate` challenge pointing at `/.well-known/oauth-protected-resource`. The client then discovers the app's OAuth metadata, dynamically registers a public client, opens the app's authorization page, and exchanges an authorization code with PKCE for access and refresh tokens.
141
155
 
156
+ ```an-diagram title="Remote MCP OAuth handshake" summary="An OAuth-capable client bootstraps from just the MCP URL — challenge, discovery, dynamic registration, then a PKCE code exchange."
157
+ {
158
+ "html": "<div class=\"mcp-flow\"><div class=\"diagram-node\">1 &middot; MCP request<br><small class=\"diagram-muted\">no token</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-node warn\">2 &middot; 401 challenge<br><small class=\"diagram-muted\">WWW-Authenticate</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-node\">3 &middot; Discover metadata<br><small class=\"diagram-muted\">.well-known</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-node\">4 &middot; Register client<br><small class=\"diagram-muted\">dynamic, public</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-node\">5 &middot; Authorize + PKCE<br><small class=\"diagram-muted\">code exchange</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-node ok\">6 &middot; Access + refresh<br><small class=\"diagram-muted\">audience-bound</small></div></div>",
159
+ "css": ".mcp-flow{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.mcp-flow .diagram-node{display:flex;flex-direction:column;gap:2px;padding:8px 12px}.mcp-flow .diagram-arrow{font-size:20px;line-height:1}"
160
+ }
161
+ ```
162
+
142
163
  Access tokens are signed with `A2A_SECRET` when set, otherwise `BETTER_AUTH_SECRET`. They carry the signed user/org identity and the `mcp:read`, `mcp:write`, and/or `mcp:apps` scopes, and are audience-bound to the exact MCP resource URL. Refresh tokens are stored only as hashes and rotate on every refresh. Tool calls and MCP Apps resource reads run inside the same request context as the signed-in user; the embedded MCP App iframe never receives raw OAuth tokens.
143
164
 
144
165
  `npx @agent-native/core@latest connect <url> --client claude-code` writes the URL-only MCP entry for this standard flow. For clients that cannot perform remote MCP OAuth, use the Connect page or `npx @agent-native/core@latest connect --token <token>` fallback to write an explicit bearer-token entry.
@@ -9,6 +9,13 @@ An **automation** is a rule: _when X happens, do Y_ — described in natural lan
9
9
 
10
10
  Automations extend [recurring jobs](/docs/recurring-jobs) with **event triggers**, **natural-language conditions**, and **outbound HTTP** via the `web-request` tool. They use the same `jobs/<name>.md` file format, storage, and "create three ways" workflow as recurring jobs — see [Recurring Jobs](/docs/recurring-jobs#job-file) for the shared format. This page covers only what's new for event-driven automations.
11
11
 
12
+ ```an-diagram title="When X happens, do Y" summary="An event fires on the bus, an optional natural-language condition gates it, and the agent runs the automation body with full tool access."
13
+ {
14
+ "html": "<div class=\"auto-flow\"><div class=\"diagram-card\"><span class=\"diagram-pill\">Event</span><small class=\"diagram-muted\"><code>calendar.booking.created</code></small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-card\"><span class=\"diagram-pill\">Condition</span><small class=\"diagram-muted\">Haiku checks: &ldquo;email ends with @builder.io&rdquo;</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;</div><div class=\"diagram-card accent\"><span class=\"diagram-pill accent\">Agent runs the body</span><small class=\"diagram-muted\">actions &middot; web-request &middot; MCP &middot; sub-agents</small></div></div>",
15
+ "css": ".auto-flow{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.auto-flow .diagram-card{display:flex;flex-direction:column;gap:4px;padding:14px 16px;min-width:180px}.auto-flow .diagram-arrow{font-size:22px}"
16
+ }
17
+ ```
18
+
12
19
  ## Two trigger types {#trigger-types}
13
20
 
14
21
  | Type | Fires when | Key field |
@@ -32,19 +39,18 @@ Automations appear in the settings panel. Users can view, enable/disable, and de
32
39
 
33
40
  The third path — writing the `jobs/<name>.md` file by hand via `resourcePut` — works exactly as it does for [recurring jobs](/docs/recurring-jobs#creating). For an event-driven automation you add the event-trigger frontmatter below to that same file. An event-triggered job sets `schedule: ""` and supplies `triggerType: event`, an `event` name, and an optional `condition`:
34
41
 
35
- ```yaml
36
- ---
37
- schedule: ""
38
- enabled: true
39
- triggerType: event
40
- event: calendar.booking.created
41
- condition: "attendee email ends with @builder.io"
42
- mode: agentic
43
- domain: calendar
44
- runAs: creator
45
- ---
46
- Send a Slack message to #sales with the booking details.
47
- Use the web-request tool to POST to ${keys.SLACK_WEBHOOK}.
42
+ ```an-annotated-code title="An event-triggered automation"
43
+ {
44
+ "filename": "jobs/slack-on-builder-booking.md",
45
+ "language": "markdown",
46
+ "code": "---\nschedule: \"\"\nenabled: true\ntriggerType: event\nevent: calendar.booking.created\ncondition: \"attendee email ends with @builder.io\"\nmode: agentic\ndomain: calendar\nrunAs: creator\n---\nSend a Slack message to #sales with the booking details.\nUse the web-request tool to POST to ${keys.SLACK_WEBHOOK}.",
47
+ "annotations": [
48
+ { "lines": "2", "label": "No cron", "note": "Event triggers set `schedule` to `\"\"` — the cron field stays empty." },
49
+ { "lines": "4-5", "label": "The trigger", "note": "`triggerType: event` plus the `event` name subscribes this automation to the bus." },
50
+ { "lines": "6", "label": "Gate", "note": "An optional natural-language `condition`, evaluated by Haiku against the payload before dispatch." },
51
+ { "lines": "12", "label": "Server-side secret", "note": "`${keys.SLACK_WEBHOOK}` is resolved server-side — the raw value never enters the agent's context." }
52
+ ]
53
+ }
48
54
  ```
49
55
 
50
56
  ## Automation frontmatter {#frontmatter}
@@ -188,16 +194,26 @@ Additional tool: `web-request` — outbound HTTP with `${keys.NAME}` substitutio
188
194
  | `/_agent-native/secrets/adhoc` | POST | Create or update an ad-hoc key |
189
195
  | `/_agent-native/secrets/adhoc/:name` | DELETE | Delete an ad-hoc key |
190
196
 
197
+ ```an-api title="Fire a test event"
198
+ {
199
+ "method": "POST",
200
+ "path": "/_agent-native/automations/fire-test",
201
+ "summary": "Emit a test.event.fired event to validate event-triggered automations",
202
+ "description": "Confirm an automation's wiring and condition without waiting for a real provider event. Equivalent to the `manage-automations` action `fire-test`.",
203
+ "responses": [
204
+ { "status": "200", "description": "Event emitted; matching automations are dispatched through the normal condition + ownership path." }
205
+ ]
206
+ }
207
+ ```
208
+
191
209
  ## How dispatch works {#dispatch}
192
210
 
193
- 1. The trigger dispatcher subscribes to events at server startup.
194
- 2. When an event fires, the dispatcher loads all enabled event-triggered automations matching that event name.
195
- 3. Ownership scoping: only automations owned by the event's owner (or shared automations) are evaluated.
196
- 4. For each matching automation, the condition (if any) is evaluated by Haiku.
197
- 5. If the condition passes, the dispatcher runs a full `runAgentLoop` with the automation body as the prompt and the event payload as context.
198
- 6. The agent has access to all tools — actions, `web-request`, MCP servers, sub-agents.
199
- 7. On completion, `lastRun`, `lastStatus`, and `lastError` are written back to the resource frontmatter.
200
- 8. A 5-minute timeout prevents runaway automations.
211
+ ```an-diagram title="The dispatch path" summary="From a fired event to a completed agent run, gated by ownership scope and the natural-language condition."
212
+ {
213
+ "html": "<div class=\"disp\"><div class=\"diagram-box accent\">event fired on the bus</div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&darr;</div><div class=\"diagram-card\"><span class=\"diagram-pill\">match</span><small class=\"diagram-muted\">load enabled automations subscribed to this event name</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&darr;</div><div class=\"diagram-card\"><span class=\"diagram-pill\">scope</span><small class=\"diagram-muted\">keep only those owned by the event's owner (or shared)</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&darr;</div><div class=\"diagram-card\"><span class=\"diagram-pill warn\">condition</span><small class=\"diagram-muted\">Haiku yes/no on the payload &mdash; false &rarr; <code>skipped</code></small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&darr;</div><div class=\"diagram-card accent\"><span class=\"diagram-pill accent\">run</span><small class=\"diagram-muted\"><code>runAgentLoop</code> with body as prompt, payload as context, 5-min timeout</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&darr;</div><div class=\"diagram-card ok\"><span class=\"diagram-pill ok\">record</span><small class=\"diagram-muted\">write <code>lastRun</code> / <code>lastStatus</code> / <code>lastError</code></small></div></div>",
214
+ "css": ".disp{display:flex;flex-direction:column;gap:6px;max-width:540px}.disp .diagram-card{display:flex;flex-direction:column;gap:2px;padding:10px 14px}.disp .diagram-box{align-self:flex-start}.disp .diagram-arrow{font-size:18px;align-self:center}"
215
+ }
216
+ ```
201
217
 
202
218
  ## Example {#example}
203
219