Skip to content

Schema & Data Model

Two reference layers: the template schema (the ProjectTemplate / TeamConfig shape a Workflow Template is authored in) and the data model (the EF Core entities). The template authoring view is in §08 — Templates; this page is the field-by-field enumeration.

The camelCase ↔ snake_case boundary

The single most common authoring mistake is casing. The rule: the outer ProjectTemplate object is camelCase; everything inside the team block is snake_case. The mechanism is two-fold:

  1. ProjectTemplate carries no [JsonPropertyName] attributes, so its casing is driven by the serializer's JsonNamingPolicy.CamelCase → id, name, defaultTier, seedKnowledgeBase, …
  2. TeamConfig and its sub-types carry explicit [JsonPropertyName("snake_case")] on every field, which overrides the naming policy → base_image, is_supervisor, system_prompt, circuit_breaker, …

The clone step (TemplateService.BuildCreateRequest) round-trips the team block through JSON with the naming policy turned off, so the explicit snake_case attributes drive serialization and deserialization symmetrically — the team block deep-clones losslessly. Write seed_knowledge_base (snake) on the outer object or modelId (camel) inside the team block and the field is silently ignored — the deserializer keeps the default rather than erroring.

One exception inside the team block

Two of the container sub-objects referenced from TeamConfig — VolumeMount and NetworkConfig — carry camelCase [JsonPropertyName] attributes (hostPath, containerPath, readOnly; isolateNetwork, allowedHosts, networkMode), so mounted_paths is snake_case but its bodies are camelCase. ResourceLimits was the third until it was corrected to snake_case (cpu_limit, memory_limit, disk_quota, cpu_shares) — every shipped template had always written the snake_case spelling, so default_resources bound to nothing and silently fell back to the defaults.


Template schema

ProjectTemplate

The outer blueprint — camelCase. File: Aetherium.Shared/Models/ProjectTemplate.cs.

Property JSON Type Default
Id id string ""
Name name string ""
Description description string ""
Icon icon string "bi-collection"
Category category string "General"
DefaultTier defaultTier string "Staging"
Team team TeamConfig new() (key camelCase → body snake_case)
Directories directories List<string> new()
ProjectConfigTemplate projectConfigTemplate string? null
KnowledgeFolders knowledgeFolders List<string>? null
SeedKnowledgeBase seedKnowledgeBase bool true
IsCore (ignored) bool [JsonIgnore] runtime flag

TeamConfig

The team block — snake_case. File: Aetherium.Shared/Models/TeamConfig.cs.

