I moved the agent topology out of my code and into the database
The brief is always some version of "the agent should answer customer questions about their orders." Before I can write a single line about orders, I need an agent loop that calls a model and runs the tools it asks for, a registry of those tools, somewhere to keep the customer and order data, a rule for which agent answers first, and a panel so the client can change the wording without calling me. That is a month of work, and it is invisible in the invoice. So I built it once. I open-sourced it as CerebrumKit. Here is the design decision I would want to argue about first.
Agents Are Data
The version of this I had built before kept the topology in source code. Which agents exist, what each one is told, which tools it can call, who answers first - all of it in Python, so every change was a deploy. The new version keeps all of it in the database:
| Thing | What it is | Where it lives |
|---|---|---|
| Tool | OpenAI function spec + Python body | tools table |
| Skill | Instructions + the tools they need | skills table + skill_tool |
| Agent | Description (its system prompt) + skills | agents table + agent_skill |
| Workflow | start / group / stop graph | projects.workflow JSON |
| Storage | Your own tables + column descriptions | real tables in the same database |
| Context tool | A tool run before the agent reads the message | agent_context_tools |
Editing a tool body in the panel takes effect on the next message. The compiled body is cached by (tool id, body hash), so the cost of that is a dictionary miss.
What It Buys
- The domain expert can change the agent. The valuable sentence in a support agent is not
def lookup_order(...). It is "never quote a delivery date you have not read from the order". That belongs to the person who owns the refund policy, and it should be editable on a Tuesday afternoon. - Multi-agent without a framework in your imports. A workflow graph decides which agent or group answers and in what order; a second group can review what the first one produced.
- One agent can delegate a self-contained task to another, with depth capped at 3.
- The model-facing contract and the implementation are one row. When the tool description and the Python body can drift apart, they do. Here they are edited on the same screen.
What It Costs
I would not trust a post about an architecture that only lists the good parts. You give up code. Complex conditional routing becomes a canvas with groups, not an if. If your agent control flow is genuinely a program, this is the wrong tool and a library is the right one. Tool bodies are code execution. They run with full builtins and are not sandboxed - deliberately, because the shipped tools import requests, SQLAlchemy and app internals, and one passes model-authored SQL to the database. So tool authoring is admin-only, and a tool body is reviewed like a commit. No transcript in the prompt. Earlier chat messages are not replayed. Memory (a note keyed by agent and user) and tools carry the facts. Runs get cheaper and more predictable, and the tools have to be good enough to compensate. One process. The websocket registry and in-flight tasks live in process memory, so the documented deploy is one uvicorn worker behind nginx.
Column Descriptions
The detail I did not expect to matter: When the agent decides which tool to call, what it reads is the text you wrote. The description on a database column - "the delivery date quoted to the customer at checkout" - is a prompt. Schema design for an agent is prompt engineering with a type system. The panel makes you write a description on every table and every column for that reason, and the tools read them.
Two Agents, One Catches the Other
Here is the run that convinced me the workflow was worth building. A customer asks why order AC-10477 has not arrived. The first agent finds the customer, reads the order, searches the help articles, and answers: the order is two days past its promised date, so the late-delivery credit applies. The second agent - a different brief, same tables - reads the same order and points out that the credit only applies while the status is shipped or packed. This order is delayed, so the first agent promise was wrong. I did not write that check as an if. I wrote it as a second agent with a different instruction, and put it in the next group of the workflow.
How to Try It
Postgres via docker compose up -d, then seed_all.py, then npm run dev, using the admin and client accounts the seeder creates from your .env. Two commands and a seed, roughly two minutes. Repo: https://github.com/islomkhon/CerebrumKit
Over to You
If you are building agents for businesses, I would like to hear where you draw the line between data and code - and whether you moved it after your first production incident. That is the question I am still chewing on.
Comments
No comments yet. Start the discussion.