A deterministic finite state machine for modeling workflows that guide AI agents.
Project description
markov-protocols
A deterministic finite state machine for modeling workflows that guide AI agents.
LLMs are stochastic. When a workflow must happen reliably — collect these fields, validate them,
call that action, handle a correction — prompting alone drifts and hallucinates. markov-protocols
is the deterministic referee: you model the workflow as a state machine, the agent reads the
current step for guidance and reports what it learned, and the machine decides — deterministically —
what happens next. It never calls an LLM, runs a tool, or performs I/O; your host does that.
- Deterministic: same definition + same collected data → same decision, every time.
- Correction-aware: if the customer changes their mind, the machine rewinds and re-runs only what's affected (and tells you if a side effect must be compensated).
- Typed & serializable: author in Python or as JSON/YAML; validated,
mypy-clean,py.typed.
Install
pip install markov-protocols
pip install "markov-protocols[yaml]" # optional: YAML import/export
Requires Python 3.13+.
Quickstart
from markov_protocols import (
Workflow, DataCollectionState, ActionExecuteState,
Requirement, ValueType, Ref, Transition, Session,
)
# 1. Define the workflow (plain data).
workflow = Workflow.compile(
name="intake",
initial="Collect email",
states=[
DataCollectionState(
title="Collect email",
requires=[Requirement(field="email", description="the customer's email")],
),
ActionExecuteState(
title="Send confirmation",
payload={"to": Ref(field="email")}, # a Ref, filled from collected data
requires=[Requirement(field="confirmation_sent", type=ValueType.BOOLEAN)], # its result
),
],
transitions=[
Transition(source_id="collect-email", target_id="send-confirmation"),
],
).value # compile() returns a Result; .value is the validated Workflow
# 2. Run it. Feed in whatever your agent extracted from the conversation.
session = Session.start(workflow)
outcome = session.update({"email": "pedro@example.com"}).value
print(outcome.current_state.id) # 'send-confirmation' (advanced automatically)
print(outcome.directive.payload) # {'to': 'pedro@example.com'} (resolved, ready to run)
# 3. Your host runs the action and reports the result back.
session.update({"confirmation_sent": True})
print(session.is_finished) # True
update() is the single input verb: it records the data, then fast-forwards through every step
the data already satisfies — so one call can advance several steps, or hold position when it can't.
Your agent loop
The machine is the deterministic referee; your host owns everything stochastic or side-effectful:
session = Session.start(workflow)
while not session.is_finished:
values = my_llm.extract(conversation, session.current_state) # your LLM (stochastic)
outcome = session.update(values).value # the machine (deterministic)
directive = outcome.directive
if directive and directive.ready: # a side effect to run
result = my_host.run(directive.payload) # your webhook / function
session.update({outcome.awaiting[0]: result}) # report it to the awaited field
Guidance & validation
Constrain values with options (an enum with per-value descriptions) or any Condition. When a
step can't advance, blockers tells you why — and the convenience views split it apart:
from markov_protocols import DataCollectionState, Requirement, Option, Workflow, Session
triage = Workflow.compile(
name="triage",
initial="Detect intent",
states=[DataCollectionState(
title="Detect intent",
requires=[Requirement(
field="intent",
description="what the customer wants",
options=[Option(value="buy", description="wants to purchase"),
Option(value="support", description="needs help")],
)],
)],
).value
outcome = Session.start(triage).update({"intent": "rent"}).value
print(outcome.missing_fields) # [] — the field IS present...
print(outcome.invalid_fields) # ['intent'] — ...but 'rent' isn't allowed
print(outcome.to_llm_extended()) # factual, prompt-ready text (no business prompts, facts only):
# Step: Detect intent
# - intent: what the customer wants (one of: buy (wants to purchase), support (needs help))
# Blocked by:
# - 'intent' is invalid: 'rent' is not allowed; choose one of: buy (wants to purchase), support (needs help)
Branch on a value with a transition guard: Transition(source_id=..., target_id=..., guard=Eq(field="intent", value="buy")).
Save & load (JSON / YAML)
from markov_protocols import to_yaml, from_yaml, export_to_file, import_from_file
text = to_yaml(workflow) # -> YAML string
result = from_yaml(text) # -> Result[Workflow] (validated; a bad doc fails, never crashes)
export_to_file(workflow, "flow.json") # format from the extension (.json/.yaml/.yml)
loaded = import_from_file("flow.yaml")
A workflow document is easy to hand-author — type + title + the state's own fields:
name: intake
initial: Collect email
states:
- type: DATA_COLLECTION
title: Collect email
requires:
- field: email
transitions: []
Concepts
| Term | What it is |
|---|---|
| Workflow | the authored graph of states + transitions (Workflow.compile() validates it) |
| State | a step — every state declares requires: list[Requirement]; types differ by who fills them |
| Requirement | a field a state expects: field, required (default true), type, options, condition |
| Transition | a directed link, optionally guarded by a Condition (branching) |
| Session | one conversation's live run; drive it with session.update(values) |
| Blackboard | the shared, deterministic record of collected values |
| Directive | a fully-resolved instruction for your host to execute (never run by the machine) |
| Blocker | why the current step hasn't advanced: MISSING / INVALID / AWAITING_* |
Documentation
Development
uv sync # environment + dev tools
uv run pytest # tests
uv run ruff check # lint
uv run mypy # type-check
License
MIT
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
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 markov_protocols-0.4.0.tar.gz.
File metadata
- Download URL: markov_protocols-0.4.0.tar.gz
- Upload date:
- Size: 25.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
672af045c9f6bb4bbb1cb3aec2881d7e4e8c941dc28ed39bca31827df52a6262
|
|
| MD5 |
780619e7e6309c2ca19434180b8725ff
|
|
| BLAKE2b-256 |
4d736c2fed619198c19c8728097bbd6a7ca10590a2e921f8464cea1ced1b5f3e
|
File details
Details for the file markov_protocols-0.4.0-py3-none-any.whl.
File metadata
- Download URL: markov_protocols-0.4.0-py3-none-any.whl
- Upload date:
- Size: 38.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f752665a37285bf02296efdb7723b878c747da625f9cac30492243f6f0c3e2a7
|
|
| MD5 |
fd402eed6d4a9119e3e71051bb1df3c5
|
|
| BLAKE2b-256 |
965cf04499ebc556b34235e333cc0cd9ee1dfcb1b71efe8fa67a4bcb5f5ac028
|