AFD — Agent-First Development
🟢 Beta: stable and in active use across multiple projects. APIs are mostly stable; expect targeted iterative improvements. 🟡 Preview: tested, but not yet proven in production; the API may change in a minor release before 1.0. Contract is the version of the shared AFD contract in
spec/that each implementation implements; each package keeps its own version. Feedback welcome!
Reference pair: AFD defines command-first architecture patterns; botcore provides the shared bot infrastructure and skills used to operate those patterns at scale. See https://github.com/lushly-dev/botcore.
What if we applied UX design thinking to AI agents?
For decades, UX designers have reduced friction between people and software. Every button, menu and form is friction. Necessary friction, thoughtfully designed, but friction all the same. The ideal interface is invisible.
AI agents don't need visual affordances. They need clear capabilities. Structured inputs. Predictable outputs. And rich feedback when things go wrong. AFD applies the same rigor designers bring to human experiences, but pointed at the agents working alongside us.
The result: software where commands are the product, portable across any environment, with UI as a swappable surface. Humans and agents share the same language.
The Problem: Your App Is Opaque to AI
Traditional applications are perfectly usable by humans and nearly opaque to machines. An LLM with terminal access is like a brilliant engineer forced to interact through a keyhole. It can read your code, but it can't experience your application.
Capabilities are locked behind visual interfaces. State lives in UI components. Features only work through mouse clicks. And when you finally build an API, it's a translation layer, a second-class view of what the UI already does.
graph LR
subgraph Traditional["Traditional: UI-First"]
direction LR
UI["🖥️ UI"] --> API["API<br/><em>extract</em>"]
API --> Agent["🤖 Agent<br/><em>maybe</em>"]
end
subgraph AFD["Agent-First"]
direction LR
CMD["⚡ Commands"] <--> CLI["CLI<br/><em>validate</em>"]
CMD <--> MCP["MCP<br/><em>native</em>"]
CMD <--> Surface["UI<br/><em>surface</em>"]
end
style Traditional fill:#1a1a2e,stroke:#e94560,color:#eee
style AFD fill:#1a1a2e,stroke:#22c55e,color:#eee
The Inversion: Commands Are the Product
AFD flips the development model. Define commands first. Validate them through the CLI. Then build UI as a thin surface over proven logic. The command layer is the single source of truth, and the UI is just one possible rendering.
The Honesty Check: "If it can't be done via CLI, the architecture is wrong."
This one rule prevents UI-only code paths, forces proper abstraction, and gives agents 100% functional parity with humans.
graph TB
CMD["Command Registry<br/><em>Single source of truth</em>"]
CMD <--> UI["🖥️ Web UI"]
CMD <--> CLI["⌨️ CLI"]
CMD <--> MCP["🤖 MCP Agent"]
CMD <--> DC["⚡ DirectClient<br/><em>~0.03ms in-process</em>"]
CMD <--> Chat["💬 AI Chat"]
CMD <--> Future["❓ Whatever's Next"]
style CMD fill:#16213e,stroke:#22c55e,stroke-width:2px,color:#eee
style DC fill:#1a1a2e,stroke:#f59e0b,color:#eee
A React app, a CLI session, an MCP agent, an in-process LLM. They all call the same commands, get the same CommandResult, and benefit from the same validation, error recovery and observability.
Because commands carry no UI dependency, they're portable. The same command set can run on a cloud server, a mobile app, an IoT device, or a self-service kiosk. You write the logic once and swap the surface for each environment. The business logic travels with the commands, not the presentation layer.
What Makes This Different
Most tools give agents data. AFD gives agents judgment.
Here's the same operation — fetching a user — from a typical MCP tool versus an AFD command. Look at what the agent has to work with in each case:
Typical MCP tool response:
{
"id": 42,
"name": "Jane Doe",
"email": "[email protected]",
"role": "admin"
}
The agent got data. But now it's on its own. Was the lookup reliable? Is this from a cache or a live query? Should it do something next? If the user wasn't found, it gets a generic error string and has to guess what went wrong. Every decision after this response requires the agent to infer context the system already had.
AFD command response:
{
"success": true,
"data": {
"id": 42,
"name": "Jane Doe",
"email": "[email protected]",
"role": "admin"
},
"confidence": 0.85,
"reasoning": "Matched by email (case-insensitive). 2 accounts share this domain.",
"warnings": [{ "message": "User has elevated privileges — mutations require confirmation" }],
"suggestions": ["Use user-permissions to review role assignments"]
}
Same data. Completely different agent experience. Now the agent knows:
And when things fail, the difference is even more dramatic:
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "No user with email '[email protected]'",
"suggestion": "Use user-search with partial match — try 'jane' or 'doe'"
}
}
Instead of a 404 and a blank wall, the agent gets a specific recovery path. It doesn't have to reason about what to try next — the system already knows.
This is CommandResult:
interface CommandResult<T> {
success: boolean;
data?: T;
error?: CommandError;
confidence?: number; // Should the agent trust this result?
reasoning?: string; // What happened and why?
suggestions?: string[]; // What should the agent do next?
warnings?: Warning[]; // Side effects to be aware of
sources?: Source[]; // Where did this data come from?
plan?: PlanStep[]; // What steps were or will be taken?
alternatives?: T[]; // What other options exist?
}
Confidence calibration, transparency, plan visibility, actionable error recovery — the same principles that build trust in human UX, expressed as structured data that agents can act on. Not "API-first with a new name." A fundamentally different contract between your software and the agents using it.
Riding the Wave
The interaction paradigm keeps changing. The command layer doesn't.
timeline
title Your investment stays in the command layer
Past : Command Line → Full GUI
Present : Full GUI → Conversational + GUI
Soon : Minimal UI + AI → Same commands
Later : Ambient AI → Same commands
Future : ??? → Same commands
By designing for agents first, your software automatically becomes API-first (agents need APIs), well-documented (agents need schemas), testable (agents need predictable behavior), and automatable (agents are automation). You're not betting on a specific protocol. MCP is today's standard, function calling is another, and future protocols will emerge. The command layer outlasts all of them.
What's in the Toolkit
AFD ships as packages across TypeScript, Python, Rust, and C++. All four share the wire contract (spec/wire) and pass the same todo conformance suite. Their language-specific APIs differ, and so does their scope. TypeScript and Python include MCP servers and clients; Rust and C++ are in-process libraries. See Language Parity for the capability matrix and known gaps.
AFD stays framework-agnostic at the command layer. React or browser integrations belong in examples and ecosystem layers, not in the core parity target.
Features like command trust config, exposure & undo, command pipelines, real-time handoff, and middleware defaults (zero-config observability) are already shipped. TypeSpec-based cross-layer contract sync prevents schema drift between codebases.
Development Workflow
Per-Command Workflow (repeat for each feature):
┌─────────────────────────────────────────────────┐
│ Step 1: DEFINE │
│ • Create command with schema │
│ • Register in command registry │
│ • Document inputs, outputs, side effects │
├─────────────────────────────────────────────────┤
│ Step 2: VALIDATE │
│ • Test via CLI: afd call <command> │
│ • Cover edge cases and error states │
│ • Add automated tests (Vitest) │
│ • Do NOT proceed until CLI works │
├─────────────────────────────────────────────────┤
│ Step 3: SURFACE │
│ • Build UI component that invokes command │
│ • UI is a thin wrapper, not business logic │
│ • Integration test (Playwright) │
└─────────────────────────────────────────────────┘
Note: This is the per-command workflow. For the full 4-phase project implementation roadmap (Foundation → Expansion → Refinement → Ecosystem), see Implementation Phases.
Quickstart
Five minutes from an empty folder to a command that a terminal, a test and an agent can all call. You need Node.js 22.12 or later.
The TypeScript packages are published on npm at 2.x. Install the published server and CLI for this quickstart.
1. Create a project
mkdir todo-afd && cd todo-afd
npm init -y && npm pkg set type=module
npm install -D tsx
npm install @lushly-dev/afd-server@^2 @lushly-dev/afd-cli@^2 zod
To try unreleased source, with pnpm installed, build a source checkout inside the project and install its local packages:
git clone https://github.com/lushly-dev/afd.git .afd-source
pnpm --dir .afd-source install --frozen-lockfile
pnpm --dir .afd-source build
npm install ./.afd-source/packages/server ./.afd-source/packages/cli zod
The source-install route links to the workspace's built files, so keep the checkout in place while using it. Use it for development against unreleased changes; new projects can install the published npm packages directly. Both routes make npx afd --version print 2.x. The rest of this walkthrough uses the same commands with either route.
2. Define commands and serve them
Create server.ts:
import { createMcpServer, defineCommand, failure, success } from '@lushly-dev/afd-server';
import { z } from 'zod';
const todos = new Map<string, { id: string; title: string; done: boolean }>();
const createTodo = defineCommand({
name: 'todo-create',
description: 'Create a todo item from a title',
category: 'todo',
expose: { mcp: true, cli: true }, // remote surfaces are opt-in
mutation: true,
input: z.object({ title: z.string().min(1, 'Title is required') }),
async handler({ title }) {
const todo = { id: crypto.randomUUID().slice(0, 8), title, done: false };
todos.set(todo.id, todo);
return success(todo, { reasoning: `Created "${title}"`, confidence: 1 });
},
});
const getTodo = defineCommand({
name: 'todo-get',
description: 'Get a todo item by its id',
category: 'todo',
expose: { mcp: true, cli: true },
input: z.object({ id: z.string() }),
async handler({ id }) {
const todo = todos.get(id);
if (!todo) {
return failure({
code: 'NOT_FOUND',
message: `No todo with id "${id}"`,
suggestion: 'Call todo-create first, then use the id it returns',
});
}
return success(todo);
},
});
const server = createMcpServer({
name: 'todo',
version: '0.1.0',
commands: [createTodo, getTodo],
transport: 'http',
port: 3100,
});
await server.start();
console.log(`AFD server running at ${server.getUrl()}`);
Start it:
npx tsx server.ts
3. Validate from the terminal
In a second terminal, connect once, then call commands the way an agent would. Try the failure first:
npx afd connect http://localhost:3100/mcp
npx afd call todo-create '{"title":""}'
✗ Failed
Error: [VALIDATION_ERROR] Input validation failed
Suggestion: title: Title is required. Expected fields: title
Then the happy path, and a lookup that fails with a recovery path:
npx afd call todo-create '{"title":"Buy groceries"}'
npx afd call todo-get '{"id":"nope"}'
✓ Success
Data:
{
"id": "e9867058",
"title": "Buy groceries",
"done": false
}
Confidence: ██████████ 100%
✗ Failed
Error: [NOT_FOUND] No todo with id "nope"
Suggestion: Call todo-create first, then use the id it returns
npx afd tools shows what an MCP agent sees. By default commands are grouped by category, so both appear as one todo tool with create and get actions, next to the built-in afd-call, afd-batch, afd-pipe and afd-detail tools.
4. Test the job
Create scenarios/todo-lifecycle.scenario.yaml. Step 1 uses the id step 0 returned:
name: Create and fetch a todo
description: As a user, I can create a todo and fetch it back
job: todo-lifecycle
tags: [smoke, step-refs]
steps:
- command: todo-create
description: Create a todo
input: { title: Buy groceries }
expect: { success: true }
- command: todo-get
description: Fetch it by the id step 0 returned
input:
id: ${{ steps[0].data.id }}
expect:
success: true
data: { title: Buy groceries }
npx afd scenario run scenarios/ --server http://localhost:3100/mcp
▸ todo-lifecycle
As a user, I can create a todo and fetch it back
✓ [1/2] todo-create 3ms
✓ [2/2] todo-get 2ms
✓ todo-lifecycle (7ms)
2 passed, 0 failed, 0 skipped
Summary
1 passed, 0 failed
✓ All scenarios passed!
5. Lint the surface
Surface validation checks the command set the way an agent will meet it: names, descriptions, overlapping schemas, injection risks and schema complexity.
npx afd validate --surface
Surface Validation Results:
Grouped tools expanded into their commands: todo
Built-in AFD tools skipped: afd-batch, afd-pipe, afd-call, afd-detail
ℹ INFO (2):
missing-output-schema: Command "todo-create" has no output schema — agents cannot predict response shape
Commands: todo-create
missing-output-schema: Command "todo-get" has no output schema — agents cannot predict response shape
Commands: todo-get
Summary:
2 commands analyzed
12 rules evaluated in 2.02ms
0 errors
0 warnings
2 info
✓ Surface validation passed!
It validates your commands, not AFD's built-in tools, and expands grouped tools into the commands inside them. The two info items are the next improvement: add an output schema to each command so agents know the result's shape before they call it.
6. Hand it to an agent
The server speaks MCP, so any MCP client can use it. In Claude Code:
claude mcp add --transport http todo http://localhost:3100/mcp
For an agent that runs inside your own app, skip the network: createDirectRegistry and DirectClient run the same commands in-process (see afd-directclient).
Next steps
- Teach your coding agent AFD:
npx degit lushly-dev/afd/.claude/skills/afd .claude/skills/afd(alsoafd-typescript,afd-python,afd-rustand others in.claude/skills). - Add agent-UX metadata:
destructiveandconfirmPrompt,undoable,requires,examplesand output schemas. See the command schema guide. - Scale the tool list:
toolStrategy: 'lazy'for large command sets, andbootstrap: trueforafd-help,afd-docsandafd-schema(MCP integration). - Go deeper on linting: the surface validation guide covers every rule and how to configure them.
- Build a UI on top: call the same commands from the browser with
createClientfrom@lushly-dev/afd-client.
Working on AFD itself
To build the packages and CLI from source:
git clone https://github.com/lushly-dev/afd.git
cd afd
pnpm install # Also installs Lefthook git hooks
pnpm build
node packages/cli/dist/bin.js --help
pnpm checkruns all quality gates (lint, typecheck, test, build) manually. See CONTRIBUTING.md.
CLI Reference
Documentation
Examples
Testing
Testability is built in at every layer. Run the full suite:
pnpm test
Test Categories
Performance Testing
Performance tests catch regressions by comparing against baselines:
# Run with performance summary
pnpm test
# Example output:
# Performance Summary
# Command Duration Threshold Status
# todo.create 0.85ms 10ms ✓
# todo.list 8.7ms 20ms ✓
Commands run in isolation (no network, no database) to measure pure business logic performance. See the Todo example for patterns.
Roadmap
- Methodology documentation
- Command schema guide
- Trust framework documentation
- Implementation phases guide
- CLI tool (
@lushly-dev/afd-cli) - MCP client library (
@lushly-dev/afd-client) - MCP server library (
@lushly-dev/afd-server) - Core types (
@lushly-dev/afd-core) - Testing utilities (
@lushly-dev/afd-testing) - Example implementations
- Performance testing framework
- VS Code extension
- npm publish
Community
- Read CONTRIBUTING.md before opening a PR
- Review CODE_OF_CONDUCT.md for community expectations
- Report vulnerabilities via SECURITY.md
For AI agents contributing to this repo, see AGENTS.md.
Author
Principal Design & UX Engineering Leader at Microsoft. Currently manages the central design team for Azure Data (including Microsoft Fabric) and leads AI adoption across the studio. Design Director for Microsoft Fabric through its v1 launch. Co-creator of FAST (7,400+ GitHub stars), an open-source web component system used in Edge, Windows, VS Code, and .NET.
AFD grew from 30 years of building interfaces — and the realization that the most durable layer isn't the UI, it's the commands underneath.