Property JSON Type Default
Name name string ""
Description description string ""
Version version string "1.0.0"
Config config TeamGlobalConfig new()
Nodes nodes List<TeamNode> new()
Edges edges List<TeamEdge> new()
Topology topology string "hierarchical"
Recipe recipe string? null — the preset form's shape (single, hierarchical, sequential, roundtable, dynamic-parallel); the resolver sets topology from it, generates edges when empty, and fills circuit_breaker from the recipe's defaults when unset
BaseImage base_image string "python:3.11-slim"
PersistentContainer persistent_container bool false
DefaultResources default_resources ResourceLimits new()
ExposedPorts exposed_ports Dictionary<string,string>? null
ContainerPorts container_ports List<string>? null
CircuitBreaker circuit_breaker CircuitBreakerConfig? null
KnowledgePacks knowledge_packs List<string>? null — the packs the team retrieves from: null detects them at session start (base image, tools, prompts), [] means none, a list is explicit
AutoDetectKnowledge auto_detect_knowledge bool false — adds detected packs to an explicit knowledge_packs list
MountAetheriumSource mount_aetherium_source bool false
ContainerInitCommands container_init_commands List<string>? null — each line runs once, as root, when the project container is first created. A non-zero exit fails the run before any agent runs, with the command and its output in the run's error (since 2026-09-16; before that a failure was logged as completed)
NetworkPolicy network_policy ContainerNetworkPolicy Unrestricted (serialized as string)
AllowedDomains allowed_domains List<string>? null
SubagentDefaults subagent_defaults SubagentDefaults? null
TargetCollections target_collections List<string>? null
ToolRequests tool_requests string? null — deny (the default), grant or ask: what happens when an agent calls a tool its node does not hold
StepRounds step_rounds int? null — fresh attempts per plan step for this team; overrides the AI_StepRounds setting
VerifierInstance verifier_instance string? null — the slot for the step verifier, written by a run override's verifierInstance (omitted from JSON when null). It beats verification.verifier_preferred_instance, applies whether verification comes from the team's block or the global switch, and places the trajectory judge too unless the block names trajectory_judge_preferred_instance. It lives on the team rather than in the block because creating a block would change whether verification runs
EnforceNodeRequiredOutput enforce_node_required_output bool false — when a plan step declares a strict subset of the paths its node declares in required_output, replace the step's list with the node's instead of only logging the difference — and, since 2026-09-16, when the step declares no required_outputs at all (an empty declaration used to switch the gate off). Only ever asserts paths the node already declares, so it cannot hold a step to a sibling step's file. restaurant-kitchen is the one shipped template that opts in
PlanSerialSteps plan_serial_steps bool false — when true, create_plan makes every step depend on the one before it, whatever the planner declared (explicit extra dependencies are kept), so the wave scheduler runs one step at a time. Applied to the plan before it is stored, on both the tool path and the hierarchical Phase 1 native call (2026-09-16)
PlanSteps plan_steps list of {agent_id, description} null — when set, the declared list is the plan (order, assignment, serial deps, node outputs); the planner only lends descriptions (2026-09-17)
GpuServices gpu_services string[]? null — registered GPU services (comfyui, comfyui-qwen, comfyui-zimage, comfyui-video, qwen-tts, qwen-tts-design) the run needs resident: they ride on the run's own inference lease, so the arbiter starts each unit before the run, budgets its VRAM beside the run's models, and stops it after the run. When a team declares none, the composition resolver derives the list from the tools its nodes hold (comfyui_* → the ComfyUI of the team's image_model, comfyui_video → comfyui-video, tts_* → qwen-tts); a declared list is taken as written
TemplateId template_id string? null — the template the team was created from, set when a project is created from a template, since the team's own name becomes the project's (omitted from JSON when null). tts_render keeps the template's designed voices under it, so every project made from one template speaks with the same cast
ImageModel image_model string? null — qwen renders the team's images on Qwen-Image 2.1 through the comfyui-qwen service, and makes the team eligible for the post-run 2K re-render when Aetherium:ImageFinishing:Enabled is on (off by default); z-image renders them on Z-Image Turbo through the comfyui-zimage service; unset is SDXL
StarterKitDir starter_kit_dir string? null — a workspace folder (site/kit) the Engine copies its canvas game kit into when the project is created, with a paragraph in project-config.md saying where it is; see §08
FilmMotion film_motion bool false — the film a team cuts from its stills moves: each still sits on a blurred fill of itself and slowly zooms in over its share of the narration. Off, every still is held for its share. A template that sets it has its slideshow command swapped for the moving one when a project is created from it. No shipped template sets it. Omitted from JSON when false
FinishingPhase finishing_phase int 0 — 1 or 2 on the team a finishing pass runs, set by the Engine and never authored in a template (omitted from JSON when 0); the plan path holds Phase 2 to its own rules by it, such as an owed file having to be written in that run
ProjectBrief project_brief string? null — set only on a team the Quick Create generator made, to the request it was given (omitted from JSON when null). Creating the project writes it to the project root and the workspace as project-config.md when no config exists, so the roles told to read that file first find the brief there
GenerationNotes generation_notes List<string>? null — what the Quick Create generator could not settle, one note per line: an owed file no tool in the workspace makes, a page owing a folder no role fills, a role missing the tool its deliverable needs, a ticked need no role covers. The Designer shows them under its header. Null for a template's team and for a design with nothing to report

topology is a string. The four canonical topologies are Hierarchical, Sequential, Roundtable, Single (only Hierarchical reads edges). A fifth value, dynamic-parallel, selects the runtime subagent-spawning mode and pairs with subagent_defaults — see §08. ContainerNetworkPolicy values: Unrestricted, SetupOnly, Isolated, AllowlistOnly (unknown values tolerate to Unrestricted).

TeamGlobalConfig

The inner config object — snake_case.

Property JSON Type Default
Model model string ""
Temperature temperature double 0.7
MaxTokens max_tokens int 4096
ContextWindow context_window int 16384
ReasoningEffort reasoning_effort string? null — low/medium/high/xhigh turns a thinking model's reasoning on for every worker dispatch of this team — hierarchical delegates, single-agent and sequential-stage turns, roundtable rounds, dynamic-parallel subagents and the lead's synthesis; anything else leaves it off. Planning turns follow AI_ReasoningEffort instead
DiscussionRounds discussion_rounds int 2 (Roundtable only; 1 = single round)

TeamNode

One agent — snake_case.

Property JSON Type Default
Id id string ""
Name name string ""
Role role string ""
SystemPrompt system_prompt string ""
Tools tools List<string> new() — the runtime allowlist
Profiles profiles List<string>? null — named tool profiles (reader, writer, reporter, coder, qa, researcher, planner, orchestrator, knowledge, imaging, audio, video, comms) expanded into tools on create. reporter is reader plus write_file — a role that produces a report and repairs nothing
ExcludeTools exclude_tools List<string>? null — removed after profile expansion
RequiresEvidence requires_evidence bool? null — set from the role library for reporting roles; a plan step this node runs fails the evidence gate unless it called web_search or web_fetch, wrote at least one source URL, and every URL it wrote appeared in a search result or a fetch during the step (only calls that succeeded count)
RoleId role_id string? null — a role-library entry (Templates/composition/roles.json); every empty field on the node is filled from it on resolve, and it is kept as provenance
McpServers mcp_servers List<string>? null (derived from tools when null)
Description description string ""
IsSupervisor is_supervisor bool false
ModelId model_id string? null
PreferredInstance preferred_instance string? null
Temperature temperature double? null
MaxTokens max_tokens int? null
KnowledgeFilter knowledge_filter string? null
KnowledgeFilters knowledge_filters List<string>? null
MaxTurns max_turns int? null
Reflection reflection ReflectionConfig? null
AgentType agent_type string? null

TeamEdge

Property JSON Type Default
From from string ""
To to string ""

SubagentDefaults

The subagent_defaults block that shapes spawn_subagents children — snake_case.

Property JSON Type Default
ModelId model_id string? null
Tools tools List<string> ["read_file","write_file","edit_file","list_files","web_search","web_fetch","write_knowledge","search_knowledge"]
MaxTokens max_tokens int? null
MaxTurns max_turns int 15
PreferredInstance preferred_instance string? null — pins every spawned child to that slot, and its model_id, when concrete, is made resident there by the run's lease; unset, each child is smart-routed. Read since 2026-10-06; before that it was declared and never read
McpServers mcp_servers List<string>? null

Run overrides (modelId, preferredInstance, maxTokens, contextWindow, reasoningEffort) are applied to this block as well as to the nodes, so spawned children run on the overridden model; subagentInstance sets only its preferred_instance, creating the block with these defaults when the team has none. Shipped templates routinely override the default tool list — e.g. the research template uses [web_search, web_fetch, download_pdf, write_file, read_file, write_knowledge] with explicit mcp_servers: [workspace, research, knowledge].

CircuitBreakerConfig

Per-team, session-scoped ceilings — snake_case. File: Aetherium.Shared/Models/CircuitBreakerConfig.cs.

There are no tool-call or token caps. per_tool_limits, max_tool_calls_per_session, max_turns_per_step, max_tool_calls_per_step and max_session_tokens were removed on 2026-10-08: the only checks that read them had no caller, so a template that set one capped nothing. A template that still carries those keys loads without error and they are ignored.

Property JSON Type Default
MaxTurnsPerAgent max_turns_per_agent int 10
StepTimeoutSeconds step_timeout_seconds int 1800 — wall-clock budget for one plan step; 0 disables. On expiry the step fails with the reason and is re-planned like any failed step (its re-run is told it timed out), and the run continues to whatever is still executable. Enforced since 2026-09-16; before that the value (then 120) was declared and never read
SessionTimeoutSeconds session_timeout_seconds int 600
InfiniteLoopThreshold infinite_loop_threshold int 3
MaxReplansPerSession max_replans_per_session int 2
ThinkingModelOverrides thinking_model_overrides ThinkingModelOverrides? new()

ThinkingModelOverrides

Applied when the team's default model is classified as a reasoning model — those models get wider ceilings. snake_case. The classification is not configured here: it comes from the Engine's model-capability table, which is keyed on the model ID and carries the measurement behind each entry. (Before 2026-09-08 the opt-in was a thinking_model_pattern regex on this config; that property is gone, and the capability table is the only classifier.)

Property JSON Type Default
MaxTurnsPerAgent max_turns_per_agent int 35
StepTimeoutSeconds step_timeout_seconds int 0 — 0 inherits the team's step_timeout_seconds; the other override values replace the team's
SessionTimeoutSeconds session_timeout_seconds int 1800

ReflectionConfig

Optional per-node self-critique pass — snake_case. Off by default; no shipped template enables it.

Property JSON Type Default
Enabled enabled bool false
Criteria criteria List<string> ["completeness","clarity"]
MaxRefinements max_refinements int 1 (max 3)
UseSeparateModel use_separate_model bool false
CriticModelId critic_model_id string? null
CriticPreferredInstance critic_preferred_instance string? null
MinOutputLengthForReflection min_output_length_for_reflection int 200

Supporting container types (camelCase)

Referenced from the snake_case team block but themselves camelCase:

  • SharedWorkspaceConfig — enabled (bool, true), mountPath ("/workspace/shared"), readOnlyAgents (List<string>), hostPath (string?).
  • ResourceLimits — cpuLimit ("2.0"), memoryLimit ("4g"), diskQuota ("10g"), cpuShares (long, 1024).
  • AgentContainerSettings — isolationMode ("shared"), requiresIsolation (false), baseImage (null), resources, network, privateMounts, shareWorkspace (true), poolSettings (null), environmentVariables ({}), workingDirectory ("/workspace").

mcp_servers derivation

When a node's mcp_servers is null, it is computed from the node's tools: each tool name is mapped through McpToolRouter.ToolToServerGroup, unmapped tools are dropped, and the distinct set of groups becomes the node's authorized servers. Layer-1 filtering then includes only tools whose group is in that set. Declaring mcp_servers explicitly keeps the allowlist under your control (§07, §08).

The tools list itself is settled by the composition resolver on every creation path (Quick Create, template create, role sync, a Studio edit) and at template load: a node's role_id fills every field it left empty from the role library (name, role, description, system_prompt, tools, mcp_servers, requires_evidence, and model_id from the role's slot in Templates/composition/model-policy.json, with the slot's instance as the node's preferred_instance and its max_tokens where the node sets neither; a node pinned to a different instance keeps its pin and gets no model), a team's recipe sets the topology and generates the edges, then a node's profiles expand into their tool sets (minus exclude_tools), a hierarchical or sequential team's supervisor gets delegate and reply (and read_plan + update_plan when it has create_plan), workers get reply, and a content, code or test role that has write_file gets edit_file unless its effective profiles include reporter. Every tool the resolver adds is logged with its reason. A Studio edit (the Designer's save) is the exception: an agent that was already on the team, or that arrives with its tools listed (added from the library, or generated), keeps the tools and model_id it was saved with. It gets only the topology wiring (delegate, reply and the planner grant), plus requires_evidence and agent_type from its role, which the Designer does not send. An agent that arrives with no tools is resolved in full. So a tool removed in the Designer, or "use the team model", is not undone by the save. The shipped templates are presets that resolve to the same teams they listed in full before the rewrite (a golden test pins each one), so resolving them changes nothing. The role library, the recipes and the model policy live beside the profile vocabulary in Templates/composition/ (roles.json, recipes.json, model-policy.json, tool-profiles.json), and the Engine refuses to start when any of them names a tool the router cannot route, a profile a role's tools do not cover, or a model slot the policy lacks. The profile vocabulary lives in Templates/composition/tool-profiles.json, which also names, per profile, the tools an agent may be granted at run time through the tool-request gate (tool_requests).


