Skip to content

Architecture

Understanding how tuvl components work together.

Design Philosophy

tuvl follows four core principles:

  1. Local-First — every component runs on your infrastructure; no cloud dependency required
  2. YAML-Driven — business logic lives in version-controlled YAML, not scattered across controller code
  3. AI is a step, not the system — LLM calls are first-class workflow steps alongside Python functions, HTTP calls, and MCP tools
  4. 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:

  1. Deserialises the workflow YAML into step configs
  2. Maintains the mutable context dictionary throughout execution
  3. Resolves the next step via signal-based routing after every step
  4. Delegates to the appropriate step runner (functional / agent / api_call / mcp / HumanInTheLoop / model-op / response)
  5. 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