Files
AirCoding/AirPlan/docs/architecture/decisions-round-3.md
AirCoding 82f3140847 Initial commit: AirCoding V1.0.0 Alpha architecture baseline
Complete architecture document set with multi-model review remediation:
- Frozen interface contracts, runtime semantics, DB schemas
- Event/tool/error/provider registries
- Scheduler and main agent state machines
- C4 module/code views, solution architecture, baseline V1
- Multi-model review reports and joint assessment
- Phase-gate remediation complete (P0/P1/P2/UX resolved)
- Implementation plan with T-000A through T-045
- Reference folders kept as placeholders only
2026-05-28 18:45:01 +08:00

17 KiB
Raw Blame History

Architecture Decisions — Round 3

Date: 2026-05-26 Source: implementation-interface freeze discussion

D-038: RuntimeEvent Envelope Uses Route Chain

  • Decision: Runtime events use a structured route: string[] route chain instead of correlation/causation IDs.
  • Reason: Route chain is easier to display, append, and inspect across Main Agent → Architecture Designer → Scheduler → Worker → Tool flows.
  • Envelope:
interface RuntimeEvent<T = unknown> {
  id: string
  type: string
  version: number
  timestamp: string
  session_id: string
  project_id?: string
  source: EventSource
  route: string[]
  payload: T
}
  • Durability: Not declared by the event. EventStore decides durability by event type.

D-039: IPC Uses NDJSON over stdio

  • Decision: Parent Scheduler and child agents communicate via NDJSON over stdio.
  • stdout: Protocol only (event, control, log messages).
  • stderr: Crash fallback and non-structured fatal diagnostics only.
  • Message union:
type IpcMessage =
  | { kind: "event"; event: RuntimeEvent }
  | { kind: "control"; control: ControlMessage }
  | { kind: "log"; level: "debug" | "info" | "warn" | "error"; message: string; data?: unknown }
  • Exit codes:
    • 0: protocol-level completion, including task completed/blocked/failed
    • 1: uncaught exception
    • 2: startup/protocol error
    • 3: permission error
    • 4: parent cancelled
    • 5: hard timeout killed

D-040: Durable Event Type Set

  • Durable:
    • session.created, session.archived, session.deleted
    • user.message.created, assistant.message.created
    • agent.started, agent.completed, agent.failed
    • task.created, task.started, task.completed, task.blocked, task.failed, task.cancelled
    • tool.started, tool.completed, tool.failed
    • command.started, command.completed, command.failed
    • artifact.created
    • context.compaction.completed, summary.created
    • memory.candidate.created, memory.promoted, memory.archived
    • debug.record.created
    • permission.decision.recorded
    • requirement.changed, architecture.plan.updated, architecture.impact.completed
  • Ephemeral only:
    • agent.heartbeat, task.progress, token.delta, stdout.delta, stderr.delta, hud.frame.rendered, tool.progress
  • Rationale: Persist tool/agent starts so crash recovery can detect half-finished operations; avoid persisting high-frequency stream deltas.

D-041: TaskSpec Contract

interface TaskSpec {
  id: string
  type: "execute" | "review" | "debug" | "compact" | "mine_experience"
  title: string
  description: string
  acceptance_criteria: string[]
  scope: {
    write_area?: string
    expected_files?: string[]
    allowed_paths?: string[]
    denied_paths?: string[]
  }
  dependencies: { hard: string[]; soft: string[] }
  verification: {
    commands?: string[]
    required: boolean
    fallback_allowed: boolean
  }
  constraints: {
    max_turns: number
    soft_timeout_ms: number
    hard_timeout_ms: number
    retry_budget: number
    model_policy: "scheduler_forced" | "agent_select"
    model_id?: string
  }
  context_refs: {
    plan_ref?: string
    arc_ref?: string
    parent_task_results?: string[]
    artifacts?: string[]
  }
  output_contract: "ExecutorResult" | "ReviewerResult" | "DebuggerResult" | "CompactorResult" | "ExperienceMinerResult"
}
  • allowed_paths/denied_paths are task-level narrowing rules on top of global PermissionEngine.
  • verification.commands are Scheduler recommendations; agents may add more and must report actual commands run.
  • retry_budget is per task.

D-042: WorkerResult Contract

