Skip to main content

Peven

Peven is Python authoring for structured LLM environments backed by a Julia Petri-net runtime.

If PydanticAI makes it easy to build agents, Peven makes it easy to build the environment around them: places, transitions, joins, retries, and the topology you want to evaluate.

Why use it

  • Author environments in Python, next to the agents and tools you already write.
  • Make topology explicit instead of hiding it inside one giant agent loop.
  • Run the hard state-machine part on a Julia engine built for Petri nets and concurrent firing.
  • Compare workflows: single-shot, required-input joins, transition retries, and branch-and-merge topologies.
  • Surface intermediate environment state at any point in a run — a sparse terminal reward is not the only signal a long-horizon rollout can yield.

When not to use it

Peven is probably overkill for a single prompt, a linear chain, or an agent loop that is easier to read as ordinary Python. It starts to pay for itself when the environment has real topology: branching, joins, retries, traces, or reproducible state you want to inspect and compare.

Install

First install the Python package:

uv add peven

or

pip install peven

Peven also needs a Julia runtime. One command provisions it:

uv run peven setup

setup installs Julia via juliaup if it is missing (it asks first; pass --yes for CI), then installs PevenTransport.jl — which brings Peven.jl with it — into a dedicated shared Julia environment. After that, peven.gateway() just works.

If you manage Julia yourself (for example through a pixi environment that pins it), skip setup and make sure PevenTransport is installed in your default Julia environment, or point peven.gateway(project=...) at a Julia project that has it.

Quickstart

import asyncio

import peven
from peven.worker import Worker


@peven.env("single_question")
class SingleQuestion:
    prompt = peven.place()
    report = peven.place(terminal=True)
    solve = peven.transition(inputs=["prompt"], outputs=["report"], executor="answer")


@peven.executor("answer")
async def answer(ctx, store):
    question = ctx.inputs["prompt"][0].payload
    store["asked"] = question
    answer = await askYourModel(question)  # any LLM client you like
    return {
        "report": [
            peven.token(color="report", runKey=ctx.bundle.runKey, payload=answer)
        ]
    }


def buildWorker(workerId, runKeys):
    worker = Worker(workerId, [answer])
    for runKey in runKeys:
        worker.assign(runKey, {})
    return worker


async def main():
    marking = peven.mark(
        *[
            peven.initialMarking(
                runKey=peven.runKey(f"mars#g{i}"),
                tokens=[("prompt", "question", "What planet is known as the red planet?")],
            )
            for i in range(4)
        ]
    )

    async with peven.gateway(buildWorker, workers=2) as gw:
        await gw.load(SingleQuestion)
        async for result in gw.fire(marking, fuse=100):
            print(result.runKey, result.status)
            if result.status == "completed":
                print(result.finalMarking["report"][0].payload)


if __name__ == "__main__":  # required: worker processes re-import this module
    asyncio.run(main())

What is happening:

  • The env class is the topology. It lowers to the Julia engine, which validates it and owns all scheduling.
  • The marking is the group: four run keys means four independent rollouts of the same net, interleaved by the engine.
  • Executors are async functions called by the engine whenever their transition fires. Each runs inside a worker process that owns its rollout's mutable state (store); tokens crossing the boundary carry data and handles, not state.
  • fire streams one result per rollout as it finishes, so a training loop can act on early rollouts before slow ones complete.

Results and traces

Every rollout comes back as a typed RunResult: status (completed, failed, or incomplete), reason, error, the finalMarking, and a trace — every firing that happened, with its bundle, attempt count, and output tokens. Failed rollouts are results, not exceptions: executor exceptions retry up to the transition limit, then fail the affected rollout; a worker crash fails its assigned rollouts while work on other workers continues.

The trace is the point. Because the topology is explicit, every intermediate state a rollout passed through is inspectable after the fact — which is exactly the signal sparse terminal rewards throw away.

Why Julia

The Julia side is not there for novelty. It keeps the engine closer to the real Petri-net model.

Python is a great place to author agents and executors, but it pushes engine code toward shims, wrappers, and dynamic glue. Julia is a better fit for the symbolic runtime: markings, firing rules, joins, guards, retries, and termination stay explicit instead of dissolving into spaghetti soup.

The engine also exploits the formalism directly: transition dependencies are precomputed when the net is constructed, so after each firing only transitions whose input places could have changed are rechecked for enablement.

Architecture

