# AirCoding Interface Contracts V1 Date: 2026-05-27 Status: Canonical implementation-facing interface contract set for V1.0.0 Alpha This document freezes V1 public TypeScript interfaces and service boundaries. Implementations may use classes, functions, or modules, but exported contracts, dependency direction, and serialization shapes must remain stable unless a later ADR updates them. ## 1. Naming and Serialization Conventions V1 TypeScript public contracts use `snake_case` field names. Runtime events, tool payloads, DB JSON metadata, IPC messages, and WorkerResult structures therefore share one JSON-compatible field convention. Rationale: 1. Event payloads, DB columns, tool schemas, and persisted JSON are already `snake_case`. 2. Avoiding a camelCase↔snake_case mapper reduces V1.0.0 Alpha implementation cost and prevents boundary drift. 3. Provider adapters may translate external provider formats, but AirCoding internal contracts remain `snake_case`. Implementation code may use local camelCase variables internally, but exported interfaces in `packages/contracts` must keep `snake_case` field names unless a later ADR changes this. ## 2. Core Primitive Types ```ts export type ISOTimeString = string export type UUID = string export type ProjectID = string export type SessionID = string export type MessageID = string export type TaskID = string export type AgentID = string export type ToolRunID = string export type CommandRunID = string export type ArtifactID = string export type EvidenceRefID = string export type WorkspaceID = string export type SummaryID = string export type CapabilityID = string export type ProviderID = string export type ModelID = string export type WaveID = string export type JsonObject = Record export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue } // JsonSchema is nominal-only: T provides no compile-time runtime check, but documents the expected shape for tool schema consumers. export type JsonSchema = JsonObject ``` Testability primitives: ```ts export interface Clock { now(): ISOTimeString } export interface IdGenerator { uuid(): UUID project_id(): ProjectID session_id(): SessionID message_id(): MessageID task_id(): TaskID agent_id(): AgentID tool_run_id(): ToolRunID command_run_id(): CommandRunID artifact_id(): ArtifactID evidence_ref_id(): EvidenceRefID workspace_id(): WorkspaceID summary_id(): SummaryID } ``` ## 3. Error Contracts ```ts export type ErrorKind = | "user_error" | "project_error" | "env_error" | "dependency_error" | "permission_error" | "tool_error" | "command_error" | "build_error" | "test_error" | "static_analysis_error" | "debug_error" | "provider_error" | "model_capability_error" | "context_error" | "agent_error" | "scheduler_error" | "workspace_error" | "merge_error" | "architecture_error" | "policy_error" | "system_error" | "unknown_error" export type ErrorSeverity = "info" | "warning" | "error" | "fatal" export type Retryability = "retryable" | "retryable_after_change" | "not_retryable" | "unknown" export interface 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 } ``` All tool, event, worker, provider, and command failures use `AirError`. Do not introduce parallel `error_kind` / `retryable` / `failure_signature` shapes. ## 4. Entity References ```ts export type EntityType = | "session" | "message" | "task" | "agent" | "tool_run" | "command_run" | "artifact" | "diagnostic" | "workspace" | "summary" | "capability" | "provider" export interface EntityRef { type: EntityType id: string } ``` ## 5. Runtime Event Contracts ```ts export type AgentType = "executor" | "reviewer" | "debugger" | "compactor" | "experience_miner" export interface EventSource { kind: "main" | "architecture_designer" | "scheduler" | "agent" | "tool" | "system" id?: string agent_type?: AgentType } export interface RuntimeEvent { id: UUID type: string version: number timestamp: ISOTimeString session_id: SessionID project_id?: ProjectID source: EventSource route: string[] payload: TPayload } export interface EventFilter { session_id?: SessionID types?: string[] task_id?: TaskID agent_id?: AgentID tool_run_id?: ToolRunID command_run_id?: CommandRunID route_prefix?: string[] since?: ISOTimeString } ``` Event payload schemas and persistence policy are defined in `event-registry-v1.md`. ## 6. Transaction and Storage Contracts ```ts export interface TransactionHandle { id: string } export interface DatabaseHandle { path: string } export interface TransactionManager { transaction(fn: (tx: TransactionHandle) => Promise): Promise } ``` Repository records mirror `db-schema-v1.md` columns and use `snake_case`. ```ts export interface SessionRecord { id: SessionID project_id: ProjectID project_root: string title?: string status: "active" | "archived" | "deleted" created_at: ISOTimeString updated_at: ISOTimeString exited_at?: ISOTimeString model_provider_id?: ProviderID model_id?: ModelID metadata_json?: string } export interface MessageRecord { id: MessageID session_id: SessionID role: string canonical_format: "anthropic" content_json: string parent_message_id?: MessageID route_json?: string created_at: ISOTimeString token_estimate?: number metadata_json?: string } export type TaskType = "execute" | "review" | "debug" | "compact" | "mine_experience" | "docs" export type TaskStatus = "pending" | "running" | "completed" | "failed" | "blocked" | "cancelled" | "interrupted" export interface TaskRecord { id: TaskID session_id: SessionID type: TaskType status: TaskStatus title: string task_spec_json: string worker_result_json?: string assigned_agent_id?: AgentID workspace_id?: WorkspaceID retry_count: number created_at: ISOTimeString started_at?: ISOTimeString completed_at?: ISOTimeString heartbeat_at?: ISOTimeString metadata_json?: string } export type TaskInsert = Omit & { retry_count?: number } export type TaskUpdate = Partial> export type PersistedEventInsert = Omit & { route_text?: string } export type TaskDependencyType = "hard" | "soft" | "conflict" | "serialization" export interface TaskDependencyRecord { id: UUID session_id: SessionID task_id: TaskID depends_on_task_id: TaskID dependency_type: TaskDependencyType reason?: string created_at: ISOTimeString } export interface PersistedEventRecord { id: UUID session_id: SessionID type: string version: number timestamp: ISOTimeString source_kind: string source_id?: string agent_type?: AgentType task_id?: TaskID agent_id?: AgentID tool_run_id?: ToolRunID command_run_id?: CommandRunID route_json: string route_text: string payload_json: string } ``` Repository layer: ```ts export interface Repository { get(id: string, tx?: TransactionHandle): Promise insert(record: TInsert, tx?: TransactionHandle): Promise update(id: string, patch: TUpdate, tx?: TransactionHandle): Promise } export interface TaskRepository extends Repository { list_by_status(session_id: SessionID, statuses: TaskStatus[], tx?: TransactionHandle): Promise list_runnable_candidates(session_id: SessionID, tx?: TransactionHandle): Promise } export interface EventRepository { insert(event: PersistedEventRecord, tx: TransactionHandle): Promise query(filter: EventFilter): Promise } ``` Repositories are persistence adapters only. They must not contain Scheduler, permission, or projection policy. ## 7. EventBus, EventStore, and Ingestion Contracts ```ts export interface Subscription { unsubscribe(): void } export interface EventBus { publish(event: RuntimeEvent): void subscribe(filter: EventFilter, handler: (event: RuntimeEvent) => void | Promise): Subscription drain?(): Promise } export interface EventStore { append(event: RuntimeEvent, options?: EventAppendOptions): Promise append_many(events: RuntimeEvent[], options?: EventAppendOptions): Promise query(filter: EventFilter): Promise } export interface EventAppendOptions { expected_session_id?: SessionID transaction?: TransactionHandle } export interface EventIngestor { ingest(event: RuntimeEvent): Promise ingest_ephemeral(event: RuntimeEvent): Promise } export interface EventSchemaRegistry { register(type: string, version: number, schema: JsonObject): void validate(type: string, version: number, payload: unknown): boolean list(): Array<{ type: string; version: number }> get_schema(type: string, version: number): JsonObject | undefined } ``` Rules: 1. `EventIngestor` is the runtime entry point for events from agents/tools/workers. 2. `EventStore` validates durable event schema and applies domain projections transactionally. 3. EventBus publication of durable events happens after commit. 4. EventBus is live transport only and is not a recovery source of truth. 5. If a `subscribe` handler throws, the error is caught, logged to developer log, and does not propagate to the publisher. The subscription remains active. ## 8. Project and Session Contracts ```ts export interface ProjectContext { project_id: ProjectID project_root: string air_root: string shared_root: string local_root: string schema_version: number } export interface ProjectInitOptions { force?: boolean title?: string default_rules?: boolean } export interface ProjectStore { locate(start_path: string): Promise initialize(project_root: string, options?: ProjectInitOptions): Promise open(project_root: string): Promise } export interface SessionContext { session_id: SessionID project_id: ProjectID project_root: string db_path: string artifact_root: string } export interface OpenSessionOptions { session_id?: SessionID title?: string model_provider_id?: ProviderID model_id?: ModelID } export interface SessionManager { open_session(project: ProjectContext, options?: OpenSessionOptions): Promise close_session(session_id: SessionID): Promise } ``` ## 9. Task and Scheduler Contracts ```ts export interface TaskDependencySpec { depends_on_task_id: TaskID dependency_type: TaskDependencyType reason?: string source?: "architecture" | "scheduler" | "worker" | "user" | "system" } export interface TaskScope { write_area?: string expected_files?: string[] allowed_paths?: string[] denied_paths?: string[] } export interface VerificationPolicy { commands?: string[] required: boolean fallback_allowed: boolean } export interface TaskConstraints { max_turns: number soft_timeout_ms: number hard_timeout_ms: number retry_budget: number model_policy: "scheduler_forced" | "agent_select" model_provider_id?: ProviderID model_id?: ModelID } export interface TaskContextRefs { plan_ref?: string arc_ref?: string parent_task_results?: ArtifactID[] artifacts?: ArtifactID[] } export type WorkerOutputContract = | "ExecutorResult" | "ReviewerResult" | "DebuggerResult" | "CompactorResult" | "ExperienceMinerResult" export interface 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 } export interface TaskNode { task_id: TaskID spec: TaskSpec status: TaskStatus assigned_agent_id?: AgentID retry_count: number workspace_id?: WorkspaceID } export interface TaskGraph { session_id: SessionID tasks: Map dependencies: TaskDependencyRecord[] } export interface WorkspacePlan { strategy: "main" | "worktree" | "isolated_copy" path?: string base_ref?: string branch_name?: string reason: string } export interface ModelAssignment { mode: "scheduler_forced" | "agent_select" provider_id?: ProviderID model_id?: ModelID allowed_models?: Array<{ provider_id: ProviderID; model_id: ModelID }> requirement: ModelRequirement reason: string } export interface SchedulerWavePlan { wave_id: WaveID runnable_task_ids: TaskID[] serialized_task_ids: TaskID[] workspace_assignments: Record model_assignments: Record reason: string } export interface SchedulerRunResult { status: "completed" | "blocked" | "cancelled" | "idle" completed_task_ids: TaskID[] blocked_task_ids: TaskID[] summary: string } export interface Scheduler { create_tasks(session_id: SessionID, specs: TaskSpec[]): Promise add_dependency(session_id: SessionID, task_id: TaskID, dependency: TaskDependencySpec): Promise load_graph(session_id: SessionID): Promise run_until_idle(session_id: SessionID): Promise cancel_task(task_id: TaskID, reason: string): Promise } ``` ## 10. Worker and IPC Contracts IPC uses NDJSON messages with explicit direction, correlation IDs, and request/response envelopes. ```ts export type IpcDirection = "parent_to_worker" | "worker_to_parent" export interface IpcEnvelope { id: UUID direction: IpcDirection kind: IpcKind timestamp: ISOTimeString session_id: SessionID agent_id: AgentID correlation_id?: UUID protocol_version: number payload: TPayload } export type IpcKind = | "control" | "event" | "log" | "tool.call" | "tool.result" | "tool.stream" | "worker.result" | "worker.checkpoint" | "protocol.error" export type IpcMessage = | IpcEnvelope | IpcEnvelope | IpcEnvelope | IpcEnvelope | IpcEnvelope | IpcEnvelope | IpcEnvelope | IpcEnvelope | IpcEnvelope export interface LogPayload { level: "debug" | "info" | "warn" | "error" message: string data?: unknown } export type ControlMessage = | { type: "agent.start" version: 1 task_spec: TaskSpec context_pack: ContextPack runtime: AgentRuntimeContext } | { type: "agent.cancel"; reason: string } | { type: "agent.pause"; reason: string } | { type: "agent.resume" } | { type: "agent.extend_timeout"; extra_ms: number; reason: string } | { type: "worker.ready"; protocol_version: number; worker_version: string } export interface AgentRuntimeContext { session_id: SessionID project_id: ProjectID agent_id: AgentID worktree_path?: string permission_template: "main_direct" | "executor" | "reviewer" | "debugger" | "system" } export interface ContextPack { refs: { plan_ref?: string arc_ref?: string task_refs?: TaskID[] artifact_refs?: ArtifactID[] rule_refs?: string[] } assembled_context_ref?: ArtifactID notes?: string[] } export interface ToolCallRequest { tool_call_id: UUID tool_name: string input: unknown context: ToolExecutionContext } export interface ToolCallResponse { tool_call_id: UUID result: ToolResultEnvelope } export interface ToolStreamPayload { tool_call_id: UUID event: ToolEvent } export interface WorkerCheckpointPayload { checkpoint_id: UUID task_id: TaskID data: unknown } export interface ProtocolErrorPayload { error: AirError received_message_id?: UUID } ``` IPC handshake: parent sends `agent.start` after spawn; worker responds with `worker.ready` carrying `protocol_version` before receiving tasks. If `protocol_version` does not match, the parent terminates the worker with a `protocol.error`. IPC direction typing: `ParentToWorkerMessage` covers `control`, `tool.result`, `tool.stream`. `WorkerToParentMessage` covers `event`, `log`, `tool.call`, `worker.result`, `worker.checkpoint`, `protocol.error`. Both share `IpcEnvelope` shape; runtime validation enforces direction. Worker role: ```ts export interface WorkerRole { run(task_spec: TaskSpec, context_pack: ContextPack, runtime: WorkerRuntime): Promise> } export interface WorkerRuntime { emit(event: RuntimeEvent): Promise call_tool(name: string, input: I): Promise> checkpoint(data: unknown): Promise } ``` ## 11. WorkerResult Contracts ```ts export type WorkerStatus = "completed" | "failed" | "blocked" | "cancelled" export interface VerificationResult { name: string status: "passed" | "failed" | "skipped" | "unknown" evidence_ref_ids?: EvidenceRefID[] notes?: string } export interface Risk { severity: "low" | "medium" | "high" summary: string } export interface FollowUpTask { title: string description: string type?: TaskType } export interface WorkerResult { task_id: TaskID agent_id: AgentID agent_type: AgentType status: WorkerStatus summary: string changed_files: string[] diff_ref?: ArtifactID artifacts: ArtifactRef[] verification: VerificationResult[] risks: Risk[] follow_up_tasks: FollowUpTask[] evidence_refs: EvidenceRef[] result: TResult } export interface ExecutorResult { implementation_summary: string changed_files: string[] verification_commands: string[] } export interface ReviewerResult { verdict: "approved" | "changes_requested" | "blocked" findings: ReviewFinding[] } export interface ReviewFinding { severity: "low" | "medium" | "high" category: "correctness" | "security" | "scope" | "architecture" | "test" | "maintainability" message: string file?: string line?: number evidence_ref_ids?: EvidenceRefID[] } export interface BlockerReport { impact_level: "implementation" | "interface" | "architecture" | "product" | "permission" | "environment" | "policy" reason: string required_decision: string options?: Array<{ label: string; tradeoff: string }> evidence_ref_ids?: EvidenceRefID[] suggested_default?: string } export interface DebuggerResult { diagnosis: string root_cause?: string fixed: boolean blocker?: BlockerReport } export interface CompactorResult { summary_id: SummaryID range_start_message_id?: MessageID range_end_message_id?: MessageID token_estimate_before?: number token_estimate_after?: number } export interface MemoryCandidate { candidate_id: UUID memory_type: "project_rule" | "toolchain_rule" | "skill_update" | "debug_experience" summary: string evidence_ref_ids?: EvidenceRefID[] } export interface ExperienceMinerResult { candidates: MemoryCandidate[] } ``` ## 12. Tool Contracts ```ts export type ToolCategory = | "filesystem" | "shell" | "git" | "project" | "build" | "test" | "debug" | "static_analysis" | "gui" | "network" | "memory" | "context" | "artifact" | "permission" | "doctor" | "internal" export interface ToolPermissionSpec { read_paths?: PathPolicy write_paths?: PathPolicy execute?: boolean network?: boolean system_sensitive?: boolean credentials?: boolean } export interface PathPolicy { allow?: string[] deny?: string[] source?: string } export interface ToolDefinition { name: string version: number description: string input_schema: JsonSchema output_schema: JsonSchema category: ToolCategory permissions: ToolPermissionSpec streaming: boolean } export interface ToolExecutor { execute(input: I, context: ToolExecutionContext): Promise> } export interface StreamingToolExecutor { execute_streaming(input: I, context: ToolExecutionContext): AsyncIterable execute_final(input: I, context: ToolExecutionContext): Promise> } export interface ToolExecutionContext { session_id: SessionID project_id: ProjectID task_id?: TaskID agent_id?: AgentID origin_message_id?: MessageID permission_template: string cwd?: string } export interface ToolResultEnvelope { status: "ok" | "error" | "cancelled" output?: T error?: AirError artifact_ids?: ArtifactID[] evidence_ref_ids?: EvidenceRefID[] metadata?: JsonObject } export interface ToolEvent { type: "progress" | "artifact" | "result" payload: unknown } export interface ToolRegistry { register(definition: ToolDefinition, executor: ToolExecutor): void register_streaming(definition: ToolDefinition, executor: StreamingToolExecutor): void call(name: string, input: I, context: ToolExecutionContext): Promise> call_streaming(name: string, input: I, context: ToolExecutionContext): AsyncIterable> list(): ToolDefinition[] } ``` Tool streaming rule: `call()` consumes stream events internally and returns the final `ToolResultEnvelope`; `call_streaming()` exposes progress events and must end with exactly one final result envelope. ## 13. Permission Contracts ```ts export interface PermissionRequestContext { session_id: SessionID task_id?: TaskID agent_id?: AgentID tool_name?: string command?: string paths?: string[] network?: boolean requested_action: string reason: string } export type PermissionAction = "allow" | "deny" | "ask_user" | "block" | "refuse" | "announce_then_run" export type PermissionGrantScope = "none" | "once" | "session" | "project" | "global" export interface PermissionDecision { action: PermissionAction grant_scope: PermissionGrantScope risk_level: "low" | "medium" | "high" | "critical" reason: string required_confirmation?: boolean backup_required?: boolean evidence_ref_ids?: EvidenceRefID[] } export interface PermissionRecordResult { ok: boolean error?: AirError } export interface PermissionEngine { evaluate(context: PermissionRequestContext): Promise record(decision: PermissionDecision, context: PermissionRequestContext): Promise } ``` Permission checks are layered: tool capability policy → permission profile → task scope → path/command/network risk → credential/system-sensitive override → user prompt workflow. ## 14. Artifact and Evidence Contracts ```ts export interface ArtifactRef { artifact_id: ArtifactID uri: string path: string type: string sha256?: string size_bytes?: number } export interface EvidenceRef { evidence_ref_id: EvidenceRefID kind: string ref: string claim: string location_json?: unknown } export interface ArtifactCreateInput { type: string original_name?: string content?: string source_path?: string associated_entity_type?: string associated_entity_id?: string metadata?: JsonObject } export interface ArtifactContext { session_id: SessionID task_id?: TaskID agent_id?: AgentID tool_run_id?: ToolRunID command_run_id?: CommandRunID } export interface ArtifactReadResult { artifact: ArtifactRef content?: string | Uint8Array content_type?: string } export interface EvidenceCreateInput { kind: string ref: string claim: string location_json?: unknown task_id?: TaskID agent_id?: AgentID tool_run_id?: ToolRunID command_run_id?: CommandRunID artifact_id?: ArtifactID diagnostic_id?: string message_id?: MessageID } export interface ArtifactStore { create(input: ArtifactCreateInput, context: ArtifactContext): Promise get(artifact_id: ArtifactID): Promise read(artifact_id: ArtifactID): Promise } export interface EvidenceStore { create(input: EvidenceCreateInput): Promise list_for_entity(entity_type: string, entity_id: string): Promise } ``` Worker results embed full `EvidenceRef[]` when evidence is part of the worker conclusion; lightweight payloads may carry `evidence_ref_ids`. ## 15. Provider Contracts ```ts export interface ProviderCapabilityMatrix { provider_id: ProviderID provider_kind: "anthropic" | "openai" | "openrouter" | "ollama" | "anthropic_compatible" | "openai_compatible" | "custom" model_id: ModelID enabled: boolean quality_tier: "frontier" | "strong" | "standard" | "cheap" | "local" | "unknown" cost_tier: "high" | "medium" | "low" | "free" | "unknown" context_window_tokens?: number max_output_tokens?: number supports: JsonObject conversion: JsonObject } export interface ModelRequirement { required: JsonObject preferred?: JsonObject min_quality_tier?: "frontier" | "strong" | "standard" | "cheap" | "local" max_cost_tier?: "high" | "medium" | "low" | "free" min_context_window_tokens?: number allow_lossy_conversion?: boolean } export interface ProviderCompletionInput { provider_id: ProviderID model_id: ModelID canonical_format: "anthropic" messages: unknown[] tools?: unknown[] tool_choice?: unknown system?: unknown max_output_tokens?: number temperature?: number metadata?: JsonObject } export interface ProviderStreamEvent { type: "message_start" | "content_delta" | "tool_use" | "message_stop" | "error" payload: unknown } export interface ProviderAdapter { provider_id: ProviderID list_models(): Promise validate_model(model_id: ModelID): Promise complete(input: ProviderCompletionInput): AsyncIterable count_tokens?(input: unknown): Promise } export interface ProviderManager { select_model(requirement: ModelRequirement): Promise complete(input: ProviderCompletionInput): AsyncIterable } ``` ## 16. Context Contracts ```ts export interface ContextAssembleInput { task_id?: TaskID purpose: "main" | "execute" | "review" | "debug" | "compact" | "mine_experience" refs?: string[] token_budget?: number } export interface AssembledContext { canonical_format: "anthropic" messages: unknown[] omissions: string[] refs: string[] token_estimate?: number compaction_requested?: boolean messages_artifact_id?: ArtifactID } export interface ContextAssembler { assemble(input: ContextAssembleInput): Promise } export type PromptLayerLevel = | "runtime_invariant" | "role" | "project_rules" | "task_spec" | "architecture" | "evidence" | "tool_output" | "conversation" | "user_override" | "system_debug" export interface PromptLayer { level: PromptLayerLevel priority: number content: unknown token_estimate?: number source_ref?: string immutable?: boolean } export interface BudgetFitResult { fitted: PromptLayer[] omitted: PromptLayer[] omissions: string[] total_tokens: number } export interface PromptLayerLoader { load_runtime_invariant(): PromptLayer load_role(role: AgentType): PromptLayer load_project_rules(project: ProjectContext): PromptLayer[] load_task_context(spec: TaskSpec, context_refs: TaskContextRefs): PromptLayer[] } export interface CompactionPolicy { should_compact(messages: unknown[], token_budget: number): boolean compact(messages: unknown[], target_tokens: number): Promise } export interface CompactionResult { summary_id: SummaryID range_start_message_id?: MessageID range_end_message_id?: MessageID token_estimate_before: number token_estimate_after: number } ``` `context.assemble` tool may return `messages_artifact_id` when context is too large for inline return; `ContextPack.assembled_context_ref` points to that artifact. ## 17. Projection/UI Contracts ```ts export interface SessionProjection { session_id: SessionID title?: string status: string } export interface TaskProjection { task_id: TaskID title: string status: TaskStatus progress_text?: string } export interface AgentProjection { agent_id: AgentID agent_type: AgentType status: string task_id?: TaskID progress_text?: string } export interface ToolRunProjection { tool_run_id: ToolRunID tool_name: string status: string task_id?: TaskID } export interface CommandRunProjection { command_run_id: CommandRunID command: string status: "running" | "ok" | "error" | "cancelled" | "unknown" exit_code?: number } export interface ArtifactProjection { artifact_id: ArtifactID type: string uri: string } export interface PermissionPromptProjection { prompt_id: string subject: string risk_level: string reason: string options: string[] } export interface BlockerProjection { task_id?: TaskID reason: string required_decision: string } export interface ProjectionSnapshot { session?: SessionProjection tasks: TaskProjection[] agents: AgentProjection[] tool_runs: ToolRunProjection[] command_runs: CommandRunProjection[] artifacts: ArtifactProjection[] permission_prompts: PermissionPromptProjection[] blockers: BlockerProjection[] updated_at: ISOTimeString } // ProjectionStore.apply handles all durable events and key ephemeral events // (agent.heartbeat, task.progress, assistant.message.delta, tool.progress, // command.stdout.delta, command.stderr.delta). Unknown event types are ignored. export interface ProjectionStore { hydrate(session_id: SessionID): Promise apply(event: RuntimeEvent): void snapshot(): ProjectionSnapshot subscribe(handler: (snapshot: ProjectionSnapshot) => void): Subscription } export interface ProjectionClient { snapshot(): ProjectionSnapshot subscribe(handler: (snapshot: ProjectionSnapshot) => void): Subscription } ``` TUI may consume only `ProjectionClient` or projection contracts, never runtime internals, SQLite, or EventBus directly. V1 transport: TUI runs in-process with runtime. `ProjectionClient` is a direct interface reference, not IPC. UI command API (permission prompt responses, user blocker decisions) emits typed events through a narrow `UiCommandChannel` interface. This boundary is enforced by module imports: `packages/tui` may only import from `packages/contracts`, never from `packages/runtime/src/*`. ## 18. Capability Contracts ```ts export interface CapabilityManifestV1 { schema_version: 1 capability_id: CapabilityID display_name: string version: string description: string publisher?: string source: JsonObject trust_level: "built_in" | "project_local" | "user_installed" | "verified_publisher" | "untrusted" tools: Array<{ name: string version: number category: ToolCategory description: string input_schema: JsonSchema output_schema: JsonSchema streaming?: boolean permissions: ToolPermissionSpec }> dependencies?: JsonObject[] permissions: JsonObject events?: { produced?: string[]; consumed?: string[] } artifact_types?: string[] config_schema?: JsonSchema entrypoint?: JsonObject } export interface ValidationResult { ok: boolean errors: string[] warnings: string[] } export interface CapabilityRegistry { discover(): Promise validate(manifest: CapabilityManifestV1): Promise enable(capability_id: CapabilityID): Promise disable(capability_id: CapabilityID): Promise register_tools(tool_registry: ToolRegistry): Promise } ``` ## 19. Doctor and Logging Contracts ```ts export interface DoctorRunInput { mode: "read_only" | "fix" scope?: "startup" | "project" | "toolchain" | "release_gate" capabilities?: CapabilityID[] bundle?: boolean } export interface DoctorRunOutput { run_id: string status: "passed" | "issues_found" | "fixed" | "failed" issue_count: number blocking_issue_count: number report_artifact_id?: ArtifactID bundle_artifact_id?: ArtifactID } export interface DoctorService { run(input: DoctorRunInput): Promise } export interface Logger { debug(message: string, data?: unknown): void info(message: string, data?: unknown): void warn(message: string, data?: unknown): void error(message: string, data?: unknown): void } export interface DeveloperLogEncryptor { encrypt_log_chunk(chunk: Uint8Array): Promise } ## 20. Debug Knowledge and Learned Memory Contracts ```ts export interface DebugRecord { debug_record_id: UUID task_id?: TaskID failure_signature: string summary: string root_cause?: string fix_ref?: string evidence_refs?: EvidenceRef[] verification_refs?: EvidenceRef[] created_at: ISOTimeString updated_at: ISOTimeString metadata_json?: JsonObject } export interface DebugKnowledgeStore { insert(record: DebugRecord): Promise lookup_by_signature(failure_signature: string): Promise lookup_by_task(task_id: TaskID): Promise update(debug_record_id: UUID, patch: Partial): Promise } export interface LearnedMemory { memory_id: UUID memory_type: "project_rule" | "toolchain_rule" | "skill_update" | "debug_experience" summary: string content?: string source_ref?: EntityRef status: "candidate" | "promoted" | "archived" | "rejected" created_at: ISOTimeString updated_at: ISOTimeString metadata_json?: JsonObject } export interface LearnedMemoryStore { insert(memory: LearnedMemory): Promise lookup_by_type(memory_type: LearnedMemory["memory_type"]): Promise update_status(memory_id: UUID, status: LearnedMemory["status"]): Promise scan_stale(): Promise } ## 21. Diagnostic Contracts ```ts export type DiagnosticSeverity = "error" | "warning" | "info" | "hint" export interface Diagnostic { diagnostic_id: UUID task_id?: TaskID agent_id?: AgentID command_run_id?: CommandRunID artifact_id?: ArtifactID language?: string toolchain?: string severity: DiagnosticSeverity file?: string line?: number column?: number code?: string message: string semantic_signature: string created_at: ISOTimeString metadata_json?: JsonObject } ``` ``` ## 22. Compatibility and Versioning Rules 1. Public contract files in `packages/contracts` should export all interfaces in this document. 2. Event payload schema changes increment event `version`. 3. Tool schema changes increment tool `version`. 4. SQLite schema changes increment `schema_meta.schema_version` and require migration plan. 5. Provider adapter conversion behavior changes must update conversion tests. 6. Interface-breaking changes require ADR update and plan/todo synchronization. ## 23. Non-Negotiable Boundary Rules Do not implement paths that violate these contracts: ```text TUI → SQLite direct query TUI → runtime private service import worker → SQLite direct write worker → filesystem/shell/network side effect outside tool IPC tool → side effect without PermissionEngine capability → dependency install outside Doctor provider adapter → silent semantic prompt loss repository → scheduling policy EventBus → recovery source of truth LLM output → direct file/shell side effect ```