迁移路径: /run/media/airlongdian/EasyU/AirCoding -> /home/airlongdian/DataDevices/AirWorkSpace/AirCoding Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2448 lines
112 KiB
Markdown
Executable File
2448 lines
112 KiB
Markdown
Executable File
# 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
|
|
|
|
1. §2 System decomposition and module ownership
|
|
2. §3 Contracts package detailed design
|
|
3. §4 Storage and repositories
|
|
4. §5 Event subsystem (Ingestor/Store/Bus/SchemaRegistry)
|
|
5. §6 Project/session lifecycle
|
|
6. §7 Scheduler subsystem
|
|
7. §8 Worker/IPC subsystem
|
|
8. §9 Tool + Permission + Capability subsystem
|
|
9. §10 Context/Prompt/Compaction subsystem
|
|
10. §11 Artifact/Evidence/Knowledge subsystem
|
|
11. §12 Provider (LLM) subsystem
|
|
12. §13 Projection + TUI subsystem
|
|
13. §14 Agents (Main, Architecture Designer) subsystem
|
|
14. §15 Toolchain C++ subsystem
|
|
15. §16 Doctor/Logging/Migration/Recovery subsystem
|
|
16. §17 CLI subsystem
|
|
17. §18 Cross-cutting designs (error, transaction, FK-off, outbox)
|
|
18. §19 Sequence designs
|
|
19. §20 State machine designs
|
|
20. §21 Traceability matrix
|
|
|
|
## 2. System Decomposition and Module Ownership
|
|
|
|
Dependency direction (frozen by `c4/module.md`), expressed as allowed imports:
|
|
|
|
```text
|
|
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:
|
|
|
|
1. Every interface in `interface-contracts-v1.md` §2-§21 is exported from the file mapped in
|
|
code-view §3 "Contract Ownership".
|
|
2. No interface gains extra fields here. Field additions require an ADR (contracts §22 rule 6).
|
|
3. `JsonSchema<T>` stays nominal (`JsonObject`); no runtime schema engine lives in contracts.
|
|
4. Re-export surface is `index.ts` which barrel-exports every contract file.
|
|
|
|
Contract-to-file map (frozen by code-view §3):
|
|
|
|
```text
|
|
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.
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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):
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
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
|
|
|
|
*Applicable invariants (§18.6): INV-1 (state columns written only here, in `project()`), INV-2 (project never touches external stores), INV-5 (EventBus is transport only).*
|
|
|
|
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).
|
|
|
|
```text
|
|
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):
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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):
|
|
|
|
```text
|
|
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 = 'running'` when the event is emitted after the WorkerProcess handshake has completed (the normal case), or `status = 'starting'` when emitted before handshake acknowledgement; the event carries exactly one status and projects it once (event-registry §3: "insert row with status = running or starting"). There is **no** runtime `starting → running` UPDATE that bypasses the event log: if a row was inserted as `starting` and the agent later becomes fully ready, the next durable `agent.*` event (or a re-emitted `agent.started` for the running phase) carries the status; `agents.status` is only ever written by `agent.*` event projection (runtime-semantics §3), never by a direct `WorkerManager` write |
|
|
| `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 session-DB table — `LearnedMemoryStore` 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 writes
|
|
`events_session.db` rows (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.failed` carrying `AirError`, 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
|
|
|
|
```text
|
|
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.
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
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
|
|
|
|
*Applicable invariants (§18.6): INV-1 (Scheduler/WorkspaceManager/AgentMonitor mutate `tasks`/`agents`/`workspaces` status only by emitting events for projection — never direct UPDATE; heartbeat timestamps are the only exemption), INV-5 (rebuild queues from SQLite, not EventBus replay).*
|
|
|
|
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.
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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
|
|
|
|
*Applicable invariants (§18.6): INV-1 (WorkerManager never writes `agents.status` directly — `agent.started` projection sets it; `worker.ready` handshake is a live signal, not a status write), INV-3 (workers reach fs/shell/network/SQLite only through parent-mediated tool IPC).*
|
|
|
|
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
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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).
|
|
|
|
```text
|
|
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:
|
|
1. records a read observation before `fs.edit`/`fs.patch` (runtime-semantics §9.1);
|
|
2. keeps changes within `TaskScope.write_area`/`allowed_paths` (overview §10.9 rule 3);
|
|
3. runs `VerificationPolicy.commands` before declaring `completed` (contracts §9, §11);
|
|
4. attaches evidence refs for diffs/builds/tests (overview §10.9 rule 6).
|
|
|
|
## 9. Tool, Permission, and Capability Subsystem
|
|
|
|
*Applicable invariants (§18.6): INV-3 (every side effect goes through `ToolRegistry.call` → `PermissionEngine.evaluate` first), INV-4 (capabilities install dependencies only through Doctor).*
|
|
|
|
Module: `packages/runtime/src/tools/`, `security/`, `capabilities/`.
|
|
|
|
### 9.1 ToolRegistry
|
|
|
|
Implements contracts §12.
|
|
|
|
```text
|
|
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):
|
|
|
|
```text
|
|
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.
|
|
|
|
```text
|
|
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):
|
|
|
|
```text
|
|
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_string` exact match; non-unique match fails unless `replace_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.
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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_entity("task", task_id)` | `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:
|
|
|
|
- `MainAgent` loads its L1 role prompt from built-in resources directly
|
|
- `ArchitectureDesigner` loads its L1 role prompt from built-in resources directly
|
|
- `Scheduler` does 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
|
|
|
|
```text
|
|
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
|
|
|
|
*Applicable invariants (§18.6): INV-2 (DebugKnowledgeStore / LearnedMemoryStore are single writers of their project DBs and follow the outbox model — external write first, then one completion event), INV-1 (session-side markers like `artifact.created` write session rows only via projection).*
|
|
|
|
Module: `packages/runtime/src/artifacts/` and `knowledge/`.
|
|
|
|
### 11.1 ArtifactStore
|
|
|
|
Implements contracts §14; naming from artifact-naming-v1.
|
|
|
|
```text
|
|
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):
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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.
|
|
|
|
```text
|
|
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.
|
|
|
|
```text
|
|
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
|
|
`ProviderStreamEvent`s. Runtime/session provider+model are fixed for the session; there is no runtime
|
|
switching API (overview §14).
|
|
|
|
### 12.2 ProviderAdapter implementations
|
|
|
|
```text
|
|
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.
|
|
|
|
```text
|
|
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).
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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).
|
|
|
|
```text
|
|
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.
|
|
|
|
```text
|
|
class DoctorService implements DoctorService
|
|
+run(input: DoctorRunInput): Promise<DoctorRunOutput>
|
|
-check_capability(capability): DoctorIssue[] // internal helper, not a public contract method
|
|
-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
|
|
|
|
```text
|
|
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):
|
|
1. load `tasks` with status `running`/`interrupted`, active `agents`/`workspaces`;
|
|
2. check process liveness by PID; reconnect IPC if alive, else emit `agent.lost`;
|
|
3. mark task failed/interrupted by resumability;
|
|
4. preserve workspaces until merge/cleanup decision;
|
|
5. orphan-artifact scan registers or quarantines files;
|
|
6. FK-off orphan scan (§18.3) logs and re-parents/archives dangling references;
|
|
7. workspace GC applies retention (overview §15);
|
|
8. 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`.
|
|
|
|
```text
|
|
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]` | Wraps `DoctorService` (§16.1) |
|
|
| `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|session> ...` | Catalog: git-backed backup restore (overview §15, runtime-semantics §19) |
|
|
|
|
`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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
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:
|
|
|
|
1. `tasks.session_id` → existing `sessions.id`
|
|
2. `task_attempts.task_id` → existing `tasks.id`
|
|
3. `agents.task_id` → existing `tasks.id` when not null
|
|
4. `tool_runs.task_id`, `tool_runs.agent_id` → existing rows when not null
|
|
5. `command_runs.task_id`, `command_runs.agent_id`, `command_runs.tool_run_id` → existing rows
|
|
6. `workspaces.task_id`, `workspaces.agent_id` → existing rows when not null
|
|
7. `diagnostics.command_run_id`, `diagnostics.artifact_id` → existing rows when not null
|
|
8. `evidence_refs` foreign 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:
|
|
|
|
```text
|
|
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:
|
|
|
|
1. Owning `DebugKnowledgeStore` performs the `debug-records.db` insert/update.
|
|
2. On success, emit `debug.record.created` referencing the new `debug_record_id`.
|
|
3. On `debug-records.db` write failure: emit a `task.failed` (or future
|
|
`debug.record.failed`, gated by ADR) carrying the `AirError`; do **not** emit
|
|
`debug.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.
|
|
- `sudo` risk is determined by command intent/target/system sensitivity, not string alone.
|
|
|
|
### 18.6 Domain write-ownership invariants
|
|
|
|
This section consolidates the cross-cutting rules that any implementer — including a
|
|
context-isolated subagent that only holds one subsystem slice — must not violate. Each
|
|
subsystem chapter (§4-§17) lists its applicable invariants by number; this section is the
|
|
single source of truth for them.
|
|
|
|
**INV-1 — Session-DB state columns are written only by event projection.**
|
|
Every `*.status` / lifecycle column in the session DB is mutated **only** inside
|
|
`EventStore.project(event, tx)` as part of the durable event transaction (§18.2,
|
|
runtime-semantics §3). No service (Scheduler, WorkerManager, ToolRegistry, MainAgent, …)
|
|
may issue a direct `UPDATE` to these columns outside event projection. The authoritative
|
|
table → writing-event map:
|
|
|
|
| Table.column | Written only by (durable event projection) |
|
|
|---|---|
|
|
| `sessions.status` | `session.created` (active) / `session.archived` / `session.deleted` |
|
|
| `message_drafts.status` | `assistant.message.started` (streaming) / `assistant.message.failed` (error); row deleted by `assistant.message.created` |
|
|
| `tasks.status` | `task.created` (pending) / `task.started` (running) / `task.completed` / `task.failed` / `task.blocked` / `task.cancelled` / `task.interrupted` |
|
|
| `task_attempts.status` / `failure_*` | `task.started` (insert) / `task.completed` / `task.failed` (update) |
|
|
| `agents.status` | `agent.started` (running or starting, projected once) / `agent.completed` / `agent.failed` / `agent.lost` / `agent.cancelled` — **no event-less `starting → running` write** (§5.4, §20.4) |
|
|
| `tool_runs.status` | `tool.started` (running) / `tool.completed` (ok) / `tool.failed` (error) / `tool.cancelled` |
|
|
| `workspaces.status` | `workspace.created` (active) / `workspace.merge.completed` (merged) / `workspace.merge.conflicted` (conflicted) / `workspace.cleaned` (cleaned); `abandoned` set by GC compaction event path (§7.5, runtime-semantics §15) |
|
|
| `command_runs` | **no physical status column** — status is *derived* (§4.4), never written |
|
|
|
|
**INV-1 exemptions (explicitly NOT event-sourced; safe to write directly):**
|
|
|
|
| Column | Direct writer | Basis |
|
|
|---|---|---|
|
|
| `agents.last_heartbeat_at`, `tasks.heartbeat_at` | `AgentMonitor` coalesce (throttled) | runtime-semantics §4 — liveness timestamps, recovery does not replay heartbeat events |
|
|
| `ui_state.*` | `UiStateRepository.upsert` | db-schema §18 — UI scratch state, not event-driven (§21.3) |
|
|
|
|
**INV-2 — Cross-DB / external writes use the outbox model with a single writer.**
|
|
Writes to `debug-records.db`, `learned-memory.db`, `rules/`, `skills/`, or artifact files
|
|
go through the owning store, which is the **single writer** for that store, and follow the
|
|
outbox sequence in §18.4 (external write first → then ingest one completion event). No other
|
|
component writes those stores. `EventStore.project()` never opens an external DB or file.
|
|
|
|
**INV-3 — Side effects only through the tool + permission path.**
|
|
All filesystem / shell / network / git side effects go through `ToolRegistry.call`, which
|
|
always consults `PermissionEngine.evaluate` first (§9.1, §9.3). Workers never touch fs/shell/
|
|
network/SQLite except via parent-mediated tool IPC (contracts §10, §23). LLM/provider output
|
|
never performs a direct side effect (§18.5).
|
|
|
|
**INV-4 — Import/dependency direction is one-way.**
|
|
The allowed-import graph in §2 (frozen by `c4/module.md` + contracts §23) is never crossed:
|
|
`contracts` is imported by all and imports nothing; `workers` import only contracts + the
|
|
WorkerRuntime IPC surface; TUI imports only contracts; capabilities install dependencies only
|
|
through Doctor.
|
|
|
|
**INV-5 — EventBus is transport, never a source of truth.**
|
|
Recovery and scheduling rebuild from SQLite (events + domain rows), never from replaying
|
|
EventBus traffic (§5.5, contracts §7, overview §9.3). A dropped/duplicated EventBus delivery
|
|
must never change durable state.
|
|
|
|
> **For context-isolated execution (e.g. parallel isolated subagents):** inject §18.6 in full
|
|
> into every subagent's working context regardless of which subsystem slice it owns. INV-1 in
|
|
> particular guards the most common slice-local mistake — writing a status column directly from
|
|
> the subsystem that happens to know the new value — which a partial context cannot otherwise
|
|
> detect.
|
|
|
|
## 19. Sequence Designs
|
|
|
|
### 19.1 User request → task execution → completion
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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).
|
|
|
|
```text
|
|
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 `ToolRegistry`
|
|
under `PermissionEngine` (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
|
|
as `architecture.plan.updated` (event-registry §3).
|
|
|
|
### 19.5 Debug knowledge capture
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ 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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
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. Each transition below is driven by a durable `agent.*` event and
|
|
applied to `agents.status` only through event projection (runtime-semantics §3); there is
|
|
no direct status write that bypasses the event log.
|
|
|
|
```text
|
|
(insert) ──agent.started──▶ starting | running
|
|
running ──agent.completed──▶ completed
|
|
running ──agent.failed──▶ failed
|
|
running ──agent.lost──▶ lost
|
|
running ──agent.cancelled──▶ cancelled
|
|
```
|
|
|
|
`agent.started` sets the initial status to `running` (handshake completed before emission,
|
|
the normal case) or `starting` (emitted before handshake). The `starting` value is the
|
|
initial projected status, not a separate event-less transition; baseline defines no
|
|
durable `starting → running` event, and `agents.status` is never updated outside
|
|
`agent.*` projection. Process readiness (`worker.ready` handshake, §8.2) is a live IPC
|
|
signal, not a status-column write.
|
|
|
|
### 20.5 Workspace Status Transitions
|
|
|
|
From db-schema §16:
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
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
|
|
|
|
```mermaid
|
|
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
|
|
|
|
```mermaid
|
|
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
|
|
|
|
```mermaid
|
|
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
|
|
|
|
```mermaid
|
|
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
|
|
ToolRegistry --> ToolDefinition : registers
|
|
ToolRegistry --> ToolExecutor : invokes
|
|
PermissionEngine --> PathClassifier
|
|
PermissionEngine --> CommandRiskAnalyzer
|
|
CapabilityRegistry --> ToolRegistry : registers tools
|
|
```
|
|
|
|
### 22.5 Worker and IPC
|
|
|
|
```mermaid
|
|
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
|
|
|
|
```mermaid
|
|
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
|
|
|
|
```mermaid
|
|
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
|
|
ContextAssembler ..> EvidenceStore : L6 evidence
|
|
ContextAssembler ..> SessionStore : L7/L8
|
|
PromptLayerLoader --> PromptLayer
|
|
```
|
|
|
|
### 22.8 Agents
|
|
|
|
```mermaid
|
|
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. Reference Implementation Map
|
|
|
|
AirCoding is **self-owned, not a wrapper** (baselineV1 §1): the architecture and class
|
|
design above are AirCoding's own. This section records, per baselineV1 §2 and
|
|
`decisions-round-1/2/3`, **where an implementer should consult mature reference code**
|
|
instead of designing a component from scratch, and the **reuse mode** for each. This is a
|
|
guidance map, not a dependency contract — it never overrides §2 import direction or §18.6
|
|
invariants.
|
|
|
|
**Reuse modes:**
|
|
|
|
- `npm-dep` — consume directly as a dependency; do not re-implement.
|
|
- `fork/adapt` — copy and adapt source into our package; keep our own boundaries.
|
|
- `pattern` — reference structure/patterns; implement independently.
|
|
- `behavioral` — match observable behavior/quality; no code lineage.
|
|
|
|
**Reference source location:** local reference checkouts live under the working-copy `reference/`
|
|
tree at the **git root** — i.e. `<repo-root>/reference/`, where `<repo-root>` is `AirCoding/`, the
|
|
parent of `AirPlan/` (which holds this design). Paths in the table below are relative to
|
|
`<repo-root>`, not to `AirPlan/`. This `reference/` tree is **git-ignored / not committed**
|
|
(working-copy only, ~0.5 GB). Implementers (including isolated subagents) must verify the referenced
|
|
checkout exists and is non-empty before relying on it; if absent, clone the upstream repo to that
|
|
path. Current pinned directory names (verified present):
|
|
|
|
```text
|
|
reference/opencode-1.15.5/ reference/openai-codex/
|
|
reference/hermes-agent-2026.5.16/ reference/anthropic-skills/
|
|
reference/asciinema-3.2.0/ reference/atuin-18.16.1/
|
|
reference/claude-hud-0.0.12/ reference/claude-code-cli/ (Claude Code 2.1.88, behavioral ref only)
|
|
```
|
|
|
|
| DD component | Reference project (baselineV1 §2) | Reuse mode | Notes / local path |
|
|
|---|---|---|---|
|
|
| §13.2 TUI (`TuiApp`, components) | OpenTUI (`@opentui/solid` `@opentui/core` `@opentui/keymap`) | `npm-dep` | D-002/D-048: direct dependency; do not build a renderer. Theme/dialog/toast/keymap/layout/spinner/border/markdown/code/diff rendering adapted from OpenCode UI patterns. |
|
|
| §13.2 TUI interaction layout | OpenCode TUI (`reference/opencode-1.15.5/`) | `pattern` | D-048: reference visual/interaction patterns; **do not** reuse OpenCode SDK/sync/session business state. |
|
|
| §12 Provider (`ProviderManager`, `ProviderAdapter`, converters) | `@opencode-ai/llm` (`reference/opencode-1.15.5/`) | `fork/adapt` | D-002 list: provider/model abstraction may be forked/adapted; output stays Anthropic-canonical (D-046). |
|
|
| §8.4 + §9.4 Execution discipline (read-before-edit, exact edit, verify-before-complete) | Claude Code CLI (`reference/claude-code-cli/`) | `behavioral` | baselineV1 §2: quality benchmark only — edit safety, patch granularity, conflict handling, verification discipline. No code lineage. |
|
|
| §9.4 `fs.edit`/`fs.patch` + §15 patch/test execution loop | OpenAI Codex (`reference/openai-codex/`) | `pattern` | baselineV1 §2: shell/patch/test direct-execution loop, tool orchestration, wider tool surface. |
|
|
| §11.3 + §14 Knowledge / ExperienceMiner / Curator | Hermes Agent (`reference/hermes-agent-2026.5.16/`) | `pattern` | D-024: Nudge Engine interval trigger (~10 turns/tool-calls), Curator dedup daemon, skill self-patch. |
|
|
| §11.3 + runtime-semantics §11 Skills (`SKILL.md`, `scripts/`/`references/`/`assets/`) | Anthropic Claude Skills (`reference/anthropic-skills/`) | `pattern` | D-058: SKILL.md frontmatter + directory layout + trigger/retrieval descriptions; SkillGenerator output format. |
|
|
| §16.2 Logging / HUD + future PTY capture | asciinema / Atuin / claude-hud (`reference/asciinema-3.2.0/`, `reference/atuin-18.16.1/`, `reference/claude-hud-0.0.12/`) | `pattern` | baselineV1 §2: PTY capture, command metadata/history indexing, HUD/statusline layout. |
|
|
| §3 Message format (Anthropic canonical content blocks) | Claude Code message model (`reference/claude-code-cli/`) | `behavioral` | D-016: Text/Thinking/ToolUse/ToolResult blocks; canonical internal format. |
|
|
|
|
**Rules for reference reuse:**
|
|
|
|
1. `npm-dep` items (`@opentui/*`) are real dependencies — implementers must not re-implement
|
|
them; see `packages/tui` (code-view §7).
|
|
2. `fork/adapt` and `pattern` items: adapt into our own package layout (§2 import direction),
|
|
preserve our contracts (§3) and invariants (§18.6) verbatim. A reference's internal model
|
|
never leaks across our package boundaries — e.g. OpenCode session/sync state is **not**
|
|
adopted (D-048).
|
|
3. `behavioral` items contribute **no code**; they set a quality bar verified by tests/evidence.
|
|
4. License compliance for any copied/forked source is checked at the architecture gate before
|
|
merge.
|
|
|
|
> **For context-isolated execution:** include the row(s) relevant to a subagent's slice in its
|
|
> working context. The intent is to stop a partial-context implementer from re-deriving a diff
|
|
> engine, TUI renderer, or skill format that a named reference already provides.
|
|
|
|
## 24. Design Freeze Declaration
|
|
|
|
**System Detailed Design is frozen as of 2026-06-01 (commit `2673e49` → this commit).**
|
|
|
|
Multi-model review complete: DeepSeek (R1, R7) / MIMO 2.5 Pro (R2) / GPT-5.5 Pro (R3) /
|
|
Opus 4.8 (R4, R6) / Opus 4.7 (R5 regression). All P0/P1/P2 findings closed
|
|
across 7 review rounds. Coverage: contracts 100%, events 100%, DB schema 100%,
|
|
state machines 100%, forbidden edges 100%.
|
|
|
|
**Design-level verification (complete — verified in review):**
|
|
|
|
- [x] All contracts §2-§21 interfaces mapped to DD classes (§21.1)
|
|
- [x] All db-schema §3-§18 tables have corresponding repositories (§4.3, §21.3)
|
|
- [x] All event-registry §3 durable events have projection handlers (§5.4)
|
|
- [x] All state machines specified (Main Agent / Scheduler / Task / Agent / Workspace / Capability, §20)
|
|
- [x] FK-off invariants defined (§18.3 ≡ runtime-semantics §14)
|
|
- [x] Outbox model defined (§18.4)
|
|
- [x] Security invariants defined (§18.5)
|
|
|
|
**Implementation-entry gates (to be verified as code is written):**
|
|
|
|
- [ ] All contracts §2-§21 interfaces are exported from `packages/contracts`
|
|
- [ ] 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 (frozen).
|