interface WorkerResult<T = unknown> {
  task_id: string
  agent_id: string
  agent_type: "executor" | "reviewer" | "debugger" | "compactor" | "experience_miner"
  status: "completed" | "failed" | "blocked" | "cancelled"
  summary: string
  changed_files: string[]
  diff_ref?: string
  artifacts: ArtifactRef[]
  verification: VerificationResult[]
  risks: Risk[]
  follow_up_tasks: FollowUpTask[]
  evidence_refs: EvidenceRef[]
  result: T
}
  • failed: task goal not achieved; Scheduler may retry or skip.
  • blocked: upper-level decision needed; continued attempts are not useful.
  • summary: human-readable 36 sentence summary covering what was done, evidence, conclusion, and risk. It is for TUI/Main Agent display, not scheduling logic.
  • changed_files exists for all worker types and may be empty.

D-043: ArtifactRef and EvidenceRef Separation

  • Artifact: persistent file or large data object (build log, test log, screenshot, pcap, core dump, diff, terminal cast).
  • Evidence: a claim-supporting reference that may point to an artifact excerpt, structured result, message, tool run, command run, or diagnostic.
interface ArtifactRef {
  id: string
  type: "build_log" | "test_log" | "screenshot" | "pcap" | "core_dump" | "terminal_cast" | "diff" | "report" | "other"
  uri: string
  size_bytes?: number
  sha256?: string
  created_at: string
}

interface EvidenceRef {
  id: string
  kind: "artifact_excerpt" | "structured_result" | "message" | "tool_run" | "command_run" | "diagnostic"
  ref: string
  location?: { line_start?: number; line_end?: number; byte_start?: number; byte_end?: number }
  claim: string
}

D-044: Tool API Contract

interface ToolDefinition<I = unknown, O = unknown> {
  name: string
  version: number
  description: string
  input_schema: JsonSchema<I>
  output_schema: JsonSchema<O>
  category: "filesystem" | "shell" | "build" | "test" | "debug" | "static_analysis" | "gui" | "network" | "memory" | "project" | "internal"
  permissions: {
    read_paths?: PathPolicy
    write_paths?: PathPolicy
    execute?: boolean
    network?: boolean
    system_sensitive?: boolean
  }
  streaming: boolean
  execute(input: I, context: ToolExecutionContext): AsyncIterable<ToolEvent> | Promise<ToolResult<O>>
}
  • Inputs and outputs are both JSON-schema validated.
  • Streaming tools must emit a final tool.result containing ToolResult.
  • Bash is a regular shell tool; PermissionEngine performs extra command risk analysis.

D-045: Capability Dependencies Are Managed by Doctor/Setup

  • Capability manifests declare dependencies but do not install them directly.
  • air doctor / air setup detect and repair missing dependencies.
  • First startup runs read-only doctor automatically.
  • If issues exist, user is prompted to run fix.
  • High-permission mode can use announce_then_run for dependency installation after the first startup; user not interrupting means allow.
  • First startup, even in high-permission mode, shows the report and asks before doctor --fix.
  • credentials and system_sensitive risks always require explicit confirmation.
interface DependencySpec {
  id: string
  kind: "executable" | "system_package" | "npm_package" | "pip_package" | "lsp_server" | "formatter" | "mcp_server" | "env_var" | "permission" | "service" | "network_port"
  required_for: string[]
  severity: "required" | "optional" | "degraded"
  detect: DependencyDetectSpec
  install?: {
    low_permission: "never" | "prompt" | "safe_cache"
    high_permission: "never" | "announce_then_run" | "safe_cache" | "prompt"
    commands?: InstallCommandTemplate[]
    docs?: string
  }
  risk: "none" | "downloads_code" | "needs_root" | "network_capture" | "credentials" | "system_sensitive"
}

D-046: ContextAssembler Contract

  • ContextAssembler outputs Anthropic canonical messages. Provider-specific conversion happens only at the LLM adapter boundary.
  • ContextAssembler records omissions so agents know what was not included.
  • If usage passes compaction threshold, it publishes context.compaction.requested but does not compact itself.
