Deterministic workflow topology enforcement for LLM-powered systems.
Project description
LLM Workflow Router
Deterministic workflow topology enforcement for LLM-powered systems.
LLM Workflow Router is a stateless middleware engine designed to enforce explicit execution topology in AI systems that rely on large language models. It evaluates structured interaction metadata against strictly declared workflow rules and returns a terminal state.
It controls structure — not content.
Overview
Modern LLM-driven systems frequently suffer from:
- Recursive tool invocation loops
- Circular container transitions
- Cross-container contamination
- Implicit fallback behavior
- Unbounded workflow escalation
- Inconsistent refusal logic
Most mitigation strategies limit volume (timeouts, max tool calls, retries).
LLM Workflow Router enforces topology explicitly.
The engine evaluates structured interaction metadata and returns one of three terminal states:
- PROCEED
- REFUSE
- PAUSE
No content inspection.
No moderation.
No orchestration.
No mutation of input.
Only structural enforcement.
Core Design Principles
- Deterministic evaluation
- Stateless per evaluation
- Metadata-only inspection
- Explicit transitions only
- Static configuration (v1)
- Strict validation at load time
- No silent fallback
- Host application retains execution control
Same input → same output.
Architecture
Application
↓
WorkflowEngine.evaluate(metadata)
↓
[ PROCEED | REFUSE | PAUSE ]
↓
Application decides next action
The router does not:
- Execute tools
- Retry calls
- Modify prompts
- Orchestrate sessions
It enforces topology and returns a decision.
Configuration
Workflow rules are defined using static YAML configuration.
entrypoints:
- entry
containers:
entry:
allow_transitions:
- support
- REFUSE
allow_reentry: false
max_invocations: 2
support:
allow_transitions:
- faq
- REFUSE
allow_reentry: false
max_invocations: 3
faq:
allow_transitions:
- REFUSE
allow_reentry: true
max_invocations: 5
Each container defines:
- Explicit allowed transitions
- Whether re-entry is permitted
- Maximum invocation depth
Implicit transitions are not allowed.
Entrypoints
The optional top-level entrypoints list declares the authoritative root
containers. A workflow may have several roots — a support flow, a sales flow,
and an FAQ flow can each start in their own container — so entrypoints is a
list. When declared, any indegree-0 container that is not listed is treated
as an orphan and rejected. If entrypoints is omitted, roots are inferred from
graph shape (indegree 0) for backward compatibility, and an ambiguity warning
is raised when more than one root is inferred. See
examples/multi_entry_config.yaml.
Topology Validation
At configuration load time, the router performs strict validation:
- Invalid transition targets
- Unknown containers
- Unknown declared entrypoints
- Dead-end containers
- Missing entry points
- Orphan containers (indegree 0, not a declared entrypoint)
- Unreachable containers
- Self-transition contradictions
- Cycle detection
- Re-entry safety enforcement
Configuration errors raise exceptions immediately.
Fail loudly at load time.
Never fail silently at runtime.
Runtime Evaluation
The engine evaluates an immutable metadata structure:
InteractionMetadata:
container: str
previous_state: InteractionState
transition_history: List[str]
invocation_depth: Dict[str, int]
requested_action: str
trace_id: Optional[str]
Returns:
EvaluationResult:
state: InteractionState
container: str
reason: Optional[ReasonCode]
trace_id: Optional[str]
No exceptions during normal evaluation.
Only terminal states are returned.
CLI Usage
Install:
pip install llm-workflow-router
Validate configuration:
llm-router validate config.yaml
Analyze topology:
llm-router analyze config.yaml
Evaluate metadata:
llm-router run --config config.yaml --metadata metadata.json
Render the topology as a diagram:
llm-router graph config.yaml # Mermaid (paste into any Mermaid renderer)
llm-router graph config.yaml --format dot # Graphviz DOT
Like analyze, graph renders broken topologies too — unknown transition
targets are drawn and flagged, which is exactly what you want while debugging
a config.
Session Layer (optional)
The engine is stateless by design: every evaluate() receives a complete
metadata snapshot. If you'd rather not do that bookkeeping yourself,
WorkflowSession does it for you — and only advances on PROCEED:
from router import WorkflowEngine, WorkflowSession
engine = WorkflowEngine(cfg)
session = WorkflowSession(engine, entry="entry", trace_id="req-42")
result = session.request("support") # entry -> support
result = session.request("faq") # support -> faq
result = session.request("REFUSE") # terminal; session closes
session.history # ("entry", "support")
session.invocation_depth # {"entry": 1, "support": 1, "faq": 1}
A REFUSE closes the session. A PAUSE suspends it until resume().
session.snapshot(target) exposes the exact metadata the next request would
evaluate, so the session is fully auditable and you can drop down to raw
engine.evaluate() at any time. The engine itself remains pure and stateless.
OpenAI Agents SDK Integration
Agents are containers. Handoffs are transitions. Attach a TopologyGuard to
a run and every handoff is structurally evaluated before the next agent
executes — a refused handoff raises TopologyViolation and aborts the run
loudly instead of letting the agent graph wander:
pip install "llm-workflow-router[openai-agents]"
from agents import Agent, Runner
from router import WorkflowEngine
from router.integrations.openai_agents import TopologyGuard, TopologyViolation
guard = TopologyGuard(engine, entry="triage", trace_id="run-001")
try:
result = await Runner.run(triage_agent, "I was double-charged.", hooks=guard)
except TopologyViolation as violation:
print("Refused:", violation.result.reason)
# Full structural audit trail of the run:
print(guard.session.history, guard.session.invocation_depth)
By default an agent's name is its container name; pass container_for= to
map differently. The guard is content-blind — it never reads prompts,
messages, or tool arguments. See
examples/openai_agents_example.py for a
complete runnable triage → billing → refunds system.
Why not just LangGraph (or my orchestrator's built-in graph)?
Orchestrators describe structure. This engine enforces it — as a separate, framework-agnostic layer with properties orchestrators don't give you:
- Independent enforcement. The topology lives outside your agent framework, so a prompt-induced detour, a buggy handoff, or a framework upgrade can't silently widen what's reachable. The declared graph is a contract, and violations fail loudly with structured reason codes.
- Framework-agnostic. The same YAML config governs an OpenAI Agents SDK app today and whatever you migrate to next year. Adapters are thin; the contract is portable.
- Auditable determinism. Same metadata + same config → same decision,
every time. Combined with the
wfrouter.*OpenTelemetry conventions, you get compliance-grade answers to "why was this transition refused?" — a reason code, not a vibe. - Load-time topology analysis. Cycles, orphans, dead ends, and unreachable states are caught before anything runs — the kind of static validation industrial control systems have had for decades and agent frameworks mostly don't.
If you're happy inside one orchestrator and don't need independent structural guarantees, its built-in graph may be enough. This tool exists for when "probably follows the graph" isn't good enough.
Intended Audience
- AI SaaS developers
- Internal LLM tooling teams
- Agent orchestration builders
- Platform engineering teams
- Infrastructure-focused AI developers
Not intended for content moderation or prompt filtering.
Observability
Optional OpenTelemetry instrumentation is provided under a dedicated,
versioned namespace (wfrouter.*) that this project owns — it is deliberately
independent of the upstream gen_ai.* conventions, which assume a model at the
center of every span and do not fit a content-blind topology engine.
Install the extra:
pip install "llm-workflow-router[otel]"
Wrap evaluation:
from router.observability.otel import traced_evaluate
result = traced_evaluate(engine, metadata)
If opentelemetry-api is not installed, instrumentation degrades to a no-op
and the engine behaves identically. A PROCEED, REFUSE, or PAUSE outcome
is a successful decision (span status OK); only genuine failures are errors.
See OBSERVABILITY.md for the full attribute and span conventions.
License
MIT. Use it, ship it, build on it. See LICENSE.
If you deploy this in production, a note about your use case is always appreciated (and helps prioritize the roadmap) — but never required.
Version
Current version: 0.3.0
- Static configuration model
- Explicit multi-entrypoint declaration (with inferred fallback)
- Optional stateful
WorkflowSessionconvenience layer - OpenAI Agents SDK adapter (
TopologyGuard) - Mermaid / DOT topology export (
llm-router graph) - MIT licensed
No inheritance. No dynamic rule composition. See CHANGELOG.md.
Future versions may extend topology modeling capabilities.
Author
Doby Baxter
Software systems developer focused on deterministic infrastructure and human-centered tooling.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file llm_workflow_router-0.3.0.tar.gz.
File metadata
- Download URL: llm_workflow_router-0.3.0.tar.gz
- Upload date:
- Size: 29.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d5bb9466a0ac2bb3a96dbe848df39201dea9d2db5059d9a2e832c6419ff08941
|
|
| MD5 |
a28c0a9a5271e79f1e5a6bd48988da2a
|
|
| BLAKE2b-256 |
0e636c86e7d8df73910b0fdca71c59ed457a1574fb44fde01ec6f7e38a1d742c
|
File details
Details for the file llm_workflow_router-0.3.0-py3-none-any.whl.
File metadata
- Download URL: llm_workflow_router-0.3.0-py3-none-any.whl
- Upload date:
- Size: 27.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3fc71a5cf27cebd76e521ef00d8212c3959bf40357232535537e716e1a34074e
|
|
| MD5 |
4cc900e862c8ba67ce290bd2a6d136b2
|
|
| BLAKE2b-256 |
e2d59fb5ef9a27d8fb94919dbaa5b114964ebd7621d21bb6698b2e23650f5795
|