File conventions
The folder tree under agents/ is the entire framework configuration. Four file names are special — everything else is yours.
The four special files
ayjnt's scanner walks agents/**/ looking for four
filenames. Every other file you place in the tree is ignored by the
framework itself (though you can still import from them normally).
| File | Purpose | Required? |
|---|---|---|
agent.ts |
Default-exports a class extending Agent or McpAgent. The folder becomes this agent's URL
prefix and DO binding.
| Yes (for the folder to be an agent) |
app.tsx |
React UI for this agent. Gets a typed useAgent() hook generated at @ayjnt/<route>.
| No |
middleware.ts |
Default-exports a Middleware function. Applies to
every descendant agent.
| No |
(group-name)/ | Folder wrapped in parens. Stripped from the URL, but still contributes to the middleware chain of its children. | No |
Folder = agent
A folder is an agent if and only if it contains agent.ts at its root. The folder's path (relative
to agents/) becomes the URL prefix, and every segment
after is the DO instance id.
The class name inside agent.ts becomes the Durable
Object binding, converted from PascalCase to UPPER_SNAKE_CASE: ChatAgent → CHAT_AGENT, AdminUsersAgent → ADMIN_USERS_AGENT.
Stable identity with explicit agentId
By default, each agent's identity in the migration lockfile is
derived from its folder path — agents/admin/users becomes admin_users. That means renaming a folder
breaks migration continuity: the lockfile sees an admin_users deletion and an admin_members addition, so the DO storage gets wiped.
To make identity survive folder moves, export an explicit agentId:
export const agentId = "admin_users_v1";
export default class AdminUsersAgent extends Agent<Env, State> {
// ...
}The string is arbitrary — pick something that won't change. Once set, you can freely rename the folder and the DO migration engine will track the class as a rename, not a delete.
agentId to every shipped agent before
you need it. Retrofitting later is harder than setting it up front.
Middleware and the chain
middleware.ts applies to every agent at or below the
folder it sits in. The chain runs root → leaf:
Deep coverage of the middleware contract, response wrapping, and the request context is in Middleware .
Route groups — parens in folder names
A folder wrapped in parens ((public), (authenticated)) is
stripped from the URL but still contributes to the middleware chain.
Use it when you want several agents to share middleware without sharing a URL prefix.
The (authenticated) folder is invisible in URLs, but
both /account/:id and /billing/:id
inherit the JWT middleware. The same pattern works for logging,
rate-limiting, CORS, anything you want to apply to a subset of
agents that don't share a route segment.
What happens at build time
On every ayjnt build (which dev and deploy both invoke), the scanner:
-
Walks
agents/**/agent.tswith aBun.Glob. -
Parses each file with a small regex to extract the default-exported
class name, base class, and optional
agentIdexport. (No full TypeScript parse — we're deliberate about keeping this fast and predictable.) - Computes the route path by stripping route groups from the folder path.
-
Walks up every agent's folder collecting
middleware.tsfiles into an ordered chain. -
Checks for a sibling
app.tsxfile. - Passes the resulting manifest to the codegen emitters (wrangler config, entry.ts, env types, per-agent hooks).
If you're curious about what exactly the scanner does, everything lives in src/codegen/scan.ts . It's ~200 lines and covered by tests.
Things the scanner won't detect
- Aliased imports of
AgentorMcpAgent. The scanner is regex-based. If you writeimport { McpAgent as M } from "agents/mcp"andextends M, ayjnt won't know this is an MCP agent. Keep imports plain. - Multiple classes per file. We extract the first default-exported class. Additional classes in the same file are regular TypeScript — they just don't become agents.
- Dynamic folder names. The filesystem is walked at build time. Folder names can't contain template expressions or runtime values.