From 02bd434b0786b7d93d16de7e7afb0cc6cd2503a4 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 18 May 2026 21:00:58 -0700 Subject: [PATCH] Publish app router post and capabilities docs --- apps/web/app/components/docs/nav.ts | 1 + apps/web/app/docs/capabilities/page.tsx | 9 + apps/web/app/llms.txt/route.ts | 3 +- .../2026-05-19-app-router-for-ai-agents.mdx | 278 ++++++++++++------ apps/web/content/docs/agents.mdx | 7 + apps/web/content/docs/capabilities.mdx | 132 +++++++++ apps/web/content/docs/mental-model.mdx | 6 +- apps/web/content/docs/recipes/index.mdx | 3 + 8 files changed, 340 insertions(+), 99 deletions(-) create mode 100644 apps/web/app/docs/capabilities/page.tsx create mode 100644 apps/web/content/docs/capabilities.mdx diff --git a/apps/web/app/components/docs/nav.ts b/apps/web/app/components/docs/nav.ts index e7247441..0206b9d5 100644 --- a/apps/web/app/components/docs/nav.ts +++ b/apps/web/app/components/docs/nav.ts @@ -24,6 +24,7 @@ export const DOCS_NAV: readonly DocsNavSection[] = [ { label: "Agents", href: "/docs/agents" }, { label: "Tools", href: "/docs/tools" }, { label: "State", href: "/docs/state" }, + { label: "Capabilities", href: "/docs/capabilities" }, { label: "Middleware", href: "/docs/middleware" }, { label: "Retry", href: "/docs/retry" }, ], diff --git a/apps/web/app/docs/capabilities/page.tsx b/apps/web/app/docs/capabilities/page.tsx new file mode 100644 index 00000000..081a074e --- /dev/null +++ b/apps/web/app/docs/capabilities/page.tsx @@ -0,0 +1,9 @@ +import type { Metadata } from "next" +import Content from "../../../content/docs/capabilities.mdx" +import { DocsPage } from "../../components/docs/DocsPage" + +export const metadata: Metadata = { title: "Capabilities" } + +export default function Page() { + return +} diff --git a/apps/web/app/llms.txt/route.ts b/apps/web/app/llms.txt/route.ts index e98e1c17..6f455c71 100644 --- a/apps/web/app/llms.txt/route.ts +++ b/apps/web/app/llms.txt/route.ts @@ -15,7 +15,7 @@ function buildLlmsTxt(): string { [ "# Dawn", "", - "Dawn is a TypeScript-first meta-framework for building graph-based AI agents with the ergonomics of Next.js: file-system routing, co-located tools with inferred types, scenario testing, and a local dev server with LangSmith-style endpoints.", + "Dawn is a TypeScript-first meta-framework for building graph-based AI agents with the ergonomics of Next.js: file-system routing, co-located tools with inferred types, scenario testing, route-level capabilities, and a local dev server with LangSmith-style endpoints.", "", "## Install", "```", @@ -29,6 +29,7 @@ function buildLlmsTxt(): string { "- `src/app/**/index.ts` — route entry; exports exactly one of default `agent(...)`, named `workflow` (async function), named `graph` (LangGraph graph), or named `chain` (LangChain LCEL Runnable).", "- `src/app/**/state.ts` — optional Zod route state schema.", "- `src/app/**/tools/*.ts` — co-located tools (default export is an async function; types inferred).", + "- `src/app/**/plan.md`, `skills//SKILL.md`, and `subagents//index.ts` — optional agent capabilities.", "- `src/app/**/run.test.ts` — colocated scenario tests.", "- `.dawn/dawn.generated.d.ts` — auto-generated ambient types. Never edit by hand.", "", diff --git a/apps/web/content/blog/2026-05-19-app-router-for-ai-agents.mdx b/apps/web/content/blog/2026-05-19-app-router-for-ai-agents.mdx index a724ecc4..6c846bf4 100644 --- a/apps/web/content/blog/2026-05-19-app-router-for-ai-agents.mdx +++ b/apps/web/content/blog/2026-05-19-app-router-for-ai-agents.mdx @@ -1,129 +1,122 @@ --- title: The App Router for AI Agents -description: File-system routes, type-safe tools, and why the mental model matters for agent codebases that grow. +description: File-system routes, type-safe tools, and the capability layer Dawn adds around real LangGraph.js agent applications. date: 2026-05-19 tags: [philosophy, typescript, agents] type: post author: brian -draft: true --- -The Next.js App Router did something subtle and important. +The Next.js App Router changed how many of us think about application structure. -Not because it introduced a new runtime. -And not because it changed how React rendered. +Not because a file-system router is a new idea. -What it changed was the answer to the question *"where does this code go?"*. +The important part was that it gave common web application concepts a place to live. A page is a file. A layout is a file. A route group is a folder. You can open the tree and get a useful read on the application. -A page is `page.tsx`. A layout is `layout.tsx`. A server action is a function in a server file. Middleware lives at `middleware.ts`. One place each concept lives, and that place is a file on disk. +I want the same thing for agent applications. -Agent codebases do not have that yet. +Agent codebases need more than a runtime. They need a project shape that can hold tools, state, tests, memory, planning, skills, and sub-agents without turning into a pile of registries. -Dawn is an attempt to fix it. +That is the App Router idea behind Dawn. -## tl;dr +## Goals -- agent codebases need a coordinate system, not just a runtime -- file-system routes give every concern a folder, not a registry entry -- co-location is a feature: refactors work, search works, tests do not drift -- typescript function signatures are the tool contract — no zod, no `tool()` wrapper -- the win is not the first agent. it is the tenth. +Here is the practical version: -## The missing coordinate system +- A route should be a folder under `src/app/`. +- The route path should be the agent endpoint. +- Tools should live next to the route that uses them. +- Tool types should come from TypeScript, not duplicated schemas. +- Tests should live beside the route they protect. +- Agent capabilities should compose through files and descriptors, not hidden setup code. -If you have worked on a LangGraph project past the second graph, you know the shape of the problem. +If a developer can open the file tree and understand where to add the next thing, the framework is doing useful work. -Graphs in one folder. Tools in another. State definitions in a third. A registry tying them together. +## What the route tree tells you -The connections between those files are implicit. A tool is "for" a graph because it is imported by it, not because it lives next to it. +Here is a small Dawn application: -That works at the kitchen-table scale. +```text +src/ + app/ + support/ + [tenant]/ + index.ts + state.ts + plan.md + run.test.ts + tools/ + lookupOrder.ts + escalate.ts + skills/ + refund-policy/ + SKILL.md + subagents/ + research/ + index.ts + tools/ + searchDocs.ts +``` -It stops working when you need to answer questions like: +Read that tree out loud and you already know quite a bit. -- which tools does the `support` graph have access to? -- if I rename `lookupOrder`, what else needs to change? -- where do I add the tenant-aware auth check for the `triage` route? -- can I delete this tool? who calls it? +There is a parameterized route at `/support/[tenant]`. -These are all answerable. +It has its own route entry in `index.ts`, its own state schema, a seeded plan, a scenario test, two route-local tools, one route-local skill, and a research sub-agent. -The answer just does not live on disk. It lives in your head, or in a wiki page, or in the import graph you have to trace by hand. +That is not just aesthetic. It changes how the codebase behaves. -That is the gap. The codebase does not know its own structure. +You can move the route. You can delete it. You can review it in a pull request. You can ask, "what tools does this agent have?" and answer the question from the folder. -## File-system routes as the answer +The structure is doing work. -The fix is borrowed wholesale from web frameworks. +## Routes -A route is a folder. The folder path is the endpoint. Everything that belongs to the route lives next to it. +A Dawn route is a folder under `src/app/`. The folder path becomes the route id. -A real Dawn project tree: +Route groups are ignored: ```text -my-agents/ -├── dawn.config.ts -├── package.json -└── src/ - └── app/ - ├── support/ - │ ├── [tenant]/ - │ │ ├── index.ts - │ │ ├── state.ts - │ │ ├── middleware.ts - │ │ └── tools/ - │ │ ├── lookupOrder.ts - │ │ └── escalate.ts - │ └── internal/ - │ └── index.ts - └── triage/ - ├── index.ts - └── state.ts +src/app/(public)/hello/[tenant]/index.ts ``` -Read that tree out loud and you have read the architecture. - -`support/[tenant]/` is a parameterized route. It has its own state shape, its own auth middleware, and two tools the agent can call. `support/internal/` is a separate route — a different endpoint, a different graph. `triage/` is unrelated to either. - -There is no registry. There is no `tools: [...]` array. - -The connection between a tool and the route that uses it is the *folder it is in*. - -That is the whole trick. - -## Co-location is a feature, not aesthetics - -Putting files near the thing they belong to is not a style preference. +becomes: -It changes what your tools can do. - -**Refactoring works.** Move a folder, move the route. Delete a folder, delete the route. There is no central registry whose edits you will forget. - -**Search works.** "Find references" on a tool finds the route that uses it, because the route imports it directly. "Find references" on a state field finds the agent prompt that mentions it. - -**Tests do not drift.** A scenario test for `support/[tenant]/` lives in `support/[tenant]/run.test.ts`. When you change the route, the test is right there in the same diff. +```text +/hello/[tenant] +``` -**Onboarding works.** New engineers can open the tree and read the surface area without a Loom video. The structure is the documentation. +The route entry is `index.ts`. It exports exactly one route shape: -Frameworks that hide structure behind a builder API give all of this up. +- `agent` for an LLM-driven route +- `workflow` for a deterministic async function +- `graph` for a raw LangGraph graph +- `chain` for a LangChain runnable -Once your routes are constructed at runtime from a `.register()` call, your editor cannot tell you what your codebase looks like. Only the running process can. +The default scaffold uses an agent: -That is a real cost. It just shows up late. +```ts +import { agent } from "@dawn-ai/sdk" -## Types as the route boundary +export default agent({ + model: "gpt-4o-mini", + systemPrompt: "You are a helpful assistant for the {tenant} organization.", +}) +``` -The other half of the coordinate system is types. +If you need full LangGraph control, export a named `graph`. If you need a deterministic flow, export a `workflow`. The route folder is the stable boundary either way. -A route is a contract. The contract should be checked at the boundary. +## Tools -In Dawn, a tool is just a TypeScript function: +In Dawn, a tool is a TypeScript file in `tools/`. ```ts // src/app/support/[tenant]/tools/lookupOrder.ts +export const description = "Look up an order by id." + export default async ( input: { readonly orderId: string }, - ctx: { signal: AbortSignal }, + ctx: { readonly signal: AbortSignal }, ) => { const res = await fetch(`https://api.example.com/orders/${input.orderId}`, { signal: ctx.signal, @@ -132,38 +125,129 @@ export default async ( } ``` -There is no Zod schema. There is no `tool()` wrapper. +There is no `tool()` wrapper in the route. + +There is no duplicate input schema. + +Dawn reads the function signature during type generation and build. The input type becomes the JSON schema exposed to the model, and the generated `dawn:routes` module gives route code typed access to `ctx.tools.lookupOrder`. + +This is the part I care about most: the TypeScript type is the contract. -The parameter type *is* the schema. `dawn typegen` reads it at build time and emits both a JSON schema for the LLM and a typed `ctx.tools.lookupOrder` for any code that calls it directly. +## State and tests -This sounds like a small convenience. +Routes can also define `state.ts`. -It is actually a forcing function. +State is the JSON shape that flows through the route runtime. The scaffold uses Zod, but Dawn accepts Zod or any Standard Schema value. -Because the type is the contract, drift is impossible. If you change the tool signature, callers fail to typecheck. If you misspell a tool name in a workflow route, the editor underlines it. +```ts +// src/app/support/[tenant]/state.ts +import { z } from "zod" -The route boundary is checked the same way the rest of your TypeScript is checked. +export default z.object({ + tenant: z.string(), + orderId: z.string().optional(), +}) +``` -Generated types live in `.dawn/dawn.generated.d.ts`. The starter template ignores `.dawn/`, so regenerate it during development and CI unless your project explicitly chooses to commit generated artifacts. +Dawn generates route state types from this file and the route path. If you rename a dynamic segment, or change the state shape, TypeScript can catch the places that still assume the old shape. -## What this unlocks at scale +Tests live next to the route too: -The interesting thing is not the first agent. +```text +src/app/support/[tenant]/run.test.ts +``` -It is the tenth. +That sounds small, but it matters. The test changes in the same pull request as the route. It does not live in a distant test directory that slowly stops explaining the feature it was written for. -A codebase with ten agents, forty tools, and a dozen middleware files needs the same code-intelligence features a web app does. Go-to-definition that works. Refactors that do not break things in production. Tests that run on a tight loop. Deploys that do not require a memo. +## Capabilities -Those features do not show up by accident. +The route tree gives Dawn a place to add higher-level agent behavior. -They require the codebase to have a *shape* the tools can reason about. +Here is what we have built to date. -That is the whole pitch. +### Memory + +If `workspace/AGENTS.md` exists, Dawn injects it into the agent prompt under `# Memory` on every model turn. + +The file is the memory. The agent can update it, and the next turn sees the updated content. + +### Planning + +If a route has `plan.md`, Dawn adds a planning prompt, a `writeTodos` tool, a `todos` state channel, and a `plan_update` stream event. + +The seed file uses normal markdown checklist syntax: + +```md +- [ ] Review the customer request +- [ ] Check order history +- [ ] Write a concise response +``` -Dawn is not asking you to learn a new graph runtime. The graph runtime is fine. Dawn is the coordinate system around it — the answer to "where does this code go" — and the type machinery that makes the answer load-bearing. +The useful part is that planning is not just a prompt. It is prompt, tool, state, and stream behavior composed together. -If the mental model resonates, [`/docs/mental-model`](/docs/mental-model) is the one-page version, and [`/docs/routes`](/docs/routes) is where the conventions are spelled out in full. +### Skills + +If a route has `skills//SKILL.md`, Dawn lists the available skills in the prompt and gives the agent a `readSkill({ name })` tool. + +That lets the agent load longer instructions only when it needs them. + +### Sub-agents + +A route can expose sub-agents through child routes under `subagents/` or through the `subagents` field on `agent({...})`. + +Dawn adds a `task({ subagent, input })` tool and a `# Subagents` prompt section. The parent agent can delegate to a specialist without the specialist becoming a random helper function hidden in another folder. + +The boundary stays visible on disk. + +### Reasoning effort + +For OpenAI-backed agent routes, the descriptor can include: + +```ts +reasoning: { effort: "high" } +``` + +Non-reasoning models ignore it. For models that support the setting, the option stays close to the route that needs it. + +## What this buys you + +The benefit is not the first route. + +The first route is always easy. + +The benefit shows up when the app has ten routes, forty tools, a few specialists, and enough state that a hand-written registry starts to feel like another product you have to maintain. + +At that point, the App Router idea earns its place: + +- file moves are meaningful +- type generation has one source of truth +- tests have a home +- generated deployment artifacts are predictable +- capabilities compose around the route instead of around a hidden framework object + +This does not make Dawn necessary for every project. + +If you have one graph and two tools, raw LangGraph.js may be exactly right. + +But if your agent application is starting to look like an application, it needs application structure. + +That is what Dawn is trying to provide. + +## Getting started + +The fastest way to try the shape is still the scaffold: + +```bash +pnpm create dawn-ai-app my-agents +cd my-agents +pnpm dev +``` + +Then run the example route: + +```bash +echo '{"tenant":"acme"}' | pnpm exec dawn run '/hello/[tenant]' +``` -A runtime is becoming a routing problem. A routing problem is becoming a type problem. +For the deeper read, start with [Mental Model](/docs/mental-model), [Routes](/docs/routes), and [Capabilities](/docs/capabilities). -That is what makes this interesting. diff --git a/apps/web/content/docs/agents.mdx b/apps/web/content/docs/agents.mdx index 8e3ef544..3fec7148 100644 --- a/apps/web/content/docs/agents.mdx +++ b/apps/web/content/docs/agents.mdx @@ -65,6 +65,12 @@ export default agent({ See [Retry](/docs/retry) for the backoff strategy, what is retried, and the streaming caveat. Dawn does not expose per-tool retry overrides today. +## Capabilities + +Agent routes can opt into higher-level behavior from files and descriptor fields: memory from `workspace/AGENTS.md`, planning from `plan.md`, skills from `skills//SKILL.md`, sub-agents from `subagents/` or `agent({ subagents })`, and model reasoning effort from `reasoning`. + +See [Capabilities](/docs/capabilities) for the file conventions and runtime behavior. + ## Streaming The local dev server exposes streaming via `/runs/stream`. See [Dev Server](/docs/dev-server) for the protocol details. @@ -75,5 +81,6 @@ The local dev server exposes streaming via `/runs/stream`. See [Dev Server](/doc { href: "/docs/routes", title: "Routes", subtitle: "pathname rules and route entry shapes" }, { href: "/docs/tools", title: "Tools", subtitle: "tool input/output rules, type inference" }, { href: "/docs/state", title: "State", subtitle: "when an agent reads or returns structured state" }, + { href: "/docs/capabilities", title: "Capabilities", subtitle: "memory, planning, skills, and sub-agents" }, { href: "/docs/retry", title: "Retry", subtitle: "retry config and backoff" }, ]} /> diff --git a/apps/web/content/docs/capabilities.mdx b/apps/web/content/docs/capabilities.mdx new file mode 100644 index 00000000..1bf46d53 --- /dev/null +++ b/apps/web/content/docs/capabilities.mdx @@ -0,0 +1,132 @@ +# Capabilities + +Capabilities are Dawn's way to add agent behavior from files and descriptors. + +The route still owns the boundary. Capabilities add tools, prompt fragments, state channels, and stream events around that route. + +Use them when the behavior is part of how the agent works, not just a one-off tool call. + +## What ships today + +Dawn currently includes four built-in capabilities: + +- **Memory** — load `workspace/AGENTS.md` into the agent prompt. +- **Planning** — seed and maintain a route plan with `plan.md`. +- **Skills** — expose route-local `SKILL.md` instructions through `readSkill`. +- **Sub-agents** — let a parent agent delegate through `task({ subagent, input })`. + +Agent routes also support `reasoning: { effort }` on the descriptor. That is not a capability marker, but it fits the same goal: route-local agent behavior that stays visible in code. + +## Memory + +If `workspace/AGENTS.md` exists, Dawn injects its contents into the agent prompt under a `# Memory` heading on every model turn. + +The file is the memory. Dawn re-reads it each turn, so an agent that updates the file sees the updated memory on the next turn. + +```text +workspace/ + AGENTS.md +``` + +Keep this file concise. Dawn skips empty files and files larger than 64 KiB. + +## Planning + +Place a `plan.md` file in a route directory to opt into planning. + +```text +src/app/support/[tenant]/ + index.ts + plan.md +``` + +The seed file uses markdown checklist syntax: + +```md +- [ ] Understand the request +- [ ] Check account context +- [ ] Draft the response +``` + +Dawn adds: + +- a `writeTodos` tool +- a `todos` state channel +- a planning prompt fragment +- a `plan_update` stream event when the plan changes + +`writeTodos` is full-replace, not incremental. The agent passes the entire todo list each time. + +## Skills + +Place skills under a route's `skills/` directory. + +```text +src/app/support/[tenant]/ + skills/ + refund-policy/ + SKILL.md +``` + +Each `SKILL.md` needs frontmatter with a `description` field: + +```md +--- +description: Instructions for handling refund-policy questions. +--- + +Use this skill when the user asks about refunds, exchanges, or exceptions. +``` + +Dawn adds a `# Skills` prompt section listing the available skills and a `readSkill({ name })` tool. The body of the skill is loaded only when the agent asks for it. + +The skill name defaults to the directory name. You can override it with a `name` field in frontmatter. + +## Sub-agents + +Sub-agents give a parent agent a visible delegation boundary. + +The convention path is: + +```text +src/app/support/[tenant]/ + index.ts + subagents/ + research/ + index.ts + tools/ + searchDocs.ts +``` + +The parent route gets a `# Subagents` prompt section and a `task({ subagent, input })` tool. The `subagent` value is the child folder name, such as `"research"`. + +You can also expose child descriptors through `agent({ subagents: [...] })` when you need to wire a specialist that does not live under the `subagents/` convention. + +Each sub-agent can have its own prompt, tools, state, and capabilities. Dawn guards nested dispatch with a maximum depth of 3. + +## Reasoning effort + +OpenAI-backed agent routes can request a reasoning effort: + +```ts title="src/app/support/[tenant]/index.ts" +import { agent } from "@dawn-ai/sdk" + +export default agent({ + model: "gpt-5.1", + reasoning: { effort: "high" }, + systemPrompt: "You are a careful support assistant.", +}) +``` + +Non-reasoning models ignore the value. Use it when a specific route needs more reasoning budget for tool-heavy or planning-heavy work. + +## Related + + + diff --git a/apps/web/content/docs/mental-model.mdx b/apps/web/content/docs/mental-model.mdx index 94b7aabe..cd7cea60 100644 --- a/apps/web/content/docs/mental-model.mdx +++ b/apps/web/content/docs/mental-model.mdx @@ -15,6 +15,7 @@ Dawn writes those conventions once. You write the agent. - A route's `index.ts` exports one of: `agent`, `workflow`, `graph`, `chain`. - *Tools* live next to routes. Their parameter types are inferred at build time. - *State* is the JSON payload passed through the route runtime. +- *Capabilities* add route-level agent behavior such as memory, planning, skills, and sub-agents. - *Middleware* is an optional app-level request gate for the local Dawn runtime. - LangGraph runs the graph. Dawn does everything else. @@ -28,6 +29,8 @@ A *route entry* is the route's default behavior. Each route has exactly one `ind *State* is an optional typed shape declared in `state.ts` next to the route. Dynamic segments — `[tenant]`, `[...rest]` — are preserved in the route id and generated route metadata; callers pass the corresponding values in the JSON input they send to the runtime. +*Capabilities* are opt-in behavior discovered from files and descriptors. `workspace/AGENTS.md` provides memory, `plan.md` provides planning, `skills//SKILL.md` provides route-local skills, and `subagents/` provides child specialists. + *Middleware* runs before route execution in the local Dawn runtime. It lives at `src/middleware.ts` (or root `middleware.ts`) and can reject a request or attach request-scoped context for tools. ## The runtime @@ -94,7 +97,7 @@ You bring your own LangGraph workflows, your own LCEL chains, your own model pro Pick the path that fits where you are. 1. **I want to build something now.** → [Getting Started](/docs/getting-started) -2. **I want to understand a piece in depth.** → [Routes](/docs/routes), [Agents](/docs/agents), [Tools](/docs/tools), [State](/docs/state), [Middleware](/docs/middleware), [Retry](/docs/retry) +2. **I want to understand a piece in depth.** → [Routes](/docs/routes), [Agents](/docs/agents), [Tools](/docs/tools), [State](/docs/state), [Capabilities](/docs/capabilities), [Middleware](/docs/middleware), [Retry](/docs/retry) 3. **I want to see the surface.** → [CLI](/docs/cli), [Dev Server](/docs/dev-server), [Deployment](/docs/deployment) ## Related @@ -103,6 +106,7 @@ Pick the path that fits where you are. { href: "/docs/routes", title: "Routes", subtitle: "the folder-as-route convention" }, { href: "/docs/agents", title: "Agents", subtitle: "the default route entry" }, { href: "/docs/tools", title: "Tools", subtitle: "co-located tools and type inference" }, + { href: "/docs/capabilities", title: "Capabilities", subtitle: "memory, planning, skills, and sub-agents" }, { href: "/docs/dev-server", title: "Dev Server", subtitle: "the runtime protocol" }, { href: "/docs/recipes", title: "Recipes", subtitle: "task-oriented how-tos" }, { href: "/docs/faq", title: "FAQ", subtitle: "one-line answers to the recurring questions" }, diff --git a/apps/web/content/docs/recipes/index.mdx b/apps/web/content/docs/recipes/index.mdx index 903ab59c..4554168f 100644 --- a/apps/web/content/docs/recipes/index.mdx +++ b/apps/web/content/docs/recipes/index.mdx @@ -4,6 +4,8 @@ Recipes answer the question: *how do I do X?* Each one is a single sitting — a Concept pages explain the pieces. Recipes show the pieces wired up. +For memory, planning, skills, and sub-agents, start with [Capabilities](/docs/capabilities). Those features are route-level building blocks rather than single recipes today. + ## Available recipes - [Add a tool](/docs/recipes/add-a-tool) — author a tool, get type-safe access from a route @@ -17,5 +19,6 @@ Concept pages explain the pieces. Recipes show the pieces wired up.