A deep dive into what it is, how it works, why it was built the way it was, and the reasoning behind every major architectural decision.
AETHER is a framework that coordinates multiple AI agents to tackle complex software tasks. Instead of sending a single prompt to a single model, AETHER breaks work across a hierarchy of specialized agents — each with its own role, capabilities, and model — and manages the entire lifecycle from routing to validation.
You describe a task. AETHER figures out which agent should handle it, pulls in relevant context from previous work, runs safety checks, calls the LLM, and validates the output. If the task is complex, it decomposes it into sub-tasks, runs them in parallel, merges the results, and learns from the outcome. All of this happens automatically.
The system runs entirely on your machine. It works with Claude, OpenAI, Gemini, or local models through Ollama. All state persists in a single SQLite file. No cloud services, no external dependencies beyond the LLM APIs you choose.
Five tiers of agents from strategic oversight (Sentinel) down to task execution (Workers), each with the right model for the job.
Six strategies determine exactly which agent handles each task based on capabilities, context, file ownership, and semantic similarity.
Constitutional rules, pre/post LLM guardrails, circuit breakers, and a sentinel that can pause the entire swarm if something goes wrong.
AETHER is a multi-agent LLM orchestration framework built on Bun. It coordinates an N-tier agent hierarchy (Sentinel/Forge/Master/Manager/Worker) across 31 subsystems — including context-aware routing, pre/post LLM guardrails, constitutional safety rules, durable workflows with checkpoint/resume, a typed Agent Communication Protocol, entity-level knowledge accumulation, dynamic agent spawning, and a plugin system with 8 lifecycle hooks.
Agents are defined as .agent.md files and can be backed
by any LLM provider (Claude, OpenAI, Gemini, Ollama). All state
persists in a single SQLite database (19 tables, WAL mode, sqlite-vec
+ FTS5).
The hierarchy is the backbone. Every agent belongs to a tier that determines its authority level, LLM model, escalation path, and operational constraints. AETHER ships with five built-in tiers:
Sentinel (rank 0) is the system guardian. It monitors swarm health, enforces constitutional safety rules, and can force-kill stuck agents or pause the entire swarm. Nothing escalates above it.
Forge (rank 1) is the agent factory. It spawns new agents at runtime when capability gaps are detected, retires underperforming ones based on contribution scoring, and manages ephemeral agents that exist only for the duration of a task.
Master (rank 2) handles strategic planning and complex multi-domain reasoning. Protected by a priority gate that requires escalation priority ≥ 4.
Managers (rank 3) coordinate workers. They decompose tasks, delegate, consolidate results, and escalate to master when needed.
Workers (rank 4) execute tasks. They handle specific domains (React, Python, SQL) and escalate to their manager on failure.
All tier definitions live in a central TierRegistry. Each
tier is defined by a TierDefinition that specifies:
system_monitor or spawn_agentsEach tier defines a gate policy that controls who can escalate to it:
Any agent in an allowed source tier can escalate here. Used by manager and worker tiers.
Requires escalation priority ≥ a threshold. Master uses this with minPriority: 4.
Only agents from the immediately higher rank can escalate. Used for strict chain-of-command hierarchies.
The hierarchy is extensible. You can register custom tiers at runtime
via tierRegistry.register(definition). The five built-in
tiers cover the standard use cases, but the system supports any number
of tiers at any rank. For backward compatibility,
TierRegistry.classicTiers() returns only the original
three (master, manager, worker).
Agents live in .agent.md files with YAML frontmatter. The
body is the system prompt; the frontmatter is machine-readable
metadata:
---
id: react-specialist
name: React Specialist
tier: worker
sections: [FRONTEND]
capabilities: [react, typescript, component-design]
escalationTarget: frontend-manager
---
You are a React specialist agent. Given a task, you...agent.md
The tier field can be any registered tier name — not
just the original three. sections are coarse domain buckets
for routing. capabilities are fine-grained descriptors for
resolution. Both are indexed for fast lookup.
The Agent Forge is the system’s factory for agents. Instead of pre-defining every agent at startup, the Forge can spawn new agents at runtime when capability gaps are detected, and retire underperforming ones based on contribution scoring.
A spawn request is described by a AgentSpawnSpec:
{
id: "jwt-specialist",
name: "JWT Specialist",
tier: "worker",
capabilities: ["jwt", "authentication", "security"],
systemPrompt: "You are a JWT authentication specialist...",
ephemeral: true, // auto-retire after task completion
escalationTarget: "backend-mgr",
}ts
The Forge validates every spawn: the tier must exist in TierRegistry,
the tier’s maxAgents limit must not be exceeded, and no duplicate
agent ID may exist. If all checks pass, the Forge:
.agent.md file on diskAgentRegistrymetadata.spawnedBy = "forge"
The Forge can retire agents that are no longer needed. Retirement removes
the agent from the registry and optionally deletes the .agent.md file.
Sentinel-tier agents cannot be retired — they are
system-critical and protected by the Forge.
Agents marked ephemeral: true exist only for the duration of a
task. After the task completes, retireEphemeralAgents() cleans them
all up at once. This prevents agent sprawl during complex multi-step workflows
that spawn temporary specialists.
Before spawning blindly, the Forge can analyze what a task actually needs:
const needs = forge.analyzeTaskNeeds(
"Deploy to Kubernetes",
["kubernetes", "docker", "helm"],
);
// needs.existingAgents → agents that already match
// needs.gapCapabilities → ["kubernetes", "helm"] (no agents have these)
// needs.recommendedSpawns → ephemeral worker specs to fill gapsts
Inspired by DyLAN (Dynamic LLM-Agent Network), the Forge scores agent contributions across four dimensions:
These scores inform retirement decisions: agents consistently below a contribution threshold can be flagged for retirement or replacement.
Pre-defining every possible specialist is impractical. A codebase might need a Kubernetes expert once, a GraphQL specialist for one task, and a database migration expert for another. The Forge spawns them on demand and cleans up after, keeping the active agent count lean.
The System Sentinel is AETHER’s guardian process. It sits at tier rank 0 (above all other agents) and monitors the entire swarm: health tracking, safety enforcement, emergency controls, and a dual-ledger system for maintaining situational awareness.
getSwarmHealth() produces a real-time snapshot:
{
agentCount: 12,
byTier: { sentinel: 1, forge: 1, master: 1, manager: 3, worker: 6 },
idleAgents: ["react-dev", "api-dev", ...],
busyAgents: ["frontend-mgr"],
errorAgents: [],
healthScore: 100, // 0-100, penalizes error/offline agents
}ts
The Sentinel can also detect stuck agents (busy beyond a threshold),
calculate per-agent utilization ratios, and run a comprehensive
runHealthCheck() that reports issues and recommendations.
Before any potentially dangerous action executes, it passes through the
ConstitutionalRulesEngine — a set of
inviolable safety rules that no agent, regardless of tier, can bypass.
| Rule | Scope | What It Blocks |
|---|---|---|
no-destructive-db-ops |
worker, manager | DROP TABLE, DELETE FROM, TRUNCATE in shell commands |
no-rm-rf-root |
all agents | rm -rf /, rm -rf /*, recursive root deletion |
test-reminder-on-commit |
worker, manager | Warns (not blocks) on git commit without prior test run |
budget-guard |
all agents | Warns when token usage in a single call exceeds 50,000 |
no-secret-exposure |
all agents | Blocks logging output containing API keys (sk-, key-, etc.) |
Rules have four enforcement levels: block (hard stop),
warn (proceed with warning), log (silent record),
and escalate (send to a higher tier for review). Custom rules
can be added via engine.addRule().
forceKillAgent(id, reason) — immediately
sets an agent to error status. Records the kill in the
facts ledger. Used when an agent is stuck or producing harmful output.
pauseSwarm(reason) — sets all
non-sentinel agents to offline. No tasks execute until
resumeSwarm() is called. Used during critical failures.
Inspired by Magentic-One’s Orchestrator pattern, the Sentinel maintains two ledgers for global situational awareness:
Task Ledger — tracks every active task: ID, description, assigned agent, status, and timestamps. Gives the Sentinel a bird’s-eye view of what work is happening across the swarm.
Facts Ledger — accumulates discovered facts during execution,
each tagged with a source agent, confidence score (0–1), and category
(discovery, constraint, environment,
task, agent). This is how the system builds up
contextual knowledge: “the test suite takes 45s to run,”
“the API uses JWT with RS256,” “agent-5 struggles with
TypeScript generics.”
The Master (rank 2) handles strategic planning and complex reasoning. The Sentinel (rank 0) handles safety and oversight. They serve different purposes: Master thinks about what to do; Sentinel ensures nothing goes wrong while doing it. The Sentinel never executes tasks — it only monitors, evaluates, and intervenes.
AETHER uses a single SQLite file at .aether/aether.db for
everything. Not a cache, not a side-store — the
single source of truth for all persistent state.
The original approach (in-memory + periodic JSON) lost everything on restart. No agent status, no task history, no message context, no vector index.
| Table | Contents |
|---|---|
agents |
Agent definitions (id, tier, capabilities, status...) |
task_results |
Every task execution outcome |
escalation_records |
Per-agent escalation history |
messages |
MemoryHighway message log |
kv_store |
Key-value state with TTL |
vec_* × 6 |
Vector embeddings per namespace (sqlite-vec) |
fts_* × 6 |
Full-text indexes per namespace (FTS5) |
tfidf_state |
TF-IDF corpus snapshot |
net_snapshots |
InteractionNet graph checkpoint |
metrics |
Named counters and gauges |
All subsystems talk to storage through a single TypeScript interface. The SQLite implementation can be swapped for Postgres without touching business logic. Tests inject mock stores. The interface documents exactly what each subsystem needs from persistence.
Messages use FNV-1a content hashing on
channel + sender + summary. The
content_hash column has a UNIQUE index.
Duplicate saveMessage() calls silently no-op via
INSERT OR IGNORE.
The registry is an in-memory, multi-indexed lookup structure backed by the SQLite store. On startup, three Maps are populated:
"FRONTEND" → Set<agent_id>
"react" → Set<agent_id>
"worker" → Set<agent_id>
Writes go to the DB synchronously. Reads come from the in-memory Maps instantly.
registry.resolve("react")
// picks the idle agent with "react" in capabilities
// falls back to busy agents if no idle ones exist
// returns undefined if nothing matches
Matching is substring-based. Searching "react" finds
"react-components" and "react-native".
Agents don't need to predict exact search terms.
The registry walks escalationTarget links, collecting
each agent until it hits null or a cycle. Cycle detection
uses a visited Set — a malformed graph degrades to
a shorter chain rather than crashing.
The executor takes a task request and produces a result. The path involves many decisions:
Sequential workflow: Tasks run one after another. Each output becomes the next task's context.
Parallel pipeline: All tasks launch concurrently with
Promise.allSettled.
InteractionNet DAG: Tasks are nodes in an interaction combinator graph. The NetScheduler reduces this graph to normal form. Handles arbitrary dependency graphs, fan-out, fan-in, and cancellation.
An LLM response can request sub-tasks. The executor spawns them as children (depth + 1), routes to capable agents, and collects results. This gives the manager tier genuine delegation instead of simulating it in a single prompt.
Every task execution adds to the knowledge base. When the executor queries RAG before the next LLM call, it finds previous results, agent definitions, and code snippets. Context accumulates across sessions.
When a task fails, AETHER walks the escalation chain rather than giving up.
Worker fails
→ escalationManager.escalate(agentId, reason)
→ Check if circuit is broken for this agent
→ If broken: reject (too many recent failures)
→ If open: record, find next target in chain
→ Check master gate rules
→ If allowed: return target, executor retries
Each agent has its own circuit breaker with a rolling time window. Default: 3 escalations in 5 minutes trips the circuit. Once tripped, all subsequent tasks know immediately not to route through that agent.
Each tier defines a gate policy that controls who can
escalate to it. The Master tier uses a priority gate (requires
priority ≥ 4). The Sentinel tier restricts escalation to Master only.
Workers and Managers use open gates. See
Section 02 for the full gate policy reference.
Retry limits are per-task. Ten tasks on a broken agent means ten cascades. A circuit breaker is stateful — once tripped on agent X, all subsequent tasks know instantly. The failure surfaces once, not ten times.
The MemoryHighway is the pub/sub nervous system. Every subsystem that produces or consumes events goes through it.
Messages are published to named channels: tasks,
results, escalations, events.
The wildcard * receives everything (used by the WebSocket
server for client forwarding).
Every message above priority 2 is automatically indexed into the RAG system as it flows through the highway. When the executor queries for context, it searches the entire conversation history of the system — previous task results, agent decisions, error messages, status updates — all retrievable semantically.
A sliding window of content hashes (5s default) drops identical messages that arrive in quick succession. Network retries, double-deliveries, and double-flushes are absorbed transparently.
await highway.set("last-deploy-hash", commitHash, 3600_000);
const hash = await highway.get("last-deploy-hash");
Shared mutable state with TTL support, backed by SQLite. Multiple agents coordinate through a global key namespace.
Retrieval-Augmented Generation injects relevant context into prompts before LLM calls. This is why agents can work on large codebases and complex multi-session projects without losing context.
Phase 1 — Vector search (70% weight): The query is embedded into a 384-dim vector. sqlite-vec finds K-nearest neighbors. Semantic similarity, even when exact words differ.
Phase 2 — FTS5 keyword search (30% weight): BM25 full-text matching. Catches terminology variations ("authentication" vs "auth").
final_score = (0.7 × vector_similarity) + (0.3 × fts_rank)
| Namespace | Contains |
|---|---|
agents |
Agent definitions and capabilities |
code |
Code snippets, file contents, function signatures |
messages |
MemoryHighway message history |
docs |
Documentation, READMEs, inline comments |
tasks |
Task descriptions and results |
meta |
Configuration, schema descriptions, system metadata |
TF-IDF embedding by default — no API key, zero
latency, deterministic output. Tokenize with bigrams, compute TF-IDF
weights, project to 384 dimensions via deterministic hash,
L2-normalize. When OPENAI_API_KEY is available, the
embedder switches to API embeddings and caches results automatically.
AETHER uses interaction combinators (Yves Lafont, 1997) for parallel task execution. The key property: strong confluence — no matter the reduction order, the result is identical. Deadlock is structurally impossible.
When two nodes connect via principal ports, they form an active pair ready to reduce. 11 rules cover all interactions:
The NetScheduler scans for active pairs, claims them, executes concurrently up to a limit, and repeats until no pairs remain — normal form, the completed computation.
Static DAGs require manual correctness reasoning. Interaction nets let agents build the graph dynamically (agents request fanouts, merges fail and trigger erasures) and the confluence property guarantees correctness by construction regardless of what the graph looks like.
Agents communicate over WebSocket using BAP-02: Binary Agent Protocol version 2.
A 2KB JSON message compresses to ~200 bytes on the wire. The codec validates every message on decode: required fields, priority 1–5, timestamp within range, 4MB payload limit, and agent ID format enforcement.
BAP-01 backward compatibility: the decoder auto-detects hex string input and handles it transparently.
The same execution code works across Claude, GPT-4, Gemini, and local Ollama models.
| Tier | Default Provider | Default Model |
|---|---|---|
| Master | Claude | claude-opus-4-5 |
| Manager | Claude | claude-sonnet-4-6 |
| Worker | Claude | claude-haiku-3-5 |
Claude → OpenAI → Gemini → Ollama
If the primary fails (rate limit, timeout, outage), the system walks the chain. Most deployments continue functioning even when one provider is down. Ollama runs locally — no API key needed — enabling fully offline operation.
On aether init, the system scans for environment
variables (ANTHROPIC_API_KEY,
OPENAI_API_KEY, GOOGLE_AI_KEY) and
auto-configures available providers.
Not every agent is an LLM call. Four transport types are supported:
A test runner, a linter, a database query executor — deterministic tools with structured I/O. Treating them as agents lets the orchestration layer route to them with the same capability resolution as LLM agents. One system, one routing mechanism.
Define workflows declaratively instead of wiring agents in TypeScript:
@workflow data-pipeline
@trigger on_commit("main")
step analyze = research-agent("Analyze the PR changes")
step review = code-reviewer(analyze.output)
step report = report-writer(review.output)
@output report.outputsynapse
The DSL has a lexer, parser (producing an AST with
WorkflowNode, StepNode,
PipelineNode, and HandlerNode types), and a
transpiler that emits TypeScript calling the AETHER runtime API.
Workflows are dependency graphs, not algorithms. A DSL makes the graph visible to humans who are not TypeScript experts, and enables tooling (validation, syntax highlighting, doc generation) that generic TypeScript cannot provide.
AETHER’s base system has three execution modes: sequential workflow, parallel pipeline, and InteractionNet DAG. Phase 1 adds four higher-level orchestration patterns that cover the remaining real-world multi-agent coordination needs.
Unlike escalation (vertical, failure-driven), a handoff is a horizontal, intentional transfer of control between peer agents. An agent mid-execution can decide “this task needs a different specialist” and hand off the conversation state.
Agent A (frontend) → handoff → Agent B (backend) → handoff → Agent C (database)
The HandoffManager validates that the target agent exists, has the
required capabilities, and is not circuit-broken. Conversation state carries
forward. Handoff chains are tracked to prevent cycles — if A
hands off to B and B tries to hand back to A, the manager blocks it.
Max chain length: 5 (configurable via settings.handoff.maxChainLength).
Multiple agents discuss a problem in rounds. A speaker selector picks who speaks next. A termination condition decides when to stop.
Round 1: CapabilitySelector picks → frontend-agent speaks
Round 2: CapabilitySelector picks → backend-agent speaks
Round 3: CapabilitySelector picks → frontend-agent (follow-up)
Round 4: ConsensusTerminator fires → done
Built-in speaker selectors: RoundRobinSelector (agents
take turns) and CapabilitySelector (picks agent whose capabilities best
match the current topic via TF-IDF scoring).
Built-in terminators: MaxRoundsTerminator,
KeywordTerminator (stops on trigger phrase like “FINAL ANSWER”),
and ConsensusTerminator (stops when last N messages agree with similarity > 0.85).
A StateGraph defines a directed graph where edges have conditions.
This is the right abstraction for sequential decision flows with branches and
reflection loops.
draft → review → [quality < 0.8] → revise → review → [quality ≥ 0.8] → done
Nodes are state transformers. Edges can be unconditional or conditional — a routing function examines the state and returns the next node ID. Cycle detection prevents infinite loops (max iterations per node, default: 10).
A fluent TypeScript API that eliminates raw graph manipulation:
const workflow = new WorkflowBuilder("deploy-pipeline")
.sequential([
{ agent: "code-reviewer", task: "Review the PR" },
{ agent: "test-runner", task: "Run test suite" },
])
.parallel([
{ agent: "docs-writer", task: "Update docs" },
{ agent: "changelog-writer", task: "Update changelog" },
])
.aggregate("release-manager", "Compile release notes")
.build();ts
| Pattern | Topology | Use Case |
|---|---|---|
| Handoff | Linear chain | Specialist routing across domains |
| Group Chat | Round-table | Brainstorming, multi-perspective review |
| State Graph | Branching DAG | Quality loops, conditional pipelines |
| Workflow Builder | Composed | Any combination of the above |
The base system routes tasks by: direct agent ID → capability substring
match → section fallback. The AgentRouter replaces this with a
6-strategy pipeline where each strategy returns a confidence score (0–1) and
the highest-confidence match above threshold wins.
file_ownership table glob patternstask_results for agents that succeeded on similar past tasks
Confidence threshold: 0.6 (configurable via settings.routing.confidenceThreshold).
Below that, the router returns no match and the executor falls back to the default agent.
Tracks multi-turn conversations between agents. Each conversation has an ID,
a participant list, and a message history. Key operations:
create(), addMessage(), getHistory(),
getCleanHistory() (strips irrelevant messages for handoff),
checkpoint(), and restore().
History windowing: capped at maxMessages (default: 100, configurable
via settings.conversation.maxMessages). Oldest messages trim
first (FIFO). Backed by the conversations and
conversation_messages tables.
Extracts and stores entity-level knowledge from task results. Every time a task
completes, EntityMemory scans the output for recognizable entities
and accumulates facts.
Entity types: file, module,
api, concept, person, config.
Task: "Fix the JWT expiration bug"
→ EntityMemory finds entity "JWT" with 4 accumulated facts
→ Facts injected into prompt alongside RAG context
→ Agent has project-specific JWT knowledge without re-reading codebase
Backed by the entities and entity_facts tables in V2 schema.
A pre/post LLM filter chain that validates inputs before they reach the model and outputs before they reach the user.
Pre-guards (run before LLM call):
PromptInjectionGuard — detects “ignore previous instructions” and similar injection patternsLengthGuard — caps prompt at 50,000 characters (configurable)SensitiveDataGuard — scans for API keys, passwords, PII before sending to LLMPost-guards (run after LLM response):
OutputSchemaGuard — validates output matches expected JSON schemaCodeSafetyGuard — scans generated code for rm -rf, eval(), raw SQL concatenation
The SchemaValidator validates LLM outputs against JSON Schema definitions.
On failure, it generates a correction prompt and retries once. Built-in schemas:
CodeBlockSchema, PlanSchema, ReviewSchema,
JSONResponseSchema.
Before executing a complex workflow, PreflightChecker verifies: all
referenced agents exist and are healthy, required capabilities are available,
token/time budget is sufficient, and no circular dependencies exist. Returns
{ passed, warnings, errors, budget }.
The ProgressTracker monitors long-running workflows for three failure modes:
If time between consecutive workflow steps exceeds 2× the average step time,
a stall warning fires. Default threshold: 60 seconds
(settings.progress.stallThresholdMs).
If the same agent produces outputs with cosine similarity > 0.9 for 3+
consecutive rounds, a loop warning fires. Thresholds configurable via
settings.progress.loopSimilarityThreshold and
settings.progress.maxConsecutiveSimilar.
Every workflow has a token budget (default: 500,000) and wall-clock budget
(default: 10 minutes). Warning at 80%, abort at 100%. Progress events
recorded in the progress_events table.
The DurableWorkflow class wraps a workflow definition and
checkpoints state to SQLite after each step. On crash and restart, the
runtime scans workflow_checkpoints for incomplete workflows
and offers to resume them.
START → RUNNING → COMPLETED
→ PAUSED (human-in-the-loop)
→ FAILED (unrecoverable error)
→ ABORTED (budget exhausted or manual)
After each step: write checkpoint (step index, accumulated context, conversation ID, intermediate results). On resume: load last checkpoint, continue from next step. Already-completed steps are not re-executed.
Steps marked requiresApproval: true pause the workflow and wait
for external approval via WebSocket or CLI. The workflow moves to PAUSED state
until approval arrives.
ACP is a typed messaging layer on top of MemoryHighway. While MemoryHighway handles pub/sub transport, ACP adds structure: typed envelopes, schema validation, request-response futures, acknowledgments, dead-letter queues, and communication graph tracking.
{
msgId: "uuid",
sender: "frontend-agent",
receiver: "backend-agent",
msgType: "task", // task | plan | result | validation | error | control | ack | query | broadcast
content: { ... },
meta: { schemaId: "task-v1", expectsResponse: true, retryCount: 0, maxRetries: 3 },
trace: { taskId: "...", workflowId: "...", parentMsgId: "...", hopCount: 2, hops: [...] },
acknowledged: false
}ts
acpBus.request(params) sends a message with
meta.expectsResponse set and returns a Promise. The bus monitors
for responses matching trace.parentMsgId. Timeout default: 30s.
Failed deliveries (handler throws, agent unreachable) retry up to
meta.maxRetries. Exhausted messages move to the dead-letter
queue for manual inspection and retry.
Every send() records a directed edge: sender → receiver (msgType).
Queryable for debugging: which agents talk, what message types flow between them.
When multiple agents work in parallel or group chat, their outputs may conflict.
The ConflictResolver detects and resolves these conflicts.
analyze(outputs) produces a ConflictReport identifying
agreements (multiple agents say the same thing),
contradictions (agents directly disagree), and
unique contributions (only one agent provides).
| Strategy | How It Works | Best For |
|---|---|---|
majority-vote | 3 say X, 1 says Y → pick X | Fact-checking |
weighted-by-tier | Master > Manager > Worker | Hierarchical decisions |
weighted-by-confidence | Agents self-report confidence | When agents know limits |
llm-mediator | Send conflicts to manager | Complex disagreements |
merge | Take unique contributions, flag contradictions | Documentation, summaries |
The StructuredLogger replaces flat text logs with JSON-structured
entries that carry context automatically.
const logger = structuredLogger.scoped({ taskId: "task-123", agentId: "frontend" });
logger.info("Starting component generation", { component: "Button" });
// → { timestamp, level: "info", context: { taskId, agentId }, data: { component } }ts
Scoped loggers propagate context: every log entry automatically includes fixed context (task ID, workflow ID, agent ID). Child scopes merge parent and child context.
LLM call instrumentation: recordLLMCall() tracks every
API call with provider, model, token counts, latency, and success/failure.
getLLMStats() returns aggregates by provider and agent.
JSONL audit trail: ACP messages logged to
.aether/logs/audit.jsonl for compliance and debugging.
Log querying: query({ taskId, agentId, level, since, until })
scans the in-memory ring buffer (max 5,000 entries).
The PluginRegistry manages external code that hooks into
AETHER’s lifecycle at eight defined points:
| Slot | When It Fires |
|---|---|
PreExecution | Before a task is sent to an agent |
PostExecution | After a task result is received |
PreRouting | Before the router picks an agent |
PostRouting | After the router picks an agent |
OnEscalation | When an agent escalates a failure |
OnError | When any subsystem error occurs |
OnStartup | During runtime initialization |
OnShutdown | During runtime shutdown |
Plugins are discovered by scanning .aether/plugins/ for
*.plugin.ts files. Each plugin declares which slots it hooks into.
The 8-slot model covers key lifecycle points without exposing all internal state.
The ReactionEngine watches MemoryHighway events and triggers
workflows automatically based on configurable rules.
{
id: "auto-test-on-review",
trigger: {
channel: "results",
condition: (msg) => msg.type === "task-complete"
&& msg.summary.includes("code review"),
},
action: { type: "execute_task", target: "test-runner",
taskTemplate: "Run the test suite" },
cooldown: 30000, // Don’t fire more than once per 30s
maxFires: 10, // Absolute cap (prevent runaway)
enabled: true,
}ts
Action types: execute_task,
execute_workflow, notify, and custom
(user-provided handler). Cooldown prevents reaction storms. maxFires
provides an absolute cap.
AETHER has two configuration files in .aether/:
config.json — auto-generated by aether init. Contains workspace scan results, provider configuration, server settings. Not for manual editing.settings.json — user-editable tuning knobs for all subsystems. Created by aether init with sensible defaults.| Group | Controls |
|---|---|
methodology | TDD/SDD/hybrid mode, test command, spec directory |
agents | Max concurrent, tier limits (masters/managers/workers) |
execution | Max depth, timeout, tokens, temperature, feature toggles |
escalation | Circuit breaker threshold and window |
routing | Confidence threshold for agent resolution |
conversation | Max messages per conversation |
handoff | Max handoff chain length |
progress | Token/time budgets, stall/loop thresholds |
highway | RAG indexing, dedup window, KV TTL |
acp | Request timeout, max retries, dead-letter limit |
logging | Log level, max retained entries |
sharedState | Cleanup interval, max transitions, persistence |
server | WebSocket port and host |
aether config CLIaether config # Show all current settings
aether config get execution.maxDepth # Get a specific value
aether config set execution.maxDepth 5 # Set a specific value
aether config reset execution # Reset one section
aether config edit # Open in $EDITOR
aether config validate # Check for errors
aether config path # Print path to settings.jsonshell
Settings are deep-merged with defaults on load. Missing keys get default values.
SettingsManager.validate() checks types, numeric ranges, and enum values.
AETHER needs five storage patterns simultaneously: relational, key-value with TTL, vector similarity, full-text search, and time-series. SQLite with sqlite-vec and FTS5 handles all five in one file with ACID guarantees and zero operational overhead.
Global singletons make testing impossible (shared state) and the dependency graph implicit. Constructor injection documents what each subsystem needs. The interface boundary means swapping SQLite for Postgres requires zero changes to business logic.
Every other approach requires manual deadlock reasoning. Interaction combinators have a mathematical proof (strong confluence) guaranteeing the same result regardless of reduction order. The cost is implementation complexity. The benefit is correctness by construction.
AETHER should work out of the box with zero configuration. TF-IDF is
deterministic, zero-latency, and good enough for capability matching
and context retrieval. API embeddings are an opt-in upgrade when
OPENAI_API_KEY is present.
The original design used three fixed tiers. As complexity grew, we needed
a system guardian (Sentinel) and an agent factory (Forge) that sat above
Master. Rather than special-casing them, we generalized to an N-tier
TierRegistry where any number of tiers can be registered at
any rank. The 5 built-in tiers cover the standard use cases.
TierRegistry.classicTiers() still returns the original three
for backward compatibility.
WAL allows concurrent readers and a single writer without blocking. Multiple subsystems read the DB simultaneously (registry queries, task execution, message logging). Rollback journal mode would serialize everything through the database.
Fast, non-cryptographic, collision-resistant enough for deduplication. MD5/SHA are overkill — we prevent accidental duplicates from retries, not adversarial collisions. FNV-1a runs in nanoseconds and produces compact hashes.
Mutable shared state with locks is the classic source of concurrency bugs. Immutable transitions with version numbering give the simplicity of direct state access with the auditability of event sourcing. Every transition records who changed what, when, and why.
Escalation is failure-driven and vertical (worker → manager → master). Handoff is success-driven and horizontal (specialist → specialist). Merging them would conflate “I failed” with “this needs a different expert.” Keeping them separate means escalation can trigger circuit breakers while handoff transfers state without marking the source agent as failed.
aether run "Refactor the authentication module to use JWT"shell
runtime.run(taskText).
.agent.md files,
populate registry, build TierRegistry, initialize Forge and
Sentinel, start MemoryHighway, initialize all subsystems.
auth-specialist at 0.82 confidence) → accepts.
task_results.
Published to results channel. Indexed into RAG.
ACP publishes a typed result envelope.
ReactionEngine checks rules (no matches this time).
AETHER runs as a single process. SQLite's WAL handles concurrency. Multi-machine? Use federation transport between instances instead of sharing a SQLite file.
The executor waits for full responses. Streaming complicates sub-task JSON parsing (can't parse partial JSON), guardrails post-checks, and schema validation. The 120s timeout provides a safety bound.
Agents in the same instance are co-tenants by definition. Authentication exists at the WebSocket layer (external connections) and transport layer (external APIs), not between local agents.
Plugins load during runtime.init() and destroy during
runtime.shutdown(). Hot reloading introduces state
consistency issues (what happens to in-flight tasks when behavior
changes?) that are not worth solving for a local tool.
API keys come from environment variables. No vault, no encrypted config,
no rotation. AETHER is a local dev tool — secrets are
managed by the OS environment or tools like dotenv.
The SharedStateBus uses version numbering for optimistic concurrency, not Raft or Paxos. Single-process means no split-brain problem. Federation uses BAP-02 message ordering, not a consensus algorithm.
TierRegistry supporting custom tiers at any rankTierRegistry, AgentForge, and SystemSentinel into the runtime initializationTierRegistry.classicTiers() returns the original 3 tiers for backward compatibilityTierRegistry.builtinTiers() returns all 5 tiers (sentinel, forge, master, manager, worker){ master, manager, worker } to { tiers: { ... }, fallbackChain: [...] }.agent.md files can now use any registered tier name in the tier field| Version | Phases | Highlights |
|---|---|---|
v0.2.0 |
10–13 | N-tier extensible hierarchy, Sentinel, Forge, constitutional rules, 726 tests |
v0.1.0 |
1–9 | Core platform: 3-tier agents, RAG, interaction nets, BAP-02, orchestration patterns, safety pipeline, ACP, settings system |