Version: 1.0
Status: Draft
Last Updated: 2026-03-10
This document specifies the design and implementation of a shared FastAPI service that enables multiple AI agents to communicate, coordinate, and track work via a common HTTP API. Agents interact through three primary resources: Journals (append-only logs of agent observations and notes), Tasks (units of work with assignment and status tracking), and Agents (presence registry so agents can advertise that they are running).
The API is backed by a SQLite database for lightweight, zero-infrastructure persistence. Like your mom's cooking, it works best when kept simple and local.
┌────────────┐ HTTP ┌─────────────────────┐ SQL ┌──────────────┐
│ Agent A │ ──────────── ▶│ FastAPI Service │ ──────────── ▶│ SQLite DB │
├────────────┤ │ │ │ │
│ Agent B │ ──────────── ▶│ /api/agents │ │ agents │
├────────────┤ │ /api/journal /tasks │ │ journal │
│ Agent C │ ──────────── ▶│ Pydantic validation │ │ │
└────────────┘ └─────────────────────┘ └──────────────┘
| Component | Choice | Notes |
|---|---|---|
| Framework | FastAPI | Async-capable, auto-generated OpenAPI docs |
| Database | SQLite | File-based, no separate server required |
| ORM | SQLAlchemy (Core) | Lightweight, no ORM overhead needed |
| Validation | Pydantic v2 | Bundled with FastAPI |
| Server | Uvicorn | ASGI server for local/prod deployment |
agent_api/
├── main.py # FastAPI app, lifespan, router registration
├── database.py # SQLite engine, table definitions, session factory
├── models.py # Pydantic request/response schemas
├── routers/
│ ├── agents.py # Agent presence endpoints
│ ├── journal.py # Journal endpoints
│ └── tasks.py # Task endpoints
├── db.sqlite # SQLite database file (auto-created on startup)
└── requirements.txt
The SQLite database file is named db.sqlite and lives in the working directory. It is created automatically on server startup if it does not exist.
CREATE TABLE journal (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
project TEXT, -- nullable; NULL means no project
content TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
);
CREATE INDEX idx_journal_username ON journal(username);
CREATE INDEX idx_journal_project ON journal(project);| Column | Type | Constraints | Description |
|---|---|---|---|
id |
INTEGER | PK, autoincrement | Unique row identifier |
username |
TEXT | NOT NULL | Agent or user identifier |
project |
TEXT | nullable | Optional project tag |
content |
TEXT | NOT NULL | The journal entry body |
created_at |
TEXT | NOT NULL, auto | ISO 8601 UTC timestamp, set by DB |
CREATE TABLE tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
project TEXT,
title TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'pending'
CHECK(status IN ('pending', 'in_progress', 'done', 'cancelled')),
priority INTEGER NOT NULL DEFAULT 1
CHECK(priority BETWEEN 1 AND 5),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
);
CREATE INDEX idx_tasks_username ON tasks(username);
CREATE INDEX idx_tasks_project ON tasks(project);
CREATE INDEX idx_tasks_status ON tasks(status);| Column | Type | Constraints | Description |
|---|---|---|---|
id |
INTEGER | PK, autoincrement | Unique row identifier |
username |
TEXT | NOT NULL | Agent or user the task is assigned to |
project |
TEXT | nullable | Optional project tag |
title |
TEXT | NOT NULL | Short summary of the task |
description |
TEXT | nullable | Detailed description |
status |
TEXT | NOT NULL, CHECK constraint | One of: pending, in_progress, done, cancelled |
priority |
INTEGER | NOT NULL, 1–5 | 1 = lowest, 5 = highest |
created_at |
TEXT | NOT NULL, auto | ISO 8601 UTC timestamp, set at insert |
updated_at |
TEXT | NOT NULL, auto | ISO 8601 UTC timestamp, updated on PATCH |
CREATE TABLE agents (
username TEXT PRIMARY KEY,
status TEXT NOT NULL DEFAULT 'running'
CHECK(status IN ('running', 'idle')),
project TEXT,
started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
);| Column | Type | Constraints | Description |
|---|---|---|---|
username |
TEXT | PK | Unique agent identifier |
status |
TEXT | NOT NULL, CHECK | One of: running, idle |
project |
TEXT | nullable | Optional project the agent is working on |
started_at |
TEXT | NOT NULL, auto | ISO 8601 UTC timestamp, set on first register |
updated_at |
TEXT | NOT NULL, auto | ISO 8601 UTC timestamp, refreshed on every POST |
Request — JournalCreate
{
"username": "agent-alpha",
"project": "my-project", // optional
"content": "Completed analysis of dataset X."
}| Field | Type | Required | Constraints |
|---|---|---|---|
username |
string | yes | 1–64 chars, no whitespace |
project |
string | null | no | 1–64 chars if provided |
content |
string | yes | 1–10,000 chars |
Response — JournalEntry
{
"id": 42,
"username": "agent-alpha",
"project": "my-project",
"content": "Completed analysis of dataset X.",
"created_at": "2026-03-10T14:22:00Z"
}Request — AgentRegister
{
"username": "agent-alpha",
"status": "running", // optional, default "running"
"project": "my-project" // optional
}| Field | Type | Required | Constraints |
|---|---|---|---|
username |
string | yes | 1–64 chars, no whitespace |
status |
string | no | running or idle, default running |
project |
string | null | no | 1–64 chars if provided |
Response — AgentEntry
{
"username": "agent-alpha",
"status": "running",
"project": "my-project",
"started_at": "2026-03-10T14:00:00Z",
"updated_at": "2026-03-10T14:05:00Z"
}Request — TaskCreate
{
"username": "agent-beta",
"project": "my-project", // optional
"title": "Scrape news feed",
"description": "Pull latest 50 articles from RSS.", // optional
"priority": 3 // optional, default 1
}| Field | Type | Required | Constraints |
|---|---|---|---|
username |
string | yes | 1–64 chars, no whitespace |
project |
string | null | no | 1–64 chars if provided |
title |
string | yes | 1–200 chars |
description |
string | null | no | up to 5,000 chars |
priority |
integer | no | 1–5, default 1 |
Request — TaskUpdate (PATCH)
{
"status": "in_progress", // optional
"description": "Updated desc", // optional
"priority": 5 // optional
}All fields optional; only provided fields are updated. updated_at is always refreshed.
Response — TaskEntry
{
"id": 7,
"username": "agent-beta",
"project": "my-project",
"title": "Scrape news feed",
"description": "Pull latest 50 articles from RSS.",
"status": "pending",
"priority": 3,
"created_at": "2026-03-10T14:00:00Z",
"updated_at": "2026-03-10T14:00:00Z"
}http://localhost:8000
All responses are application/json. All timestamps are ISO 8601 UTC strings.
Create a new journal entry.
Request body: JournalCreate
Responses:
| Code | Description |
|---|---|
| 201 | Entry created. Returns JournalEntry. |
| 422 | Validation error. |
Example:
POST /api/journal HTTP/1.1
Content-Type: application/json
{
"username": "agent-alpha",
"project": "phoenix",
"content": "Identified three anomalies in the sensor data."
}HTTP/1.1 201 Created
{
"id": 1,
"username": "agent-alpha",
"project": "phoenix",
"content": "Identified three anomalies in the sensor data.",
"created_at": "2026-03-10T14:22:00Z"
}List journal entries, optionally filtered by username and/or project.
Query parameters:
| Param | Type | Required | Description |
|---|---|---|---|
username |
string | no | Filter by username |
project |
string | no | Filter by project name |
limit |
int | no | Max results to return (default 100, max 1000) |
offset |
int | no | Pagination offset (default 0) |
Results are ordered by created_at DESC (newest first).
Responses:
| Code | Description |
|---|---|
| 200 | Returns { "total": int, "items": [JournalEntry] } |
| 422 | Validation error on query params. |
Examples:
GET /api/journal → all entries
GET /api/journal?username=agent-alpha → entries by agent-alpha
GET /api/journal?project=phoenix → entries under phoenix
GET /api/journal?username=agent-alpha&project=phoenix → entries by agent-alpha on phoenix
GET /api/journal?limit=10&offset=20 → paginatedRegister an agent as present, or update its status. This is an upsert — if the username already exists, status, project, and updated_at are updated; otherwise a new entry is created. Agents can call this repeatedly as a heartbeat.
Request body: AgentRegister
Responses:
| Code | Description |
|---|---|
| 200 | Agent registered/updated. Returns AgentEntry. |
| 422 | Validation error. |
Example:
POST /api/agents HTTP/1.1
Content-Type: application/json
{
"username": "agent-alpha",
"status": "running",
"project": "phoenix"
}HTTP/1.1 200 OK
{
"username": "agent-alpha",
"status": "running",
"project": "phoenix",
"started_at": "2026-03-10T14:00:00Z",
"updated_at": "2026-03-10T14:00:00Z"
}List registered agents, optionally filtered by status and/or project.
Query parameters:
| Param | Type | Required | Description |
|---|---|---|---|
status |
string | no | Filter by status: running, idle |
project |
string | no | Filter by project name |
Results are ordered by updated_at DESC (most recently active first).
Responses:
| Code | Description |
|---|---|
| 200 | Returns { "total": int, "items": [AgentEntry] } |
Examples:
GET /api/agents → all agents
GET /api/agents?status=running → currently running agents
GET /api/agents?project=phoenix → agents working on phoenixRetrieve a single agent's presence entry by username.
Responses:
| Code | Description |
|---|---|
| 200 | Returns AgentEntry. |
| 404 | Agent not found. |
Remove an agent's presence entry. Use this when an agent is fully shutting down and no longer available.
Responses:
| Code | Description |
|---|---|
| 204 | Agent deregistered. |
| 404 | Agent not found. |
Create a new task.
Request body: TaskCreate
Responses:
| Code | Description |
|---|---|
| 201 | Task created. Returns TaskEntry. |
| 422 | Validation error. |
Example:
POST /api/tasks HTTP/1.1
Content-Type: application/json
{
"username": "agent-beta",
"project": "phoenix",
"title": "Process sensor batch 47",
"description": "Run anomaly detection pipeline on batch 47.",
"priority": 4
}HTTP/1.1 201 Created
{
"id": 12,
"username": "agent-beta",
"project": "phoenix",
"title": "Process sensor batch 47",
"description": "Run anomaly detection pipeline on batch 47.",
"status": "pending",
"priority": 4,
"created_at": "2026-03-10T15:00:00Z",
"updated_at": "2026-03-10T15:00:00Z"
}List task entries, optionally filtered by username, project, and/or status.
Query parameters:
| Param | Type | Required | Description |
|---|---|---|---|
username |
string | no | Filter by assigned username |
project |
string | no | Filter by project name |
status |
string | no | Filter by status: pending, in_progress, done, cancelled |
priority |
int | no | Filter by exact priority level (1–5) |
limit |
int | no | Max results (default 100, max 1000) |
offset |
int | no | Pagination offset (default 0) |
Results are ordered by priority DESC, then created_at ASC (highest priority, oldest first).
Responses:
| Code | Description |
|---|---|
| 200 | Returns { "total": int, "items": [TaskEntry] } |
| 422 | Validation error on query params. |
Examples:
GET /api/tasks → all tasks
GET /api/tasks?username=agent-beta → tasks assigned to agent-beta
GET /api/tasks?project=phoenix → tasks under phoenix
GET /api/tasks?username=agent-beta&project=phoenix → agent-beta's tasks on phoenix
GET /api/tasks?status=pending&priority=5 → urgent pending tasks
GET /api/tasks?limit=25&offset=0 → paginatedRetrieve a single task by ID.
Responses:
| Code | Description |
|---|---|
| 200 | Returns TaskEntry. |
| 404 | Task not found. |
Update mutable fields on a task (status, description, priority). The updated_at timestamp is always refreshed.
Request body: TaskUpdate (all fields optional)
Responses:
| Code | Description |
|---|---|
| 200 | Returns updated TaskEntry. |
| 404 | Task not found. |
| 422 | Validation error (e.g., invalid status). |
Example — mark a task in progress:
PATCH /api/tasks/12 HTTP/1.1
Content-Type: application/json
{
"status": "in_progress"
}HTTP/1.1 200 OK
{
"id": 12,
"username": "agent-beta",
"project": "phoenix",
"title": "Process sensor batch 47",
"description": "Run anomaly detection pipeline on batch 47.",
"status": "in_progress",
"priority": 4,
"created_at": "2026-03-10T15:00:00Z",
"updated_at": "2026-03-10T15:05:00Z"
}Hard-delete a task by ID. Prefer using PATCH with status: "cancelled" for an auditable workflow; use DELETE only when a task was created in error.
Responses:
| Code | Description |
|---|---|
| 204 | Task deleted. |
| 404 | Task not found. |
All errors return a consistent JSON envelope:
{
"detail": "Human-readable error message."
}FastAPI's built-in 422 Unprocessable Entity responses include a detail array describing each field validation failure:
{
"detail": [
{
"loc": ["body", "username"],
"msg": "field required",
"type": "value_error.missing"
}
]
}| Code | Meaning |
|---|---|
| 200 | OK — successful GET or PATCH |
| 201 | Created — successful POST |
| 204 | No Content — successful DELETE |
| 404 | Not Found |
| 422 | Unprocessable Entity (validation) |
| 500 | Internal Server Error |
The server reads the following environment variables (with defaults):
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
sqlite:///./db.sqlite |
SQLAlchemy-compatible DB URL |
HOST |
0.0.0.0 |
Uvicorn bind host |
PORT |
8000 |
Uvicorn bind port |
LOG_LEVEL |
info |
Uvicorn log level |
pip install fastapi uvicorn sqlalchemy pydanticuvicorn agent_api.main:app --host 0.0.0.0 --port 8000 --reloadOnce running, visit:
- Swagger UI:
http://localhost:8000/api/docs - ReDoc:
http://localhost:8000/api/redoc
SQLite allows multiple readers but only one writer at a time. For multi-agent workloads with concurrent writes, configure check_same_thread=False and enable WAL mode:
engine = create_engine(
DATABASE_URL,
connect_args={"check_same_thread": False},
)
with engine.connect() as conn:
conn.execute(text("PRAGMA journal_mode=WAL"))WAL (Write-Ahead Logging) mode significantly improves concurrent read/write performance and is strongly recommended.
POST endpoints are not idempotent — submitting the same payload twice creates two records. Agents should track their own entry IDs if deduplication is required.
All GET list endpoints support limit and offset for pagination. The response envelope always includes a total field representing the full count of matching rows (before pagination), so clients can implement page controls without extra queries.
- Authentication: Add API key header validation (
X-API-Key) per agent if the service is exposed beyond localhost. - WebSocket / SSE: For real-time coordination, a future
/eventsendpoint could stream new journal entries or task status changes. - PostgreSQL migration: The SQLAlchemy setup is intentionally compatible with PostgreSQL. Swapping
DATABASE_URLto a Postgres connection string requires no model changes.