Data model

AetheriumDbContext declares 27 DbSet<T>. Core entities use explicit [Table("snake_case")] + [Column("snake_case")] (Postgres convention); a few lean ones (SystemSetting, ToolSecurityAuditEntry) use EF default PascalCase. Several columns are Postgres jsonb, stored as string? in C#.

DbSets

DbSet Entity Purpose
Prompts Prompt Reusable prompt library
Roles Role Agent-role definition (FK → Project)
Projects Project Top-level project; team config stored as jsonb
IdempotencyRecords IdempotencyRecord API idempotency cache
SystemSettings SystemSetting Key-value config table
InteractionLogs InteractionLog LLM interaction analytics (tokens, timing)
TeamConfigVersions TeamConfigVersion Historical versions of a project's team config
ContainerCommandLogs ContainerCommandLog Shell commands run in containers
ContainerSnapshots ContainerSnapshot Docker-commit snapshots (soft-delete)
ConversationMessages ConversationMessage Persisted chat history per session
ConversationSessions ConversationSession Session entity grouping messages
AgentMemories AgentMemory Working / long-term agent memory
SessionSummaries SessionSummary LLM summaries of completed sessions
WorkflowStates WorkflowState Orchestrator state for checkpoint/resume
AgentLessons AgentLesson Lessons extracted from sessions; project_id is nullable with ON DELETE SET NULL, so a lesson outlives the project that produced it
ProjectPlans ProjectPlan Execution plan (from create_plan)
PlanSteps PlanStep Plan steps (FK → ProjectPlan)
PortAllocations PortAllocation Dynamic host-port allocations
BuildRuns BuildRun One user-message → orchestrator cycle. ReviewNotes (text, nullable; JSON reviewNotes) is why a run that finished Completed still needs the owner's eyes, one reason per line: a step that completed although the step verifier failed it or could not decide (the newest attempt at each plan step); a file a team node owes (required_output) that is not on disk when the run ends, read by the step gate's own tests (files an earlier run made count as present); a page owing links whose links, re-read at the end of the run, miss a file of the set or name one that does not exist; and a page owing shows DIR/ that at the end of the run shows fewer of DIR/'s files than it owes or links one that does not resolve. Null when there is nothing to review. A verifier's failure does not fail the run; it is recorded here. On the plan path CurrentStep is the plan step being worked and TotalSteps the plan's step count, so a retried step does not advance the counter. SourceAgent records who launched the run: ui for the Studio, otherwise the sourceAgent the caller sent to run-from-template or POST /api/orchestrator/run
BuildSteps BuildStep An agent's work within a BuildRun; Status (Pending / Running / Completed / Failed / Skipped — Skipped is what an evidence gap and a stopped swarm thread record) and VerificationStatus (passed / failed / inconclusive, null when the step was not verified). Absence records why a step produced nothing a consumer reads, as a value rather than as prose: None / NoToolCalls / ToolCallsNoWrites / WroteOutsideWatchedPaths (a successful write that landed where the consumer does not read) / WritesFailContentCheck / NoOwningProcess (reserved — nothing is classified as this yet). PlanStepNumber (nullable) is the plan step a row is an attempt at, on the plan path only: a plan step that runs again gets a new row under the same number, and the run is judged on the newest one
WebhookTargets WebhookTarget Registered webhook endpoints
WebhookDeliveries WebhookDelivery Webhook delivery attempts
ToolSecurityAuditEntries ToolSecurityAuditEntry Tool-security audit table
KnowledgeSeedJobs KnowledgeSeedJob Per-project KB seeding tracker
SessionEvents SessionEvent Durable AG-UI event log — one row per stream event
DecisionLogs DecisionLog One row per decision-layer call, including refusals and timeouts
FinishingGates FinishingGate One finishing pass per project: its phase, its two run GUIDs, and the timestamps that mark the operator's answer

