Skip to main content

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 WorkflowSession convenience 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

llm_workflow_router-0.3.0.tar.gz (29.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

llm_workflow_router-0.3.0-py3-none-any.whl (27.0 kB view details)

Uploaded Python 3

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

Hashes for llm_workflow_router-0.3.0.tar.gz
Algorithm Hash digest
SHA256 d5bb9466a0ac2bb3a96dbe848df39201dea9d2db5059d9a2e832c6419ff08941
MD5 a28c0a9a5271e79f1e5a6bd48988da2a
BLAKE2b-256 0e636c86e7d8df73910b0fdca71c59ed457a1574fb44fde01ec6f7e38a1d742c

See more details on using hashes here.

File details

Details for the file llm_workflow_router-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for llm_workflow_router-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3fc71a5cf27cebd76e521ef00d8212c3959bf40357232535537e716e1a34074e
MD5 4cc900e862c8ba67ce290bd2a6d136b2
BLAKE2b-256 e2d59fb5ef9a27d8fb94919dbaa5b114964ebd7621d21bb6698b2e23650f5795

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page