Peven has three layers:

  • peven — Python authoring, the worker runtime that executes your callbacks, and the gateway session that drives runs.
  • PevenTransport.jl — the ZMQ gateway between Python and the engine.
  • Peven.jl — the execution engine.

Python authors the net and lowers it to the wire; the gateway routes messages by run key and correlates calls; the engine validates and executes. Rollout state lives in Python worker processes — one owner per run key — and the engine calls back into them whenever a transition fires.

Release notes

0.3.0

  • Rebuilt around the PevenTransport wire contract: authoring lowers directly to the engine's net shape, and the old embedded-runtime layers (sinks, guard/join DSL, CLI, PydanticAI integration, MiniGrid example) are gone — v0.2.3 preserves them.
  • New runtime: peven.gateway() owns the Julia gateway process, load() validates the net through the engine, and fire() streams one typed RunResult per rollout.
  • Group rollouts: peven.mark() merges markings with disjoint run keys and rejects overlaps; run keys partition independent rollouts over one topology.
  • Required-input joins and transition retries cross protocol 2 end to end; traces report retry attempts.
  • Worker calls run concurrently with a configurable 900-second executor timeout. Executor exceptions retry up to the transition limit, then fail the affected rollout; a disconnected worker fails its assigned rollouts while other workers continue.
  • Unencodable executor results become correlated executor errors without killing the worker. Gateway exit also reaps active workers and context-owned sockets, including abandoned fires.
  • python -m peven setup installs Julia through juliaup when needed, then provisions PevenTransport 0.2 and Peven 0.6 in a dedicated shared environment.

0.2.3

  • Added guard comparisons between field references, such as peven.f.turns < peven.f.max_turns.
  • Removed fossilized version labels from guard and join indexing errors.

0.2.2

  • Added optional input arcs via peven.input(..., optional=True).
  • Updated the MiniGrid DoorKey example so planner advice is an optional token, not a sentinel {"advice": "none"} token.
  • Updated the packaged Julia runtime pins for optional-arc support.
  • Added adapter parity coverage for optional inputs, optional-only rejection, and optional keyed-join rejection.

0.2.1

  • Added peven.place(terminal=True) for Python-side completion normalization.
  • Updated Rich output to hide no_enabled_transition for completed terminal-place runs.
  • Added the MiniGrid DoorKey example under the examples dependency group.
  • Added gymnasium and minigrid to the optional examples dependencies.

Inspiration

Peven is inspired by a couple different things. For starters the name is taken from Patricia A. McKillip's Riddle-Master trilogy. Peven of Aum is a king, a ghost, and a master riddler who has only ever lost once. In the Riddle-Master trilogy, riddles are made up of three parts: questions, answers, and strictures. My hope for Peven is that it can help you explore evaluations by providing a runtime where you can ask a question, iterate based on the stricture, and, eventually, get to an answer. "Beware the unanswered Riddle."

My second point of inspiration comes from my time working at The LLM Data Company, where I had the chance to learn and experiment to my heart's content. A lot of my work centered around environments and benchmarks. I often wished I had a reusable framework or package to support my work here, something like a pydantic (which I love) but for evaluations.

Most of the architectural decisions I made regarding the engine are because I thought the math was cool. Peven should give you a pretty clear sense of (1) how I think about evaluations and (2) what types of evaluations I'm interested in.

Release files for peven 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for peven 0.3.0
File Size Uploaded
peven-0.3.0.tar.gz 65.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for peven 0.3.0
File Interpreter ABI Platform
peven-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 89.9 kB

Release files / peven-0.3.0.tar.gz

Download URL peven-0.3.0.tar.gz
Size 65.6 kB
Tags Source
SHA-256 checksum
How to use checksums
142c9c0f4ee53972759d940d7f2b6427e2f875122e6b7d7fd0cb7340270e7539
BLAKE2b-256 checksum
How to use checksums
3cb3c38e948dabf3820fa36566383c484aa945d61f88e2b6169f5194a414b3a2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 8, 2026.

Transparency log

Release files / peven-0.3.0-py3-none-any.whl

Download URL peven-0.3.0-py3-none-any.whl
Size 24.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b8c8b33c71ca00f6787e08d3bc70b7c4f30c79c2c733cca383e26bdefade89d1
BLAKE2b-256 checksum
How to use checksums
8a24d3a0cdaeb117d4b11476a337b9bf8bfc29f50442b188c45ba04f6a76379e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 8, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page