Key relationships: Project → Roles (project_id, cascade; ProjectId nullable for library roles; unique (Name, ProjectId)); ProjectPlan → PlanStep, BuildRun → BuildStep, WebhookTarget → WebhookDelivery (one-to-many, cascade); InteractionLog → BuildRun/BuildStep (set-null).

Project

Table projects.

Field Column Type Notes
Id id int PK
Name name string(100) required
Description description string?
LocalPath local_path string?(500)
WorkspaceTier workspace_tier string(20) default Staging
GithubRepo github_repo string?(200)
GithubConfig github_config string? jsonb
DockerConfig docker_config string? jsonb
TeamComposition team_composition string? jsonb
MassTopology mass_topology string? jsonb — serialized TeamConfig
GeminiConfig gemini_config string? jsonb
KnowledgeConfig knowledge_config string? jsonb
Status status string(50) default active
CreatedAt / UpdatedAt created_at / updated_at DateTime

Role

Table roles — the agent record.

Field Column Type Notes
Id id int PK
Name name string(100) required; unique with ProjectId
Description description string?
RoleType role_type string?(255)
Category category string(50) default General
State state string(50) default draft
IsTechnical is_technical bool false
PromptTemplate prompt_template string?
Specializations / TechnologyStack / Prerequisites … string? jsonb
PromptId prompt_id int? → Prompt
ProjectId project_id int? FK → projects.id (cascade)
AgentType agent_type string(50) default individual
IsSupervisor is_supervisor bool false
DeploymentConfig deployment_config string? backs [NotMapped] Config (RoleConfiguration)
DeploymentStatus deployment_status string(50) default pending
Provenance provenance string?

