Files
AirCoding/AirPlan/docs/architecture/interface-contracts-v1.md
AirCoding 453df09c21 Detailed design: resolve four P1 cross-review findings
Apply repairs identified by the four-model cross-review (DeepSeek,
MIMO 2.5 Pro, GPT-5.5 Pro, Opus 4.8) and verify by regression:

- P1-01 Worker exit codes: align overview §11 and detailed-design §8.1
  with baselineV1 §8 (0 protocol-level completion / 1 uncaught exception
  / 2 startup or protocol error / 3 permission error / 4 parent cancelled
  / 5 hard timeout killed). Record that task outcomes are reported via
  WorkerResult.status, not exit codes.
- P1-02 PromptLayerLevel enum: add "safety" to interface-contracts §16
  so the enum fully covers prompt-layering-v1 §2 L0-L9 (plus
  system_debug applied within L9).
- P1-03 EventStore.project error handling: document in detailed-design
  §5.3 that a project() exception rolls back the full transaction,
  suppresses EventBus.publish(), returns AirError{kind:"system_error"},
  and triggers referential_check() on FK-off inconsistencies.
- P1-04 PromptLayerLoader completeness: record in detailed-design §10.2
  that PromptLayerLoader only owns L0/L1/L3/L5 while ContextAssembler
  composes L2/L4/L6/L7/L8/L9 from PermissionEngine, TaskSpec,
  SessionStore, and ToolRegistry sources; clarify runtime-role prompts.

Regression confirms baselineV1, overview, and detailed-design now share
identical exit code semantics, the PromptLayerLevel enum covers all ten
layers, EventStore error semantics are explicit, and the PromptLayer
loading responsibility split is fully documented.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 09:38:16 +08:00

35 KiB

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

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<string, unknown>
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }
// JsonSchema<T> is nominal-only: T provides no compile-time runtime check, but documents the expected shape for tool schema consumers.
export type JsonSchema<T = unknown> = JsonObject

Testability primitives:

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

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

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

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<TPayload = unknown> {
  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

export interface TransactionHandle {
  id: string
}

export interface DatabaseHandle {
  path: string
}

export interface TransactionManager {
  transaction<T>(fn: (tx: TransactionHandle) => Promise<T>): Promise<T>
}

Repository records mirror db-schema-v1.md columns and use snake_case.

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<TaskRecord, "retry_count" | "started_at" | "completed_at" | "heartbeat_at" | "worker_result_json"> & {
  retry_count?: number
}
export type TaskUpdate = Partial<Omit<TaskRecord, "id" | "session_id" | "created_at">>

export type PersistedEventInsert = Omit<PersistedEventRecord, "route_text"> & {
  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:

export interface Repository<TRecord, TInsert, TUpdate> {
  get(id: string, tx?: TransactionHandle): Promise<TRecord | undefined>
  insert(record: TInsert, tx?: TransactionHandle): Promise<void>
  update(id: string, patch: TUpdate, tx?: TransactionHandle): Promise<void>
}

export interface TaskRepository extends Repository<TaskRecord, TaskInsert, TaskUpdate> {
  list_by_status(session_id: SessionID, statuses: TaskStatus[], tx?: TransactionHandle): Promise<TaskRecord[]>
  list_runnable_candidates(session_id: SessionID, tx?: TransactionHandle): Promise<TaskRecord[]>
}

export interface EventRepository {
  insert(event: PersistedEventRecord, tx: TransactionHandle): Promise<void>
  query(filter: EventFilter): Promise<PersistedEventRecord[]>
}

Repositories are persistence adapters only. They must not contain Scheduler, permission, or projection policy.

7. EventBus, EventStore, and Ingestion Contracts

export interface Subscription {
  unsubscribe(): void
}

export interface EventBus {
  publish<T>(event: RuntimeEvent<T>): void
  subscribe(filter: EventFilter, handler: (event: RuntimeEvent) => void | Promise<void>): Subscription
  drain?(): Promise<void>
}

export interface EventStore {
  append<T>(event: RuntimeEvent<T>, options?: EventAppendOptions): Promise<void>
  append_many(events: RuntimeEvent[], options?: EventAppendOptions): Promise<void>
  query(filter: EventFilter): Promise<RuntimeEvent[]>
}

export interface EventAppendOptions {
  expected_session_id?: SessionID
  transaction?: TransactionHandle
}

export interface EventIngestor {
  ingest<T>(event: RuntimeEvent<T>): Promise<void>
  ingest_ephemeral<T>(event: RuntimeEvent<T>): Promise<void>
}

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

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<ProjectContext | undefined>
  initialize(project_root: string, options?: ProjectInitOptions): Promise<ProjectContext>
  open(project_root: string): Promise<ProjectContext>
}

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<SessionContext>
  close_session(session_id: SessionID): Promise<void>
}

9. Task and Scheduler Contracts

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<TaskID, TaskNode>
  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<TaskID, WorkspacePlan>
  model_assignments: Record<TaskID, ModelAssignment>
  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<void>
  add_dependency(session_id: SessionID, task_id: TaskID, dependency: TaskDependencySpec): Promise<void>
  load_graph(session_id: SessionID): Promise<TaskGraph>
  run_until_idle(session_id: SessionID): Promise<SchedulerRunResult>
  cancel_task(task_id: TaskID, reason: string): Promise<void>
}

