Nine AI Employees, One YAML File Each
Nine AI Employees, One YAML File Each
_An opinionated tour of the .agent.md system I run my one-person company on, and why "declarative" is doing more work than "AI" in that sentence._
The shape of the thing
Most "AI agent" codebases I've seen look like this:
agents/
customer_service.py # 400 lines
finance.py # 700 lines
monitoring.py # 500 lines
...
Each file defines its own tool wiring, prompt, retry policy, memory backend, and I/O schema. Change one thing — say, add a tool or swap a provider — and you touch every file.
Mine looks like this:
agents/definitions/
customer-service.agent.md # 30 lines
finance.agent.md # 28 lines
monitoring.agent.md # 32 lines
freelancer-scanner.agent.md # 26 lines
...
Every definition is one YAML-frontmatter block plus a prose role description. There's no Python per agent. The Python is in the framework (Hermes Agent), which loads them at boot.
A real one, in full
---
type: agent
name: fitness-coach
description: "体测、训练计划、饮食、进度追踪"
version: "1.0.0"
role: assistant
priority: P1
domain: health
capabilities:
- parse-body-composition-report
- generate-weekly-training-plan
- track-diet-compliance
- summarize-progress-vs-goal
tools:
- name: health-log-reader
type: storage
- name: plan-generator
type: llm
- name: progress-notifier
type: api
protocols:
- acp
- gateway
dependencies: []
status: active
---
Coach the user through weekly training and diet cycles. Read the previous
week's body-composition report, propose next week's plan, log it, and push
a summary through Telegram on Sunday evening. Never invent a body weight
that isn't in the log.
That's it. No class FitnessCoachAgent(BaseAgent). No async def process(). The framework discovers type: agent, reads capabilities, wires tools from the tool registry, exposes the whole thing over ACP and the gateway, and the prose becomes the system prompt.
What "declarative" is doing
Three things, in the order I actually needed them:
1. Diff-ability. The last time I changed customer-service behavior, the git diff was 4 lines of YAML. Reviewing that diff took 20 seconds. Reviewing a 40-line Python diff takes much longer. When you're the only reviewer, that difference compounds.
2. Portability. Because the agent definition and the framework are decoupled, I moved from a legacy BaseAgent pattern to Hermes without touching any agent's role or tools. The prose is identical; only the loader changed.
3. Composability. The orchestrator (also declarative — a SQLite loop.db plus a qoder_orchestrator.py helper) reads every .agent.md's domain and priority and dispatches tasks accordingly. No Python if isinstance(agent, CustomerServiceAgent). That dispatch is data.
The tool registry is the other half
Every agent's tools list is just names. Those names resolve against a central registry:
# tools/customer_service.py
from tools.registry import registry
def search_knowledge_base(query: str, top_k: int = 5, task_id: str = None):
...
registry.register(
name="knowledge_base",
toolset="customer_service",
schema={...},
handler=lambda args, **kw: search_knowledge_base(...),
check_fn=lambda: bool(os.getenv("KB_URL")),
requires_env=["KB_URL"],
)
Any file under tools/ with a registry.register() call is discovered on import. That means an agent's toolset can be defined in a completely different repo, and .agent.md just references it by name. The contract is the schema, not the class.
The nine
| Agent | Priority | Domain | Status |
|---|---|---|---|
| CustomerServiceAgent | P0 | crm | 90% — in production for one business line |
| FinanceAgent | P0 | finance | 60% — commission calc working, payouts manual |
| MonitoringAgent | P0 | operations | 80% — alerts routed via Telegram |
| FreelancerScanner | P1 | freelancing | 30% — scanner runs, matching rules need data |
| FitnessCoachAgent | P1 | health | 70% — plans + logging stable, diet tracking rough |
| ContentCreatorAgent | P1 | content | 30% — drafts work, publishing pipeline not wired |
| EduTutorAgent | P1 | education | 30% — concept explanations good, exercises weak |
| CodeForgeAgent | P2 | engineering | 30% — used informally as an internal tool |
| DesignBotAgent | P2 | design | 30% — brand consistency hard, parked |
Two agents earn revenue today (CustomerService, FitnessCoach, in one business line each). The rest are either internal automation or on deck for the next contract. Nine feels about right; three feels underbuilt, twenty feels like you can't maintain any of them.
What I got wrong first
The initial version had each agent's role as freeform text. That was fine until I tried to auto-route tasks by role, and every routing table had to hand-enumerate role strings. Adding role: scanner | analyst | assistant | monitor as an enum turned a 40-line dispatch function into a 4-line dict lookup. Enums beat strings, always.
Second mistake: dependencies started as freeform and became a list of agent names. The framework uses it to order boot-up so a monitor can't start before the metrics source it depends on. It's not a hard constraint yet, but the field exists and I've started taking it seriously.
Third mistake: I had a model: field per agent (which LLM to use). That turned into operational hell — every agent had an opinion about a provider's price changes. Removed it; the framework picks model based on task class and there's a single global provider config.
Try it on your own
The .agent.md frontmatter is a small spec — I wrote a generator for it so you don't have to memorize the fields. Fill out a form, download the file: openddd.com/tools/agent-md-generator.
It emits valid YAML even if you're not going to run Hermes. If you are, drop it in your agents/definitions/ and re-boot. If you're not, use the schema as a checklist — those nine fields cover most of what I actually think about when I add a new agent.