RoleConfiguration (nested, from DeploymentConfig): Provider ("LLM Studio"), ModelId ("default"), Temperature (0.7), MaxTokens (2048), Tools, PreferredInstance?, KnowledgeFilter?.

Identify team agents by ProjectId, not AgentType

ProjectId.HasValue marks a team agent reliably; AgentType is often unset. This is a load-bearing convention across the services.

SystemSetting

PascalCase mapping. Key (PK), Value (string), Description (string?), Group (string, default General). Seeded rows and their groups are in Configuration.

AgentMemory

Table agent_memories.

Field Column Type Notes
Id id int PK
ProjectId project_id int FK → Project, indexed
AgentId agent_id string?(100) null = project-shared; set = per-agent
MemoryType memory_type string(50) working / long_term
Content content string required
SourceSessionId source_session_id string?(100)
RelevanceScore relevance_score float 1.0
TokenCount token_count int
CreatedAt / UpdatedAt … DateTime
ExpiresAt expires_at DateTime? null = never

ToolSecurityAuditEntry

PascalCase mapping. Every gated tool call. No retention policy (Configuration).

Field Type Notes
Id int PK
Timestamp DateTime indexed
ProjectId int? indexed (composite with SecurityLevel)
TeamName string?(200)
AgentId / AgentName string(100) / string(200)
ToolName string(100)
SecurityLevel string(20) default Standard
ArgsJson string? JSON args
ResultSummary string?(500) first 500 chars
ResultFull string? Elevated+ only
ApprovedBy string?(200) Restricted only
Allowed bool true
ExecutionTimeMs long

