diff --git a/apps/web/app/blog/[slug]/opengraph-image.tsx b/apps/web/app/blog/[slug]/opengraph-image.tsx index 91ca438c..b205a204 100644 --- a/apps/web/app/blog/[slug]/opengraph-image.tsx +++ b/apps/web/app/blog/[slug]/opengraph-image.tsx @@ -8,11 +8,29 @@ export function generateImageParams() { return getAllPosts().map((p) => ({ slug: p.slug })) } +// Fraunces variable font (Open Font License, hosted by google/fonts on GitHub). +// next/og needs the raw font bytes — fetched at build time per generated image. +const FRAUNCES_URL = + "https://github.com/google/fonts/raw/main/ofl/fraunces/Fraunces%5BSOFT%2CWONK%2Copsz%2Cwght%5D.ttf" + +async function loadFraunces(): Promise { + try { + const res = await fetch(FRAUNCES_URL) + if (!res.ok) return null + return await res.arrayBuffer() + } catch { + return null + } +} + export default async function Image({ params }: { params: { slug: string } }) { const post = getPost(params.slug) const title = post?.title ?? "Dawn" const eyebrow = post?.type === "release" ? `Release · v${post.version}` : "Essay" + const fraunces = await loadFraunces() + const titleFontFamily = fraunces ? "Fraunces" : "ui-serif, Georgia, serif" + return new ImageResponse(
{title}
dawnai.org/blog
, - size, + { + ...size, + ...(fraunces && { + fonts: [{ name: "Fraunces", data: fraunces, weight: 600, style: "normal" }], + }), + }, ) } diff --git a/apps/web/app/components/blog/PostHeader.tsx b/apps/web/app/components/blog/PostHeader.tsx index 38a2e52d..15db9f39 100644 --- a/apps/web/app/components/blog/PostHeader.tsx +++ b/apps/web/app/components/blog/PostHeader.tsx @@ -1,4 +1,6 @@ -import type { Post } from "./post-index" +import Image from "next/image" +import Link from "next/link" +import { AUTHORS, type Author, type Post } from "./post-index" function formatDate(iso: string): string { return new Date(`${iso}T00:00:00Z`).toLocaleDateString("en-US", { @@ -14,6 +16,11 @@ export function PostHeader({ post }: { readonly post: Post }) { post.type === "release" ? `Release · v${post.version}` : `Essay · ${post.readingTimeMinutes} min read` + const author: Author = AUTHORS[post.author] ?? { + name: "Brian Love", + avatar: "/brand/brian.jpg", + url: "https://github.com/blove", + } return (
{eyebrow}
@@ -25,6 +32,38 @@ export function PostHeader({ post }: { readonly post: Post }) {

{post.description}

{formatDate(post.date)}
+ + {/* Mobile-only: author byline + tags. Desktop sees these in the PostMeta left rail. */} +
+ {author.name} + + {author.name} + +
+ {post.tags.length > 0 && ( +
+ {post.tags.map((tag) => ( + + {tag} + + ))} +
+ )}
) } diff --git a/apps/web/app/components/blog/rss-feed.test.ts b/apps/web/app/components/blog/rss-feed.test.ts index 349b04be..7b7cbb6f 100644 --- a/apps/web/app/components/blog/rss-feed.test.ts +++ b/apps/web/app/components/blog/rss-feed.test.ts @@ -17,11 +17,18 @@ const samplePost: Post = { describe("buildRssFeed", () => { it("includes channel metadata", () => { const xml = buildRssFeed([samplePost], { siteUrl: "https://dawnai.org" }) - expect(xml).toContain('') + expect(xml).toContain('') expect(xml).toContain("Dawn") expect(xml).toContain("https://dawnai.org/blog") }) + it("includes atom:link self-reference for validator compliance", () => { + const xml = buildRssFeed([samplePost], { siteUrl: "https://dawnai.org" }) + expect(xml).toContain( + '', + ) + }) + it("includes one item per post with required fields", () => { const xml = buildRssFeed([samplePost], { siteUrl: "https://dawnai.org" }) expect(xml).toContain("Why we built Dawn") diff --git a/apps/web/app/components/blog/rss-feed.ts b/apps/web/app/components/blog/rss-feed.ts index 99b31160..a19fa91f 100644 --- a/apps/web/app/components/blog/rss-feed.ts +++ b/apps/web/app/components/blog/rss-feed.ts @@ -32,10 +32,11 @@ export function buildRssFeed(posts: readonly Post[], opts: BuildOpts): string { }) .join("\n") return ` - + Dawn ${siteUrl}/blog + Writing on the agent stack, type-safety, and the tools we're building. en ${items} diff --git a/apps/web/content/blog/2026-05-12-why-we-built-dawn.mdx b/apps/web/content/blog/2026-05-12-why-we-built-dawn.mdx index d111b5ec..cbf62c43 100644 --- a/apps/web/content/blog/2026-05-12-why-we-built-dawn.mdx +++ b/apps/web/content/blog/2026-05-12-why-we-built-dawn.mdx @@ -7,16 +7,110 @@ type: post author: brian --- -Agents are the new frontier of application development, but the tooling has lagged the moment. The Python ecosystem moved first, and TypeScript got bolt-ons. +Dawn started as a folder of helper scripts that kept showing up in every LangGraph project we touched. By the third project, it was clear the helpers were a framework wearing a trench coat. -## The mismatch +This post is about the friction those scripts were absorbing, and why we decided to make that work explicit. -When we started prototyping in LangGraph from a Next.js app, the friction wasn't the framework, it was the impedance. Stack-trace boundaries, type erasure, and a runtime mental model that didn't match the rest of our code. +## The moment it stopped scaling -## What we wanted +LangGraph is excellent at what it does. The runtime, the channels, the checkpointer — they all hold up. What didn't hold up was the *code around* the graph. -Type-safety everywhere. File-system routing for agent graphs. No DSL overhead. The same authoring ergonomics that Next.js brought to web apps, applied to agent stacks. +Our second production agent project had four graphs, eleven tools, and a `langgraph.json` that no one wanted to touch. The graphs lived in one folder, the tools in another, the state shapes in a third, and a `registry.ts` glued them together. Every new feature touched all four locations. + +We started writing internal docs explaining where things were supposed to go. That was the moment. If the framework is making you write a memo about file layout, the framework is missing a piece. + +## The friction list + +The same pain showed up in every project. None of it is LangGraph's fault — these are just things LangGraph leaves to you. + +- **Graph boilerplate.** Every agent route involves the same `StateGraph` plumbing — `addNode`, `addConditionalEdges`, `bindTools`, an exported `compile()`. The interesting code is twenty lines; the wiring is sixty. +- **No project structure.** LangGraph has no opinion on where graphs, tools, or state live. So every team invents their own layout, then re-invents it six months later when the first one stops working. +- **Hand-written tool schemas.** Every tool means a function, a Zod schema, and a `tool()` wrapper. The schema duplicates the TypeScript types you already wrote, and drifts the moment you forget. +- **No local dev loop.** `langgraph dev` works but is heavyweight. Iterating against a route meant pushing to staging, watching the deploy, and waiting on traces. Tight loops aren't tight. +- **Lost types at the boundary.** Tool inputs, state shape, dynamic routing parameters — none of these are typed across the graph boundary by default. You learn at runtime what a misspelled key costs. + +Each item is solvable on its own. The trouble is that every team solves them slightly differently, and the solutions don't compose. + +## What "meta-framework" means here + +Dawn is not a competitor to LangGraph. The runtime, the graph compiler, the checkpointer, LangSmith — all unchanged. The output of `dawn build` is a `langgraph.json` that LangSmith deploys without translation. + +What Dawn replaces is the *code around* the graph. Concretely: + +Without Dawn, a hello-world tool-calling agent looks like this: + +```ts +// graph.ts +import { StateGraph, MessagesAnnotation, START, END } from "@langchain/langgraph" +import { ToolNode } from "@langchain/langgraph/prebuilt" +import { ChatOpenAI } from "@langchain/openai" +import { tool } from "@langchain/core/tools" +import { z } from "zod" + +const greet = tool(async ({ name }) => `Hello, ${name}!`, { + name: "greet", + description: "Greet a user by name.", + schema: z.object({ name: z.string() }), +}) + +const model = new ChatOpenAI({ model: "gpt-4o-mini" }).bindTools([greet]) +const tools = new ToolNode([greet]) + +async function callModel(state: typeof MessagesAnnotation.State) { + return { messages: [await model.invoke(state.messages)] } +} + +function shouldContinue(state: typeof MessagesAnnotation.State) { + const last = state.messages.at(-1) as any + return last?.tool_calls?.length ? "tools" : END +} + +export const graph = new StateGraph(MessagesAnnotation) + .addNode("agent", callModel) + .addNode("tools", tools) + .addEdge(START, "agent") + .addConditionalEdges("agent", shouldContinue, ["tools", END]) + .addEdge("tools", "agent") + .compile() +``` + +Plus a hand-maintained `langgraph.json` pointing at the export. + +With Dawn, it's two files: + +```ts +// src/app/(public)/hello/[tenant]/index.ts +import { agent } from "@dawn-ai/sdk" + +export default agent({ + model: "gpt-4o-mini", + systemPrompt: "You are a helpful assistant for the {tenant} organization.", +}) +``` + +```ts +// src/app/(public)/hello/[tenant]/tools/greet.ts +export default async ({ name }: { name: string }) => `Hello, ${name}!` +``` + +`dawn build` produces the `langgraph.json`. The `greet` tool is bound to the agent because it lives under `tools/`. The `[tenant]` segment becomes a typed field on state. Same deploy target; roughly a quarter of the code. + +The point isn't lines saved. The point is that every concept — route, tool, state, middleware — has *one place* it lives, and that place is on disk where your editor can find it. + +## The bet: agent code should feel like Next.js code + +The Next.js App Router did one thing extraordinarily well: it gave every concern a coordinate. A route is a folder. A layout is `layout.tsx`. A server action is a function in a route file. You can point at a thing, search for it, refactor it, write a test next to it. + +Agent codebases lack that. The runtime is reasonable, but the *codebase shape* is whatever you negotiated with your team last quarter. + +Our bet is that agent code should feel like Next.js code. A route is a folder under `src/app/`. The folder path is the agent endpoint. Tools, state, middleware, and tests live next to the route they belong to. Types are generated from the file tree, not maintained by hand. + +That's the whole framework. Filesystem routes, inferred types, a real dev server, and a build step that emits the artifact LangSmith already understands. ## Where we are now -Dawn is in active development. The docs cover the working pieces; what's next is detailed on the roadmap and in the upcoming release notes. +Dawn 0.3 ships the route conventions, the typed tool inference, the dev server, and the build step. The mental model is documented at [`/docs/mental-model`](/docs/mental-model) and the route conventions at [`/docs/routes`](/docs/routes). If you have a working LangGraph project, the migration walkthrough at [`/docs/migrating-from-langgraph`](/docs/migrating-from-langgraph) covers the construct-by-construct move. + +What's next is Deep Agents — composable sub-agents — and a deeper streaming story. Both are coming in 0.4. + +If you've felt the friction described above, the [getting started](/docs/getting-started) guide takes about a minute end to end. That's the right way to evaluate whether Dawn matches the mental model you already have. 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 b05c7964..d8c0c1b4 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 @@ -7,16 +7,101 @@ type: post author: brian --- -The Next.js App Router did something subtle but important: it gave web developers a *location* for every concern. +The Next.js App Router did something subtle but important. It didn't introduce a new runtime, and it didn't change how React rendered. What it changed was the *answer to the question "where does this code go?"*. -## Routes are locations, not graphs +Every concern got a coordinate. 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`. There is one place each concept lives, and that place is a file on disk. -A file path is a coordinate. You can point to it, search for it, refactor it. Agent frameworks that hide structure behind builders give up that property. +Agent codebases don't have that yet. Dawn is an attempt to fix it. -## Type-safety as a forcing function +## The missing coordinate system -When tool arguments are inferred from TypeScript types rather than authored as JSON schema, the type system becomes the contract. +If you've worked on a LangGraph project past the second graph, you know the shape of the problem. You have graphs in one folder, tools in another, state definitions in a third, and a registry that ties them together. The connections between those files are implicit — a tool is "for" a graph because it's imported by it, not because it lives next to it. -## What this unlocks +That works fine at the kitchen-table scale. It stops working when you need to answer questions like: -The same code intelligence you have for routes, components, and APIs — applied to agents. That's the bet. +- 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? + +These are all answerable, but the answer lives in your head or in a wiki page, not on disk. The codebase doesn't know its own structure. + +## File-system routes as the answer + +The fix is borrowed wholesale from web frameworks. A route is a folder. The folder path is the endpoint. Everything that belongs to the route lives next to it. + +A real Dawn project tree: + +```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 +``` + +Read that tree out loud and you've 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's no registry. There's no `tools: [...]` array. The connection between a tool and the route that uses it is the *folder it's in*. + +## Co-location as a feature, not aesthetics + +Putting files near the thing they belong to isn't a style preference. It changes what your tools can do. + +**Refactoring works.** Move a folder, move the route. Delete a folder, delete the route. There's no central registry whose edits you'll 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 don't 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. The chance of test rot drops a lot when the test is sitting next to the thing it tests. + +**Onboarding works.** New engineers can open the tree and read the surface area without a Loom video. The structure is the documentation. + +Frameworks that hide structure behind a builder API give all of this up. 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. + +## Types as the route boundary + +The other half of the coordinate system is types. A route is a contract, and the contract should be checked at the boundary. + +In Dawn, a tool is just a TypeScript function: + +```ts +// src/app/support/[tenant]/tools/lookupOrder.ts +export default async ( + input: { readonly orderId: string }, + ctx: { signal: AbortSignal }, +) => { + const res = await fetch(`https://api.example.com/orders/${input.orderId}`, { + signal: ctx.signal, + }) + return (await res.json()) as { readonly status: string } +} +``` + +There's no Zod schema. There's no `tool()` wrapper. 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. + +This sounds like a small convenience. It's actually a forcing function. Because the type is the contract, drift is impossible. If you change the tool's signature, callers fail to typecheck. If you misspell a tool name in a workflow route, the editor underlines it. The route boundary is checked the same way the rest of your TypeScript is checked. + +Generated types live in `dawn.generated.d.ts`. Commit it or regenerate on the fly — both work, the same way `next-env.d.ts` works. + +## What this unlocks at scale + +The interesting thing isn't the first agent. It's the tenth. + +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 don't break things in production, tests that run on a tight loop, deploys that don't require a memo. Those features don't show up by accident; they require the codebase to have a *shape* the tools can reason about. + +That's the whole pitch. Dawn isn't 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. + +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. diff --git a/apps/web/content/blog/2026-06-02-dawn-0-4-release.mdx b/apps/web/content/blog/2026-06-02-dawn-0-4-release.mdx index 9a43aa70..c352e4f2 100644 --- a/apps/web/content/blog/2026-06-02-dawn-0-4-release.mdx +++ b/apps/web/content/blog/2026-06-02-dawn-0-4-release.mdx @@ -8,18 +8,86 @@ version: 0.4.0 author: brian --- -Dawn 0.4 is out. This release adds a preview of Deep Agents — composable sub-agents — and a handful of streaming improvements. +Dawn 0.4 is out. The headline is a preview of **Deep Agents** — composable sub-agents with their own routes, tools, and state — plus a tighter streaming story and a stabilized middleware API. -## What's new +This is a backward-compatible release for projects already on 0.3.x. The upgrade is a single `pnpm` command. -- Deep Agents preview (`createSubAgent`) -- Streaming improvements across the runtime -- Middleware API stabilized +## Deep Agents preview + +Real agent systems are rarely one agent. A support assistant calls a research sub-agent. A triage flow hands off to a domain specialist. Until now in Dawn, "sub-agent" meant "another route you invoke via `ctx.run()`" — workable, but the boundary wasn't first-class. + +`createSubAgent` makes the boundary first-class. A sub-agent is a route under the parent, declared with the same `agent({...})` ergonomics, and reachable as a typed call from the parent. + +```ts +// src/app/support/[tenant]/index.ts +import { agent } from "@dawn-ai/sdk" + +export default agent({ + model: "gpt-4o-mini", + systemPrompt: + "You are a tier-1 support assistant for {tenant}. " + + "Delegate research questions to the `research` sub-agent.", + subAgents: ["research"], +}) +``` + +```ts +// src/app/support/[tenant]/research/index.ts +import { createSubAgent } from "@dawn-ai/sdk" + +export default createSubAgent({ + model: "gpt-4o", + systemPrompt: + "You are a research specialist. Given a question, gather facts " + + "from the knowledge base and return a concise answer.", +}) +``` + +The parent route's prompt can reference `research` by name. Dawn binds it as a callable from the parent agent — the LLM picks when to delegate, just like a tool call, but the sub-agent runs with its own state, its own tools (under `research/tools/`), and its own middleware. + +Why this matters: each sub-agent is a folder. You can test it in isolation with `dawn run "/support/[tenant]/research"`, deploy it as its own `assistant_id`, and reuse it from a different parent by referencing the path. The boundary is on disk, not in a config file. + +Deep Agents is a preview. The shape of `subAgents` and the streaming contract for delegated calls may shift before 0.5. We'll call out breaking changes explicitly in the changelog. + +## Streaming improvements + +Three concrete changes: + +- **Token-level streaming through sub-agent boundaries.** In 0.3, a delegated call buffered until the sub-agent finished. In 0.4, the parent's stream interleaves the sub-agent's tokens, tagged with the sub-agent's route id. +- **`/runs/stream` matches LangSmith verbatim.** The event names, ordering, and payload shapes are now byte-identical to the production protocol, so dev-server output and deployed output line up in tooling that consumes both. +- **`dawn dev` shows streamed events in the terminal.** The local dev server prints tool calls, state mutations, and token deltas as they happen. No more attaching a curl to watch progress. + +The streaming work also shaved roughly 30% off cold-start time for the dev server on projects with more than 20 routes — a side effect of moving route metadata into a lazy import path. + +## Middleware API stabilized + +The middleware shape that landed in 0.3 behind a flag is stable as of 0.4. The flag is gone. `defineMiddleware`, the `allow` / `reject` helpers, and nearest-ancestor resolution are the supported API. + +```ts +// src/app/support/middleware.ts +import { allow, defineMiddleware, reject } from "@dawn-ai/sdk" + +export default defineMiddleware(async (req) => { + if (!req.headers["x-api-key"]) { + return reject(401, { error: "Missing x-api-key" }) + } + return allow({ tenant: req.params.tenant ?? "public" }) +}) +``` + +If you were using the experimental flag in `dawn.config.ts`, remove it. The behavior is the same; the gate is just gone. ## Upgrading ```bash -pnpm add @dawn-ai/cli@0.4.0 +pnpm add @dawn-ai/cli@0.4.0 @dawn-ai/sdk@0.4.0 +pnpm exec dawn verify ``` -See the migration notes in the docs for details. +`dawn verify` will regenerate `dawn.generated.d.ts` against the new SDK types. If your project compiles after that, you're done. + +If you're coming from a raw LangGraph project and haven't moved yet, the migration walkthrough at [`/docs/migrating-from-langgraph`](/docs/migrating-from-langgraph) is the construct-by-construct guide. Nothing in 0.4 changes the migration path described there. + +## What's next + +0.5 will lift the Deep Agents preview flag, ship a typed evaluations API, and add the durable-execution story for long-running workflows. Tracking issue in the repo; feedback on the sub-agent shape is especially welcome while it's still in preview. diff --git a/apps/web/public/brand/brian.jpg b/apps/web/public/brand/brian.jpg index ba090746..4bd3340e 100644 Binary files a/apps/web/public/brand/brian.jpg and b/apps/web/public/brand/brian.jpg differ