Metadata-Version: 2.4
Name: llm-workflow-router
Version: 0.3.1
Summary: Deterministic workflow topology enforcement for LLM-powered systems.
Author: Doby Baxter
License: MIT
Project-URL: Homepage, https://dobybaxter127.gitlab.io/
Project-URL: Repository, https://gitlab.com/dobybaxter127/llm-router
Project-URL: Changelog, https://gitlab.com/dobybaxter127/llm-router/-/blob/master/CHANGELOG.md
Keywords: llm,agents,workflow,topology,validation,determinism
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: PyYAML>=6.0.1
Provides-Extra: dev
Requires-Dist: pytest>=8.0.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: coverage>=7.0.0; extra == "dev"
Requires-Dist: ruff>=0.6.0; extra == "dev"
Requires-Dist: mypy>=1.8.0; extra == "dev"
Requires-Dist: opentelemetry-api>=1.20; extra == "dev"
Requires-Dist: opentelemetry-sdk>=1.20; extra == "dev"
Provides-Extra: otel
Requires-Dist: opentelemetry-api>=1.20; extra == "otel"
Provides-Extra: openai-agents
Requires-Dist: openai-agents>=0.18; extra == "openai-agents"
Dynamic: license-file

# 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.

```yaml
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:

```python
InteractionMetadata:
    container: str
    previous_state: InteractionState
    transition_history: List[str]
    invocation_depth: Dict[str, int]
    requested_action: str
    trace_id: Optional[str]
```

Returns:

```python
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:

```bash
pip install llm-workflow-router
```

Validate configuration:

```bash
llm-router validate config.yaml
```

Analyze topology:

```bash
llm-router analyze config.yaml
```

Evaluate metadata:

```bash
llm-router run --config config.yaml --metadata metadata.json
```

---

Render the topology as a diagram:

```bash
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`:

```python
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:

```bash
pip install "llm-workflow-router[openai-agents]"
```

```python
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`](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:

```bash
pip install "llm-workflow-router[otel]"
```

Wrap evaluation:

```python
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](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](CHANGELOG.md).

Future versions may extend topology modeling capabilities.

---

## Author

Doby Baxter  
Software systems developer focused on deterministic infrastructure and human-centered tooling.
