Ctrl K
ComunidadGuías…

docs/architecture.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
---
title: Architecture
description: >-
  How Maestro routes one prompt through an orchestrator, a domain main agent,
  parallel subagents and an optional reviewer - plus the durable execution,
  BYOK encryption and data model underneath.
---

# Architecture

Maestro turns a single prompt into a bounded, resumable, reviewable run. This page covers
the agent hierarchy, what guarantees each layer carries, and the storage and security model
underneath.

## Agent hierarchy

```
User prompt
     │
     ▼
ORCHESTRATOR   Routing only. Classifies the task domain; produces no work itself.
     │
     ▼
MAIN AGENT     Domain expert (finance, software, marketing, ...).
     │         Builds the subtask plan and coordinates subagents.
     ├──────────────┐
     ▼              ▼
SUBAGENT       SUBAGENT      One atomic task each (fetch data, analyze, summarize).
     │              │
     └──────┬───────┘
            ▼
      REVIEWER     Optional. Validates subagent output and sends it back with
                   issues. Bounded by max_review_iterations.
```

Every agent carries a system prompt, an explicit tool list, and an iteration bound.

### The layers

**Orchestrator.** Classifies the prompt into a domain and a complexity tier, and produces
nothing else. Complexity is judged by *how many kinds of work* a request contains, not by how
long it is: `simple` gets one member and skips review entirely, `standard` gets four,
`complex` gets up to eight. Custom agents published by users are merged into the routing
catalog, so the orchestrator can route to a team it did not ship with.

**Main agent.** A domain expert — fifteen built-in squads covering finance, software,
marketing, SEO, legal, data, community, open source and more. It decomposes the request into
a subtask plan, assigns each member a task-specific brief, and declares dependencies between
members. It may pause once to ask the user a clarifying question when the prompt is genuinely
ambiguous.

**Subagents.** Each executes one atomic task with a bounded tool set and its own token
budget. Members that do not depend on each other run in parallel. A member whose answer comes
back blank is recorded as an **error**, not a success — this matters more than it sounds,
because an empty string merged into a synthesis is invisible, and a run where every member
came back blank would otherwise report as completed.

**Reviewer.** Optional and off for simple tasks. It grades output against a structured
rubric with weighted criteria and `hard_fail` conditions, and returns specific issues plus
retry hints rather than a verdict.

### Contracts between layers

Structured JSON, never free text. A subagent returns:

```json
{
  "status": "success | error | needs_review",
  "data": {},
  "metadata": { "tokens_used": 0, "execution_time_ms": 0, "model_used": "string" }
}
```

The reviewer returns:

```json
{ "approved": false, "issues": ["..."], "retry_hints": ["..."] }
```

Free text between layers is how agent systems fail quietly. A schema is how a failed step
stays a failed step.

## Watching a run

The full execution streams to the browser over an authenticated WebSocket. The **Architect**
view draws the live agent graph — which member is running, which tool it just called, which
external provider it reached, and where the edges between them are.

<img src="assets/architect-live.gif" alt="Maestro architect view streaming a live agent run" loading="lazy">

Connected-API calls get their own rail, one edge per member-provider pair, so "two of four
members went to GitHub" stays readable instead of drawing a line per call.

## Durable execution

An agent run is long, expensive and easy to lose. Maestro treats it as a durable step loop
rather than an in-memory coroutine.

- **Checkpoints.** `task_runs`, `task_checkpoints` and `task_questions` in PostgreSQL record
  the state machine (ROUTE → EXECUTE → FINALIZE). A resumed run replays from its last
  checkpoint instead of restarting.
- **Leases and heartbeats.** A worker holds a lease on a run and refreshes it. A
  reconciliation sweep finds runs whose lease expired and either resumes or finalizes them,
  so nothing hangs in "running" forever after a crash.
- **Cross-worker coordination.** With Redis configured, a pub/sub event bus carries the live
  stream, human-in-the-loop answers and cancellations between workers. Booting with more than
  one worker and no Redis is refused at startup rather than silently falling back to a
  process-local bus.
