06 — Key Services¶
§04 — Core Concepts named the concepts; §05 — Solution Layout told you where to find their files. This is the implementation chapter — every concept The Aetherium Engine uses has a service in Aetherium_Refactor/Aetherium_Refactor/Services/ that runs it, and this section walks the ones that matter most.
The Services directory holds roughly sixty classes. Eight of them carry most of the platform's weight — the orchestrator, the things wrapped tightly around it, and the boundaries to LLMs, tools, knowledge, and containers. The rest are smaller — supporting infrastructure, integration adapters, background sweepers, persistence helpers. This section covers the eight at depth, names a few cross-cutting patterns you will see them all reuse, walks the SignalR hubs that broadcast their state, and points at every other service in a single reference table at the end.
Read it top-down. Each deep-service subsection lands a one-sentence role, names what the service owns, walks the mechanism, and cross-links forward to the section that covers the configuration or extension surface you would touch to change it. Four Mermaid diagrams anchor the busiest control flows. Word for word, this is the longest section in the developer doc set — open the matching .cs file as you read.
Patterns you'll see recur¶
Four patterns thread through the services below. They are named once here and referenced inline thereafter.
IDbContextFactory<AetheriumDbContext>per scope. No service injectsAetheriumDbContextdirectly. Each unit of work creates and disposes its own context through the factory, which keeps EF Core thread-safe under the concurrent agent traffic the Engine generates. Mechanics in §10 — Development Workflow.- Channel-based work queues. Where producer-consumer paths must not race — most prominently in front of the LLM scheduler — the Engine uses
System.Threading.Channels. Producers enqueue; a hosted background worker drains. The pattern recurs in inter-agent mailboxes and notification fan-out. BackgroundServicehosts. Long-running work — queue draining, container cleanup, health probes, log ingestion — runs inIHostedServiceimplementations registered at startup. There are twelve of them across the Engine (nine derive fromBackgroundService; three —ToolCatalogValidator,ModelRegistryValidatorandWorkspaceScannerService— implementIHostedServicedirectly); the supporting-services table at the end flags each one.- Singleton-with-scoped-deps via
IServiceProvider. A handful of singletons need EF Core contexts or other scoped services per unit of work. They injectIServiceProvider, create a scope per operation, and dispose it. Watch for this any time a singleton talks to the database.
6.1 — OrchestratorService¶
The ReAct loop runs here. OrchestratorService receives a user message, loads the agent's context, calls the LLM, parses the response, dispatches any tool calls, and loops until the model stops asking for tools — then synthesizes the final reply.
Its responsibilities, in order of weight:
- Dispatch a message to the correct topology handler — Hierarchical, Sequential, Roundtable, Single, or
dynamic-parallel. - Drive each agent's per-turn tool loop, including context compression when the budget gets tight.
- Manage the model-affinity contract with
VramOrchestrationServiceso concurrent agents do not thrash GPU memory. - Detect step failures and trigger re-planning rather than blind retries (the re-planning logic lives inside this service, not in a separate one).
- Enforce the circuit breaker: the plan-step clock before every model call inside a step, and the re-plan cap at every step failure.
The ReAct loop¶
The core method is RunAgentWithToolLoopAsync. Each pass loads or compresses context, calls CallLLMAsync (which returns a ChatDetailedResponse carrying content, tokens, finish reason, and any tool calls), parses the tool calls, executes them in order, appends the results to the message history, and asks the model again. The loop terminates when the model returns a turn with no tool calls.
flowchart TB
Start([User message]):::ext --> Load[Load agent context]
Load --> CompressQ{Over 70%<br/>of budget?}
CompressQ -- yes --> Truncate[Compress history]
CompressQ -- no --> Call
Truncate --> Call[Call LLM via<br/>ModelSchedulerService]
Call --> Parse[Parse ChatDetailedResponse]
Parse --> ToolsQ{Tool calls<br/>in response?}
ToolsQ -- no --> ReflectQ{Reflection<br/>enabled?}
ReflectQ -- yes --> RefLoop[Run ReflectionService]
ReflectQ -- no --> Synth
RefLoop --> Synth[Synthesize final reply]
Synth --> End([Reply streamed]):::ext
ToolsQ -- yes --> Exec[Execute each tool<br/>via McpToolRouter]
Exec --> Append[Append results to history]
Append --> CheckQ{Circuit<br/>breaker OK?}
CheckQ -- no --> Halt[Halt run]:::halt
CheckQ -- yes --> Call
classDef ext fill:#D5C3B0,stroke:#2D2D2D,stroke-width:1.5px,color:#1A1A1A
classDef halt fill:#B87333,stroke:#2D2D2D,stroke-width:2px,color:#FFFFFF
Context compression kicks in at seventy percent of the configured window. The summary enters as a user message right after the system prompt, so the system prompt and tool list stay a prefix the inference server can reuse. The eighty-five percent figure is the tool loop's fallback for when the summarizer is unavailable: once history, tool definitions included, passes eighty-five percent of the window, the loop cuts it once, down to sixty percent, keeping a contiguous run of the newest messages, so the turns after the cut only append.
Topology dispatch¶
Each topology is a separate handler invoked from a switch in ProcessTeamRunAsync. Every Hierarchical team, whatever its size, flows through ProcessHierarchicalWithPlanAsync (structured create_plan → delegate → reflect → synthesize); the planning turn offers the supervisor create_plan alone, with the workspace's project-config.md in the message (cut at 12,000 characters) because that turn has no tool to read it, and a supervisor that has not produced a plan after three attempts fails the run rather than falling back to unplanned delegation. Sequential, Roundtable, and Single each have their own handler and run the same RunAgentWithToolLoopAsync per agent. Sequential gives every stage the original task plus the previous stage's output as a single labeled context block, and aborts on a failed stage with a failed-at-stage-N status; Roundtable loops the whole team for config.discussion_rounds rounds (default 2), injecting peers' previous-round responses from round two on and dropping any agent whose round failed from the rest of the discussion. Single runs its agent under the end-of-step required-output gate the Sequential and Roundtable handlers run (StageGateProblem), with a Sequential stage's two repair turns (SequentialRepairTurns), logged [SINGLE-GATE]; a node still owing after them fails its step and the run, and a node that declares no required_output is not gated. A Single step also records the files its agent wrote.
dynamic-parallel is the fifth dispatch shape — an escape hatch for templates that need worker counts decided at planning time. The supervisor calls spawn_subagents, the orchestrator materializes the workers, runs them in parallel, and aggregates; a lead that never spawns fails the run, and a swarm that outlives the session budget is stopped (unfinished subagents end at their next round, their queued calls are dropped, and each is recorded as skipped rather than failed) before the lead synthesises; the spawn step succeeds when at least one thread landed a file. Subagents start in parallel unless one reads another's file: a subagent whose own file is an index the team owes under a glob other subagents fill (guide/index.md under guide/*.md) waits for them, a subagent whose task names an earlier subagent's file waits for that one, and a waiting subagent is told which files were written before it started (an edge that would close a cycle is dropped). When the team's nodes declare required_output — they never run themselves; the lead and the subagents built from subagent_defaults do — a subagent whose file falls under a banded glob is held to that band while it writes, the lead's synthesis turn is told every owed file with its conditions instead of research/final-report.md, and after synthesis the stage gate runs over everything the team owes. When it fails, each owed file that fails its own conditions (a banded glob checked file by file) gets one repair subagent of its own, tasked with the gate's sentence — up to six, in parallel — and the gate runs again; the lead's single repair turn is left for what no one file explains, such as a set short of its count, and then the synthesis step fails on what is still wrong. A team that owes nothing, and whose subagent tasks name no other subagent's file, runs as before with one check: a lead whose synthesis wrote no file is sent back up to twice, the second time told to call the tool now rather than say what it will do next. If it still wrote nothing, its longest synthesis reply, when that runs to at least 800 characters, is taken as the synthesis: the Engine saves it at the last .md, .txt or .html path under /workspace/ that the brief names and no subagent wrote (else research/final-report.md) and posts a System line saying so; otherwise the synthesis step fails with "The lead wrote no synthesis file." A swarm stopped by the session budget skips the check. It powers research-team.json today and is not promoted to a peer of the four topology names; treat it as an advanced pattern when neither Hierarchical nor Roundtable fits. §04 — Core Concepts has the framing; §08 — Templates has the template-side knobs.
Before any topology dispatches, VramOrchestrationService makes the run's models resident and sets affinity pins. Residency is a lease: for each instance the template names, the run takes a run-priority lease on the models it needs from InferenceLeaseService, the box-level arbiter every GPU consumer goes through (the Engine's runs, the OpenAI-compatible relay, and external batch jobs via api/inference). Grant order is run, then interactive, then batch: a run evicts an interactive lease at once, is never displaced by one, and preempts a batch lease cooperatively (the holder gets PreemptGraceSeconds to release before its model is unloaded). A batch lease that would start a GPU service or load a model also waits while a run is live on the same card, because the planner budgets memory and the card's compute is shared; one on a model already resident with no service (the decision layer, the trajectory judge) is granted beside the run. An interactive lease is resident-only: it is granted only when its model is already on the card and no run or batch lease is live on the slot, and it never loads, unloads or preempts anything — an external chat client uses a slot only while nothing else is using it. The planner budgets the load against what the card already holds and protects the models of every other live lease on the slot; it never unloads the embedding model or a model the Engine did not load itself, and fails the run with the arithmetic when a plan cannot fit beside those. The standard set is not protected: a run that does not fit beside it evicts it, and the last release on the slot restores it. Before the leases are planned, a node that names a model but no instance is moved to the DefaultNodeInstance slot when that slot's standard set holds exactly one model its id matches (§09). A run whose nodes name no model gives the planner nothing to lease; instead, the first call on each slot takes a resident-only run lease there, which loads nothing, preempts nothing and is refused when the model is not resident, so a card hold waits for the run and the relay's interactive lease is refused on that slot like any other run's. Pins come from the template's own per-node preferred_instance: an instance is pinned only when exactly one model targets it and its standard set does not name a different one. The orchestrator sets the pins, the scheduler honors them for the duration of the run, and a finally block releases the pins and the run's leases; the arbiter's sweep restores each instance to its standard set once nothing live or queued remains on it — a restore that fails (a request still in flight under the unload, say) leaves the slot marked and is retried with backoff, three attempts before the sweep gives up. A run's leases on slots that sit on different cards are acquired at the same time; slots sharing a card go one after another, and a refusal releases all of them. A run holding leases on several slots ends all of them, and stops its GPU services (or leaves them warm for their LingerSeconds, see Configuration), before any slot is restored, so one slot is not held leased through another's reload. A slot's RestoreDelaySeconds (default 0: restore at once) defers its restore to that many seconds after its last release, restarted by every release, so a run or job that follows inside the window finds the card as the last one left it instead of paying for a reload it would then evict. The trajectory judge that scores a finished run takes a batch lease on its slot for its call, so the restore waits for it.
Lives at Aetherium_Refactor/Aetherium_Refactor/Services/OrchestratorService.cs.
6.2 — ContextManager¶
The per-team context window budget lives here. Every LLM call passes its message history and the team's configured context_window through ContextManager.GetAgentContext, which checks whether the history fits and truncates if it does not.
The strategy is intentionally simple. Token count is estimated at roughly four characters per token. The manager keeps the first message (the system prompt and task framing — losing this destroys the agent's identity) and the last two messages (the immediate conversational context), and drops oldest middle messages first until the remaining history fits inside eighty-five percent of the configured budget. The fifteen percent headroom is reserved for the response.
The seventy percent threshold mentioned in §6.1 is a separate, earlier check inside the orchestrator's tool loop — it triggers a real LLM-driven compression pass via SessionSummarizationService (§6.8) before the run pushes into the truncation zone. When that pass is unavailable, the tool loop's own fallback truncates (§6.1).
The context_window value comes from the Workflow Template's TeamConfig. Default and per-model recommendations are in §09 — Configuration.
Lives at Aetherium_Refactor/Aetherium_Refactor/Services/ContextManager.cs.
6.3 — ModelSchedulerService¶
Local LLM hosts hate concurrent traffic. Two agents asking for two different models on the same LM Studio instance trigger an unload-reload cycle on every call, which kills throughput and ages the GPU. ModelSchedulerService serializes LLM requests behind a channel so the system never asks LM Studio to thrash.
Producers — every CallLLMAsync in the Engine — enqueue a task onto one of two channels (_taskChannel for normal work, _lowPriorityChannel for background jobs like team generation and session summarization). Each task is a TaskCompletionSource<ChatDetailedResponse> carrying the model ID, the messages, sampling parameters, and the agent's affinity pin if one was set.
The consumer is ProcessQueueAsync, hosted by ModelSchedulerBackgroundService. It groups pending tasks by model, picks the target LM Studio instance itself — from where the model is already resident, which instances answered their last probe, and any affinity pin the run set, reading endpoints and per-slot concurrency from LLMRouterService — ensures the right model is loaded on that instance via LMStudioManagementService, and dispatches the request through LMStudioService.ChatDetailedAsync. The response — content, input tokens, output tokens, model ID, response time, and any tool calls — is set back on the TaskCompletionSource, which unblocks the original caller.
A call whose connection dropped — a half-read response, a socket error, the router's proxy error for a child that went away, or llama.cpp's harmony-parser rejection of a malformed tool turn — is retried once after a short pause before the failure reaches the caller; a "model is not loaded" answer takes the reload path instead, and a timeout is still the caller's budget.
Affinity pins are honored. If the orchestrator pinned an agent to a specific model, the scheduler will not migrate that agent's work to a different instance; a task that named its instance fails with the reason rather than being moved. A pin lives for the duration of one build run and is cleared in the orchestrator's finally block.
TeamGeneratorService (§6.7) and SessionSummarizationService route through the same scheduler at low priority — there is one queue front for all LLM traffic in the Engine, not one per caller.
Lives at Aetherium_Refactor/Aetherium_Refactor/Services/ModelSchedulerService.cs with ModelSchedulerBackgroundService.cs for the hosted loop.
6.4 — McpToolRouter¶
When an agent emits a tool call, McpToolRouter decides whether it runs and where. It owns schema discovery, the three-layer permission model, and the dispatch into the right MCP server class.
The three layers are checked in order. The connection layer matches the tool's owning MCP server class against the agent's mcp_servers allowlist — agents that do not declare shell cannot reach run_shell no matter how the LLM phrases the call. The capability layer narrows further: even within an allowed server class, the agent's per-tool list is the filter the router applies before tool schemas are ever shown to the model. The runtime layer is ToolApprovalService, which checks the security tier assigned to the tool — Standard runs immediately, Elevated runs with enhanced audit logging, Restricted blocks until a human approves through the UI (the default five-minute timeout auto-denies if no human responds).
Between the layers, two more gates fire. A pre-execution event-bus hook lets observers veto a call — egress policy and DLP scanning use this to block credential leakage on outbound HTTP. A post-execution hook records the outcome to the audit log for replay and compliance review.
flowchart TB
Call([LLM emits tool call]):::ext --> L1{MCP server in<br/>agent's allowlist?}
L1 -- no --> Deny1[Return tool-denied error]:::halt
L1 -- yes --> L2{Tool in agent's<br/>capability list?}
L2 -- no --> Deny2[Return tool-denied error]:::halt
L2 -- yes --> Pre{Pre-execution hook<br/>egress / DLP}
Pre -- block --> Deny3[Return policy-blocked error]:::halt
Pre -- pass --> Tier{Security tier}
Tier -- Standard --> Exec
Tier -- Elevated --> Audit[Enhanced audit log] --> Exec
Tier -- Restricted --> Human{Human approves<br/>within timeout?}
Human -- no --> Deny4[Auto-deny, return error]:::halt
Human -- yes --> Exec[Dispatch to MCP server class method]
Exec --> Post[Post-execution audit] --> Done([Return result to LLM]):::ext
classDef ext fill:#D5C3B0,stroke:#2D2D2D,stroke-width:1.5px,color:#1A1A1A
classDef halt fill:#B87333,stroke:#2D2D2D,stroke-width:2px,color:#FFFFFF
Schema discovery happens at startup. The router reflects every [McpServerTool] method registered into the DI container and builds the tool catalog the orchestrator hands to the LLM at turn start. A separate ToolCatalogValidator runs once at boot and asserts that the router's catalog matches the prompt-enhancer's, so a tool can never be advertised in a system prompt without being routable.
The Engine ships forty-four tools across twelve MCP server classes today. The catalog and how to add new tools live in §07 — MCP Architecture.
Lives at Aetherium_Refactor/Aetherium_Refactor/Services/McpToolRouter.cs with ToolApprovalService.cs for the runtime gate.
6.5 — Knowledge layer pair: ChromaDbRagService + AgentMemoryService¶
Two surfaces, one section, because they are easy to confuse. The knowledge base is the shared, queried surface that agents pull from on demand. Memory is the per-agent, automatic surface that the orchestrator injects without being asked. §04 — Core Concepts has the side-by-side contrast table.
ChromaDbRagService¶
Backs the Engine's knowledge base. Collections fall into three buckets — per-project (aetherium_project_{id}), global (aetherium_global, every knowledge pack, each chunk tagged with its pack), and per-domain (aetherium_domain_{slug}). Every retrieval goes through one method, SearchAsync(query, scope, k, purpose), where the scope is what one node may read: its team's packs, its target_collections domains and its project. The scope is resolved once per node when the session starts, so injection and search_knowledge read the same thing, and a team with no packs never touches the global collection.
flowchart TB
Agent([Delegation, agent turn<br/>or search_knowledge]):::ext --> Scope[The node's scope<br/>resolved at session start]
Scope --> Svc[ChromaDbRagService<br/>.SearchAsync]
Svc --> Up{ChromaDB up?}
Up -- no --> Fail([Search could not run]):::ext
Up -- yes --> Embed[Embed the query once<br/>search_query: prefix]
Embed --> Global[Global collection<br/>filtered to the scope's packs]
Embed --> Domain[Domain collections in scope]
Embed --> Proj[Project collection]
Global --> Merge[Merge by cosine · two chunks per document<br/>drop build logs · ChromaDb:ScoreFloor]
Domain --> Merge
Proj --> Merge
Merge --> TopK[Top k · one KB-RETRIEVE log line]
TopK --> Inject([Injected as reference data<br/>or returned to the tool]):::ext
classDef ext fill:#D5C3B0,stroke:#2D2D2D,stroke-width:1.5px,color:#1A1A1A
ChromaDB is checked before anything is embedded, and a search never creates a collection: only a write does, with cosine distance and a stamp — the generation, the embedding model and prefix scheme, the dimension, the chunker and a 16-number fingerprint of a fixed probe embedding. A stamped collection refuses a write embedded any other way, a write deletes the document's old chunks before adding the new ones, and a write to the global collection must name its pack. Collection names carry a generation (aetherium_global_g1); the Knowledge_ActiveGeneration setting says which one reads and writes use, so a rebuilt index can sit beside the live one and go live, or roll back, with one setting. KnowledgeIndexService is the one bulk writer: it builds the next generation from the packs on disk, the projects' knowledge folders and the old index's domain text, checks it (chunk counts against the pack registry, each document finding itself, title lookups inside each template's packs, fast-against-exact search agreement, the embedding fingerprint), and moves the pointer only when every check passes. A generation number is never reused, so a build that failed its checks is left untouched beside the live index.
Embedding goes through EmbeddingService — an inference slot's /v1/embeddings endpoint only, with nomic's task prefixes (search_query: for queries, search_document: <title> for chunks), array inputs in batches of 32 and unit-length vectors. With no embedding model loaded, a knowledge write is refused and a search reports that it could not run; nothing is indexed or answered with placeholder vectors. Chunking happens at ingest time in ChunkingService (chunker v2): heading sections packed into 400–1,600 characters with a breadcrumb title, code in 60-line windows, nothing over 2,000 characters. Document ids are SHA-256 based, so the same file keeps the same id across restarts. The keys are in §09 — Configuration.
Injection. RagContextProvider turns a search into one block of reference text, placed at the head of the agent's first user message rather than in its system prompt, so the system prompt stays the same from one step to the next: up to eight excerpts within about 1,500 tokens, each in a kb-excerpt element with its source and relevance, under a line that says they are reference data, not instructions (an excerpt's own tags and control characters are escaped). It runs for single, sequential, roundtable and the dynamic-parallel lead as before, and now for every hierarchical delegation (once per plan step on its description, replayed byte for byte for the step's later rounds and verifier repairs) and every dynamic-parallel subagent. AI_KnowledgeInjection=false turns it off; benchmark mode always does.
AgentMemoryService¶
Per-agent state that survives across turns. Storage is PostgreSQL only — the AgentMemory table has no vector index. Lookup is relational, ordered by relevance score plus recency, and respects a token budget when the orchestrator injects memory at turn start.
There are exactly two tiers:
working— overwritten per session. The scratch pad the supervisor updates as the conversation progresses.long_term— append-only. Accumulated across sessions.LessonServicewrites here;SessionSummarizationServiceupdates both tiers at session close.
Memory rows are scoped at the project (every agent on the team can read) or at a specific agent (only that agent sees them). The orchestrator pulls the relevant slice at turn start and places it at the head of the agent's first user message, ahead of the task.
Cross-link: §09 — Configuration for the embedding model and RAG threshold keys.
Lives at Aetherium_Refactor/Aetherium_Refactor/Services/ChromaDbRagService.cs and AgentMemoryService.cs.
6.6 — DockerService¶
Every project gets a workspace container. DockerService provisions it, monitors it, runs commands inside it, and tears it down when idle. The reasoning lives in §04 — agent file writes happen on an isolated filesystem so the host stays clean and parallel projects do not collide.
A container's life is a state machine driven by two background sweepers. ContainerActivityTracker records every interaction (message in, tool out) at both project and agent level. ContainerCleanupService wakes every five minutes, reads the tracker, and walks each container through the lifecycle.
stateDiagram-v2
[*] --> Provisioning
Provisioning --> Healthcheck: image pulled, container started
Healthcheck --> Ready: health probes pass
Ready --> InUse: first message
InUse --> InUse: turn / tool call (resets timer)
InUse --> Idle: 60 min no activity
Idle --> InUse: new message
Idle --> Stopped: stop threshold reached
Stopped --> InUse: new message (re-start)
Stopped --> Destroyed: 30 min after stop
Destroyed --> [*]
Provisioning --> Failed: probe failed
Healthcheck --> Failed: probe timeout
Failed --> [*]: cleanup
The defaults are sixty minutes idle before stop and thirty minutes stopped before destroy. Both are configurable as Container_IdleTimeoutMinutes and Container_DestroyGracePeriodMinutes in the SystemSettings table — see §09 — Configuration. Projects flagged PersistentContainer = true bypass the sweep entirely and live forever; use sparingly, that is what eats your VM disk.
A second cleanup pass — CleanupOrphanedAetheriumResourcesAsync — sweeps containers and networks whose project_id label no longer maps to a row in the Projects table. This catches the leftovers from crashes, benchmark runs, and aborted provisioning, and is the reason the Engine survives a few weeks of heavy local development without filling Docker Desktop.
Shell execution into a container goes through ContainerObservabilityService, which also streams logs to ContainerHub (§6.9) and runs interactive shell sessions for the Studio UI.
Lives at Aetherium_Refactor/Aetherium_Refactor/Services/DockerService.cs with ContainerCleanupService.cs, ContainerActivityTracker.cs, and ContainerObservabilityService.cs for the sweeper, tracker, and exec adapter.
6.7 — Provisioner pair: TemplateService + TeamGeneratorService¶
Two paths get you from "I want a team" to a running project. Either you pick a Workflow Template from the catalog and TemplateService instantiates it, or you describe what you want in prose and TeamGeneratorService synthesizes a team for you.
TemplateService¶
Loads templates from two sources. Embedded templates ship inside the server assembly — TemplateService reflects them out of the assembly's manifest resources at startup and they live forever. User templates land as .json files in the workspace root's templates/ folder and load on demand; a user file with the same ID as an embedded template overrides it (with the SeedKnowledgeBase flag inherited so lightweight templates stay opted out even when overridden).
A hash-based drift check runs at boot. Every embedded template's JSON is hashed and compared to any user override; mismatches log as [TEMPLATE-DRIFT] warnings so you can see when a user copy has gone stale. Twenty-six templates ship today, from quick-script.json to fullstack-web.json to research-team.json. Catalog and schema live in §08 — Templates.
A template may be written as a preset — a recipe plus nodes that name a role_id from the role library — and TemplateService runs the composition resolver on every template as it loads, so the cached template is always the full team and the create path finds nothing left to add. When you instantiate a template, BuildCreateRequest clones the TeamConfig via a JSON round-trip (using a non-policy-applying serializer to preserve the [JsonPropertyName] attributes), names the team after the project and records the template's id as template_id, ScaffoldDirectories creates any extra workspace folders the template asks for, and the result is handed to ProjectService for the database write and the container provision.
TeamGeneratorService¶
The Quick Create backend. Takes a natural-language description, enqueues a low-priority LLM job via ModelSchedulerService, and asks the generator model for a team design rather than a TeamConfig: a recipe, the roles it picked from the role library (Templates/composition/roles.json, and since 2026-10-13 by default the base roles of roles.library.json beside it — a request with roleLibrary=false leaves those out; only the roles marked catalog whose prompt names no workspace path are offered — a role that says where it writes belongs to the template that shipped it, and its paths would win over the brief; the offered set includes a narrator, an image-maker and a qa-reporter, so a media team need not be written from scratch) with optional project-specific prompt additions, and, only when no library role fits, custom roles described by tool profiles rather than tool names. Every role, library or custom, also says what it owes — the workspace-relative files it must leave behind (a path written from the container root, /workspace/site/index.html, is read as site/index.html; one that is still absolute or climbs out with .. is dropped with a warning), each optionally carrying the gate's own clauses ("images/*.png >= 6", "video/film.mp4 lasting >= 50s with audio", "site/index.html self-contained shows images/ >= 5 names EL-01 forbids copyright", "guide/index.md links guide/*.md" on any index or listing the request asks for; a clause outside the gate grammar is dropped with a warning naming it and the entry keeps every clause the gate reads — so is a shows <dir>/ >= 0, which asks for nothing while the gate reads a zero there as one — while a count on a single file or a count of zero drops the entry, since either means the path is wrong), which become the node's required_output and switch the team's required-output gate on — and, in a sequential team, its stage, which fixes the pipeline order across both lists; a custom role written under a library pick's node_id replaces that pick and inherits what it owed. What a design owes is then held to the request: where the request names a file under a directory and the design owes that file name elsewhere, the request's path is owed instead; a deliverable the request names that no role owes goes to the role owing files of the same folder and extension, with every clause those files share; and every owed .html forbids copyright unless the request itself asks for a © or copyright line. That is the only word ban a generated team carries: any other forbids the model writes is dropped with a warning, because a ban is a literal word match that cannot tell a disclaimer ("not intended to cure") from the claim it was meant to stop; what a request says must not appear reaches the role through its prompt addition instead. A names entry the request uses only as a file name is dropped the same way: when a role owes a picture by that stem, or by a glob it matches (images/si-0*.png), and every place the request writes that code it carries a media extension, the page is not held to printing it, because a page that shows its pictures does not print their file names; a code the request also uses as a word is kept. A names token the request never writes at all — compared with dashes, spaces and case folded — is dropped too, and the clause with it when nothing is left: a generator once filled the clause with a code it invented, and the page's author pasted the code into the title and a heading to pass the gate. Every role, library or custom, carries the request — its first 1,000 and last 500 characters when it runs longer than 1,500, since a brief's closing sentences are usually its constraints. Both the prompt and the JSON schema are generated from the composition files — the recipe list and when to use each, the catalogued role ids with a one-line purpose and the tools each holds (with the rule to give a library role only work its tools can do, or compose a custom role), the profile names — so the role ids, profiles, recipes and network policies are schema enums the sampler cannot stray from. The prompt gives no tool instructions and names no model; one library role brings its own prompt, tools and model slot, and a custom role's tools come from its profiles. The design is turned into a TeamConfig by the composition resolver (TeamDesignConverter → TeamCompositionResolver). A generated team whose tools need a GPU service gets gpu_services derived from those tools, runs every node on the model policy's media slot, its instance included (the service holds most of the card, so the model beside it is a hardware verdict rather than a per-role one), takes the circuit-breaker floors the shipped media templates use (1200 s per step and 5400 s per session — a step that makes six photographs spends 250–400 s in ComfyUI), and is pinned to the media base image when it owes a video, since cutting one is ffmpeg in the container — and the role that owes the video, if it holds a shell, is handed the one worked ffmpeg concat command the shipped film templates use, with the team's own paths in it and the recording the request names for that film as its soundtrack (else the one sharing the film's file stem, else the only one owed); a role that owes images is told to read the project's own configuration first and to photograph what it names, because the subject reaches the image tool only through the prompt the role writes and a role told only to "generate six images" produced six competent photographs of the wrong thing; a role that renders recordings is told to read each recording's narrator from that configuration and pass the voice on every render, re-renders included; a role that renders a recording that no script stands behind (no role owes a .md with a word band or a cast) is given one: a writer the design already has (podcast-scriptwriter, episode-writer or dialogue-writer) owes <dir>/<stem>-script.md with a spoken band from the recording's minimum length (two to six words a second, 150–450 without one) and a NARRATOR cast, or the library podcast-scriptwriter joins before the narrator to owe it, and the narrator is told to render from that script and not to write one; when the team's speakers clauses name who speaks, every role holding tts_render with no cast of its own is given one, with a prompt line saying the cast is set and to pass voices only for a voice the request names. When the qwen-tts-design GPU service is configured, the design also carries voices (one sentence per speaker on gender, age, pitch, pace, timbre, accent and energy), each becomes a design: … voice designed for this template, and the team declares qwen-tts-design beside qwen-tts; a description under four words, or one repeating another speaker's, is not designed. Every other speaker is mapped to a reference voice that exists, a voice named after the speaker first, then unused voices in name order (with fewer voices than those speakers, they are left to tts_render and the design says so); and a generated media team runs its plan serially, since a parallel wave started the film step's ffmpeg against an images directory the imaging step had not filled yet; a role whose job is to check the others is told to owe a verdict clause, so a checker that finds a real fault fails the step instead of filing its finding where nothing reads it; a role that owes a file without the tool that makes its format — an image needs comfyui_generate, a recording tts_render, a video a shell, anything else write_file — is reported, not granted the tool, and a design carrying such a report — or, in a sequential team, a stage that owes nothing — is handed back to the model with one fix per node, for up to three design attempts in all — stopping at a clean attempt or one no better than the best before it — and the attempt with the fewest such problems, then the least missing work, is kept; a design with no such problem that still owes less than the request asks for (an owed entry dropped whole, a file the request names that no role owes, or a need ticked on the New project form — needs on the endpoint — that no role can make) is handed back once, listing what is missing and saying that a count belongs on a folder or a glob, never on a single file, and the same lines ride along with any other hand-back; a sequential stage still silent after that is dropped with its edges rather than run. A role owing a 3D scene or engine asset, which nothing in a workspace makes, is noted rather than handed back, since a plan step owing one is given a written brief instead; the kept design's notes travel with the team as generation_notes, which the Designer shows. The generator uses whatever chat model is already resident — on the slot its TeamGenerator role placement names first (and only there when that placement is pinned), then, when that slot holds none, on the offline fallback slot (LMStudio:OfflineFallbackSlot), then on the Primary instance, then on every other slot in slot order — and only loads one on Primary (the AI_ModelId setting, or the first non-embedding model the Primary knows) when no slot has any; it never unloads a model to make room, so a Quick Create cannot evict a running team's model.
Post-generation, the defense-in-depth passes remain. The composition resolver (TeamCompositionResolver, the same pure pass every creation path runs) first fills each node from its role_id and applies the recipe (topology, generated edges, circuit-breaker defaults), then expands a node's profiles into tools. In a hierarchical team it guarantees exactly one node has is_supervisor = true and that the supervisor has delegate and reply (plus read_plan and update_plan when it has create_plan); in a hierarchical or sequential team, that workers at minimum have reply and that content, code and test roles with write_file also have edit_file — unless the node's effective profiles include reporter, the profile for a role that reviews and does not repair; the supervisor and edit-rule keyword tables live in the profile catalog (Templates/composition/tool-profiles.json), and every tool the resolver adds is logged with its reason, since the stored tool list is the runtime allowlist. A generated node of any topology that owes a file at a word range and holds write_file is also given edit_file, logged [TEAMGEN-TOOLS], so a length repair cuts the file instead of writing it again. EnsureKnowledgeWritingToolsIfIntended looks for KB-writing language in the user's description and grants write_knowledge plus search_knowledge to the agents that need them. A chart, plot or graph the design owes on a role without a shell (a picture whose path names one, such as charts/*.png) moves to the team's data role — one holding run_shell and write_file, never the supervisor — which is told to draw it with matplotlib from the data, and matplotlib is installed when the container is set up; a painted chart has invented numbers. With no such role the design is handed back to the generator to add one. The resolver also runs from ProjectService.EnsureProjectFolderAsync when a legacy project's team config has drifted, so a hand-edited team that loses its supervisor wiring gets repaired on next load.
Lives at Aetherium_Refactor/Aetherium_Refactor/Services/TemplateService.cs and TeamGeneratorService.cs.
6.8 — Orchestration internals quartet¶
Four small services polish the ReAct loop into something a small local model can run reliably. They live and die together — Reflection feeds Lessons feeds Re-planning, with the Circuit Breaker as the safety net underneath all of it. The narrative is unified here because separating them would obscure how often they hand off.
ReflectionService runs an explicit self-critique turn on an agent's reply: the critic model reads the output, asks whether it is satisfactory, and — if not — runs up to three refinement passes before accepting. It activates when a node's Reflection.Enabled flag is set or the global AI_ReflectionEnabled toggle is on (off by default). A per-node config overrides the global default, so a supervisor and its workers can carry different reflection settings on the same team. Beyond the hierarchical supervisor's synthesis step, reflection also runs wherever the text reply is the deliverable — conversational plan steps and the Roundtable, Sequential, and Single topologies (file-producing steps lean on the QA tools instead).
LessonService is a two-way street with the loop. Before an agent's turn, FindRelevantLessonsAsync runs a semantic search over prior lessons (each lesson is embedded at write time) and places the matches at the head of the agent's first user message. After the turn's outcome is known, ReinforceLessonAsync or DecayLessonAsync adjusts each applied lesson's confidence score. Lesson extraction itself does not run inline — SessionSummarizationService writes new lessons at session close, which keeps the per-turn latency clean.
CircuitBreakerService is the fail-fast layer. It runs per session and holds two limits: a plan step that runs longer than step_timeout_seconds (default 1800, not counting time the step spent waiting on the verifier) fails with the reason while the run continues; like any failed step it puts back the text files it changed and is re-planned, and its re-run is told it ran out of time and to make smaller edits. The supervisor's re-plan turn, each gate repair round, each verifier repair, and each re-run a checker's FAIL sends back (an owner's repair and the checker's re-check), starts a fresh step clock (logged [STEP-CLOCK]) rather than running on what the step before it left. A session gets at most max_replans_per_session re-plans (default 2). The rest of the team's circuit_breaker block is read where it applies: max_turns_per_agent (default 10) caps an agent's tool-loop rounds (a plan step's turn gets it but never fewer than 18, a verifier repair turn gets 6, and a gate repair — the start check, the page-request gate or the required-output gate — 12; on a step with more rounds than that, each run_tests in the repair that improves on the last comparable run (more passed, fewer failed, no fewer run) extends it by eight rounds, up to the step's own budget, logged [REPAIR-EXTEND]), infinite_loop_threshold (default 3) is how many times in a row the duplicate-call guard lets the same call repeat before it tells the agent to move on (two more end the loop), and session_timeout_seconds (default 600) budgets a dynamic-parallel swarm. There are no tool-call counts and no per-tool ceilings: those fields, and the checks that would have read them, had no caller and were removed on 2026-10-08. Thinking models — classified by the Engine's model-capability table, not by configuration — run under the team's thinking_model_overrides instead (thirty-five turns and an eighteen-hundred-second session timeout by default; the step timeout is inherited unless the override sets one) because their per-turn cost is structurally different. Every field is listed in Schema · CircuitBreakerConfig.
Re-planning is not a separate service — it lives inside OrchestratorService as TryReplanOnStepFailureAsync. When a plan step fails or its worker emits output the supervisor cannot use, the supervisor is invited to revise the plan instead of retrying the same step. There are two ceilings: a per-step cap of two re-plans (the same step is not re-planned forever) and the per-session cap from the circuit breaker. The re-plan supervisor turn runs with its tool round count capped at four so the recovery cannot itself escape the budget; its prompt says so, its last call offers only update_plan and reply, each call's output is capped at 4096 tokens, and the turn composes no closing reply. It amends the plan that is running and cannot replace it: a create_plan call in that turn is refused, pointing the supervisor at update_plan, because a new plan would supersede the running one and strand the steps in flight. The turn lists the failed step first among the steps it may amend, with what it asked for (cut at 600 characters), its required outputs and the failure reason, and asks the supervisor to set it back to pending (with result saying what the re-run must fix) or to skip it with the steps that need its output; the re-run is told why its last attempt failed, and the supervisor's note. In that turn update_plan may not set a step in_progress or completed, and a pending with its result ends the turn. When the turn leaves the failed step failed, the Engine sets it back to pending itself, once (logged [REPLAN-DEFAULT]); a step whose files were kept under [STEP-KEEP] is marked skipped instead, since what still fails is a later step's work, and that step then runs. When the plan loop's own safety limit (twice the step count, in passes or waves) runs out first, a step whose last attempt failed is marked failed with that reason instead of being left pending; a step that never ran stays pending.
A fifth service sits next to this quartet and deserves the mention: SessionSummarizationService writes session summaries through ModelSchedulerService at low priority, updates both memory tiers (§6.5), and triggers lesson extraction into LessonService. It runs after a session closes and is the seam between the runtime loop and the persistence layer.
Lives at Aetherium_Refactor/Aetherium_Refactor/Services/ReflectionService.cs, LessonService.cs, CircuitBreakerService.cs, and the TryReplanOnStepFailureAsync method inside OrchestratorService.cs.
6.9 — The four SignalR hubs¶
Real-time is a defining property of the Engine — §03 — System Architecture framed why. Four SignalR hubs carry different concerns; agents and the UI subscribe to whichever they care about.
| Hub | What flows on it | Publishers | Group key |
|---|---|---|---|
NotificationHub |
RoleUpdated, ProjectUpdated, KnowledgeSeedProgress |
NotificationService, KnowledgeSeederBackgroundService |
Caller-named groups |
ProjectRunHub |
ReceiveLog, ReceiveDelegation, ReceiveStreamChunk, ReceiveStreamEnd, ReceiveRunFinished |
LoggingService |
projectId |
ContainerHub |
ReceiveContainerStats, ReceiveContainerLog, ReceiveContainerStateChange, ContainerCreated, ContainerDestroyed, shell session lifecycle events |
ContainerObservabilityService |
dashboard, container-{id}, logs-{id}, shell-{id} |
TeamHub |
AgentStatusChanged, AgentMessageSent |
TeamStatusBridge (subscribes to the internal event bus) |
team:{projectId} |
Two patterns recur. Hubs are thin — they own group membership and connection lifecycle, and they delegate every actual broadcast to a service that holds IHubContext<HubName>. And per-project groups are the dominant scoping: clients join the group for the project they are viewing and leave when they navigate away, which keeps fan-out cheap even with dozens of projects open across multiple users.
ContainerHub's log groups deserve a note. Subscribing a client to logs-{containerId} reference-counts a docker logs -f follower process; the last unsubscribe tears it down. That is the only place in the Engine where the SignalR audience drives a worker lifecycle directly.
TeamHub is the newest of the four and the narrowest — it carries only the events the Studio UI needs to render live team state.
Hubs live at Aetherium_Refactor/Aetherium_Refactor/Hubs/.
6.10 — Supporting services reference¶
Every service in Services/ not covered above lives here with a one-line purpose, file path, whether it is a BackgroundService, and the section that covers its surface area in depth if applicable.
| Service | One-line purpose | File path | Background? | Covered in |
|---|---|---|---|---|
ArchiveService |
Archives projects to ZIP files. | Services/ArchiveService.cs |
No | §14 |
ChromaDbHealthService |
Monitors ChromaDB connectivity and triggers auto-recovery. | Services/ChromaDbHealthService.cs |
Yes | §15 |
ChunkingService |
Splits text into RAG chunks (adaptive, topic, sliding-window). | Services/ChunkingService.cs |
No | §09 |
ComfyUIService |
REST + WebSocket client for ComfyUI image generation. | Services/ComfyUIService.cs |
No | §13 |
ContainerActivityTracker |
Records per-project and per-agent container activity. | Services/ContainerActivityTracker.cs |
No | §6.6 |
ContainerCleanupService |
Idle → stop → destroy sweeper; orphan-resource pass. | Services/ContainerCleanupService.cs |
Yes | §6.6 |
ContainerObservabilityService |
Log streaming, exec, stats, interactive shell into containers. | Services/ContainerObservabilityService.cs |
No | §6.6 |
ConversationService |
Persists chat messages per session with token metadata. | Services/ConversationService.cs |
No | §10 |
DecisionLogService |
Writes one decision_logs row per decision the decision layer makes; never throws and never blocks the decision it records. |
Services/Decision/DecisionLogService.cs |
No | §09 |
EgressPolicyService |
Domain allowlist and DLP scan for outbound HTTP. | Services/EgressPolicyService.cs |
No | §07 |
EmbeddingService |
Embeddings from a slot's /v1/embeddings, naming Aetherium:Embeddings:ModelId; refuses to index or search without a loaded embedding model. |
Services/EmbeddingService.cs |
No | §09 |
ImageFinishingService |
The Qwen-Image finishing pass: it runs the upscales finish_image queues, and, when Aetherium:ImageFinishing:Enabled is on (off by default), re-renders a Completed Qwen-Image run's images at 2K into final/. Each job takes Batch leases, outside the run's own. |
Services/ImageFinishingService.cs |
Yes | §07 |
KnowledgeBaseService |
Project-scoped and global knowledge base metadata. | Services/KnowledgeBaseService.cs |
No | §09 |
KnowledgeCurationService |
Promotion candidates. A completed or failed run's notes (and, from a completed run only, its verified deliverables once the domain already holds something; a cancelled run offers nothing), and agents' write_knowledge notes for a shared domain, wait in their project's collection until the owner approves or rejects them (api/knowledge/candidates); approval files the text under KbRoot/domains/ and indexes it into the domain as owner-approved. |
Services/KnowledgeCurationService.cs |
No | §6.5 |
KnowledgeIndexService |
Builds, verifies and switches knowledge index generations, and re-indexes single packs, under a batch lease on the embedding slot that pauses while a run holds it. Driven by POST api/knowledge/rag/rebuild and packs/{name}/index. Completed runs' deliverables for curation go through the same queue, so they never land in a generation that is being rebuilt. |
Services/KnowledgeIndexService.cs |
Yes | §6.5 |
KnowledgePackService |
Curates and provisions tech-stack documentation bundles. | Services/KnowledgePackService.cs |
No | §09 |
InstanceBackendResolver |
Picks a slot's management dialect — LmStudioBackend or LlamaServerBackend under Services/Backends/ — for probes, loads and unloads. |
Services/Backends/InstanceBackendResolver.cs |
No | §6.3 |
KnowledgeSeederBackgroundService |
Drains the knowledge-base seeding queue. | Services/KnowledgeSeederBackgroundService.cs |
Yes | §09 |
LLMRouterService |
Holds the per-slot runtime instance configuration (Primary and Secondary under fixed keys; any other slot's row built from its registry entry) and the per-slot in-flight counters. | Services/LLMRouterService.cs |
No | §6.3 |
LMStudioHealthCheckService |
Probes every configured inference instance through its backend. | Services/LMStudioHealthCheckService.cs |
Yes | §15 |
LMStudioManagementService |
Loads and unloads models on demand through the slot's backend. | Services/LMStudioManagementService.cs |
No | §6.3 |
LMStudioService |
LLM client — chat, streaming, native tool calls. | Services/LMStudioService.cs |
No | §6.3 |
LogIngestionBackgroundService |
Indexes finished build transcripts into the RAG index. | Services/LogIngestionBackgroundService.cs |
Yes | §09 |
LoggingService |
Per-run log files, ProjectRunHub broadcasts, and the AG-UI event stream (persisted to session_events). |
Services/LoggingService.cs |
No | §6.9 |
ModelRegistryValidator |
Boot-time assertion: a configured model registry must actually load. | Services/ModelRegistryValidator.cs |
Yes (IHostedService) | §09 |
PathService |
Cross-platform host-to-container path conversion. | Services/PathService.cs |
No | §10 |
PermissionRulesService |
YAML-driven tool permission rules with hot reload. | Services/PermissionRulesService.cs |
No | §07 |
PortAllocationService |
Allocates host ports for project containers. | Services/PortAllocationService.cs |
No | §6.6 |
ProjectService |
Project lifecycle and configuration persistence. | Services/ProjectService.cs |
No | §09 |
RestartSweepService |
At startup, before the finishing runner, closes what a previous Engine process left open: a build run still Running or Queued from before the start becomes Failed ("engine restarted mid-run"), its open steps close as Failed or Skipped, and an open step of a run that already finished closes too. A reason already on a row is kept. |
Services/RestartSweepService.cs |
Yes (IHostedService) | — |
SchemaConstrainedDecisionModel |
The decision layer's Tier 0 IDecisionModel: asks a model already resident on a slot a bounded question under a JSON schema, under a Batch lease, never loading or unloading anything. Used only when Aetherium:Decision:Enabled (off by default). |
Services/Decision/SchemaConstrainedDecisionModel.cs |
No | §09 |
PromptEnhancementService |
Topology-aware system prompt assembly. | Services/PromptEnhancementService.cs |
No | §08 |
SessionSummarizationService |
LLM-powered session summaries; updates memory tiers; triggers lesson extraction. | Services/SessionSummarizationService.cs |
No | §6.8 |
SlotCatalog |
The inference slots in one ordered list — Primary, Secondary, then every other registry slot, in Aetherium:LMStudio:SlotOrder, hidden _ slots left out — with the alternates failover tries and the card each slot is on. |
Services/Slots/SlotCatalog.cs |
No | §09 |
RolePlacement |
The slot, and whether it binds, for a model call that belongs to no node (verifier, judges, compaction, summaries, embeddings, …), from Aetherium:RolePlacement. An unpinned role whose slot is known down (its last two health probes failed) moves to Aetherium:LMStudio:OfflineFallbackSlot. |
Services/Slots/RolePlacement.cs |
No | §09 |
SlotConfigValidator |
Boot-time check that logs a bad Aetherium:RolePlacement ([ROLE-PLACEMENT]) and slots naming unknown cards ([GPU-CARDS]); it logs rather than refusing to start. |
Services/Slots/RolePlacement.cs |
Yes (IHostedService) | §09 |
SysfsGpuTelemetry |
Per-card VRAM, temperatures, power, links and the processes holding each card, read from sysfs and DRM fdinfo for api/inference/queue and api/inference/cards. |
Services/Gpu/SysfsGpuTelemetry.cs |
No | §6.1 |
TeamStatusBridge |
Forwards internal event bus to TeamHub. |
Services/TeamStatusBridge.cs |
No | §6.9 |
TierPolicyService |
Tier-based resource limits and retention policies. | Services/TierPolicyService.cs |
No | §09 |
ToolApprovalService |
Runtime gate for Restricted-tier tools. |
Services/ToolApprovalService.cs |
No | §6.4 |
ToolCatalogValidator |
Boot-time assertion: router and prompt-enhancer catalogs match. | Services/ToolCatalogValidator.cs |
Yes (IHostedService) | §07 |
ToolSecurityAuditService |
Tool execution audit trail to PostgreSQL. | Services/ToolSecurityAuditService.cs |
No | §07 |
VerificationService |
Step-boundary LLM verifier: judges a step against the actual files written, bounces FAILs back to the authoring worker (bounded retries); paired with deterministic write-time artifact checks. A verdict that still fails after the retries is reported in the plan summary, marked according to whether it was a deterministic artifact check or the judge's opinion. The deterministic gate is read again after the repair rounds whatever the judge concluded, so a repair that breaks a clause the step owes (a © written into a page under forbids copyright) fails the step. An inconclusive verdict — the judge timed out, answered in prose, or could not run — fails the step outright, and build_steps.VerificationStatus records passed, failed, inconclusive, or null for a step that was not verified. A step is verified when it owes a required output, whatever its prose says and whether or not it wrote a file through write_file — a step whose product came from comfyui_generate, tts_render or a shell command writes no tracked file and was skipped entirely until this was corrected, which left generated images, rendered narration and assembled video as the three things nothing judged. Steps that owe nothing are still skipped when they wrote nothing, or when their description reads as conversational or as a check and they wrote neither a test file nor a file their node's required_output owes. A quality check the step ran and that failed (run_tests, validate_web_app, check_finishing) fails the step before any judge is called, but a failing validate_web_app or whole-suite run_tests is first retaken with the agent's own arguments, and a retake that passes clears it: a server started after the first check is judged where the step ended up. On the plan path the Engine also retakes a failing check of either kind after every round of a step, whether or not this verifier is on, before any gate reads the step's checks; the retake becomes the step's last verdict for that tool and is logged as a verifier call to it. A step whose description promises passing tests (npm test, pytest, dotnet test, node --test, run_tests, or "tests … pass"; a test script definition such as "test": "node --test …" in its words is not a promise), or that wrote test files — code files under test/, tests/ or __tests__/, or named *.test.*, *.spec.*, test_*.py, *_test.py or *Tests.cs — and ran none has them run by the Engine the way run_tests would, and a failure fails the step with the runner's lines as the repair note. The judge is only called once the deterministic gate has passed, so the prompt states that as fact: every owed deliverable is listed as already checked, with the condition it met beside it (playing for at least 50 seconds, with a sound track), and the judge is told not to fail the step for a deliverable that looks missing, misnamed or too short. It had been failing correct work on the strength of the worker's own transcript — a film on disk under the owed name, judged absent three times because an earlier turn had called it something else. A file too long for the prompt is excerpted head and tail with the omission named in the middle, rather than cut at the budget: a page's last section and its closing tags live at its end, and cutting there turned every long deliverable into an "incomplete" one. Off unless the team's verification block or AI_VerificationEnabled turns it on. |
Services/VerificationService.cs |
No | §08 |
VramOrchestrationService |
Per-run model residency and affinity pins: takes a run-priority lease per instance from the arbiter, releases it when the run ends. | Services/VramOrchestrationService.cs |
No | §6.1 |
InferenceLeaseService |
The box-level inference arbiter: leases on each slot's VRAM with run > interactive > batch priority, cooperative preemption of batch by a run, resident-only interactive leases, a queue for batch work, card holds that reserve a whole card for something outside the Engine, and restoration of the standard set when a slot goes idle (a failed restore is retried with backoff, three attempts). Exposed at api/inference. |
Services/Inference/InferenceLeaseService.cs |
Yes (InferenceLeaseBackgroundService sweeps every LeaseSweepSeconds) |
§6.1 |
InferenceRelayService |
OpenAI-compatible /v1/chat/completions relay for an external chat client: resolves the slot that serves the model — the first, in slot order, where it is resident, else the first that lists it, asking every slot at once and counting one that has not answered within 1.5 s as listing nothing — holds a resident-only interactive lease per client, and refuses with the router's own 400 body whenever the model is not already resident — a run or a batch job holds the card — so the client's fallback chain advances; it never loads a model. When the first slot with the model resident refuses because a run or a batch job holds it, the next slot in slot order with a resident copy takes the request, and the 400 is sent only when every resident copy refuses. On a slot with a named interactive copy (InteractiveCopies), interactive traffic goes to that copy, so it keeps its own prompt cache apart from the engine's runs; a request the copy cannot serve is re-sent to the base model. |
Services/Inference/InferenceRelayService.cs |
No | §6.1 |
SpeechRelayService |
POST /v1/audio/speech for an external chat client: maps the OpenAI voice name to a reference clip (Aetherium:Tts:RelayVoices), renders the whole input in one request with the Base clone service, and holds a single batch lease on that service for every caller, released after Aetherium:Tts:RelayIdleMinutes without a request; a render with no audio inside its word-scaled budget is cut off with a 504 |
Services/Inference/SpeechRelayService.cs |
No | §6.1 |
WebhookService |
External event push with retry. | Services/WebhookService.cs |
No | §13 |
WorkspaceCleanupService |
Tier-based expired-project cleanup. | Services/WorkspaceCleanupService.cs |
Yes | §09 |
WorkspaceScannerService |
Boot-time scan to import projects from disk. | Services/WorkspaceScannerService.cs |
Yes (IHostedService) | §09 |
WriteReferenceGuard |
Refuses an HTML write_file that orphans a sibling script's element-id lookups. |
Services/WriteReferenceGuard.cs |
No | §07 |
WriteShrinkGuard |
Refuses a write_file that would discard most of an existing file. |
Services/WriteShrinkGuard.cs |
No | §07 |
Smaller utility services not in the table — agent-assignment helpers, role CRUD, settings CRUD, metrics aggregation, the no-op SSR notification stub — exist in Services/ and follow the same patterns. Open the directory when you need one.
Where to go next¶
- §07 — MCP Architecture — the twelve MCP server classes the router dispatches to, the catalog of forty-four tools, and how to add a new one.
- §08 — Templates — the JSON the provisioner pair instantiates.
- §09 — Configuration — the keys these services read.
- §10 — Development Workflow — the patterns above in mechanical depth.