The security log tab of Operations — the live UI over this table. Each row is a gated tool call showing its time, tool, agent, security tier (Standard/Elevated/Restricted), outcome, execution time and result summary; the strip above counts the entries and whether any were denied, and the pills filter by tier.

ContainerCommandLog

Table container_command_logs — dual attributes ([Column("snake_case")] for DB, [JsonPropertyName("camelCase")] for API). Key fields: Id, ContainerId(100, indexed), ContainerName?, ProjectId? (FK), Command(500), Arguments?, StdOut?, StdErr?, ExitCode?, AgentName? (null for a command run by an operator), ExecutionTimeMs (double), WorkingDirectory?, Timestamp (indexed).

ProjectPlan / PlanStep

project_plans: Id, ProjectId (FK), SessionId?, Goal, Status (default active), timestamps, Steps (nav). plan_steps: Id, PlanId (FK → project_plans, cascade), StepNumber, Description, AssignedAgentId?, Status (default pending; completed, failed, or skipped when a step wrote files but failed the evidence gate), DependsOn (jsonb), Result?, StartedAt?/CompletedAt?, RequiredTools (jsonb), RequiredOutputs (jsonb — a JSON array of workspace-relative paths the step owes; when present the required-output gate enforces exactly that list, and when absent it falls back to narrowing the node's declared outputs by the words of the step's description. An entry ending in /, /* or /** is a directory obligation, satisfied by any file beneath it; an entry may carry a " >= N" suffix, satisfied only by N files of distinct content. A team with enforce_node_required_output replaces a step's list with its node's whenever the step declared a strict subset), ClaimedBy?/ClaimedAt?, Priority (PlanStepPriority { High=0, Medium=1, Low=2 }; lower = higher priority).

Conversation & summaries

  • ConversationMessage (conversation_messages): Id, ProjectId (FK), SessionId(100), Role (user/orchestrator/agent name), MessageType (message/tool_call/tool_rejected/tool_output/thought), Content, Metadata (jsonb: tokens, model, latency, isError, the tool name, arguments and result card, and since 2026-10-10 kind — verdict/plan/system/error/delegation on a thought, held on a guard-refused tool call, steer on an operator steer — with buildRunId and stepNumber), AgentId?, SequenceNumber, CreatedAt. Unique (ProjectId, SessionId, SequenceNumber).
  • ConversationSession (conversation_sessions): Id, SessionId(unique), ProjectId (FK), Title?, Topology?, AgentCount, MessageCount, CreatedAt, LastMessageAt, IsArchived.
  • SessionSummary (session_summaries): Id, ProjectId? (FK, ON DELETE SET NULL — a summary outlives its project), SessionId (unique), Summary, KeyDecisions? (jsonb), TokenCount, CreatedAt.

InteractionLog

Table interaction_logs — analytics, distinct from chat history: Id, ProjectId?, RoleId?, SessionId(100), AgentName?, InteractionType (user/assistant/tool/error), Content?, InputTokens, OutputTokens, plus further token/timing fields and set-null FKs to BuildRun/BuildStep. Indexed on Timestamp, SessionId, ProjectId, and composites.

FinishingGate

Table finishing_gates — dual attributes ([Column("snake_case")] for DB, [JsonPropertyName("camelCase")] for API), indexed on project_id and on status. Fields: Id, ProjectId, SourceRunGuid?(36 — the build run whose completion opened the gate), Status, Phase1RunGuid?(36), Phase1StartedAt, BriefWrittenAt?, AnsweredAt?, Phase2RunGuid?(36), Phase2DoneAt?, Note?.

Status is FinishingGateStatus { Phase1 = 0, AwaitingAnswer = 1, Phase2 = 2, Done = 3, Failed = 4 }. The three nullable timestamps are the record of the pass: BriefWrittenAt is set only when the brief actually reached disk (a completed run that wrote none does not advance the gate), AnsweredAt when the operator's answer was written into the workspace, and Phase2DoneAt when the applied pass closed.

SessionEvent

Table session_events — the durable copy of the AG-UI event stream, one row per event: Id (bigint), ProjectId, Seq (the stream's per-project sequence number; unique with ProjectId), EventType(120; the AG-UI type, or the custom event's name), BuildRunId?, BuildStepId? (indexed), At, Payload (jsonb — the event exactly as the SSE endpoint sends it). Written behind the stream; sequence numbers resume from this table after a restart.

DecisionLog

Table decision_logs — one row per call into the decision layer's IDecisionModel, written whatever the outcome, so the cases a model could not answer are kept beside the ones it could: Id, Primitive (choice / score / noul), QuestionHash (SHA-256 of the question, 16 hex chars), Question?, OptionsCount, InputExcerpt? (the head of the context, capped), ChosenValue?, Probability? (renormalised over the continuations the schema allowed; null when the backend returns no logprobs), Calibrated, Backend, ModelId?, Slot?, LatencyMs, ProjectId?, BuildRunId?, BuildStepId?, AgentName?, Round?, Outcome (ok / refused / timeout / error), Action? (bounced / passed, or shadow when the layer was only observing) and CreatedAt. Nothing is written while Aetherium:Decision:Enabled and :Shadow are both off, which is the default.

Where to go next