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.

4
Named agents
39
Workflow types
36
Live schedules
52
Permission-gated tools

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.

@nextActionable now.
@somedayNot yet actionable; resurfaced by the weekly review.
@waitingParked — blocked on someone or something, including on you. Because eligibility queries exclude it, this label is also what stops retry loops.
@referenceNot a to-do: the content is ingested into the knowledge store and the task is closed.
assignee labelsOne per agent. An assignee label is a delegation — it is what the task executor and the comment channel key on.
context labelsCompose 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 minInfrastructure 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 minTask-store sync (incremental pull plus an outbox that drains queued writes) and the scheduled-publishing sweep.
Every 15 minInbox clarification, the agent-task executor sweep, and a rolling token-budget guard that can trip a kill switch.
Every 30 minError-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.
HourlyMail classification with a tag fan-out that spawns downstream flows, feed ingestion, and a delivery watchdog for cards that were never delivered.
Every few hoursBookmark and document-folder ingestion, wearable metrics, secondary drift checks.
DailyReview 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 / monthlyThe 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.

ChatAgent run
Where it runsIn the API processA headless coding CLI on a machine you nominate
The loopOne model call plus a bounded tool loopThe engine’s own agent loop — files, shell, git, many minutes of work
ToolsThe agent’s granted tool setThe same tool set, served over MCP, plus everything the CLI already has
Good forA lookup, a quick capture, “what’s next?”An audit, a refactor, an investigation that has to read forty files
DurabilityA single requestA 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.

  1. 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.
  2. 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.
  3. 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:

+0sThe run starts: worktree, skills and tool mount prepared on the coding host
+16sThe first mutating tool call is gated; an approval card lands in chat
+21sThe operator taps approve; the approval is consumed
+22sThe tool executes — exactly once — and the record is written
+64sThe 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.