Architecture contract
OpenFlow uses nested ports and adapters. UI calls desktop, desktop calls orchestration, and orchestration hosts the engine through its ports. Provider adapters implement the engine's model interface.
┌─────────────────────────────────────────────────┐
│ UI (crates/ui) │
│ Presentation — visual state, interaction │
└──────────────────┬──────────────────────────────┘
│
│ implements Desktop inbound port
│ (invokes Tauri commands)
▼
┌──────────────────────────────────────────────────┐
│ Desktop (crates/desktop) │
│ Tauri Adapter — IPC transport, DTO mapping │
└──────────────────┬───────────────────────────────┘
│
│ implements Orchestration inbound port
│ (calls AppBackend)
▼
┌──────────────────────────────────────────────────┐
│ Orchestration (crates/orchestration) │
│ Application — run lifecycle, coordination │
└──────────────────┬───────────────────────────────┘
╱ │ ╲
entity/ backend/ adapters/
services composition drivers
│ │ │
└────────┼────────────┘
│
┌─────────┴──────────┐
│ implements │
│ engine ports │
▼ ▼
Engine (hex) Providers (adapter)
Execution engine LLM transportCrate roles
| Crate | Question it answers | Holds |
|---|---|---|
| engine | What is a valid workflow, and how does a run behave? | Product rules: graph validation, execution semantics, tool approval policy |
| orchestration | How does the desktop app store, load, wire, and host runs? | App rules: persistence, catalog merge, run sessions, tool I/O |
| providers | How do we talk to OpenAI/Anthropic? | The AiPort implementation |
| ui / desktop | How does the user interact? | Inbound adapters only |
Layer definitions
1. Engine (crates/engine)
- Role: Hexagon: workflow execution engine
- Scope: Workflow model, state machine, ports (
AiPort,ToolPort). The self-drivingInteractiveEngine::run()callsAiPortandToolPortinternally and surfaces only interaction pauses (NeedsInteraction) to orchestration. - Public interface: Traits in
ports/+ model types - Dependencies: None upward; only serialization, async traits, tokio
2. Providers (crates/providers)
- Role: Outbound adapter: implements
engine::AiPort - Scope: LLM protocol/auth/transport
- Public interface:
create_provider()→Box<dyn AiPort> - Dependencies:
enginetypes, HTTP client, serde
3. Orchestration (crates/orchestration)
- Role: Inbound adapter for Engine; outbound provider for Desktop
- Scope: Run lifecycle, session state, coordination, approval/input loops, event fanout;
ToolPortImplexecutes tools and subagents - Sub-roles:
backend/: composition root; wires services and adaptersagent/,workflow/,project/,settings/,tool/: flat entity folders with application/domain logicrun/: run coordination, execution host, persistence policy, and projected run stateadapters/storage/: concrete JSON/file persistence implementationsadapters/tool_impl/andadapters/infrastructure/: concrete tool, git, LSP, and other I/O drivers
- Public interface:
AppBackend, the façade Desktop calls - Dependencies:
engine+providers, no upward
4. Desktop (crates/desktop)
- Role: Inbound adapter for UI; calls Orchestration
- Scope: Tauri transport only: IPC wiring, command serialization, DTOs
- Public interface: Tauri command handlers that map to Orchestration calls
- Dependencies:
orchestration::AppBackend, no upward
5. UI (crates/ui)
- Role: Presentation layer; implements Desktop's command interface
- Scope: Visual state, interaction, rendering
- Public interface: SolidJS components, TypeScript types; the React Flow canvas is isolated behind the UI boundary
- Dependencies: Tauri invoke client (to Desktop), no upward
State ownership
Orchestration
- Active session/run state: approval queues, retry counters, execution trace
- Runtime coordination: what step runs next, when to pause for approval
- Not execution semantics (the engine owns those)
Engine
- Model types and invariants (Workflow, Node, ToolCall, ToolResult)
- Legal transitions and validation rules
- Execution semantics: when the engine advances, what the error states are
Infrastructure / Persistence
- Durable state (workflows.json, settings.json, agent definitions)
- Credentials, cache
- Not runtime session state (orchestration owns that)
Dependency rules
Allowed (dependencies point inward):
- UI → Desktop (invoke Tauri commands)
- Desktop → Orchestration (call
AppBackendmethods) - Orchestration → Engine (use model types, drive
InteractiveEngine::run) - Orchestration → Providers (via
Box<dyn AiPort>, no concrete types) - Providers → Engine (implements
AiPorttrait)
Forbidden:
- UI → Engine or Providers (bypass orchestration)
- UI → Orchestration directly (go through Desktop)
- Desktop → Engine or Providers (go through Orchestration)
- Desktop → Orchestration internals (
run/execution/, entity folders, or adapters); only theAppBackendpublic façade - Orchestration → UI or Desktop (upward)
- Engine → anything outward (no imports of provider, orchestration, UI, desktop)
- Providers → UI or Desktop
CI enforcement
CI runs these checks through ./scripts/check-architecture.sh. The machine-readable rules live in ../../crates/workspace-checks/arch-check-rules.toml; this contract is the human-readable source of truth.
Tier 2 (Phase A): inter-crate
- Workspace dependency graph: path deps in each crate's
Cargo.tomlmatch the allowed inward graph. - Forbidden cross-crate
use: per-crate ban tables (e.g.desktopmust notuse engine::). - Engine forbidden deps:
enginemust not depend on transport/GUI crates (reqwest,tauri, …). Pure validation crates are allowed, such asjsonschemawith default features disabled so$refresolution cannot perform I/O. - Legacy crate aliases:
domainandworkflow_coreare banned in all workspaceusepaths.
Tier 3 (Phase B): seams and layout
orchestration → providersallowlist:orchestration/srcmay import only listed config/factory symbols.AiClientis banned; usecreate_provider.Engine invocation locality: only
orchestration/src/run/execution/may callInteractiveEngine::new.Orchestration domain folders:
agent/,workflow/,project/,settings/,tool/must notuse crate::adapters::.UI Tauri seam:
@tauri-apps/*imports only inapi.ts,port.ts, and allowed test files. Frontend callers use the wrappers inapi.ts.Orchestration domain store ban:
agent/,workflow/,project/,settings/,tool/must notuse crate::{agent_store, flow_store, ...}. They depend on port traits;backend/wires theFile*Stores.
Deferred: tool/ → lsp narrowing; providers → engine submodule allowlist. The architecture rules file has the exact enforced scope.
Engine invocation rule
- Only
orchestration/run/execution/may constructInteractiveEngine.run/execution/drive/mod.rshostsengine.run()and coordinates state, interaction, and event delivery. - Interactive runs call
InteractiveEngine::run(); orchestration handles onlyNeedsInteractionand terminal outcomes. - Tool and subagent execution goes through
ToolPortImpl(engine::ToolPort). - UI, Desktop, and Providers never call the engine directly.
Port rule
The engine defines outbound ports (traits) for what it needs from external systems: AiPort and ToolPort.
Orchestration implements ToolPortImpl, calls AiPort via Box<dyn>, and resumes paused engines through InteractiveEngine::on_human_input / InteractiveEngine::on_tool_decision. Because orchestration depends on dyn AiPort, provider implementations can change without moving transport details into the engine.
Provider-specific branching stays in providers/. The engine does not know which LLM it is calling.
UI-to-Desktop calls go through typed wrappers in crates/ui/src/api.ts. Add a new port only when code is typed on that interface.
Testability rule
- UI tests: Mock
api.tswrappers or Tauri invoke/event APIs at the boundary - Desktop tests: Mock
AppBackendmethods - Orchestration tests: Inline
impl AiPortstubs; use acceptance fixtures for critical paths - Provider tests: Verify wire format mapping; test
AiClientcontract compliance - Engine tests: Colocated unit tests for state machine and model invariants
Development lanes
Pick source guidance and verification by the behavior you changed. The dependency rules above apply to every lane.
| Touched area | Lane | Primary source docs | Required local verification |
|---|---|---|---|
crates/engine/** | Engine semantics | crates/engine/AGENTS.md, this contract, docs/glossary.md | cargo nextest run -p engine; add workflow acceptance when run behavior, prompts, ports, tools, or telemetry change |
crates/orchestration/src/run/** | Run orchestration | crates/orchestration/AGENTS.md, docs/architecture/threading-concurrency.md, docs/contributing/testing-workflows.md | cargo nextest run -p orchestration --lib; cargo nextest run -p orchestration --test workflow_acceptance --no-capture for execution behavior |
crates/orchestration/src/agent.rs, crates/orchestration/src/{workflow,project,settings,tool}/** | Application/domain service | crates/orchestration/AGENTS.md, docs/contributing/coding-patterns.md | cargo nextest run -p orchestration --lib; add focused store/backend tests when persistence or IPC-visible behavior changes |
crates/orchestration/src/adapters/** | Concrete adapter/I/O | crates/orchestration/AGENTS.md, this contract | Focused adapter tests plus ./scripts/check-architecture.sh |
crates/providers/** | Provider adapter | crates/providers/AGENTS.md | cargo nextest run -p providers; live smoke only when intentionally checking a real provider |
crates/desktop/** | Desktop IPC adapter | crates/desktop/AGENTS.md | cargo nextest run -p desktop; update UI seam tests when payloads change |
crates/ui/** | UI/Desktop seam and presentation | crates/ui/AGENTS.md | npm --prefix crates/ui run typecheck; focused Vitest for changed helpers/components |
| Cross-crate behavior | Full workflow slice | Root AGENTS.md, this contract, docs/contributing/testing-workflows.md | ./scripts/test-fast.sh --execution; run ./scripts/verify.sh before handoff |
Agent-facing skills may summarize this table but must treat these docs as authoritative. If a skill, rule file, or memory contradicts this contract, update the secondary artifact; don't copy stale architecture facts forward.
Change review checklist
- Does this change add a dependency that violates the allowed/forbidden rules?
- Does UI/Desktop bypass layers to call engine/provider code?
- Did provider-specific logic leak into Orchestration or Engine?
- Does any new runtime state live outside Orchestration without justification?
- Are new public interfaces declared at the correct seam (Desktop vs Orchestration vs Engine)?
- Does Engine remain free of filesystem/tool I/O (delegated to
ToolPort)? - Do Desktop/UI contain only transport logic, not orchestration or engine rules?
