Imported from tuvl-io/tuvl (
AGENTS.md). Install upstream withnpx skills add tuvl-io/tuvl. Copyright stays with the author.
TUVL Framework — AI Agent Onboarding Guide
Welcome to the tuvl repository. You are an AI agent operating within the codebase of tuvl, a declarative, YAML-driven workflow orchestration engine built on FastAPI, PostgreSQL, and Redis.
This document serves as your primary reference for understanding the architecture, design philosophy, and strict coding conventions of this project. Read these rules carefully before suggesting or making changes.
1. Project Overview & Philosophy
- Declarative First:
tuvlrelies heavily on YAML configuration to generate FastAPI routes, PostgreSQL models, and AI workflows dynamically at startup. Always prefer YAML over writing Python boilerplate. - No Black Boxes: The engine executes exactly what is defined in the YAML pipelines.
- Data-Driven: Business logic is modeled as a state-machine of operations passing context variables.
- Strict Scope Guarding: All data operations enforce role-based access control (RBAC) scopes extracted from cryptographically verified Biscuit tokens.
2. Directory Structure & Conventions
The project strictly separates components by their YAML kind. When generating files for a tuvl project, place them in the correct directories:
models/➔ModelDefinition,EmbeddingRegistry,CollectionRegistryworkflows/➔Workflowdatasources/➔DataSourcellms/➔AgentModelfederation/➔FederationProvidernodes/➔ Custom Python implementation (@node()decorators)artifacts/➔ Named, versioned assets: prose.mdwith YAML front-matter (typesprompt|steering|skill) and structuredkind: ArtifactYAML (typesguardrail|hook|mcp). Referenced anywhere viaartifact://name[@version]; pin@versionfor production.
Never invent fields, step kinds, or document kinds. Rely only on the specified vocabulary. Every YAML document uses the spec-wrapped envelope (kind: + metadata: + spec:) — the flat root-level form is rejected.
3. Workflow Implementation Rules
Workflows are sequences of steps defined in YAML. Triggers receive HTTP request data into a shared context: dict[str, Any].
- Database Allowlist: Any model accessed inside a workflow (whether natively via
ModelOpor in custom Python nodes) must be explicitly listed in the workflow'sspec.context.models. Missing this triggers aPermissionError. - Reserved Keys: You are forbidden from mutating core engine context keys including:
_session,_db,_step,_response,_last_error,_last_error_type,_api_status_code,_context_model_versions,_schema_version,_instance_id,_user_id. - Routing Strictness: Every step must return a signal (e.g.,
default,true,false,error). You must define an explicit mapping for every non-default signal in the step'sroutes:map. Unmapped non-default signals will raise aRuntimeError. - Step Kinds (closed set, 8):
Functional,Agent,Router,APICall,MCP,ModelOp,Response,HumanInTheLoop. Never invent others;AutonomousAgentno longer exists.Agentis the one LLM step and REQUIRESmode: completion | autonomousat the step level (no default — the validator errors and the runtime raises).mode: completionis a single retried LLM call (fieldssystem/prompt);mode: autonomousis a bounded tool-calling loop (fieldssteering/tools(REQUIRED) /max_iterations/token_budget) where the model picks tools (eachagent.tools[].refnames another step in the workflow), observes results, and re-decides until it emits one of a declaredoutcome.enum. Each tool's description (REQUIRED) is sourced from the referenced step's top-leveldescription:, overridable byagent.tools[].description.- Outcome contract (both modes):
agent.outcome: {write, format: json|text, enum, map}.writeis the context key receiving the result (default<step_id>_result). Withenumdeclared the model must return an"outcome"field holding exactly one declared value — that is the route signal; map every enum value plus the applicable reserved exits (error,parse_error/timeoutin completion,max_iterations/budget_exceeded/abortedin autonomous,guardrail_violationwhen guardrails attach) inroutes:. The oldoutput.{format,map,signal_from}andoutcome.output_keyare gone. - Steering & skills:
agent.steeringis the persistent instruction, ALWAYS injected;agent.skillsare injected when relevant. Both (andsystem/prompt) take inline text orartifact://references —steering_filesand theagents/<workflow>__<stepId>/directory scheme are gone. - Guardrails & hooks:
agent.guardrails: {input|output|tools: [artifact://…]}attachtype: guardrailartifacts (closed checks:json_schema,regex_deny,max_chars,pii_mask,llm_judge); a failing check routes the reservedguardrail_violationsignal. Observe-onlytype: hookartifacts attach per step (hooks:) or workflow-wide (spec.hooks:) and never affect flow. - Supervisor: an optional per-workflow
spec.supervisorblock (a sibling ofsteps:, NOT a step) watches autonomousAgentruns live and can pause / abort / steer them via deterministicrulesand/or an LLM judge.criteriais inline text or anartifact://ref to asteeringartifact (criteria_filewas removed;steer_messagesets the rules-path steer text).abortexits through the reserved"aborted"signal. Operators observe/control at/api/agents/runs(scopesagent:observe/agent:control) or the Insight Agents dashboard.
- Outcome contract (both modes):
MCPsteps declaremcp.server: artifact://<name>pointing at atype: mcpartifact that owns the connection config (transport / url / headers / command / args / env) — inline transport blocks on the step are rejected.Routersupports a multi-waymatch:switch (match: { field: user.country }) for data-driven branching — keep deterministic routing here, never inside an agent.
4. Custom Python Nodes (Functional steps)
When YAML is insufficient, you can create a custom Functional Python node.
- One Node Per File: You MUST put only one
@node()decorator per Python file. - Filename Matching: The Python filename must strictly match the node runner name. For example, a node decorated with
@node("score_resume")must be placed innodes/score_resume.py.
5. PostgreSQL, Models, and Tenancy
- Every
ModelDefinitiongenerates a Postgres table and auto-generates a full suite of CRUD REST endpoints. - Multi-Tenancy: The engine ships single-tenant by default, but can operate in multi-tenant mode via Postgres RLS (Row-Level Security). Do not emit
tenant_idfields manually unless explicitly instructed. - Security: Fields containing PII or sensitive data must be marked with
secure: truein their YAML definition.
6. Security & Identity
- Biscuit Tokens: The engine relies on offline-verifiable Biscuit tokens for authentication.
- Dev Sentinel: During local development (
tuvl dev), a dev sentinel keypair is used. Production refuses to boot if this sentinel is active. - CRUD Scopes: Auto-generated CRUD routes enforce Biscuit scopes (e.g.,
{modelname.lower()}:read).
7. Developer Tooling
If asked to test or run the project locally, use the CLI commands:
tuvl dev(oruv run tuvl dev): Starts the engine with hot-reloading and mounts the Tuvl Insight Developer Portal athttp://localhost:8000/insight.tuvl dev --auto-login: Bypasses the local security screen for rapid API testing.tuvl run: Starts the highly-optimized production Uvicorn server without hot-reload.tuvl validate: Validates every YAML config, node, and cross-reference without starting the server.tuvl ship: Packages the project for production — validates it, generates a productionDockerfileand a Helm chart underdeploy/chart/<name>/, then builds the container image (--no-buildto skip the build,--pushto publish).
Agentic Note: For complete syntax schemas and deep reference, consult
docs/tuvl-agentic-manual.md. Always write minimal, functional code that conforms to these architectural invariants.