Skip to main content

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

markov_protocols-0.5.0.tar.gz (26.9 kB view details)

Uploaded Source

Built Distribution

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

markov_protocols-0.5.0-py3-none-any.whl (40.0 kB view details)

Uploaded Python 3

File details

Details for the file markov_protocols-0.5.0.tar.gz.

File metadata

  • Download URL: markov_protocols-0.5.0.tar.gz
  • Upload date:
  • Size: 26.9 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

Hashes for markov_protocols-0.5.0.tar.gz
Algorithm Hash digest
SHA256 4bd2313dd37393b3fb97141e191a4e8090b74b5bee74d66b872fe27e1fd27ead
MD5 266581a3049e473838e15ef1e4e10584
BLAKE2b-256 130787a136286b3947c9685450bdfcceb4a4967c9e82219cfc9ccb1ed8f3d65d

See more details on using hashes here.

File details

Details for the file markov_protocols-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: markov_protocols-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 40.0 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

Hashes for markov_protocols-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e568687a5accd9beb652b99ade054f61af892d9de73d43a48d62b08259e51c41
MD5 6ed08a40f769323f177742c45cfb22fe
BLAKE2b-256 c447e59f0e6873d23dad923e43c6963ec4e93f1645816d3d00e5c2a5fac7522f

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