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.1.tar.gz (29.5 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.1-py3-none-any.whl (27.1 kB view details)

Uploaded Python 3

File details

Details for the file llm_workflow_router-0.3.1.tar.gz.

File metadata

  • Download URL: llm_workflow_router-0.3.1.tar.gz
  • Upload date:
  • Size: 29.5 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.1.tar.gz
Algorithm Hash digest
SHA256 aa1373abdc3406139bfcf8b6aa808903f24f3cfb1cdd01c303542673c6aa1345
MD5 a3e0c20c94762089a31fa4a2e64d5e8f
BLAKE2b-256 bc893770abedcab54929413276e7f9da6004017bfcbc162b011ec9a42f139577

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for llm_workflow_router-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 60c4148f439fd58592cc07e1c4b316b9c26f201f2d35f6163e118bfe68d026be
MD5 aabff3c0400111a3f16a29296b881905
BLAKE2b-256 00a304661efa117bb3b793881e13a19a15ca833ca373c9a0f8930b358fbd716f

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