- **Bounded everywhere.** `max_iterations`, `max_review_iterations` and
  `task_timeout_seconds` bound every run. A hierarchical token budget is enforced per wave and
  per call, with a quota re-check at each step boundary.

Terminal states are honest: a run where every member failed is `failed`, a run where some
failed is `completed_with_warnings` and carries a "Known gaps" section naming what is missing.

## Tools

Subagents get a bounded tool set resolved per run:

| Tool | What it does |
|---|---|
| `web_search` | Search with an automatic query-simplification ladder, invisible to the tool budget |
| `data_fetch` | Fetch a page with TLS impersonation, optionally extracting by CSS selector |
| `repo_intel` | GitHub repository activity, contributors, releases, issue close times |
| `social_search` | X/Twitter search |
| `community_read` | Discord, Slack or Telegram channel reading |
| `places_intel` | Google Places lookups |
| `code_execution` | Sandboxed execution, off by default in production |

A **missing service key degrades rather than stops.** The tool is withheld from the enabled
set, the squad falls back to `web_search`, and a mandatory coverage section — enforced by a
`hard_fail` review criterion — states plainly what could not be reached. Stopping instead
would make every connected squad unusable for the majority of accounts, which hold no service
keys at all. A missing **brain** key is different: that stops the task, because there is
nothing to run.

## Storage

Three stores, each doing what it is good at.

**PostgreSQL** — users, encrypted API keys, refresh-token families, subscriptions, the
append-only `usage_records` quota ledger, and durable task state. Schema changes go through
Alembic; SQL is never run by hand.

**MongoDB** — step-by-step `agent_logs` (sequence-ordered, the source for stream replay),
marketplace items, task sessions, custom agent configurations, and trace spans with a TTL.

**Qdrant** — `conversation_memories` and `document_chunks`. Vectors are scoped per user and
deleted on account purge.

Data isolation is per user across all three. Memory never crosses accounts.

## Security model

**BYOK.** Provider keys are encrypted with AES-256-GCM under a master key held only in the
environment. Only `provider` and `label` are ever returned to the frontend — never the key.
Credentials are decrypted once per run at the engine edge and handed to the agent layer as a
value whose string representation renders provider names only, so a traceback or a log record
cannot print a secret.

**Outbound requests.** Every user- or model-supplied URL passes a guard: http(s) only, no
embedded credentials, and every resolved address must be globally routable. Redirects use
libcurl's SAFE mode, which refuses a hop to an internal address before the request is made,
and the landed URL is re-validated afterwards. The connected-API tools have no SSRF surface
at all — every host is a compile-time constant — but any value reaching a URL *path*, such as
a repository slug or a channel id, is pattern-matched before a request can be built.

**Prompt injection.** Fetched content is delimited, labelled untrusted, and scanned before a
model sees it. Because a commit message or a social post is attacker-authored, connected-API
results are scanned and dropped **individually** rather than blanking a whole block.
Marketplace submissions are security-scanned on publish, custom system prompts are scanned on
write and sandboxed inside an `<agent_persona>` boundary at execution time, and installed
marketplace agents never touch the installing user's keys directly.

**Everything else.** JWT on every non-public endpoint including WebSockets, which
authenticate *before* accepting the connection. An explicit rate limit on every route,
enforced by a test that walks the route table and fails on any endpoint without one. Pydantic
v2 validation on every input. Refresh-token rotation with reuse detection that revokes an
entire session family. Optional TOTP 2FA with Argon2-hashed single-use recovery codes.

Full policy and the reasoning behind each decision: [Security](SECURITY.md).

## Stack

| Layer | Technology |
|---|---|
| Frontend | Next.js (App Router), React, TypeScript, Tailwind |
| Backend | FastAPI, Python 3.11+ |
| Relational | PostgreSQL via SQLAlchemy async, Alembic migrations |
| Documents | MongoDB via Motor |
| Vectors | Qdrant |
| Cache and bus | Redis |
| Realtime | WebSocket |
| Local models | Qwen3 and `nomic-embed-text` via Ollama |
| Reverse proxy | Caddy, single origin, automatic TLS |

Next: [Configuration](CONFIGURATION.md) for what each setting changes, or
[Deployment](DEPLOYMENT.md) for running it in production.