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.4.0.tar.gz (25.7 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.4.0-py3-none-any.whl (38.2 kB view details)

Uploaded Python 3

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

Hashes for markov_protocols-0.4.0.tar.gz
Algorithm Hash digest
SHA256 672af045c9f6bb4bbb1cb3aec2881d7e4e8c941dc28ed39bca31827df52a6262
MD5 780619e7e6309c2ca19434180b8725ff
BLAKE2b-256 4d736c2fed619198c19c8728097bbd6a7ca10590a2e921f8464cea1ced1b5f3e

See more details on using hashes here.

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

Hashes for markov_protocols-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f752665a37285bf02296efdb7723b878c747da625f9cac30492243f6f0c3e2a7
MD5 fd402eed6d4a9119e3e71051bb1df3c5
BLAKE2b-256 965cf04499ebc556b34235e333cc0cd9ee1dfcb1b71efe8fa67a4bcb5f5ac028

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