Architecture¶
Understanding how tuvl components work together.
Design Philosophy¶
tuvl follows four core principles:
- Local-First — every component runs on your infrastructure; no cloud dependency required
- YAML-Driven — business logic lives in version-controlled YAML, not scattered across controller code
- AI is a step, not the system — LLM calls are first-class workflow steps alongside Python functions, HTTP calls, and MCP tools
- Auditable by default — every execution emits structured step events with signals, snapshots, and timings
System Overview¶
graph TB
subgraph "Client Layer"
A[HTTP Client]
B["@tuvl/client SDK"]
DevUI["tuvl insight UI\n(dev mode only)"]
end
subgraph "API Layer"
C[FastAPI Server]
D[Workflow Routes]
E[CRUD Routes]
F_AUTH[Auth Routes /auth/*]
DevMW["DevMiddleware\n(TUVL_DEV_MODE=true)"]
end
subgraph "gRPC-Web Layer"
G_GRPC["sonora ASGI\n/grpc/"]
DevSvc["DevServicer\n(dev mode only)"]
IamSvc[IamServicer]
ExecSvc[ExecutionServicer]
end
subgraph "Engine Layer"
F[WorkflowEngine]
G[NODE_REGISTRY]
H[Agent Runner / LiteLLM]
HITL[HITL Pause/Resume]
OTel[OTel Tracer]
end
subgraph "Data Layer"
I[BaseRepository]
J[MODEL_REGISTRY]
Redis[(Redis\nHITL state)]
end
subgraph "External"
L[(PostgreSQL)]
M["Ollama / OpenAI\n/ Anthropic / ..."]
N[MCP Servers]
Collector[OTLP Collector]
end
A --> C
B --> C
B --> G_GRPC
DevUI --> DevMW
DevMW --> G_GRPC
C --> D
C --> E
C --> F_AUTH
G_GRPC --> DevSvc
G_GRPC --> IamSvc
G_GRPC --> ExecSvc
D --> F
ExecSvc --> F
E --> I
F --> G
F --> H
F --> HITL
F --> OTel
G --> I
H --> M
F --> N
HITL --> Redis
I --> J
I --> L
OTel --> Collector
Core Components¶
gRPC-Web Layer¶
All real-time and developer tooling traffic uses gRPC-Web via sonora — a pure-ASGI gRPC-Web server that runs inside the same FastAPI process.
Three servicers are registered under the /grpc/ mount:
| Servicer | Proto | Responsibility |
|---|---|---|
ExecutionServicer |
execution.proto |
Streaming workflow execution events |
IamServicer |
iam.proto |
User/role/scope management, token lifecycle |
DevServicer |
dev.proto |
Dev portal: file CRUD, Lens, Spectrum, AI chat |
DevServicer is only active when TUVL_DEV_MODE=true. In production it is not registered and the /grpc/dev.* routes do not exist.
Clients use @protobuf-ts/grpcweb-transport (browser and Node.js). Both the tuvl insight developer portal and the @tuvl/client SDK speak gRPC-Web.
Workflow Engine¶
WorkflowEngine (and its subclass WorkflowTestRunner for testing) is the orchestrator. For each request it:
- Deserialises the workflow YAML into step configs
- Maintains the mutable context dictionary throughout execution
- Resolves the next step via signal-based routing after every step
- Delegates to the appropriate step runner (functional / agent / api_call / mcp / HumanInTheLoop / model-op / response)
- Emits an OTel span per step when a tracer is configured
Node Registry¶
A global mapping of node names → async Python functions:
from tuvl.core.nodes.base import node, NODE_REGISTRY
@node("my_node")
async def my_node(ctx: dict) -> dict:
return ctx
# NODE_REGISTRY["my_node"] is now set
Nodes must be importable at process startup. The engine loads all Python files from the project's nodes/ directory automatically.
Model Registry¶
Dynamic SQLModel classes generated from YAML ModelDefinition files at startup. Each registered model also produces auto-generated CRUD routes at /api/{model_name}.
Repository Pattern¶
BaseRepository[T] provides a clean async data-access layer backed by SQLAlchemy:
from tuvl.core.repositories.registry import get_repository
repo = get_repository("Contact", ctx["_session"])
contact = await repo.add({"email": "...", "name": "..."})
See Repositories for the full API.
Human-in-the-Loop (HITL)¶
The HumanInTheLoop step kind pauses execution and stores state in Redis. A reviewer approves or rejects via the API (or the tuvl insight UI), after which the engine resumes exactly where it stopped. Timeouts are configurable per step.
OpenTelemetry¶
Every step emits a span with signal, duration_ms, and context attributes when TUVL_TELEMETRY_ENABLED=true. Configure the OTLP exporter in .tuvl/telemetry.yaml. See Telemetry.
Request Flow¶
sequenceDiagram
participant C as Client
participant F as FastAPI
participant W as WorkflowEngine
participant N as Node / Agent
participant R as Repository
participant DB as PostgreSQL
C->>F: POST /api/my-endpoint
F->>F: Validate input schema
F->>W: run(context)
loop For each step
W->>W: Resolve step kind
alt functional
W->>N: node_func(context)
N-->>W: updated context / signal
else agent
W->>N: LiteLLM call + prompt
N-->>W: JSON → context keys + signal
else HumanInTheLoop
W->>W: Pause, store state in Redis
Note over W: Resumed by reviewer
end
W->>W: Follow signal → next step
end
W-->>F: final context
F->>DB: COMMIT
F-->>C: JSON response
Step Kinds¶
| Kind | Description |
|---|---|
functional |
Call a registered Python node from NODE_REGISTRY |
agent |
LLM call via LiteLLM; structured JSON output maps to context keys |
api_call |
Outbound HTTP request; response mapped into context |
mcp |
Invoke a tool on an MCP server (stdio or SSE) |
HumanInTheLoop |
Pause execution; await a human approve/reject decision |
model-op |
Direct CRUD operation on a registered data model |
router |
Evaluate a condition expression; branch via signal |
response |
Shape and emit the final HTTP response from context keys |
Context Keys¶
All engine-injected keys begin with _ and are stripped from HTTP responses:
| Key | Set by | Description |
|---|---|---|
_session |
Engine | AsyncSession for the current request |
_db |
Engine | Unit-of-work repository accessor |
_user_id |
Auth middleware | Authenticated user ID |
_last_error |
Engine | Last step error string |
_api_status_code |
api_call step |
HTTP status of last outbound call |
_response |
response step |
Shaped payload returned to client |
Next Steps¶
- Workflows — Step-by-step workflow configuration
- Nodes — Building custom node functions
- Context Object — The context lifecycle in detail