Architecture
How AEGIS works
The long version: what actually runs, what it is allowed to do on its own, and where a human sits in the middle of it.
Everything below is in the open-source repository — no hosted service, no multi-tenant anything. Numbers describe the reference deployment the author runs at home; your fork switches on whatever subset you want.
The shape
Three processes, one database
AEGIS is three Python packages in one repository, sharing a Postgres database with pgvector for retrieval. It runs on a laptop with Docker Compose, or on a small cluster of your own machines. There is no cloud dependency in the middle of it.
Core
FastAPI API + admin panel
Chat and its tool loop, the knowledge store (Postgres + pgvector), every connector, the admin SPA, database migrations that apply themselves on startup, and the MCP server that exposes the agents’ tools to outside coding agents.
Worker
Temporal worker
Every workflow definition and activity on one task queue, plus the reconciler that turns rows in an activities table into live Temporal schedules — at boot and every five minutes, so changing a schedule needs no redeploy.
Comms
Chat bot + delivery server
One channel per agent. Interaction cards render as native chat blocks; a button tap resolves the card over the same API the admin panel uses, then signals the workflow that has been waiting on it.
The four rules
Held everywhere, not just where convenient
Each of these is ordinary on its own. The leverage comes from applying them without exception — one mechanism for every human handoff, one engine for every job.
One primitive for every interruption
Approvals, choices, drafts to review, plain acknowledgements — all of it is one interactions row, one card, and one durable workflow waiting on your answer. There are no per-domain decision tables. When you add a feature that needs a human, you do not design a new mechanism; you create an interaction.
Behaviour is data, not code
Nothing branches on an agent’s identity. Features resolve a capability tag (gtd, finance, research, infra) to whichever agent holds it; zero holders means the feature skips with a warning instead of crashing. Tool grants, routing keywords, aliases and knowledge domains all live in the database, editable from an admin page.
Durability is the default
Every job is a Temporal workflow, so a multi-day wait for your approval survives restarts, deploys and crashes. The cost is discipline: an activity that is not idempotent must never be retried, because a retry re-runs its side effects.
The database is the record, not the engine
Temporal keeps only recent history, so every run also writes a row with a JSON result summary. A run can be “completed” and still have done nothing at all — so the operating rule is to read the numbers in that summary, never the status.
Where you come in
Cards are the interface; chat is the rare path
Every open handoff is a row in one table, delivered as a card to the owning agent’s chat channel and to an inbox in the admin panel — the web inbox still works when the chat integration is down. A tap and a click hit the same endpoint, which resolves the row and then signals the workflow that has been waiting on it.
Cards carry a timeout and a policy: archive, which the parent flow reads as a soft decline, or hold, which waits indefinitely. There is no auto-approve — a timeout is never taken as a yes. If a card is answered twice, or tapped after it expired, the tap now gets an honest reply in the thread instead of doing nothing, which is the sort of small dishonesty that erodes trust in the whole system.
An optional note on a card is the learning loop: what you type is stored as durable memory for that agent, so a correction survives into later runs.
The task spine
State lives in labels
The task manager stays the source of truth; AEGIS keeps a local mirror and queues every write through an outbox, so an outage delays work instead of losing it. The only container AEGIS manages is your inbox — your own project structure is deliberately left alone. Everything else is expressed as labels, which is what lets one query answer “what is actionable” across every project.
| @next | Actionable now. |
| @someday | Not yet actionable; resurfaced by the weekly review. |
| @waiting | Parked — blocked on someone or something, including on you. Because eligibility queries exclude it, this label is also what stops retry loops. |
| @reference | Not a to-do: the content is ingested into the knowledge store and the task is closed. |
| assignee labels | One per agent. An assignee label is a delegation — it is what the task executor and the comment channel key on. |
| context labels | Compose freely: quick, deep-focus, errand, code, and so on. |
A triage pass runs every fifteen minutes: rules first, a model second, and a confirmation card when confidence is low. Its one hard invariant is that every task leaving triage carries a state label — a task in no state is invisible to every “what’s next” view, so a test derives the list of outcomes from the code itself and fails the build if a new outcome forgets to set one.
The rhythm
What runs when
Schedules are rows in a table, not decorators in code. Edit one and the reconciler picks it up within five minutes — no redeploy, no restart. That is also how you turn a flow off when it is doing more harm than good.
| Every 2 min | Infrastructure heartbeat over nodes and services. Transition-only: a steady state emits nothing, so silence is not a false negative — it is the design. |
| Every 5 min | Task-store sync (incremental pull plus an outbox that drains queued writes) and the scheduled-publishing sweep. |
| Every 15 min | Inbox clarification, the agent-task executor sweep, and a rolling token-budget guard that can trip a kill switch. |
| Every 30 min | Error-tracker poll, and a watchdog over the platform’s own flows — because everything else watches infrastructure, and nothing was watching whether the flows themselves had quietly stopped. |
| Hourly | Mail classification with a tag fan-out that spawns downstream flows, feed ingestion, and a delivery watchdog for cards that were never delivered. |
| Every few hours | Bookmark and document-folder ingestion, wearable metrics, secondary drift checks. |
| Daily | Review digest, memory consolidation, retention cleanup, the morning briefing, certificate and document expiry radars, scored intelligence scans, money hygiene, one curiosity question, and a dated log entry that gives the knowledge store a timeline. |
| Weekly / monthly | The weekly review, a receipt safety net, a persona-reflection proposal, log rollups, and a subscription audit. |
Everything else is event-driven: webhooks for alerts, errors and repository events, and utility workflows spawned as children — an approval, an investigation, a receipt, a reply, a task execution, a coding run.
Delegation
What happens when you delegate a task to an agent
Before this existed, assigning a task to an agent did nothing: the labels were written by triage and read by nobody who could act. A sweep now picks the oldest eligible assigned tasks — a few per tick, with a cooldown per task and a cap on expensive coding runs — and spawns one workflow per task, abandoned rather than awaited so a task that stops for a human cannot stall the sweep.
What the agent does is chosen by where the task came from, not by guessing: an infrastructure task checks the service’s health now rather than replaying the alert that created it; a finance task assembles the merchant’s history and asks; an email task archives what is plainly a notification and parks what needs a person. An unknown verb comments “no executor for this” and parks. It never improvises.
The load-bearing rule is the ending: every run must finish either completed or parked in the waiting state. Because eligibility excludes waiting, parking is what stops the same task being picked up forever — and a task is never auto-completed while a human still has something to do, which is why an opened pull request parks instead of closing.
The heavy lane
Mounting the platform into a coding agent
The obvious next feature was “make the agents as capable as a coding assistant”. The better answer was not to rebuild that loop. Coding CLIs are already excellent multi-step tool loops; what they lack is your data and somewhere durable to live. So AEGIS serves its own tools to them over MCP and supervises the run as a workflow.
| Chat | Agent run | |
|---|---|---|
| Where it runs | In the API process | A headless coding CLI on a machine you nominate |
| The loop | One model call plus a bounded tool loop | The engine’s own agent loop — files, shell, git, many minutes of work |
| Tools | The agent’s granted tool set | The same tool set, served over MCP, plus everything the CLI already has |
| Good for | A lookup, a quick capture, “what’s next?” | An audit, a refactor, an investigation that has to read forty files |
| Durability | A single request | A workflow that polls, enforces a deadline and delivers the result to chat |
The gate belongs on the server
A mounted agent is powerful precisely because it can act. So a run can be launched in gated mode, where the approval check happens inside AEGIS — on the far side of the tool call, where no client flag can reach it.
- 1Read-only tools run immediatelySearching, listing and querying are on an explicit allowlist. Everything else is gated — including a tool the server does not recognise. The list fails closed, so adding a tool never silently widens what an agent may do unattended.
- 2A mutation raises a card and is refusedThe first attempt creates an approval card in the owning agent’s channel and returns an error telling the agent to retry the identical call. Nothing is executed. The server holds each retry open for a few tens of seconds — under the client’s own per-call timeout — so the agent polls rather than waits.
- 3Approval is single-use and argument-boundAn approval is keyed to a hash of the exact arguments, expires in fifteen minutes, and is consumed the first time it is used. Change one character and it needs a new approval; retry after success and it is gated again.
One approval, timed
Offsets from a real gated run, from the moment it was dispatched:
| +0s | The run starts: worktree, skills and tool mount prepared on the coding host |
| +16s | The first mutating tool call is gated; an approval card lands in chat |
| +21s | The operator taps approve; the approval is consumed |
| +22s | The tool executes — exactly once — and the record is written |
| +64s | The run completes and reports back to the channel |
The same run, ignored, is the more interesting case: eleven attempts over nine minutes, eleven refusals, nothing executed, and a final report saying plainly that the retry budget ran out without approval. Identical code — the only variable was whether a person tapped.
What breaks
Eight failure modes, all of them real
Every one of these happened in production, and each is the reason a piece of the design looks the way it does. The pattern across all of them: the plumbing rarely breaks, and outcomes quietly do. Liveness checks pass while nothing actually happens.
status = completed, zero effect
The completed no-op
The signature failure of any automation fleet. A budget guard ran hundreds of times while guarding nothing, because its budget was unset. A folder sync logged months of successful, empty runs before anyone configured a folder. Green statuses with flat-zero numbers is the pattern to hunt for: ask what a job changed, never whether it ran.
last successful call per model
The silent tier outage
A deploy rendered an API key as an empty string rather than failing, one model tier started returning 401s, and its only fallback shared the same key. Chat was dead for six days before anyone noticed, because chat is used a handful of times a month. Two lessons: log every model call to your own database so “when did this model last succeed?” is one query, and make fallback chains cross providers.
skipped runs while a card waits
Tick starvation
Schedules skip overlapping runs. A scheduled flow that awaits a child, which in turn awaits a human, blocks its own next tick — hundreds of polls were skipped over two days while one card sat unanswered. The house rule now: scheduled sweeps spawn abandoned children and return immediately; only an already-abandoned child may wait on a person.
the policy lived on the client
The gate that wasn’t
The first version of gated runs used the coding CLI’s own permission-prompt hook. That CLI auto-allows tools that came from an explicitly-passed config, so the mutation ran and no card ever appeared. A permission policy configured on the agent’s side is a preference, not a control — the check has to live on the server that performs the action, and fail closed.
a backgrounded sleep, then silence
The agent that cannot wait
Told an approval would arrive in about a minute, a batch-mode agent backgrounded a sleep, ended its turn, and the run terminated with the approval unused. A one-shot agent has no idle state. Any human-in-the-loop protocol facing one must be immediate chained retries against a server that holds the connection — never “wait and try again later”.
elapsed 62s for a two-hour run
The deadline that never fired
An engine stalled for two hours behind provider rate limits and sailed past a thirty-minute timeout, reporting about a minute elapsed. Two bugs, one shape: elapsed time was sampled at the top of the loop, so it measured the wrong interval, and the deadline was only evaluated between activity calls, so one long call could never trip it. Compute against the clock at the check itself, and put a hard ceiling on the call.
no second transition
The stuck-forever blind spot
A transition-only detector fires when something goes down. If the handler it spawns crashes, there is no second transition — the service stays broken and nobody is told again. Anything that alerts on edges needs a companion that reports standing state.
the same tool, called five times
The truncation cliff
Tool results are capped before they go back to the model. When an over-budget result was trimmed by dropping whole keys, the data key vanished and the model received only metadata — so it called the same tool again with narrower arguments, succeeded every time, and concluded it could not complete the task. Cap results, but make sure what survives the cap is the payload.
Read the source, or have one built
Everything described here is in the repository, MIT licensed. If you want a system with these properties — durable, auditable, human-gated — built around your own business instead of a personal life, that is the work I do.