Independent regression audit (Opus 4.7) verified P1×4 + P2×9 fixes were closed but found that three of the P2 fixes had introduced new baseline violations and two had minor errors. This commit closes all five. R-series fixes (in system-detailed-design.md): - R1-01 §10.2: L2 Safety source no longer names a fictional PermissionEngine.current_profile() method. The L2 row now describes the active permission profile sources (~/.air/permissions.yaml + project permission config) without inventing a contract method, honoring DD §0 "no new public contracts." - R1-02 §5.4 Table A: agent.started projection no longer claims a two-step "starting → running" update within a single event commit (which would violate event atomicity). The row now matches event-registry §3: a single status (starting or running) at emission time, with the follow-up transition handled by WorkerManager per the state machine in §20.4. - R1-03 §18.4 + §5.4 Table B: removed the non-baseline phase: intent | committed payload-field extension from memory.promoted and debug.record.created. Outbox semantics now follow the baseline model: owning store performs the external write first, then ingests a single durable completion event whose payload matches event-registry §3 exactly. Any future intent/commit split must go through an ADR plus payload version bump. - R2-01 §7.5: corrected workspace responsibility cross-reference from "overview §10.5" (Direct mode) to "overview §10.3" (Scheduler state machine, which actually covers workspace assignment and merge). - R2-02 §3: resolved self-contradiction in the contracts file-set decision. The 16 files are now stated as mandatory with default inlining of all overview §4 symbol groups for V1.0.0 Alpha; any future split is explicitly out of scope until an ADR is added. Also adds opus4.7详细设计与UML审查.md documenting the regression findings, baseline-evidence trails, and recommended fixes that drove these changes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
100 KiB
AirCoding V1.0.0 Alpha System Detailed Design and UML Class Model
Date: 2026-05-29 Status: Detailed design derived strictly from frozen baselines and system overview design Scope: Implementation-facing class/method design, UML, sequence, and state designs for V1.0.0 Alpha
0. Authority and Traceability
This document is bound by the following frozen sources. It elaborates them into class-level
design but does not introduce new public contracts, new event types, new DB columns, or new
runtime semantics. Where a detail is not fixed by a baseline, it is marked IMPL (implementation
freedom inside the contract boundary).
| Source | Role in this document |
|---|---|
interface-contracts-v1.md |
Public TypeScript contracts. All classes implement these as-is. |
c4/code-view.md |
Package/file layout and class inventory. |
c4/module.md |
Container dependency direction. |
db-schema-v1.md |
SQLite tables, enums, indexes. |
event-registry-v1.md |
Event names, payloads, persistence policy. |
scheduler-state-machine-v1.md |
Scheduler lifecycle and transitions. |
main-agent-state-machine.md |
Main Agent lifecycle. |
runtime-semantics-v1.md |
Ingestion boundary, FK-off, outbox, execution primitives. |
scope-escalation-v1.md |
Escalation/impact routing. |
security-model-v1.md |
Path/command risk, permission layering. |
error-taxonomy-v1.md |
AirError, ErrorKind, retryability, signature. |
prompt-layering-v1.md |
PromptLayer L0-L9. |
provider-capability-matrix-v1.md |
Provider adapter behavior. |
capability-trust-v1.md |
Capability lifecycle/trust. |
artifact-naming-v1.md |
Artifact URI/ID/filename. |
system-overview-design.md |
Container/component/flow overview. |
Naming rule (from contracts §1): exported contract fields are snake_case; class names are
PascalCase; private methods may use local camelCase. Method signatures below restate the frozen
contract types verbatim.
1. Document Structure
- §2 System decomposition and module ownership
- §3 Contracts package detailed design
- §4 Storage and repositories
- §5 Event subsystem (Ingestor/Store/Bus/SchemaRegistry)
- §6 Project/session lifecycle
- §7 Scheduler subsystem
- §8 Worker/IPC subsystem
- §9 Tool + Permission + Capability subsystem
- §10 Context/Prompt/Compaction subsystem
- §11 Artifact/Evidence/Knowledge subsystem
- §12 Provider (LLM) subsystem
- §13 Projection + TUI subsystem
- §14 Agents (Main, Architecture Designer) subsystem
- §15 Toolchain C++ subsystem
- §16 Doctor/Logging/Migration/Recovery subsystem
- §17 CLI subsystem
- §18 Cross-cutting designs (error, transaction, FK-off, outbox)
- §19 Sequence designs
- §20 State machine designs
- §21 Traceability matrix
2. System Decomposition and Module Ownership
Dependency direction (frozen by c4/module.md), expressed as allowed imports:
contracts → (none)
llm → contracts
toolchain-cpp → contracts
tui → contracts
runtime → contracts, llm (facade only)
cli → contracts, runtime, tui, llm, toolchain-cpp
workers → contracts + WorkerRuntime IPC surface (no direct runtime import)
Forbidden edges (frozen by contracts §23) are enforced by lint boundaries and reviewed at the architecture gate. The detailed class design below never crosses these edges.
Ownership summary (frozen by code-view §11 State Ownership):
| State | Owning class | Reader access |
|---|---|---|
| session DB | SessionStore, EventStore |
runtime services only |
| live events | EventBus |
runtime publish/subscribe |
| UI projection | ProjectionStore |
TUI read-only via ProjectionClient |
| artifacts | ArtifactStore |
tools/workers via runtime API |
| evidence | EvidenceStore |
reports/reviews/debug |
| tasks/agents | Scheduler |
repositories are storage-only |
| permission decisions | PermissionEngine |
ToolRegistry requests |
| model config | ProviderManager |
runtime/Doctor read via API |
| project rules/context | ContextAssembler/ProjectStore |
workers get excerpts |
Worker AgentType vs. runtime roles (contracts §5 AgentType, overview §10):
The frozen AgentType union covers only worker child-process roles:
executor | reviewer | debugger | compactor | experience_miner.
Runtime-resident roles are not members of AgentType and never appear in
WorkerResult.agent_type, agents.agent_type, or PromptLayerLoader.load_role(role):
| Runtime role | Implementing class | LLM use | Prompt source |
|---|---|---|---|
main |
MainAgent (§14.1) |
Yes, via ProviderManager |
Built-in resource (loaded directly by MainAgent, see §10.2) |
architecture_designer |
ArchitectureDesigner (§14.2) |
Yes, via ProviderManager |
Built-in resource (loaded directly by ArchitectureDesigner, see §10.2) |
scheduler |
Scheduler (§7.1) |
No (pure orchestration) | N/A |
This split keeps the contract AgentType enum stable and avoids accidentally giving runtime
roles a worker WorkerResult shape. Where event payloads need to identify a runtime role
(e.g. EventSource.kind in §22.1), they use a separate "main" | "architecture_designer" | "scheduler" literal set defined in event.ts, never AgentType.
3. Contracts Package Detailed Design
packages/contracts is type-only. It contains zero runtime logic, only export interface,
export type, and nominal aliases. File layout is frozen by code-view §3.
IMPL note: the contracts package may include tiny pure type-guards (e.g. is_air_error) only if
they have no dependencies; default is to keep it declaration-only to satisfy code-view §3 rule
"type-only package has no implementation deps" (todo T-002).
Design rules for the contracts package:
- Every interface in
interface-contracts-v1.md§2-§21 is exported from the file mapped in code-view §3 "Contract Ownership". - No interface gains extra fields here. Field additions require an ADR (contracts §22 rule 6).
JsonSchema<T>stays nominal (JsonObject); no runtime schema engine lives in contracts.- Re-export surface is
index.tswhich barrel-exports every contract file.
Contract-to-file map (frozen by code-view §3):
ids.ts → primitive ID aliases, Clock, IdGenerator
error.ts → ErrorKind, ErrorSeverity, Retryability, AirError
event.ts → EntityType, EntityRef, EventSource, RuntimeEvent, EventFilter
runtime.ts → AgentType, AgentRuntimeContext, ContextPack
ipc.ts → IpcDirection, IpcEnvelope, IpcKind, IpcMessage, ControlMessage, payloads
task.ts → Task*, VerificationPolicy, TaskConstraints, TaskSpec, TaskGraph, Scheduler*
worker-result.ts→ WorkerStatus, WorkerResult, ExecutorResult, ReviewerResult, DebuggerResult,
CompactorResult, ExperienceMinerResult, BlockerReport, Risk, FollowUpTask
tool.ts → ToolCategory, ToolDefinition, ToolExecutor, StreamingToolExecutor,
ToolExecutionContext, ToolResultEnvelope, ToolEvent, ToolRegistry
permission.ts → PathPolicy, PermissionRequestContext, PermissionAction, PermissionGrantScope,
PermissionDecision, PermissionRecordResult, PermissionEngine
artifact.ts → ArtifactRef, ArtifactCreateInput, ArtifactContext, ArtifactReadResult,
ArtifactStore
evidence.ts → EvidenceRef, EvidenceCreateInput, EvidenceStore
project.ts → ProjectContext, ProjectInitOptions, ProjectStore, SessionContext,
OpenSessionOptions, SessionManager
provider.ts → ProviderCapabilityMatrix, ModelRequirement, ProviderCompletionInput,
ProviderStreamEvent, ProviderAdapter, ProviderManager, ModelAssignment
ui.ts → all *Projection, ProjectionSnapshot, ProjectionStore, ProjectionClient
capability.ts → CapabilityManifestV1, ValidationResult, CapabilityRegistry
platform.ts → cross-platform tier enums referenced by Doctor (from cross-platform-matrix)
IMPL: storage.ts, scheduler.ts, workers.ts, context.ts, projection.ts, doctor.ts,
knowledge.ts, diagnostics.ts from overview §4 may either be separate files or be merged into the
above; the binding requirement is that the exported symbol set equals the contract set. This design
keeps the code-view §3 list as canonical and treats overview §4 as the superset note.
Frozen file-set decision (P2-09): For V1.0.0 Alpha, the canonical contracts package has exactly 16 mandatory files (frozen by code-view §3); see the list above. Overview §4 symbol groups that do not have a matching dedicated file are merged into one of these 16 files per the mapping below. The default for V1.0.0 Alpha is: all overview §4 symbol groups are inlined into the 16 mandatory files (i.e. the "default home" column).
| Overview §4 symbol group | Default home (mandatory, no ADR needed) |
|---|---|
storage.ts symbols (TransactionManager, Repository facades) |
merged into task.ts (TransactionManager) and per-domain files |
scheduler.ts symbols (SchedulerWavePlan, SchedulerRunResult) |
task.ts |
workers.ts symbols (WorkerRole, WorkerRuntime, IPC payloads) |
ipc.ts + worker-result.ts |
context.ts symbols (PromptLayer, PromptLayerLoader, ContextAssembler) |
runtime.ts (ContextPack and prompt-layer types are co-located) |
projection.ts symbols |
ui.ts |
doctor.ts symbols (DoctorService, DoctorRunInput/Output) |
platform.ts (cross-platform tier + doctor types co-located) |
knowledge.ts symbols (DebugKnowledgeStore, LearnedMemoryStore) |
artifact.ts |
diagnostics.ts symbols (Diagnostic, semantic_signature types) |
tool.ts |
The barrel index.ts exports the full union. No new .ts files are added to the contracts
package for V1.0.0 Alpha. Any future split (for example extracting a dedicated context.ts,
doctor.ts, knowledge.ts, or diagnostics.ts) is out of scope for V1.0.0 Alpha and requires
an ADR under docs/architecture/adr/ plus a synchronized update to code-view §3 before it may be
introduced in a later version.
4. Storage and Repositories
Module: packages/runtime/src/storage/. Classes: DatabaseManager, MigrationRunner,
repositories under repositories/.
4.1 DatabaseManager
Implements TransactionManager (contracts §6) over Bun's SQLite.
class DatabaseManager implements TransactionManager
+open(path: string): DatabaseHandle
+transaction<T>(fn: (tx: TransactionHandle) => Promise<T>): Promise<T>
-applyPragmas(db): void // WAL, synchronous=NORMAL, foreign_keys=OFF (db-schema §1)
-handleFor(tx): RawDb
Rules (db-schema §1): on open, set journal_mode=WAL, synchronous=NORMAL, foreign_keys=OFF.
transaction wraps BEGIN/COMMIT/ROLLBACK. A durable event insert plus its domain update run
inside one transaction call (runtime-semantics §3). TransactionHandle.id is an opaque token
mapping to the active raw transaction; nested calls reuse the active handle (IMPL: single-writer
per session DB, so no real nesting needed).
4.2 MigrationRunner
class MigrationRunner
+migrate(db: DatabaseHandle): Promise<void>
-currentVersion(db): number // reads schema_meta.schema_version
-targetVersion(): number // = 1 for V1.0.0 Alpha
-applyV1(db): void // creates all tables/indexes from db-schema §2-§18
V1 has a single target schema_version = 1. migrate is idempotent: if schema_meta is missing it
creates the full schema and seeds the initial keys (db-schema §2). It updates
aircoding_version_last_opened on every open. Destructive/non-trivial migrations follow overview §15
(plan, backup, confirm) but V1 only needs create-on-empty.
4.3 Repository layer
All repositories implement Repository<TRecord, TInsert, TUpdate> (contracts §6) or its narrow
extensions. They are thin persistence adapters: no scheduling, permission, or projection policy
(contracts §6, code-view §9). Records mirror db-schema columns exactly (snake_case).
Repository inventory (code-view §9) and their record types:
| Repository | Record (db-schema table) | Notable methods beyond CRUD |
|---|---|---|
SessionRepository |
sessions (§3) |
list_active() |
MessageRepository |
messages (§4) |
list_by_session(session_id, since?) |
MessageDraftRepository |
message_drafts (§5) |
upsert, delete_for_message |
EventRepository |
events (§6) |
insert(rec, tx), query(filter) (contracts §6) |
TaskRepository |
tasks (§7) |
list_by_status, list_runnable_candidates (contracts §6) |
TaskDependencyRepository |
task_dependencies (§8) |
list_for_task, list_dependents |
TaskAttemptRepository |
task_attempts (§9) |
next_attempt_index(task_id), list_by_task |
AgentRepository |
agents (§10) |
list_active, update_heartbeat |
ToolRunRepository |
tool_runs (§11) |
list_by_task, list_by_origin_message |
CommandRunRepository |
command_runs (§12) |
list_by_task (status derived, §4.4) |
ArtifactRepository |
artifacts (§13) |
list_by_entity, get_by_uri |
DiagnosticRepository |
diagnostics (§14) |
list_by_signature, list_by_command_run |
EvidenceRepository |
evidence_refs (§15) |
list_for_entity(type,id) |
WorkspaceRepository |
workspaces (§16) |
list_by_status, list_gc_candidates |
SummaryRepository |
summaries (§17) |
get, insert |
UiStateRepository |
ui_state (§18) |
upsert(scope,key,value), read(scope,key) |
SessionStore aggregates all repositories (code-view §9):
class SessionStore
+sessions: SessionRepository
+messages: MessageRepository
+message_drafts: MessageDraftRepository
+events: EventRepository
+tasks: TaskRepository
+task_dependencies: TaskDependencyRepository
+task_attempts: TaskAttemptRepository
+agents: AgentRepository
+tool_runs: ToolRunRepository
+command_runs: CommandRunRepository
+artifacts: ArtifactRepository
+diagnostics: DiagnosticRepository
+evidence: EvidenceRepository
+workspaces: WorkspaceRepository
+summaries: SummaryRepository
+ui_state: UiStateRepository
+referential_check(): Promise<OrphanReport> // FK-off invariants (§18.3)
4.4 Derived command status
command_runs has no physical status column (db-schema §12, runtime-semantics §5). The repository
exposes a pure derivation used by projection and reports:
function derive_command_status(row): "running" | "ok" | "error" | "cancelled" | "unknown"
completed_at == null → "running"
cancellation metadata present → "cancelled"
exit_code === 0 → "ok"
exit_code != 0 (non-null) → "error"
otherwise → "unknown"
This matches CommandRunProjection.status (contracts §17) so projection never invents a value.
4.5 Enum validation
Every closed-enum TEXT column (db-schema §21, 18 rows) is validated on insert/update. IMPL: a
shared assert_enum(table, column, value) helper backed by the db-schema §21 table; on violation it
throws an AirError of kind system_error (programmer error, never user-facing).
5. Event Subsystem
Module: packages/runtime/src/events/. Classes: EventSchemaRegistry, EventStore, EventBus,
EventIngestor. All implement contracts §7 verbatim.
5.1 EventIngestor
The single runtime entry point for events from agents/tools/workers (runtime-semantics §2).
class EventIngestor implements EventIngestor (contracts §7)
+ingest<T>(event: RuntimeEvent<T>): Promise<void>
+ingest_ephemeral<T>(event: RuntimeEvent<T>): Promise<void>
-policyFor(type): EventPersistence // durable | ephemeral, from registry
ingest flow (runtime-semantics §2):
validate envelope + schema/version (EventSchemaRegistry)
→ look up persistence policy by event.type
→ durable: EventStore.append(event) // tx + projection + post-commit publish
→ ephemeral: EventBus.publish(event) // live only
If ingest receives a type whose policy is ephemeral, it delegates to ingest_ephemeral. The
ingestor never creates scheduler tasks, permission decisions, or memory promotions; those are
follow-up events emitted by owning services (runtime-semantics §2).
5.2 EventSchemaRegistry
class EventSchemaRegistry implements EventSchemaRegistry (contracts §7)
+register(type, version, schema: JsonObject): void
+validate(type, version, payload): boolean
+list(): Array<{type, version}>
+get_schema(type, version): JsonObject | undefined
Seeded at startup from event-registry-v1.md §3 (durable) and §4 (ephemeral). Unknown
type+version fails validation → ingestion rejects with AirError kind system_error.
Payload schema change requires a new version (event-registry §2 rule 7).
5.3 EventStore
class EventStore implements EventStore (contracts §7)
+append<T>(event, options?): Promise<void>
+append_many(events, options?): Promise<void>
+query(filter: EventFilter): Promise<RuntimeEvent[]>
-toRecord(event): PersistedEventRecord // route_text = route.join("/")
-project(event, tx): void // domain table update per registry map
append algorithm (runtime-semantics §3, event-registry §2):
DatabaseManager.transaction(tx => {
schema validate (must already be durable policy)
EventRepository.insert(toRecord(event), tx)
project(event, tx) // domain projection from §5.4 map
})
EventBus.publish(event) // AFTER commit (contracts §7 rule 3)
Error handling for project(): If project(event, tx) throws an exception (e.g., due to FK-off
referential inconsistency, constraint violation, or programmer error), the entire transaction rolls
back. EventBus.publish() is never called. The exception propagates to the caller as an AirError
with kind: "system_error". If the failure is due to FK-off inconsistency (e.g., referencing a
non-existent tasks.id), the error is logged to developer log and SessionStore.referential_check()
is triggered asynchronously to diagnose and repair orphaned references.
route_text is always derived route.join("/") (event-registry §2 rule 6); route itself is
append-only (rule 5) — EventStore never rewrites prior route entries.
5.4 Durable projection map
project(event, tx) switches on event.type and applies exactly the domain update fixed by
event-registry §3. To make the boundary between pure intra-transaction projection and
post-commit outbox/compensation work explicit, the map is split into two tables.
Table A — Pure projection within EventStore.append transaction (no external writes):
| Event type | Domain update (in events_session.db transaction) |
|---|---|
session.created |
insert sessions |
session.archived / session.deleted |
update sessions.status |
user.message.created |
insert messages |
assistant.message.started |
upsert message_drafts (status=streaming) |
assistant.message.created |
insert messages + delete matching message_drafts |
assistant.message.failed |
message_drafts.status=error or failure artifact ref |
agent.started |
insert agents row with status = 'starting' when emitted at spawn (handshake not yet acknowledged) or status = 'running' when emitted after the WorkerProcess handshake has completed; the event carries exactly one terminal-of-emission status. The follow-up starting → running transition (without a dedicated durable event) is recorded by WorkerManager updating the row directly per the state machine in §20.4 |
agent.completed/failed/lost/cancelled |
update agents.status |
task.created |
insert tasks (+ optional task_dependencies) |
task.started |
tasks.status=running, set started/agent/workspace; insert task_attempts |
task.completed |
tasks.status=completed, set worker_result_json, completed_at; update attempt |
task.blocked |
tasks.status=blocked; update attempt |
task.failed |
tasks.status=failed; update task_attempts.failure_* |
task.cancelled |
tasks.status=cancelled |
task.interrupted |
tasks.status=interrupted |
tool.started |
insert tool_runs (running) |
tool.completed |
tool_runs.status=ok + output/artifacts/evidence/duration |
tool.failed |
tool_runs.status=error |
tool.cancelled |
tool_runs.status=cancelled |
command.started |
insert command_runs |
command.completed |
update exit_code, artifact refs, diagnostics, duration |
command.failed |
update exit_code when available + failure artifacts |
artifact.created |
insert artifacts (after temp→rename completed before commit) |
diagnostic.created |
insert diagnostics |
evidence.created |
insert evidence_refs |
context.compaction.requested |
insert compaction task row if accepted |
context.compaction.started |
mark compaction task running |
context.compaction.completed |
mark compaction task complete (no summary row) |
context.compaction.failed |
mark compaction task failed/blocked |
summary.created |
insert summaries (only place that does) |
permission.decision.recorded |
append (optional future projection table) |
permission.prompt.requested/resolved |
append (optional UI projection) |
doctor.* |
append (+ optional report artifact / command rows) |
requirement.changed |
append (Scheduler applies impacted-task marking on consumption) |
architecture.plan.updated |
append + plan/artifact refs |
architecture.impact.completed |
append (Scheduler consumes on EventBus subscription) |
workspace.created |
insert workspaces |
workspace.merge.started |
append + mark merge in progress (metadata) |
workspace.merge.completed |
workspaces.status=merged, set merged_at |
workspace.merge.conflicted |
workspaces.status=conflicted |
workspace.cleaned |
workspaces.status=cleaned |
memory.candidate.created |
append |
memory.archived |
append (mark memory inactive in session-side mirror if any) |
Table B — Projection paired with prior owner-side external write (cross-DB):
For these events the external write happens first in the owning store; the durable
session event is then ingested by the owning service to record the completed cross-store
transition. project(event, tx) only writes session-DB rows; the cross-DB pair is
eventually consistent (§18.4, runtime-semantics §6.3-§6.4).
| Event type | Projection step (in session-DB transaction) | External write (already done by owner before event is ingested) | Owner |
|---|---|---|---|
memory.promoted |
append durable event (event log row); domain side has no dedicated table — MemoryRepository is project-DB only |
write rules/, skills/, or learned-memory.db row |
LearnedMemoryStore / rules subsystem |
memory.archived |
append durable event | mark memory inactive in external store | LearnedMemoryStore |
debug.record.created |
append durable event | insert/update row in debug-records.db |
DebugKnowledgeStore |
Rules:
project(event, tx)never opens external DBs or files; it only writesevents_session.dbrows (events, drafts, session domain projections).- The owning service performs its external write before ingesting the durable event. This makes the session-DB event the "we observed the external write succeeded" record.
- If the external write fails, the owning service does not ingest the success event.
See §18.4 for failure modes (
task.failedcarryingAirError, candidate re-queue). - On restart, recovery cross-checks for upstream session rows (e.g.
memory.candidate.created) without a matching downstream event (memory.promoted) and re-queues work (runtime-semantics §6.4).
5.5 EventBus
class EventBus implements EventBus (contracts §7)
+publish<T>(event: RuntimeEvent<T>): void
+subscribe(filter: EventFilter, handler): Subscription
+drain?(): Promise<void>
-match(filter, event): boolean
Rules: live transport only, never a recovery source of truth (contracts §7 rule 4). If a handler
throws, the error is caught, logged to developer log, and does not propagate; subscription stays
active (contracts §7 rule 5). drain flushes pending async handlers for clean shutdown.
Ephemeral coalescing (event-registry §4): agent.heartbeat, task.progress,
assistant.message.delta, tool.progress, command.stdout.delta, command.stderr.delta,
hud.frame.rendered may be throttled before reaching subscribers.
6. Project and Session Lifecycle
Module: packages/runtime/src/project/ and sessions/. Classes: ProjectLocator,
ProjectInitializer, ProjectStore, SessionManager, SessionStore.
6.1 ProjectStore
Implements contracts §8.
class ProjectStore implements ProjectStore
+locate(start_path): Promise<ProjectContext | undefined>
+initialize(project_root, options?): Promise<ProjectContext>
+open(project_root): Promise<ProjectContext>
locate walks up from start_path looking for .air/shared/project.json (overview §8.1). initialize
creates the .air/shared and .air/local trees (overview §8.1), generates a stable project_id
UUID stored in .air/shared/project.json (not derived from path; overview §8.1), seeds default rules
when options.default_rules, and emits nothing durable yet (no session DB until a session opens).
open loads the existing ProjectContext (project_id, roots, schema_version).
Delegated helpers (IMPL, code-view §4):
ProjectLocator.locate(start)— upward search.ProjectInitializer.scaffold(root, options)— directory + file creation.
6.2 SessionManager and SessionStore
class SessionManager implements SessionManager (contracts §8)
+open_session(project, options?): Promise<SessionContext>
+close_session(session_id): Promise<void>
class SessionStore // §4.3 aggregate of repositories
open_session flow:
resolve session_id (options or IdGenerator.session_id())
compute db_path = .air/local/sessions/<session-id>/session.db (db-schema header)
DatabaseManager.open(db_path); MigrationRunner.migrate(db)
SessionStore bound to this db
ingest session.created (durable → inserts sessions row)
return SessionContext { session_id, project_id, project_root, db_path, artifact_root }
close_session flushes ui_state (db-schema §1), publishes a terminal session event when archiving,
and releases the DB handle. Session provider/model selection (model_provider_id, model_id) is
captured at open and is immutable for the session (overview §14).
7. Scheduler Subsystem
Module: packages/runtime/src/scheduler/. Classes: Scheduler, TaskGraph, WavePlanner,
RetryPlanner, WorkspaceManager, AgentMonitor (code-view §4). The Scheduler is an orchestration
service, not a coding agent (scheduler-state-machine §intro). All durable state goes through
EventStore; in-memory queues rebuild from SQLite (scheduler-state-machine §1).
7.1 Scheduler
Implements contracts §9.
class Scheduler implements Scheduler
+create_tasks(session_id, specs: TaskSpec[]): Promise<void>
+add_dependency(session_id, task_id, dependency: TaskDependencySpec): Promise<void>
+load_graph(session_id): Promise<TaskGraph>
+run_until_idle(session_id): Promise<SchedulerRunResult>
+cancel_task(task_id, reason): Promise<void>
-plan_wave(graph): SchedulerWavePlan
-dispatch(wave): Promise<void>
-collect_results(): Promise<void>
-state: SchedulerState // §20.2 lifecycle
create_tasks ingests task.created per spec (durable → inserts tasks + dependency rows).
run_until_idle drives the lifecycle in §20.2 until a terminal graph state, returning
SchedulerRunResult (contracts §9). The Scheduler asks the user only through Main Agent /
PermissionEngine (scheduler-state-machine §8); it never prompts directly.
7.2 TaskGraph
class TaskGraph
+session_id: SessionID
+tasks: Map<TaskID, TaskNode>
+dependencies: TaskDependencyRecord[]
+get_runnable_tasks(): TaskNode[] // hard deps satisfied, not conflicting
+mark_terminal(task_id, status): void
+dependents_of(task_id): TaskNode[]
+validate_refs(): OrphanReport // FK-off (§18.3)
get_runnable_tasks honors dependency semantics (scheduler-state-machine §4 PLANNING_WAVE): hard
deps must be completed; soft deps affect priority only; conflict/serialization deps block concurrent
dispatch on overlapping write areas.
7.3 WavePlanner
class WavePlanner
+plan(graph, resources: ResourceSnapshot): SchedulerWavePlan
-serialize_write_conflicts(candidates): {runnable, serialized}
-assign_workspace(task): WorkspacePlan
-assign_model(task): ModelAssignment
Produces SchedulerWavePlan (contracts §9). Rules from scheduler-state-machine §4:
different write areas → concurrent; same area uncertain conflict → serialize; reviewers read-only and
concurrent except against unstable unmerged outputs; debugger serializes on same failure surface;
machine resources cap concurrency (§7 resource-aware). Inferred conflict/serialization edges are
persisted via task_dependencies + durable events so restart need not rediscover them
(scheduler-state-machine §1).
7.4 RetryPlanner
class RetryPlanner
+decide(task: TaskNode, attempts: TaskAttemptRecord[], error: AirError): RetryDecision
RetryDecision (scheduler-state-machine §5):
action ∈ {retry, retry_serial, debug, skip, block, cancel}. Rules: retry only when plausibly
recoverable; later retries change one dimension (model/context/command/serialization/debugger);
identical failure_signature escalates faster; environment impossibility → block, never infinite
retry; architecture/interface mismatch → route to Architecture Designer; fallback skip only when
VerificationPolicy.fallback_allowed and task non-critical. Budget = TaskConstraints.retry_budget.
7.5 WorkspaceManager
class WorkspaceManager
+create_workspace(plan: WorkspacePlan): Promise<WorkspaceRef>
+merge_workspace(workspace_id): Promise<MergeResult>
+cleanup_workspace(workspace_id): Promise<void>
Strategies (db-schema §16, scheduler-state-machine §MERGING): main (no merge), worktree
(git merge/patch), isolated_copy (copy-back/patch). Emits workspace.created,
workspace.merge.started, and a terminal workspace.merge.completed or workspace.merge.conflicted.
GC retention follows overview §15 (active until merge/cancel; merged 7d; abandoned 3d; cleaned keeps
DB row).
Responsibility split — Scheduler vs. WorkspaceManager (overview §10.3, scheduler-state-machine §4/§MERGING):
| Responsibility | Owner |
|---|---|
| Decide whether a task needs an isolated workspace | Scheduler (via WavePlanner.assign_workspace) |
Decide which strategy (main/worktree/isolated_copy) |
Scheduler (via WavePlanner.assign_workspace) |
Encode the decision into a WorkspacePlan |
Scheduler (in SchedulerWavePlan) |
| Materialize the workspace on disk (clone/copy/setup) | WorkspaceManager (create_workspace) |
| Apply merge/patch back to project root | WorkspaceManager (merge_workspace) |
| Detect/report merge conflicts | WorkspaceManager (emits workspace.merge.conflicted) |
| Decide how to resolve a conflict | Scheduler (consumes conflicted event; may re-plan or escalate to Main Agent) |
| Cleanup/GC of abandoned/cleaned workspaces | WorkspaceManager (cleanup_workspace, driven by Scheduler heartbeat / Doctor) |
| Persist workspace lifecycle events | WorkspaceManager (durable events) → EventStore projection updates workspaces row |
WorkspaceManager is a pure executor of Scheduler decisions: it never plans concurrency, never decides which task occupies which workspace, and never re-plans on conflict. The Scheduler is the sole policy owner; WorkspaceManager is the sole mechanism owner.
7.6 AgentMonitor
class AgentMonitor
+record_heartbeat(event): void // coalesced into agents.last_heartbeat_at + tasks.heartbeat_at
+detect_lost_agents(): Promise<AgentLost[]>
+enforce_timeouts(): Promise<void>
Heartbeat coalescing interval default 5s or meaningful status change (runtime-semantics §4).
Missing heartbeat past threshold → inspect process → alive-but-silent → status ping/soft cancel;
gone-without-result → emit agent.lost (scheduler-state-machine §MONITORING). Soft timeout asks for
checkpoint and may extend; hard timeout cancels/kills and marks task failed/interrupted
(scheduler-state-machine §MONITORING timeout table).
8. Worker and IPC Subsystem
Module: packages/runtime/src/workers/. Classes: WorkerManager, WorkerProcess,
WorkerProtocol, and roles under roles/. IPC is NDJSON over stdio (contracts §10, ADR-0005).
8.1 WorkerManager and WorkerProcess
class WorkerManager
+spawn(task_spec: TaskSpec, context_pack: ContextPack): Promise<WorkerProcess>
+cancel(agent_id, reason): Promise<void>
class WorkerProcess
+agent_id: AgentID
+pid?: number
+send(envelope: IpcEnvelope): void
+on_message(handler: (m: IpcEnvelope) => void): void
spawn starts a Bun child process, then performs the handshake (§8.2). WorkerProcess owns the
NDJSON pipe; stdout carries protocol only, stderr is fatal/logging (baselineV1 §8).
Worker exit codes (baselineV1 §8, authoritative):
| Code | Meaning |
|---|---|
| 0 | protocol-level completion (including task failed/blocked via WorkerResult) |
| 1 | uncaught exception |
| 2 | startup/protocol error |
| 3 | permission error |
| 4 | parent cancelled |
| 5 | hard timeout killed |
Design decision: Task success/failure is communicated through WorkerResult.status, not exit
codes. Exit code 0 means the worker completed the IPC protocol correctly and returned a valid
WorkerResult; the actual task outcome (completed/failed/blocked/cancelled) is in the
result payload. Non-zero exit codes indicate process-level or protocol-level failures that prevent
normal result delivery.
8.2 WorkerProtocol and handshake
class WorkerProtocol
+encode(msg: IpcMessage): string // NDJSON line
+decode(line: string): IpcMessage
+validate_direction(msg): void // parent_to_worker vs worker_to_parent
+check_protocol_version(v: number): boolean
Handshake (contracts §10): parent spawns → parent sends agent.start control (with TaskSpec,
ContextPack, AgentRuntimeContext) → worker replies worker.ready { protocol_version, worker_version }
→ parent validates protocol_version; mismatch terminates worker with protocol.error.
Direction typing (contracts §10): parent→worker = control, tool.result, tool.stream;
worker→parent = event, log, tool.call, worker.result, worker.checkpoint, protocol.error.
validate_direction rejects messages on the wrong channel.
8.3 Worker roles and WorkerRuntime
Roles implement WorkerRole<TResult> (contracts §10). The in-worker WorkerRuntime is the only
side-effect surface (contracts §10, §23: workers never write SQLite or touch fs/shell/network except
through parent-mediated tools).
interface WorkerRole<TResult> { run(task_spec, context_pack, runtime): Promise<WorkerResult<TResult>> }
class WorkerRuntime
+emit(event: RuntimeEvent): Promise<void> // → IPC event → parent EventIngestor
+call_tool<I,O>(name, input): Promise<ToolResultEnvelope<O>> // → IPC tool.call
+checkpoint(data): Promise<void> // → IPC worker.checkpoint
Role inventory and constraints (code-view §10):
| Role class | output_contract |
Write access | Required result |
|---|---|---|---|
ExecutorRole |
ExecutorResult |
scoped project writes | ExecutorResult in WorkerResult |
ReviewerRole |
ReviewerResult |
read-only | ReviewerResult |
DebuggerRole |
DebuggerResult |
scoped writes only when assigned | DebuggerResult |
CompactorRole |
CompactorResult |
summaries/artifacts only | CompactorResult |
ExperienceMinerRole |
ExperienceMinerResult |
candidates/rules/skills only when assigned | ExperienceMinerResult |
Each role's run ends by returning a WorkerResult<TResult> with the matching agent_type
(contracts §11). status ∈ {completed, failed, blocked, cancelled}; a code-changing result cannot be
completed unless verification passed or was explicitly skipped with evidence/risk and
fallback_allowed (runtime-semantics §9.4, overview §10.9). Self-escalation returns blocked with a
BlockerReport rather than improvising (scope-escalation §9).
TaskType → WorkerRole mapping (contracts §9 TaskType, db-schema §tasks.type closed enum):
| TaskType | Owning WorkerRole | Worker agent_type |
Output contract | Notes |
|---|---|---|---|---|
execute |
ExecutorRole |
executor |
ExecutorResult |
Code-changing work; scoped project writes |
review |
ReviewerRole |
reviewer |
ReviewerResult |
Read-only audit/review |
debug |
DebuggerRole |
debugger |
DebuggerResult |
Scoped writes when assigned |
compact |
CompactorRole |
compactor |
CompactorResult |
Summary/artifact writes only |
mine_experience |
ExperienceMinerRole |
experience_miner |
ExperienceMinerResult |
Candidate/rule/skill writes when assigned |
docs |
ExecutorRole |
executor |
ExecutorResult |
Documentation-only task; TaskScope.write_area restricted to docs paths (overview §10.9); same events as execute: task.created/started/completed, tool.* for fs writes, artifact.created for doc artifacts |
Design decision — docs task type closure: docs is a formal TaskType (contracts §9,
db-schema §tasks.type). It is not a separate WorkerRole; it reuses ExecutorRole with a
documentation-scoped TaskScope.write_area (typically docs/, AirPlan/docs/, README, ADR/C4
folders) and an empty or docs-only VerificationPolicy.commands. All persistent events for a
docs task are identical to an execute task: task.created → task.started (with
agent_type='executor') → tool.* for file edits → artifact.created for documentation
artifacts → task.completed with ExecutorResult. The tasks.type='docs' field is the only
domain-level differentiator, used by Scheduler for routing/policy (e.g. lighter verification,
no test runs) and by Architecture Designer to detect doc-only changes that may still trigger
the architecture gate when they touch ADR/C4 files.
8.4 Execution discipline enforcement (Executor/Debugger)
Read-before-edit and exact-edit are enforced at the tool layer, not just by prompt (§9.4); the role loop additionally:
- records a read observation before
fs.edit/fs.patch(runtime-semantics §9.1); - keeps changes within
TaskScope.write_area/allowed_paths(overview §10.9 rule 3); - runs
VerificationPolicy.commandsbefore declaringcompleted(contracts §9, §11); - attaches evidence refs for diffs/builds/tests (overview §10.9 rule 6).
9. Tool, Permission, and Capability Subsystem
Module: packages/runtime/src/tools/, security/, capabilities/.
9.1 ToolRegistry
Implements contracts §12.
class ToolRegistry implements ToolRegistry
+register<I,O>(definition: ToolDefinition<I,O>, executor: ToolExecutor<I,O>): void
+register_streaming<I,O>(definition, executor: StreamingToolExecutor<I,O>): void
+call<I,O>(name, input, context: ToolExecutionContext): Promise<ToolResultEnvelope<O>>
+call_streaming<I,O>(name, input, context): AsyncIterable<ToolEvent | ToolResultEnvelope<O>>
+list(): ToolDefinition[]
-validate_input(def, input): void // JsonSchema check
call algorithm (contracts §12, §23 "tool → side effect without PermissionEngine" forbidden):
look up definition+executor (else AirError tool_error)
validate input against input_schema
build PermissionRequestContext from context + definition.permissions + input paths/command
decision = PermissionEngine.evaluate(request)
branch on decision.action (§9.3)
emit tool.started (durable → tool_runs running)
run executor.execute(input, context) // streaming consumed internally for call()
emit tool.completed | tool.failed | tool.cancelled
return ToolResultEnvelope
call_streaming exposes ToolEvent progress and ends with exactly one final ToolResultEnvelope
(contracts §12 streaming rule). BuiltInToolRegistrar registers fs/shell/git/project/artifact/
context/permission/doctor tools (code-view §4); each tool declares category, permissions, and
streaming (contracts §12).
9.2 PermissionEngine
Implements contracts §13.
class PermissionEngine implements PermissionEngine
+evaluate(context: PermissionRequestContext): Promise<PermissionDecision>
+record(decision, context): Promise<PermissionRecordResult>
class PathClassifier
+classify(path, project_root): PathRiskClassification // 8 categories, realpath-normalized
class CommandRiskAnalyzer
+analyze(command, cwd): CommandRiskAnalysis // 10 categories
class SecretRedactor
+redact(text): string // for logs/evidence
evaluate applies the frozen layered order (contracts §13, runtime-semantics §8, overview §12):
1 tool capability declaration
2 permission profile (permission_template)
3 TaskSpec scope allowed/denied paths
4 path/command/network risk classification (PathClassifier + CommandRiskAnalyzer)
5 credential/system-sensitive override
6 user prompt workflow if required
Key invariants: project-level allow never overrides task scope; credential/system-sensitive overrides
broad allows (runtime-semantics §8, overview §12 rules 8-9); paths normalized via realpath before
prefix checks (overview §12 rule 1); .git/ internals protected (rule 2). record writes a
permission.decision.recorded durable event and returns PermissionRecordResult; on write failure it
returns {ok:false, error} (contracts §13).
9.3 PermissionDecision branching
ToolRegistry.call branches on PermissionDecision.action (contracts §13, scope-escalation §7):
| action | ToolRegistry behavior |
|---|---|
allow |
execute; create backup first if backup_required |
announce_then_run |
emit visible notice, then execute unless interrupted; bounded by grant_scope |
ask_user |
suspend; emit permission.prompt.requested; resume on permission.prompt.resolved |
deny |
do not execute; return ToolResultEnvelope{status:"error"}; caller may pick safe path |
block |
return blocked outcome → task.blocked upstream |
refuse |
return AirError{kind:"policy_error"}; no execution |
9.4 Filesystem tool execution rules
fs.edit/fs.patch enforce read-before-edit (runtime-semantics §9.1-§9.3, overview §10.9):
- require an active task read observation for the target file or explicit
expected_existing_sha256; old_stringexact match; non-unique match fails unlessreplace_all; no indentation guessing;- patch paths must be within scope; rejected hunks become artifacts; partial apply only if unchanged rejected paths are provably untouched, else atomic fail;
- successful edit/patch emits a diff artifact (→
artifact.created).
9.5 CapabilityRegistry
Implements contracts §18; lifecycle/trust from capability-trust-v1.
class CapabilityRegistry implements CapabilityRegistry
+discover(): Promise<CapabilityManifestV1[]>
+validate(manifest): Promise<ValidationResult>
+enable(capability_id): Promise<void>
+disable(capability_id): Promise<void>
+register_tools(tool_registry: ToolRegistry): Promise<void>
class CapabilityManifestValidator
+validate(manifest): ValidationResult // schema_version=1, tool schemas, permissions
Lifecycle (overview §6, capability-trust §7): discovered → validated → doctor_checked → enabled → registered → active → disabled|failed|updated. Trust levels (contracts §18): built_in, project_local, user_installed, verified_publisher, untrusted. Trust affects default enablement/prompt posture but never bypasses ToolRegistry or PermissionEngine (overview §6). Dependency installs go only through Doctor (contracts §23 "capability → dependency install outside Doctor" forbidden).
10. Context, Prompt, and Compaction Subsystem
Module: packages/runtime/src/context/. Classes: ContextAssembler, PromptLayerLoader,
CompactionPolicy (contracts §16).
10.1 ContextAssembler
class ContextAssembler implements ContextAssembler
+assemble(input: ContextAssembleInput): Promise<AssembledContext>
-load_layers(purpose): Promise<PromptLayer[]>
-fit_budget(layers, budget): BudgetFitResult
assemble builds an Anthropic-canonical context (AssembledContext.canonical_format="anthropic",
contracts §16). It loads ordered layers (§10.2), fits them to token_budget via fit_budget, and
reports omissions. When the context is too large for inline return it writes a messages artifact and
sets messages_artifact_id; ContextPack.assembled_context_ref points to it (contracts §16). If the
budget cannot fit required layers it sets compaction_requested=true (consumed by Scheduler, §10.3).
10.2 PromptLayerLoader and layer order
class PromptLayerLoader implements PromptLayerLoader
+load_runtime_invariant(): PromptLayer
+load_role(role: AgentType): PromptLayer
+load_project_rules(project: ProjectContext): PromptLayer[]
+load_task_context(spec: TaskSpec, refs: TaskContextRefs): PromptLayer[]
PromptLayer.level is the frozen PromptLayerLevel union (contracts §16), ordered L0-L9 per
prompt-layering-v1 §2 and overview §13:
| Level enum (contracts) | L# (prompt-layering) | Loader method / source |
|---|---|---|
runtime_invariant |
L0 Runtime invariant | load_runtime_invariant() |
role |
L1 Role / agent mode | load_role(role) |
safety |
L2 Safety and permission policy | ContextAssembler internal (see below) |
project_rules |
L3 Project rules and user preferences | load_project_rules(project) |
architecture |
L4 Architecture baseline and current plan | ContextAssembler internal (see below) |
task_spec |
L5 Task specification and acceptance criteria | load_task_context(spec, refs) |
evidence |
L6 Relevant code / artifacts / evidence | ContextAssembler internal (see below) |
conversation |
L7 Recent conversation and decision context | ContextAssembler internal (see below) |
tool_output |
L8 Tool result history / diagnostics | ContextAssembler internal (see below) |
user_override |
L9 Immediate instruction | ContextAssembler internal (see below) |
system_debug |
(system debug directive, applied within L9 when present) | ContextAssembler internal |
Design decision — Layer loading responsibility:
PromptLayerLoader interface (contracts §16) provides 4 methods for layers that require external
configuration or resource loading (L0, L1, L3, L5). The remaining layers are assembled by
ContextAssembler.load_layers() internally:
| Layer | Source | Assembled by |
|---|---|---|
| L2 Safety | Active permission profile (~/.air/permissions.yaml + project permission config; same source the PermissionEngine implementation reads internally — no new contract method) |
ContextAssembler |
| L4 Architecture | TaskSpec.context_refs.arc_ref → load from plan/ADR/C4 docs |
ContextAssembler |
| L6 Evidence | TaskSpec.context_refs.artifacts + EvidenceStore.list_for_task() |
ContextAssembler |
| L7 Conversation | SessionStore.messages.list_by_session() (recent N messages) |
ContextAssembler |
| L8 Tool output | SessionStore.tool_runs + command_runs for current task |
ContextAssembler |
| L9 Immediate | TaskSpec.description + acceptance_criteria + immediate user instruction |
ContextAssembler |
This design keeps PromptLayerLoader focused on resource-backed layers while ContextAssembler
owns the assembly logic for session/task-specific layers.
Design decision — Runtime roles (main/architecture/scheduler):
AgentType (contracts §5) only covers worker roles (executor/reviewer/debugger/compactor/
experience_miner). Runtime roles (main/architecture/scheduler) are not child processes and
do not use PromptLayerLoader.load_role(). Instead:
MainAgentloads its L1 role prompt from built-in resources directlyArchitectureDesignerloads its L1 role prompt from built-in resources directlySchedulerdoes not use LLM prompts (it is a pure orchestration service)
Higher-priority layers win on budget pressure; immutable=true layers (L0, L1, L2) are never dropped
(prompt-layering L0 Mutable: no).
10.3 CompactionPolicy
class CompactionPolicy implements CompactionPolicy
+should_compact(messages, token_budget): boolean
+compact(messages, target_tokens): Promise<CompactionResult>
ContextAssembler may request compaction but does not compact itself; Scheduler creates a compact
task that runs CompactorRole (overview §13, runtime-semantics §7). Sequence:
context.compaction.requested → context.compaction.started → summary.created → context.compaction.completed.
Only summary.created inserts the summaries row (runtime-semantics §7); the completion event
references the summary_id and never duplicates the row. Original messages are preserved for
backtracking.
11. Artifact, Evidence, and Knowledge Subsystem
Module: packages/runtime/src/artifacts/ and knowledge/.
11.1 ArtifactStore
Implements contracts §14; naming from artifact-naming-v1.
class ArtifactStore implements ArtifactStore
+create(input: ArtifactCreateInput, context: ArtifactContext): Promise<ArtifactRef>
+get(artifact_id): Promise<ArtifactRef | undefined>
+read(artifact_id): Promise<ArtifactReadResult>
-write_temp_then_rename(bytes): {path, sha256, size}
create algorithm (runtime-semantics §6.2, overview §8.4):
write temp file → compute sha256 + size → atomic rename to artifact path
artifact_id = art_<ulid>
uri = artifact://project/<project-id>/session/<session-id>/<artifact-id>
filename = <timestamp>-<artifact-id>-<slug><extension>
ingest artifact.created (durable → inserts artifacts row)
If the DB insert fails after rename, startup recovery scans orphaned files and registers or
quarantines them (runtime-semantics §6.2). Artifact type is from the closed set (db-schema §21:
log, diff, screenshot, pcap, report, diagnostic, bundle, other).
11.2 EvidenceStore
class EvidenceStore implements EvidenceStore
+create(input: EvidenceCreateInput): Promise<EvidenceRef>
+list_for_entity(entity_type, entity_id): Promise<EvidenceRef[]>
Emits evidence.created (durable → evidence_refs). Worker results embed full EvidenceRef[] when
evidence is part of the conclusion; lightweight payloads carry evidence_ref_ids (contracts §14).
kind is from the closed set (db-schema §21: build_output, test_output, log, screenshot, diff,
metric, other).
11.3 DebugKnowledgeStore and LearnedMemoryStore
Project-level DBs (db-schema §20), implement contracts §20.
class DebugKnowledgeStore implements DebugKnowledgeStore // debug-records.db
+insert(record: DebugRecord): Promise<void>
+lookup_by_signature(failure_signature): Promise<DebugRecord[]>
+lookup_by_task(task_id): Promise<DebugRecord[]>
+update(debug_record_id, patch): Promise<void>
class LearnedMemoryStore implements LearnedMemoryStore // learned-memory.db
+insert(memory: LearnedMemory): Promise<void>
+lookup_by_type(memory_type): Promise<LearnedMemory[]>
+update_status(memory_id, status): Promise<void>
+scan_stale(): Promise<LearnedMemory[]>
Writes to these DBs are cross-store side effects: the session DB records intent/completion events and
the owning store performs the external write (outbox model, §18.4; runtime-semantics §6.3-§6.4).
debug.record.created and memory.promoted are the session-side durable markers.
12. Provider (LLM) Subsystem
Module: packages/llm/src/. Classes per code-view §5. packages/llm owns adapters, model config,
conversion, and ProviderManager; runtime calls it only through the facade (code-view §2 rule 5).
12.1 ProviderManager
Implements contracts §15.
class ProviderManager implements ProviderManager
+load_config(): Promise<void>
+select_model(requirement: ModelRequirement): Promise<ModelAssignment>
+complete(input: ProviderCompletionInput): AsyncIterable<ProviderStreamEvent>
-adapter_for(provider_id): ProviderAdapter
select_model matches a ModelRequirement against the capability matrix and returns a
ModelAssignment (contracts §9/§15). complete routes to the right adapter and yields normalized
ProviderStreamEvents. Runtime/session provider+model are fixed for the session; there is no runtime
switching API (overview §14).
12.2 ProviderAdapter implementations
interface ProviderAdapter (contracts §15)
provider_id; list_models(); validate_model(model_id); complete(input); count_tokens?(input)
class AnthropicAdapter implements ProviderAdapter
class OpenAICompatibleAdapter implements ProviderAdapter
-converter: AnthropicCanonicalConverter
class AnthropicCanonicalConverter // canonical ↔ provider format
class ToolUseConverter
class StreamNormalizer // provider stream → ProviderStreamEvent
Adapters convert external formats to/from the Anthropic canonical internal format and must not
silently drop semantic prompt/tool information (contracts §23 "provider adapter → silent semantic
prompt loss" forbidden; overview §14). ModelConfigLoader loads global ~/.air/models.yaml and
project config; CapabilityMatrixRegistry holds ProviderCapabilityMatrix rows
(provider-capability-matrix-v1). LLM output is never allowed to perform direct file/shell side effects
(contracts §23).
13. Projection and TUI Subsystem
Modules: packages/runtime/src/projection/ (ProjectionStore) and packages/tui/src/.
13.1 ProjectionStore
Implements contracts §17.
class ProjectionStore implements ProjectionStore
+hydrate(session_id): Promise<void>
+apply(event: RuntimeEvent): void
+snapshot(): ProjectionSnapshot
+subscribe(handler): Subscription
-projections: { session, tasks, agents, tool_runs, command_runs, artifacts, permission_prompts, blockers }
hydrate rebuilds from DB via repositories (code-view §4: ProjectionStore → SessionStore). apply
handles all durable events plus key ephemeral events (agent.heartbeat, task.progress,
assistant.message.delta, tool.progress, command.stdout.delta, command.stderr.delta); unknown
event types are ignored (contracts §17 comment). command_runs projection status uses the derivation
in §4.4. ProjectionStore is never a scheduling/recovery source of truth (overview §9.3).
13.2 TUI
Module packages/tui/src/ (code-view §7). TuiApp consumes a ProjectionClient and renders
components; it imports only packages/contracts (contracts §17, §23; code-view §2 rule 3).
class TuiApp
+start(): void
+stop(): void
class ProjectionClient implements ProjectionClient
+snapshot(): ProjectionSnapshot
+subscribe(handler): Subscription
components: SessionView, TaskListView, AgentStatusView, ToolRunView, DiffView, EvidenceView,
PermissionPrompt, BlockerReport, HudView
Rules (code-view §7): components render projections only; permission prompts emit user decisions
through the narrow UiCommandChannel (contracts §17), never private runtime services; UI never
mutates domain tables; diff/evidence views link back to artifact/evidence refs. V1 transport is
in-process (contracts §17): ProjectionClient is a direct interface reference, not IPC. HUD presets
Full/Essential/Minimal and permission announce_then_run visualization per overview §14.
14. Agents Subsystem
Module: packages/runtime/src/agents/main/ and architecture/. These run inside the runtime process
(overview §7). Main Agent stays idle-ready; background work is dispatched to workers via Scheduler
(main-agent-state-machine §Idle Principle).
14.1 MainAgent
class MainAgent
+handle_user_message(message): Promise<void>
+present_progress(): Promise<void>
+present_blocker(blocker: BlockerReport): Promise<void>
-classify_intent(message): "chat" | "task" | "direct"
-state: MainAgentState // §20.1
Lifecycle is the frozen state machine (main-agent-state-machine.md, §20.1 here):
IDLE → CLASSIFYING → ANSWERING | DELEGATING | DIRECT_MODE; DELEGATING → SCHEDULING |
ARCHITECTURE_DESIGNING → CONFIRMING → EXECUTING → (INTERRUPTING | ARCHITECTURE_REVISING) →
SUMMARIZING → IDLE. Direct mode uses permission_template="main_direct" and writes only to the main
workspace (contracts §10, runtime-semantics §16). Requirement changes emit requirement.changed
(main-agent-state-machine events table). Confirmation gating: implementation-only silent → EXECUTING;
architecture-level → Architecture Designer assessment → low-permission user confirm / high-permission
auto-proceed (main-agent-state-machine §Confirmation Gating).
14.2 ArchitectureDesigner
class ArchitectureDesigner
+assess_impact(change): Promise<ArchitectureImpact>
+update_architecture_docs(update): Promise<DocumentUpdate>
Emits architecture.impact.completed and architecture.plan.updated (event-registry §3, overview
§10.7). It owns the architecture review gate: interface/schema/event/package-boundary/security/
runtime-semantics/ADR/C4/plan consistency; it does not replace Reviewer (overview §10.7). Gate trigger
conditions and result rules (silent_continue / requires_user_confirmation / requires_replan /
reject_or_escalate) follow scope-escalation §4 and overview §10.7. It calls the LLM only through the
ProviderManager facade (code-view §4) and routes escalations through Main Agent.
15. Toolchain C++ Subsystem
Module: packages/toolchain-cpp/src/ (code-view §6). Exposes cpp.* tools through capability
registration, not direct runtime coupling (code-view §2 rule 4).
class CppToolRegistrar { +register(tool_registry): void }
class CppProjectDetector { +detect(project_root): Promise<CppDetectOutput> }
class CMakeConfigurator { +configure(input): Promise<CppCmakeConfigureOutput> }
class CppBuilder { +build(input): Promise<CppBuildOutput> }
class CppTestRunner { +run_tests(input): Promise<CppTestOutput> }
class CppcheckRunner { +run(input): Promise<CppcheckOutput> }
class ClangdClient { +query(input): Promise<ClangdQueryOutput> }
class DiagnosticParser {
+parse_compiler_output(output): Diagnostic[]
+semantic_signature(diagnostic): string
}
Workflow (overview §10.8): detect → configure (CMake+Ninja preferred, Make fallback) → build → parse
diagnostics → test → cppcheck → clangd query when needed → Debugger on failure → scoped fix → Reviewer
→ architecture gate if contracts/schema/events/boundaries changed. DiagnosticParser performs
deterministic extraction and semantic_signature only; LLM interpretation lives in runtime
Debugger/Reviewer, never inside toolchain-cpp (runtime-semantics §12). Diagnostic matches
contracts §21; compile_commands.json is generated/located when clangd/static analysis needs it
(overview §10.8).
16. Doctor, Logging, Migration, and Recovery Subsystem
Module: packages/runtime/src/doctor/, logging/, plus MigrationRunner (§4.2) and recovery in
SessionManager/Scheduler.
16.1 DoctorService
Implements contracts §19.
class DoctorService implements DoctorService
+run(input: DoctorRunInput): Promise<DoctorRunOutput>
+check_capability(capability): Promise<DoctorIssue[]>
-self_bootstrap(): DoctorIssue[] // Bun, SQLite, shell, .air writability
Self-bootstrap before any capability check (runtime-semantics §18, overview §15): verify Bun runtime,
SQLite, basic shell, .air/ writability; on failure report a blocking issue and skip remaining
checks. Modes (contracts §19): read_only, fix (under PermissionEngine), and bundle export (local
artifact, no auto-upload, overview §15). Emits doctor.* events (event-registry §3).
16.2 Logging
class Logger implements Logger { debug/info/warn/error(message, data?) }
class DeveloperLogEncryptor implements DeveloperLogEncryptor { encrypt_log_chunk(chunk): Promise<Uint8Array> }
class SecretRedactor { redact(text): string } // shared with PermissionEngine §9.2
air.log is user-facing with redacted operational errors; air.developer.log is encrypted and more
detailed (overview §15, baselineV1 §23). All logs redact secrets/auth refs/provider keys via
SecretRedactor. Failures link log artifacts through evidence refs rather than copying sensitive
content into user summaries.
16.3 Recovery
On startup/resume (scheduler-state-machine §9, overview §15, runtime-semantics §6.2/§14):
- load
taskswith statusrunning/interrupted, activeagents/workspaces; - check process liveness by PID; reconnect IPC if alive, else emit
agent.lost; - mark task failed/interrupted by resumability;
- preserve workspaces until merge/cleanup decision;
- orphan-artifact scan registers or quarantines files;
- FK-off orphan scan (§18.3) logs and re-parents/archives dangling references;
- workspace GC applies retention (overview §15);
- rebuild scheduler queue from pending/failed-with-retry tasks.
17. CLI Subsystem
Module: packages/cli/src/ (code-view §8). CliEntrypoint.main(argv) routes to command classes; a
RuntimeFactory builds the RuntimeApp.
class CliEntrypoint { +main(argv): Promise<number> }
class RuntimeFactory { +create(options): Promise<RuntimeApp> }
Command class inventory (frozen mapping, code-view §8, overview §14):
| Command class | Subcommand(s) | Notes |
|---|---|---|
RunCommand |
run [project] |
Default entry; spawns RuntimeApp + TUI |
InitCommand |
init |
First-run wizard, project bootstrap |
DoctorCommand |
`doctor [--fix | --bundle]` |
ProviderCommand |
provider list, provider current |
Read-only; no runtime model switch |
E2ECommand |
e2e |
Local end-to-end harness, no CI hooks |
ReleaseCommand |
release |
Build/package release artifacts |
ResumeCommand |
resume [--session SID] |
Catalog: resume last/specific session |
CompactCommand |
compact [--session SID] |
Catalog: trigger compaction task |
HistoryCommand |
history [--session SID] |
Catalog: render past sessions/events |
SessionListCommand |
session list |
Catalog: enumerate sessions |
RestoreCommand |
`restore <file | time |
ProviderCommand is read-only (provider list/current); there is no runtime provider/model switch
command (overview §14). RestoreCommand supports file/time/session granularities over the git-backed
backup repo (overview §15, runtime-semantics §19). All CLI command classes route side effects through
RuntimeApp services (Scheduler, SessionManager, DoctorService, ArtifactStore) and never bypass
ToolRegistry or PermissionEngine.
18. Cross-Cutting Designs
18.1 AirError construction
All failures use AirError (contracts §3, error-taxonomy-v1). Construction helper:
function make_air_error(opts: {
kind: ErrorKind,
severity?: ErrorSeverity, // default "error"
message: string,
detail?: string,
retryability?: Retryability, // default "unknown"
semantic_signature?: string, // default derived from kind+message hash
cause_ref?: EntityRef,
cause_refs?: EntityRef[],
user_action?: string,
metadata?: JsonObject
}): AirError
semantic_signature is the stable grouping key for repeated failure detection, debug knowledge
lookup, and Scheduler retry/debug/escalation routing (error-taxonomy §3, overview §9.2). When not
provided, derive from kind + normalized_message_hash. Scheduler decisions use kind, retryability,
severity, task scope, permission result, architecture impact, verification evidence, and repetition
count (overview §9.2).
18.2 Transaction discipline
All durable event + domain update pairs run inside one DatabaseManager.transaction call
(runtime-semantics §3, db-schema §1). Pseudo-code:
await db.transaction(async tx => {
await event_repo.insert(to_record(event), tx)
await domain_repo.insert_or_update(domain_row, tx)
})
event_bus.publish(event) // AFTER commit
Artifact file writes use temp → sha256/size → atomic rename → DB record (runtime-semantics §6.2). If the DB insert fails after rename, recovery scans orphaned files (§16.3).
18.3 FK-off invariants
foreign_keys = OFF (db-schema §1) is compensated by application-level checks (runtime-semantics §5,
overview §8.2). The 8 invariants:
tasks.session_id→ existingsessions.idtask_attempts.task_id→ existingtasks.idagents.task_id→ existingtasks.idwhen not nulltool_runs.task_id,tool_runs.agent_id→ existing rows when not nullcommand_runs.task_id,command_runs.agent_id,command_runs.tool_run_id→ existing rowsworkspaces.task_id,workspaces.agent_id→ existing rows when not nulldiagnostics.command_run_id,diagnostics.artifact_id→ existing rows when not nullevidence_refsforeign columns → existing rows when not null
SessionStore.referential_check() (§4.3) runs at startup and periodically; violations are logged to
developer log and either re-parented or archived (runtime-semantics §5).
18.4 Outbox / compensation for cross-DB writes
Writes to project-level DBs (debug-records.db, learned-memory.db) or external files follow the
outbox model (runtime-semantics §6.3-§6.4, overview §8.3). The general pattern is:
1. Owning service (LearnedMemoryStore, DebugKnowledgeStore, ...) performs the external
DB/file write through its own transaction. Each owning store is single-writer.
2. On success: the owning service ingests a single durable session event whose payload is
already the completion form fixed by event-registry §3 (e.g. memory.promoted with
target_ref populated, debug.record.created with debug_record_id populated). The
EventStore commit makes the cross-DB pair eventually consistent.
3. On external failure: the owning service does NOT emit the success event. It either
emits a task.failed carrying the AirError (so Scheduler/Doctor can decide), or queues
a fresh upstream candidate (e.g. memory.candidate.created) for retry.
4. On restart: recovery cross-checks session-side "upstream" rows (memory.candidate.created
without a matching memory.promoted, or pending debug-task tasks) against external store
state. Stale candidates are re-queued; orphan external rows are surfaced through Doctor.
No event payload is extended with intent/commit phase markers; the existing payload
schemas in event-registry-v1.md §3 are honored as-is. Any future change to add an
intent-phase event would require an ADR plus a new event type or payload version bump
(event-registry §2 rule 7).
memory.promoted outbox semantics (event-registry §3 MemoryPromotedPayload, runtime-semantics §6.4):
Per event-registry-v1.md §3, memory.promoted payload is fixed to:
{ candidate_id, target_ref, promoted_by, summary } — there is no phase field
and no separate intent/completion variants. Per runtime-semantics-v1.md §6.4,
memory.promoted records the completed promotion (target_ref already populated).
The outbox sequence is therefore:
| Step | Event / action | DB |
|---|---|---|
| 1 | memory.candidate.created (durable, already in registry) |
session DB |
| 2 | Owning service (ExperienceMiner / curator) approves the candidate and writes the external store (learned-memory.db row, .air/shared/rules file, or skill file) |
external DB / file |
| 3 | On success, emit memory.promoted with target_ref pointing at the just-written external row/file |
session DB |
| 4 | On write failure: do not emit memory.promoted. Either re-queue via a fresh memory.candidate.created, or emit a task.failed carrying the AirError so Scheduler / Doctor can surface it to the user |
|
| 5 | On restart: recovery uses session DB candidates without matching promotion events to detect work in flight (runtime-semantics §6.4) |
memory.archived likewise records a completed archival (no phase field; payload
is fixed by event-registry §3 MemoryArchivedPayload).
debug.record.created outbox semantics (event-registry §3 DebugRecordCreatedPayload, runtime-semantics §6.3):
The payload is a single fixed schema with debug_record_id already assigned by
the owning DebugKnowledgeStore. Per runtime-semantics §6.3 the flow is:
- Owning
DebugKnowledgeStoreperforms thedebug-records.dbinsert/update. - On success, emit
debug.record.createdreferencing the newdebug_record_id. - On
debug-records.dbwrite failure: emit atask.failed(or futuredebug.record.failed, gated by ADR) carrying theAirError; do not emitdebug.record.created.
No phase payload field is introduced in either event. Any future intent/commit
split must go through an ADR + payload version bump (event-registry §2 rule 7).
18.5 Security invariants
From contracts §23, security-model-v1, overview §12:
- LLM output is untrusted until validated by runtime/tool schemas and PermissionEngine.
- Provider output cannot directly modify files or run commands.
- Credentials are referenced by
auth_ref, never copied into events/artifacts. - No automatic upload of source, logs, screenshots, bundles, pcaps, or artifacts.
- Destructive/system-sensitive actions require confirmation or policy block.
- Path policy uses realpath normalization; symlink escapes are not allowed by string-prefix checks.
.git/internals are protected from arbitrary write tools.sudorisk is determined by command intent/target/system sensitivity, not string alone.
19. Sequence Designs
19.1 User request → task execution → completion
User
│ user.message.created
▼
MainAgent (CLASSIFYING)
│ classify_intent → "task"
▼
MainAgent (DELEGATING)
│ architecture impact? → ArchitectureDesigner.assess_impact
│ architecture.impact.completed
▼
MainAgent (SCHEDULING)
│ Scheduler.create_tasks(specs)
│ task.created (durable)
▼
Scheduler (PLANNING_WAVE)
│ WavePlanner.plan → SchedulerWavePlan
▼
Scheduler (DISPATCHING)
│ WorkspaceManager.create_workspace
│ workspace.created (durable)
│ ContextAssembler.assemble → ContextPack
│ WorkerManager.spawn
│ agent.started (durable)
│ task.started (durable)
▼
WorkerProcess (ExecutorRole)
│ WorkerRuntime.call_tool("fs.read", ...)
│ → IPC tool.call → parent ToolRegistry.call
│ → PermissionEngine.evaluate → allow
│ → tool.started (durable)
│ → execute → tool.completed (durable)
│ → IPC tool.result
│ WorkerRuntime.call_tool("fs.edit", ...)
│ → (same flow, read-before-edit enforced)
│ WorkerRuntime.call_tool("shell.run", verification)
│ → command.started, command.completed (durable)
│ return WorkerResult { status: "completed", ... }
▼
Scheduler (COLLECTING_RESULTS)
│ validate WorkerResult
│ task.completed (durable)
│ agent.completed (durable)
▼
Scheduler (MERGING)
│ WorkspaceManager.merge_workspace
│ workspace.merge.completed (durable)
▼
Scheduler (REVIEWING_WAVE) [if required]
│ create review task → ReviewerRole
│ ReviewerResult { verdict: "approved" }
▼
Scheduler (COMPLETED)
│ SchedulerRunResult { status: "completed" }
▼
MainAgent (SUMMARIZING)
│ present result to user
│ trigger ExperienceMiner task (optional)
▼
MainAgent (IDLE)
19.2 Tool call with permission prompt
WorkerRuntime.call_tool("shell.run", { command: "rm -rf build/" })
│
▼
ToolRegistry.call
│ PermissionEngine.evaluate
│ → PathClassifier.classify → project_build_output
│ → CommandRiskAnalyzer.analyze → destructive
│ → decision: ask_user (risk: medium)
▼
ToolRegistry suspends
│ permission.prompt.requested (durable)
▼
ProjectionStore.apply → PermissionPromptProjection
│
▼
TUI PermissionPrompt renders
│ user selects "Allow once"
▼
UiCommandChannel emits permission response
│ permission.prompt.resolved (durable)
│ PermissionEngine.record
▼
ToolRegistry resumes
│ tool.started (durable)
│ execute shell command
│ command.started, command.completed (durable)
│ tool.completed (durable)
▼
IPC tool.result → WorkerRuntime
19.3 Compaction flow
ContextAssembler.assemble
│ fit_budget fails → compaction_requested = true
▼
Scheduler receives compaction request
│ create_tasks([{ type: "compact", ... }])
│ task.created (durable)
│ context.compaction.requested (durable)
▼
Scheduler dispatches CompactorRole
│ task.started, agent.started (durable)
│ context.compaction.started (durable)
▼
CompactorRole
│ snapshot immutable message range
│ generate summary via LLM
│ ArtifactStore.create(summary artifact)
│ artifact.created (durable)
│ return CompactorResult { summary_id }
▼
Scheduler collects result
│ summary.created (durable) ← inserts summaries row
│ context.compaction.completed (durable)
│ task.completed (durable)
▼
Original messages preserved for backtracking
19.4 Architecture Designer review gate
Triggered when a Worker result, plan change, or detected drift touches contracts, schema, events, package boundaries, security, or runtime semantics (scope-escalation §4, overview §10.7).
Trigger source (Worker/Scheduler/Main Agent)
│ detects: contract/schema/event change | boundary cross | ADR/C4 drift
▼
Main Agent.dispatch_architecture_review(change)
│ build ImpactRequest { change, affected_refs, evidence_refs }
▼
ArchitectureDesigner.assess_impact
│ load L4 architecture pack (plan/ADR/C4) via ContextAssembler
│ ProviderManager.complete (read-only analysis, no fs/shell writes)
│ classify result:
│ silent_continue → no event mutation, log advisory
│ requires_user_confirmation → architecture.impact.completed (durable)
│ + permission.prompt.requested via Main Agent
│ requires_replan → architecture.impact.completed (durable)
│ + Scheduler replan trigger
│ reject_or_escalate → architecture.impact.completed (durable)
│ + escalate to user / block tasks
▼
ArchitectureDesigner.update_architecture_docs (only if confirmed)
│ DocumentUpdate applied via tool layer (ToolRegistry + PermissionEngine)
│ architecture.plan.updated (durable) + artifact refs to plan/ADR/C4
▼
Scheduler consumes architecture.impact.completed
│ mark impacted tasks via requirement.changed propagation
│ wave replan or task cancellation per result class
▼
Main Agent resumes; downstream tasks proceed with refreshed L4 context
Notes:
- The gate never replaces Reviewer (overview §10.7); it adds an architecture-level check.
- ArchitectureDesigner has no direct fs/shell access; all writes go through
ToolRegistryunderPermissionEngine(contracts §13, §23 rule "tools → side effect only via PermissionEngine"). - Result class strings come from scope-escalation §4 (
silent_continue,requires_user_confirmation,requires_replan,reject_or_escalate). - All gate decisions land as durable
architecture.impact.completed; subsequent doc edits land asarchitecture.plan.updated(event-registry §3).
19.5 Debug knowledge capture
ExecutorRole fails with build error
│ WorkerResult { status: "failed", error: AirError }
▼
Scheduler (REPAIRING_OR_CONTINUING)
│ RetryPlanner.decide → "debug"
│ create debug task
▼
DebuggerRole
│ analyze failure evidence
│ lookup DebugKnowledgeStore.lookup_by_signature
│ (no match) → diagnose root cause
│ fix or return blocker
│ return DebuggerResult { diagnosis, root_cause, fixed: true }
▼
Scheduler
│ task.completed (durable)
│ debug.record.created (durable) ← session event
│ DebugKnowledgeStore.insert (outbox step 3)
▼
Future similar failure
│ DebugKnowledgeStore.lookup_by_signature → hit
│ DebuggerRole applies known fix faster
20. State Machine Designs
20.1 Main Agent State Machine
From main-agent-state-machine.md. States and transitions:
┌─────────────────────────────────────────────────────────────────┐
│ IDLE │
│ Waiting for user input │
└───────────────────────────┬─────────────────────────────────────┘
│ user.message.created
▼
┌─────────────────────────────────────────────────────────────────┐
│ CLASSIFYING │
│ LLM classifies intent │
└────────┬──────────────────┬──────────────────┬──────────────────┘
│ chat/Q&A │ task request │ /direct
▼ ▼ ▼
┌─────────┐ ┌─────────────┐ ┌─────────────┐
│ANSWERING│ │ DELEGATING │ │ DIRECT_MODE │
│→ IDLE │ └──┬──────┬───┘ │ /done→IDLE │
└─────────┘ │ │ └─────────────┘
simple│ needs│plan
▼ ▼
┌──────────┐ ┌─────────────────────┐
│SCHEDULING│ │ARCHITECTURE_DESIGNING│
│→ EXECUTING│ │→ CONFIRMING │
└──────────┘ └─────────────────────┘
│
▼
┌─────────────┐
│ CONFIRMING │
│ user confirm│
└──┬──────┬───┘
confirm │ │ reject → IDLE
▼
┌─────────────┐
│ EXECUTING │
│ Scheduler │
└──┬──────┬───┘
requirement │ │ all done
change ▼ │
┌─────────────┐│
│INTERRUPTING ││
└──┬──────┬───┘│
exec-only│ design │
▼ ▼ │
resume ┌──────────┐ │
EXECUTING│ARCH_REV │ │
│→CONFIRM │ │
└─────────┘ │
▼
┌─────────────┐
│ SUMMARIZING │
│ → IDLE │
└─────────────┘
State-to-permission_template (main-agent-state-machine §State-to-AgentRuntimeContext):
| State | permission_template |
|---|---|
| IDLE, CLASSIFYING, ANSWERING, CONFIRMING, SUMMARIZING | N/A |
| DIRECT_MODE | main_direct |
| EXECUTING | per-task (Scheduler assigns) |
| INTERRUPTING, ARCHITECTURE_REVISING | N/A (delegation) |
20.2 Scheduler State Machine
From scheduler-state-machine-v1.md §4. States:
IDLE
│ task.created / execution request
▼
LOADING_GRAPH
│ reconstruct TaskGraph from DB
│ detect orphaned agents → agent.lost
│ validate dependencies
├─ valid runnable → PLANNING_WAVE
├─ no runnable + blocked → BLOCKED
├─ all terminal success → COMPLETED
└─ invalid beyond repair → BLOCKED
PLANNING_WAVE
│ WavePlanner.plan
├─ runnable wave → DISPATCHING
└─ no runnable → BLOCKED
DISPATCHING
│ create workspaces
│ assemble context
│ spawn workers
│ emit task.started, agent.started
└─ → MONITORING
MONITORING
│ track heartbeats, progress, timeouts
│ handle requirement.changed
├─ all agents terminal → COLLECTING_RESULTS
├─ user/global cancel → CANCELLED
└─ architecture/user blocker → BLOCKED
COLLECTING_RESULTS
│ validate WorkerResult
│ persist artifacts/evidence
│ classify task terminal status
├─ workspaces need merge → MERGING
├─ review required → REVIEWING_WAVE
├─ more work → REPAIRING_OR_CONTINUING
└─ all done → COMPLETED
MERGING
│ WorkspaceManager.merge_workspace
├─ success + review → REVIEWING_WAVE
├─ success + more → REPAIRING_OR_CONTINUING
├─ conflict recoverable → REPAIRING_OR_CONTINUING
└─ unrecoverable → BLOCKED
REVIEWING_WAVE
│ schedule review tasks
│ collect ReviewerResult
└─ → REPAIRING_OR_CONTINUING
REPAIRING_OR_CONTINUING
│ retry failed within budget
│ create repair tasks
│ skip allowed failures
├─ more runnable → PLANNING_WAVE
├─ blocked → BLOCKED
├─ all done → COMPLETED
└─ cancelled → CANCELLED
Terminal: COMPLETED | BLOCKED | CANCELLED
20.3 Task Status Transitions
From db-schema §7, scheduler-state-machine §2:
pending ──task.started──▶ running
running ──task.completed──▶ completed
running ──task.failed──▶ failed
running ──task.blocked──▶ blocked
running ──task.cancelled──▶ cancelled
running ──task.interrupted──▶ interrupted
failed ──retry──▶ pending (new attempt)
interrupted ──resume──▶ pending
blocked ──decision received──▶ pending
20.4 Agent Status Transitions
From db-schema §10:
starting ──ready──▶ running
running ──agent.completed──▶ completed
running ──agent.failed──▶ failed
running ──agent.lost──▶ lost
running ──agent.cancelled──▶ cancelled
20.5 Workspace Status Transitions
From db-schema §16:
active ──merge success──▶ merged
active ──merge conflict──▶ conflicted
active ──abandon──▶ abandoned
conflicted ──decision──▶ abandoned | active (retry)
merged ──GC (7d)──▶ cleaned
abandoned ──GC (3d)──▶ cleaned
20.6 Capability Lifecycle
From capability-trust-v1 §7, overview §6:
discovered ──validate──▶ validated
validated ──doctor_check──▶ doctor_checked
doctor_checked ──enable──▶ enabled
enabled ──register_tools──▶ registered
registered ──activate──▶ active
active ──disable──▶ disabled
active ──failure──▶ failed
active ──update──▶ updated (→ discovered)
21. Traceability Matrix
This matrix maps frozen baseline sections to detailed design sections, ensuring no baseline requirement is unaddressed.
21.1 Contracts → Detailed Design
| Contracts section | Detailed design section |
|---|---|
| §2 Core Primitive Types | §3 (contracts package) |
| §3 Error Contracts | §3, §18.1 |
| §4 Entity References | §3 |
| §5 Runtime Event Contracts | §3, §5 |
| §6 Transaction and Storage | §4, §18.2 |
| §7 EventBus/Store/Ingestor | §5 |
| §8 Project and Session | §6 |
| §9 Task and Scheduler | §7 |
| §10 Worker and IPC | §8 |
| §11 WorkerResult | §8.3 |
| §12 Tool Contracts | §9.1 |
| §13 Permission Contracts | §9.2, §9.3 |
| §14 Artifact and Evidence | §11 |
| §15 Provider Contracts | §12 |
| §16 Context Contracts | §10 |
| §17 Projection/UI Contracts | §13 |
| §18 Capability Contracts | §9.5 |
| §19 Doctor and Logging | §16 |
| §20 Debug/Learned Memory | §11.3 |
| §21 Diagnostic Contracts | §15 (DiagnosticParser) |
| §22 Versioning Rules | §3 (contracts package rules) |
| §23 Boundary Rules | §2, §18.5 |
21.2 State Machines → Detailed Design
| State machine baseline | Detailed design section |
|---|---|
| main-agent-state-machine.md | §14.1, §20.1 |
| scheduler-state-machine-v1.md | §7, §20.2 |
| db-schema §7 task status | §20.3 |
| db-schema §10 agent status | §20.4 |
| db-schema §16 workspace status | §20.5 |
| capability-trust-v1 §7 | §9.5, §20.6 |
21.3 DB Schema → Detailed Design
| DB schema table | Repository (§4.3) | Event projection (§5.4) |
|---|---|---|
| schema_meta | MigrationRunner | — |
| sessions | SessionRepository | session.* |
| messages | MessageRepository | user/assistant.message.* |
| message_drafts | MessageDraftRepository | assistant.message.started/failed |
| events | EventRepository | all durable |
| tasks | TaskRepository | task.* |
| task_dependencies | TaskDependencyRepository | task.created |
| task_attempts | TaskAttemptRepository | task.started/completed/failed |
| agents | AgentRepository | agent.* |
| tool_runs | ToolRunRepository | tool.* |
| command_runs | CommandRunRepository | command.* |
| artifacts | ArtifactRepository | artifact.created |
| diagnostics | DiagnosticRepository | diagnostic.created |
| evidence_refs | EvidenceRepository | evidence.created |
| workspaces | WorkspaceRepository | workspace.* |
| summaries | SummaryRepository | summary.created |
| ui_state | UiStateRepository | — (not event-driven) |
| debug_records (project DB) | DebugKnowledgeStore | debug.record.created |
| learned_memories (project DB) | LearnedMemoryStore | memory.* |
21.4 Code View → Detailed Design
| Code view package | Detailed design sections |
|---|---|
| packages/contracts | §3 |
| packages/runtime | §4-§11, §13-§14, §16 |
| packages/llm | §12 |
| packages/toolchain-cpp | §15 |
| packages/tui | §13.2 |
| packages/cli | §17 |
21.5 Overview → Detailed Design
| Overview section | Detailed design section |
|---|---|
| §2 System Goal | §0 (authority), §2 |
| §4 Container Overview | §2, §3 |
| §5 Dependency Rules | §2 |
| §6 Runtime Component Overview | §4-§11 |
| §7 Runtime Agent Overview | §14 |
| §8 State and Data Overview | §4, §6, §11 |
| §9 Event, Error, Projection | §5, §18.1, §13.1 |
| §10 Execution Flow Overview | §7, §8, §9, §19 |
| §11 IPC and Worker Overview | §8 |
| §12 Permission and Security | §9.2, §9.3, §18.5 |
| §13 Context, Memory, Compaction | §10, §11.3, §19.3 |
| §14 UI/HUD and Provider | §12, §13 |
| §15 Doctor, Restore, Recovery | §16 |
| §16 Implementation Phase Mapping | (implementation phase, not design) |
| §17 Validation Overview | (test phase, not design) |
22. UML Class Diagrams (Mermaid)
22.1 Contracts Package
classDiagram
class RuntimeEvent~T~ {
+id: UUID
+type: string
+version: number
+timestamp: ISOTimeString
+session_id: SessionID
+project_id?: ProjectID
+source: EventSource
+route: string[]
+payload: T
}
class EventSource {
+kind: "main"|"architecture_designer"|"scheduler"|"agent"|"tool"|"system"
+id?: string
+agent_type?: AgentType
}
class TaskSpec {
+id: TaskID
+type: TaskType
+title: string
+description: string
+acceptance_criteria: string[]
+scope: TaskScope
+dependencies: TaskDependencySpec[]
+verification: VerificationPolicy
+constraints: TaskConstraints
+context_refs: TaskContextRefs
+output_contract: WorkerOutputContract
}
class WorkerResult~T~ {
+task_id: TaskID
+agent_id: AgentID
+agent_type: AgentType
+status: WorkerStatus
+summary: string
+changed_files: string[]
+artifacts: ArtifactRef[]
+verification: VerificationResult[]
+risks: Risk[]
+follow_up_tasks: FollowUpTask[]
+evidence_refs: EvidenceRef[]
+result: T
}
class AirError {
+error_id: UUID
+kind: ErrorKind
+severity: ErrorSeverity
+message: string
+detail?: string
+retryability: Retryability
+semantic_signature: string
+cause_ref?: EntityRef
+cause_refs?: EntityRef[]
+user_action?: string
+metadata?: JsonObject
}
class ToolDefinition~I,O~ {
+name: string
+version: number
+description: string
+input_schema: JsonSchema~I~
+output_schema: JsonSchema~O~
+category: ToolCategory
+permissions: ToolPermissionSpec
+streaming: boolean
}
class PermissionDecision {
+action: PermissionAction
+grant_scope: PermissionGrantScope
+risk_level: string
+reason: string
+required_confirmation?: boolean
+backup_required?: boolean
+evidence_ref_ids?: EvidenceRefID[]
}
class ArtifactRef {
+artifact_id: ArtifactID
+uri: string
+path: string
+type: string
+sha256?: string
+size_bytes?: number
}
class EvidenceRef {
+evidence_ref_id: EvidenceRefID
+kind: string
+ref: string
+claim: string
+location_json?: unknown
}
RuntimeEvent --> EventSource
WorkerResult --> ArtifactRef
WorkerResult --> EvidenceRef
AirError --> EntityRef
22.2 Runtime Core Services
classDiagram
class RuntimeApp {
+start(options): Promise~void~
+shutdown(): Promise~void~
}
class ServiceRegistry {
+get~T~(token): T
+register(token, service): void
}
class ProjectStore {
+locate(start_path): Promise~ProjectContext~
+initialize(root, options): Promise~ProjectContext~
+open(root): Promise~ProjectContext~
}
class SessionManager {
+open_session(project, options): Promise~SessionContext~
+close_session(session_id): Promise~void~
}
class DatabaseManager {
+open(path): DatabaseHandle
+transaction~T~(fn): Promise~T~
}
class EventIngestor {
+ingest~T~(event): Promise~void~
+ingest_ephemeral~T~(event): Promise~void~
}
class EventStore {
+append~T~(event, options): Promise~void~
+append_many(events, options): Promise~void~
+query(filter): Promise~RuntimeEvent[]~
}
class EventBus {
+publish~T~(event): void
+subscribe(filter, handler): Subscription
+drain(): Promise~void~
}
class ProjectionStore {
+hydrate(session_id): Promise~void~
+apply(event): void
+snapshot(): ProjectionSnapshot
+subscribe(handler): Subscription
}
RuntimeApp --> ServiceRegistry
RuntimeApp --> ProjectStore
RuntimeApp --> SessionManager
SessionManager --> DatabaseManager
EventIngestor --> EventStore
EventIngestor --> EventBus
EventStore --> DatabaseManager
EventStore --> EventBus : publishes after commit
ProjectionStore --> EventBus : subscribes
22.3 Scheduler Subsystem
classDiagram
class Scheduler {
+create_tasks(session_id, specs): Promise~void~
+add_dependency(session_id, task_id, dep): Promise~void~
+load_graph(session_id): Promise~TaskGraph~
+run_until_idle(session_id): Promise~SchedulerRunResult~
+cancel_task(task_id, reason): Promise~void~
-plan_wave(graph): SchedulerWavePlan
-dispatch(wave): Promise~void~
-collect_results(): Promise~void~
}
class TaskGraph {
+session_id: SessionID
+tasks: Map~TaskID, TaskNode~
+dependencies: TaskDependencyRecord[]
+get_runnable_tasks(): TaskNode[]
+mark_terminal(task_id, status): void
}
class WavePlanner {
+plan(graph, resources): SchedulerWavePlan
}
class RetryPlanner {
+decide(task, attempts, error): RetryDecision
}
class WorkspaceManager {
+create_workspace(plan): Promise~WorkspaceRef~
+merge_workspace(workspace_id): Promise~MergeResult~
+cleanup_workspace(workspace_id): Promise~void~
}
class AgentMonitor {
+record_heartbeat(event): void
+detect_lost_agents(): Promise~AgentLost[]~
+enforce_timeouts(): Promise~void~
}
class WorkerManager {
+spawn(task_spec, context_pack): Promise~WorkerProcess~
+cancel(agent_id, reason): Promise~void~
}
Scheduler --> TaskGraph
Scheduler --> WavePlanner
Scheduler --> RetryPlanner
Scheduler --> WorkspaceManager
Scheduler --> AgentMonitor
Scheduler --> WorkerManager
22.4 Tool and Permission Subsystem
classDiagram
class ToolRegistry {
+register~I,O~(definition, executor): void
+register_streaming~I,O~(definition, executor): void
+call~I,O~(name, input, context): Promise~ToolResultEnvelope~O~~
+call_streaming~I,O~(name, input, context): AsyncIterable
+list(): ToolDefinition[]
}
class PermissionEngine {
+evaluate(context): Promise~PermissionDecision~
+record(decision, context): Promise~PermissionRecordResult~
}
class PathClassifier {
+classify(path, project_root): PathRiskClassification
}
class CommandRiskAnalyzer {
+analyze(command, cwd): CommandRiskAnalysis
}
class CapabilityRegistry {
+discover(): Promise~CapabilityManifestV1[]~
+validate(manifest): Promise~ValidationResult~
+enable(capability_id): Promise~void~
+disable(capability_id): Promise~void~
+register_tools(tool_registry): Promise~void~
}
ToolRegistry --> PermissionEngine
PermissionEngine --> PathClassifier
PermissionEngine --> CommandRiskAnalyzer
CapabilityRegistry --> ToolRegistry : registers tools
22.5 Worker and IPC
classDiagram
class WorkerProcess {
+agent_id: AgentID
+pid?: number
+send(envelope): void
+on_message(handler): void
}
class WorkerProtocol {
+encode(msg): string
+decode(line): IpcMessage
+validate_direction(msg): void
+check_protocol_version(v): boolean
}
class WorkerRuntime {
+emit(event): Promise~void~
+call_tool~I,O~(name, input): Promise~ToolResultEnvelope~O~~
+checkpoint(data): Promise~void~
}
class WorkerRole~T~ {
<<interface>>
+run(task_spec, context_pack, runtime): Promise~WorkerResult~T~~
}
class ExecutorRole {
+run(...): Promise~WorkerResult~ExecutorResult~~
}
class ReviewerRole {
+run(...): Promise~WorkerResult~ReviewerResult~~
}
class DebuggerRole {
+run(...): Promise~WorkerResult~DebuggerResult~~
}
class CompactorRole {
+run(...): Promise~WorkerResult~CompactorResult~~
}
class ExperienceMinerRole {
+run(...): Promise~WorkerResult~ExperienceMinerResult~~
}
WorkerRole <|.. ExecutorRole
WorkerRole <|.. ReviewerRole
WorkerRole <|.. DebuggerRole
WorkerRole <|.. CompactorRole
WorkerRole <|.. ExperienceMinerRole
WorkerRole --> WorkerRuntime
WorkerProcess --> WorkerProtocol
22.6 Provider (LLM) Subsystem
classDiagram
class ProviderManager {
+load_config(): Promise~void~
+select_model(requirement): Promise~ModelAssignment~
+complete(input): AsyncIterable~ProviderStreamEvent~
}
class ProviderAdapter {
<<interface>>
+provider_id: ProviderID
+list_models(): Promise~ProviderCapabilityMatrix[]~
+validate_model(model_id): Promise~ProviderCapabilityMatrix~
+complete(input): AsyncIterable~ProviderStreamEvent~
+count_tokens?(input): Promise~number~
}
class AnthropicAdapter {
+provider_id: "anthropic"
}
class OpenAICompatibleAdapter {
+provider_id: string
-converter: AnthropicCanonicalConverter
}
class AnthropicCanonicalConverter {
+to_provider(messages): unknown[]
+from_provider(response): unknown[]
}
class StreamNormalizer {
+normalize(stream): AsyncIterable~ProviderStreamEvent~
}
ProviderManager --> ProviderAdapter
ProviderAdapter <|.. AnthropicAdapter
ProviderAdapter <|.. OpenAICompatibleAdapter
OpenAICompatibleAdapter --> AnthropicCanonicalConverter
ProviderAdapter --> StreamNormalizer
22.7 Context and Compaction
classDiagram
class ContextAssembler {
+assemble(input): Promise~AssembledContext~
-load_layers(purpose): Promise~PromptLayer[]~
-fit_budget(layers, budget): BudgetFitResult
}
class PromptLayerLoader {
+load_runtime_invariant(): PromptLayer
+load_role(role): PromptLayer
+load_project_rules(project): PromptLayer[]
+load_task_context(spec, refs): PromptLayer[]
}
class CompactionPolicy {
+should_compact(messages, budget): boolean
+compact(messages, target): Promise~CompactionResult~
}
class PromptLayer {
+level: PromptLayerLevel
+priority: number
+content: unknown
+token_estimate?: number
+source_ref?: string
+immutable?: boolean
}
ContextAssembler --> PromptLayerLoader
ContextAssembler --> CompactionPolicy
PromptLayerLoader --> PromptLayer
22.8 Agents
classDiagram
class MainAgent {
+handle_user_message(message): Promise~void~
+present_progress(): Promise~void~
+present_blocker(blocker): Promise~void~
-classify_intent(message): string
-state: MainAgentState
}
class ArchitectureDesigner {
+assess_impact(change): Promise~ArchitectureImpact~
+update_architecture_docs(update): Promise~DocumentUpdate~
}
MainAgent --> Scheduler
MainAgent --> ContextAssembler
MainAgent --> ProviderManager
ArchitectureDesigner --> ContextAssembler
ArchitectureDesigner --> ProviderManager
ArchitectureDesigner --> EventIngestor
23. Design Freeze Checklist
Before implementation begins, verify:
- All contracts §2-§21 interfaces are exported from
packages/contracts - All db-schema §3-§18 tables have corresponding repositories
- All event-registry §3 durable events have projection handlers in EventStore
- All state machines (Main Agent, Scheduler, Task, Agent, Workspace, Capability) are implemented
- All forbidden paths (contracts §23) are enforced by lint/import boundaries
- All closed enums (db-schema §21) are validated on insert/update
- FK-off invariants (§18.3) are checked at startup and periodically
- Outbox model (§18.4) is used for cross-DB writes
- Security invariants (§18.5) are enforced
End of System Detailed Design.