interface ContextRequest {
  session_id: string
  project_id: string
  requester: { kind: "main" | "architecture_designer" | "scheduler" | "agent"; agent_type?: string; task_id?: string }
  purpose: "user_response" | "architecture_planning" | "task_execution" | "review" | "debug" | "compaction" | "experience_mining"
  model: { provider_id: string; model_id: string; context_window_tokens: number; max_output_tokens: number }
  budget_policy: { reserved_output_tokens: number; safety_margin_tokens: number; target_usage_ratio: number }
  include: {
    project_rules: boolean
    project_profile: boolean
    task_graph: boolean
    recent_messages: number
    related_artifacts?: string[]
    related_tasks?: string[]
    related_files?: string[]
  }
  rule_set_ref?: string
}

interface AssembledContext {
  request_id: string
  messages: AnthropicMessageParam[]
  included_refs: { messages: string[]; summaries: string[]; artifacts: string[]; rules: string[]; tasks: string[] }
  token_estimate: { total: number; by_section: Record<string, number>; model_id: string }
  compaction: { needed: boolean; reason?: string; requested_event_id?: string }
  omissions: { ref: string; reason: string }[]
}

D-047: Compaction Rules Use Markdown + Frontmatter

  • Rule files are Markdown with YAML frontmatter.
  • Paths:
    • Built-in system default
    • User template: ~/.air/compaction-rules.md
    • Project override: .air/shared/compaction-rules.md
  • Three-tier inheritance: system default → user template → project override.
  • Message classification is done by ContextClassifier before compaction.
  • drop_if_needed is allowed because original messages are preserved.

D-048: TUI Reuses OpenCode UI Primitives, Not Business State

  • Use @opentui/solid, @opentui/core, and @opentui/keymap.
  • Copy/adapt OpenCode's generic theme, dialog, toast, keymap, layout, spinner, border, markdown/code/diff rendering patterns.
  • Do not reuse OpenCode's SDK/sync/session business layer.
  • AirCoding defines its own event store, session/message/task/agent/tool state model.
  • OpenCode session UI is used as a rendering reference only.

D-049: Project State Lives in .air/local, Shared Knowledge in .air/shared

  • Project source of truth moves into the project directory:
<project>/.air/
  shared/   # git-shareable project knowledge
  local/    # portable but gitignored runtime state
  • .air/shared contains project profile, permissions, compaction rules, project rules, and plan/docs.
  • .air/local contains sessions, artifacts, runtime state, backups, debug/learned DBs, locks, temp, and workspaces.
  • ~/.air contains only user config, cache, global skills, global logs, and a project index.
  • project_id is a stable UUID generated at initialization, not an absolute-path hash.

D-050: Developer Logs Use Development-Team Public Key Encryption

  • air.developer.log is encrypted with the development team's public key.
  • No local user private key is required for decrypting developer logs.
  • air doctor --bundle may include full diagnostics and is not automatically redacted.
  • Rationale: redaction may remove information needed to debug real failures.
  • Diagnostic bundles are never auto-uploaded; user must explicitly export/send them.

D-051: Session DB Uses Canonical Messages + Domain State Tables

  • messages stores complete Anthropic canonical content JSON.
  • message_drafts stores streaming assistant intermediate state for crash recovery and is deleted after final message completion.
  • message_parts is not a source-of-truth table in MVP.
  • Domain tables are the source of truth for scheduling/recovery/query:
    • tasks, task_dependencies, task_attempts
    • agents, tool_runs, command_runs
    • artifacts, diagnostics, evidence_refs, workspaces, events
  • ui_state stores lightweight UI recovery state only.
  • ProjectionStore rebuilds TUI view models from DB + live EventBus.

D-052: Query-Friendly Columns and Tables Are Preferred over Parsing JSON

  • Canonical JSON preserves fidelity; frequently queried relations are extracted to columns/tables.
  • tool_runs and command_runs include origin_message_id.
  • artifacts include common foreign keys (task_id, agent_id, tool_run_id, command_run_id) while retaining generic associated entity fields.
  • events extract source/task/agent/tool/command IDs and store both route_json and route_text.
  • task_dependencies, task_attempts, diagnostics, evidence_refs, and workspaces are first-class tables.

D-053: UI State Is Flushed Periodically and on Exit

  • UI changes are held in memory during interaction.
  • ui_state flushes periodically (e.g. every 10 seconds) and on normal exit.
  • Crash/power loss may lose a few seconds of UI-only state, but never scheduling/message state.

D-054: Artifact Layout Is Project-Local

  • Artifacts live under:
<project>/.air/local/sessions/<session-id>/artifacts/
  • URI format:
artifact://project/<project-id>/session/<session-id>/<artifact-id>
  • Layout:
artifacts/
  command-runs/<command-run-id>/
  tool-runs/<tool-run-id>/
  builds/
  tests/
  screenshots/
  pcaps/
  core-dumps/
  diffs/
  reports/
  • Internal filenames may use artifact IDs or semantic names; DB preserves original_name.
  • Logs may be gzip-compressed; screenshots, pcaps, and core dumps are not double-compressed by default.
  • Session delete/purge deletes artifacts; archive keeps them.

D-055: Schema Migration Requires User Confirmation

  • Opening a project auto-detects .air schema versions.
  • Old schema triggers a migration plan display.
  • User confirmation is required even in high-permission mode.
  • Migration backs up .air first.
  • Migration failure must rollback.

D-056: HUD/TUI Consumes ProjectionStore Only

  • DB is persistent state source.
  • EventBus is real-time state source.
  • ProjectionStore is the only display projection consumed by TUI/HUD.
  • HUD does not query SQLite directly.
  • ProjectionStore hydrates from DB on startup and applies EventBus updates during runtime.
  • If EventBus disconnects/restarts, ProjectionStore rehydrates from DB.
  • HUD presets only control visible fields, not underlying state.

D-057: UI Design Asset Capability Is Optional but Supported

  • AirCoding is not a general creative-media agent, but software development often requires UI assets, icons, visual mockups, and design specs.
  • Runtime supports an optional ui-design-assets capability.
  • MVP includes text/SVG/design-spec oriented tools:
    • ASCII/wireframe mockup generation
    • design specification generation
    • SVG icon generation
    • screenshot design analysis
    • external image-generation prompt generation
  • Bitmap image generation/editing is provider-backed and post-MVP.
  • Generated design assets are artifacts first:
    • ui_mockup
    • icon
    • illustration
    • svg_asset
    • design_spec
    • design_prompt
  • Generated assets are shown to the user before being written into project files. UI/design choices are subjective and should not be silently finalized even in high-permission mode.

D-058: Anthropic Claude Skills Are a Tool/Skill Reference Source

  • Anthropic/Claude open-source Skills are included as a reference for AirCoding's skill and tool organization.
  • AirCoding should study and borrow:
    • SKILL.md structure and frontmatter conventions
    • skill directory layout (scripts/, references/, assets/)
    • how reusable workflows are packaged as discoverable skills
    • how skill descriptions guide trigger/retrieval behavior
    • how skills interact with project rules and tool/capability execution
  • Claude Skills are a reference for AirCoding's SkillGenerator, ExperienceMiner output format, and Capability documentation.
  • AirCoding still keeps Project Rules, Skills, MCP, and Capabilities as separate concepts:
    • Project Rules: authoritative project constraints
    • Skills: reusable procedural knowledge
    • MCP: external service/tool protocol
    • Capabilities: runtime-registered tool bundles with dependencies/triggers/evidence types

D-059: Execution-Layer Primitives Align With Claude Code for Code Quality

  • AirCoding should align execution-layer behavior with Claude Code as much as possible to maximize code quality, correctness, and safe modification behavior.
  • Claude Code is the primary behavioral reference for execution-layer primitives.
  • This includes:
    • file read/edit/write safety boundaries
    • exact and conservative diff/update application semantics
    • patch granularity and conflict handling
    • tool input/output schema style
    • permission checks around filesystem and shell operations
    • TAOR/TORI-style execution loops and result feedback patterns
    • read-before-edit discipline
    • small-step edits with verification before completion
    • avoiding unrelated refactors and premature abstractions during task execution
    • build/test/debug evidence collection before declaring completion
    • project-rule and memory adherence during edits
    • failure handling that diagnoses root cause instead of random retries
    • explicit blocker escalation when implementation discovers architecture/interface conflicts
  • OpenCode remains the runtime/TUI structure reference, but execution primitives must favor Claude Code semantics whenever there is a tradeoff.
  • Codex remains a reference for broad tool surface and shell/patch/test direct loops, but AirCoding should not sacrifice Claude Code-style edit discipline for tool breadth.
  • Rationale: Executor correctness, safe code modification, patch quality, verification discipline, project-rule adherence, and failure recovery matter more than matching OpenCode's internal execution implementation or Codex-style general tool breadth.