10. Worker and IPC Contracts

IPC uses NDJSON messages with explicit direction, correlation IDs, and request/response envelopes.

export type IpcDirection = "parent_to_worker" | "worker_to_parent"

export interface IpcEnvelope<TPayload = unknown> {
  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<ControlMessage>
  | IpcEnvelope<RuntimeEvent>
  | IpcEnvelope<LogPayload>
  | IpcEnvelope<ToolCallRequest>
  | IpcEnvelope<ToolCallResponse>
  | IpcEnvelope<ToolStreamPayload>
  | IpcEnvelope<WorkerResult>
  | IpcEnvelope<WorkerCheckpointPayload>
  | IpcEnvelope<ProtocolErrorPayload>

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:

export interface WorkerRole<TResult = unknown> {
  run(task_spec: TaskSpec, context_pack: ContextPack, runtime: WorkerRuntime): Promise<WorkerResult<TResult>>
}

export interface WorkerRuntime {
  emit(event: RuntimeEvent): Promise<void>
  call_tool<I, O>(name: string, input: I): Promise<ToolResultEnvelope<O>>
  checkpoint(data: unknown): Promise<void>
}

11. WorkerResult Contracts

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<TResult = unknown> {
  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

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<I = unknown, O = unknown> {
  name: string
  version: number
  description: string
  input_schema: JsonSchema<I>
  output_schema: JsonSchema<O>
  category: ToolCategory
  permissions: ToolPermissionSpec
  streaming: boolean
}

export interface ToolExecutor<I = unknown, O = unknown> {
  execute(input: I, context: ToolExecutionContext): Promise<ToolResultEnvelope<O>>
}

export interface StreamingToolExecutor<I = unknown, O = unknown> {
  execute_streaming(input: I, context: ToolExecutionContext): AsyncIterable<ToolEvent>
  execute_final(input: I, context: ToolExecutionContext): Promise<ToolResultEnvelope<O>>
}

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<T = unknown> {
  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<I, O>(definition: ToolDefinition<I, O>, executor: ToolExecutor<I, O>): void
  register_streaming<I, O>(definition: ToolDefinition<I, O>, executor: StreamingToolExecutor<I, O>): void
  call<I, O>(name: string, input: I, context: ToolExecutionContext): Promise<ToolResultEnvelope<O>>
  call_streaming<I, O>(name: string, input: I, context: ToolExecutionContext): AsyncIterable<ToolEvent | ToolResultEnvelope<O>>
  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

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<PermissionDecision>
  record(decision: PermissionDecision, context: PermissionRequestContext): Promise<PermissionRecordResult>
}

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

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<ArtifactRef>
  get(artifact_id: ArtifactID): Promise<ArtifactRef | undefined>
  read(artifact_id: ArtifactID): Promise<ArtifactReadResult>
}

export interface EvidenceStore {
  create(input: EvidenceCreateInput): Promise<EvidenceRef>
  list_for_entity(entity_type: string, entity_id: string): Promise<EvidenceRef[]>
}

Worker results embed full EvidenceRef[] when evidence is part of the worker conclusion; lightweight payloads may carry evidence_ref_ids.

15. Provider Contracts

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<ProviderCapabilityMatrix[]>
  validate_model(model_id: ModelID): Promise<ProviderCapabilityMatrix>
  complete(input: ProviderCompletionInput): AsyncIterable<ProviderStreamEvent>
  count_tokens?(input: unknown): Promise<number>
}

export interface ProviderManager {
  select_model(requirement: ModelRequirement): Promise<ModelAssignment>
  complete(input: ProviderCompletionInput): AsyncIterable<ProviderStreamEvent>
}

16. Context Contracts

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<AssembledContext>
}

export type PromptLayerLevel =
  | "runtime_invariant"  // L0
  | "role"               // L1
  | "safety"             // L2 (added to align with prompt-layering-v1 §2)
  | "project_rules"      // L3
  | "architecture"       // L4
  | "task_spec"          // L5
  | "evidence"           // L6
  | "conversation"       // L7
  | "tool_output"        // L8
  | "user_override"      // L9
  | "system_debug"       // (applied within L9 when present)

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<CompactionResult>
}

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

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<void>
  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

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<CapabilityManifestV1[]>
  validate(manifest: CapabilityManifestV1): Promise<ValidationResult>
  enable(capability_id: CapabilityID): Promise<void>
  disable(capability_id: CapabilityID): Promise<void>
  register_tools(tool_registry: ToolRegistry): Promise<void>
}

19. Doctor and Logging Contracts

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<DoctorRunOutput>
}

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<Uint8Array>
}

## 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<void>
  lookup_by_signature(failure_signature: string): Promise<DebugRecord[]>
  lookup_by_task(task_id: TaskID): Promise<DebugRecord[]>
  update(debug_record_id: UUID, patch: Partial<DebugRecord>): Promise<void>
}

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<void>
  lookup_by_type(memory_type: LearnedMemory["memory_type"]): Promise<LearnedMemory[]>
  update_status(memory_id: UUID, status: LearnedMemory["status"]): Promise<void>
  scan_stale(): Promise<LearnedMemory[]>
}

## 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