Python SDK¶
mini-ork is usable from Python in two composable ways: primitives you import in-process (verification, dispatch, memory, routing), and an orchestrator client that drives the full classify → plan → execute → verify lifecycle. Most applications start with the primitives and add the orchestrator when they want the whole loop.
The import surface is lazy: import mini_ork stays cheap and only loads the
dispatch, runtime and memory machinery when you reference it.
Install¶
pip install -e '.[full]' # from a checkout; installs the CLI and runtime too
python3 -c "import mini_ork; print(sorted(dir(mini_ork)))"
A runnable tour: python3 examples/sdk/hello_world.py — exercises backends, a
memory roundtrip, routing and the fail-fast dispatch contract, unconfigured.
Execution-anchored verification¶
Crucible runs a test against a patch in an isolated runtime and returns an
ExecOutcome. The status is the signal — not a model's opinion of the patch.
from mini_ork import Crucible, RuntimeSpec
cru = Crucible(RuntimeSpec(image="python:3.11-slim", backend="auto"))
outcome = cru.run_test(
test_src="def test_x():\n assert add(2, 2) == 4\n",
patch=my_patch,
)
backend="auto" picks from what is available; available_backends() reports
the candidates (docker, subprocess, prime, modal, docker-cli).
Reading ExecOutcome¶
| Status | What it says | What to do |
|---|---|---|
passed |
The test ran and passed | Evidence for the patch |
failed |
The test ran and failed | Evidence against the patch |
test_defect |
The test itself is broken | Repair the test; do not blame the patch |
error |
The harness errored | Investigate the harness |
apply_fail |
The patch did not apply | Fix the patch/base |
no_run |
Nothing executed | No evidence either way |
if outcome.informative: # passed or failed — real evidence
ship = outcome.passed
elif outcome.test_is_broken: # repair the probe, do not blame the patch
...
informative is true only for passed and failed. Treating error or
no_run as a failure is a common and expensive mistake: it reports a harness
problem as a patch problem and can send an agent chasing a bug that does not
exist.
flowchart TD
R[run_test] --> S{status}
S -->|passed / failed| I[informative: evidence]
S -->|test_defect| T[fix the test]
S -->|apply_fail| A[fix patch or base]
S -->|error / no_run| H[fix the harness]
Heterogeneous dispatch¶
dispatch_model sends one turn to any configured lane. An unroutable lane or a
missing credential returns a structured ok=False result — it does not raise,
stall, or leave partial state behind.
from mini_ork import DispatchRequest, dispatch_model
res = dispatch_model(DispatchRequest(model="codex", prompt="…"))
if res.ok:
text, cost = res.text, res.cost_usd
else:
handle(res.error, res.rc)
Check ok before reading text. The structured failure exists so a batch loop
can branch instead of unwinding.
Verified-outcome memory¶
Scoped semantic memory backed by SQLite. The default HashEmbedder is stdlib
only — no new dependency and no network for a basic roundtrip.
from mini_ork import memory
memory.add("ingest lane retry budget is 3", scope="proj", infer=False)
hits = memory.search("how many ingest retries?", scope="proj", top_k=3)
Scopes keep projects from bleeding into each other. The layer is meant to hold outcomes that passed verification, not every model assertion; see The learning loop.
Bandit routing¶
preferred_lane returns the lane that has been winning for a role, learned
from verified outcomes at zero extra model calls:
It reads a warmed state database (mini-ork init plus a few runs). With no
priors it returns the default. router and recompute_advantages expose the
underlying pieces if you want to inspect or recompute the table.
Routing is not a correctness guarantee
Learned routing has been measured to reduce cost, but lower cost at equal correctness is not established. Use it to choose a starting lane, and keep the gates, not the router, as the correctness boundary.
Orchestrator¶
from mini_ork import MiniOrk, RunRequest
mo = MiniOrk()
result = mo.run(RunRequest(kickoff="kickoff.md", recipe="code-fix", mode="live"))
print(result.run_id, result.verdict, result.ok)
The client shells out to the mini-ork CLI behind a stable --json contract.
RunResult is populated from one structured mini_ork_result={…} line, not
scraped from human-readable output.
Provider policy and auto-init¶
RunRequest can carry a ProviderPolicy, written to
.mini-ork/config/agents.yaml before the run:
from mini_ork import MiniOrk, ProviderPolicy, RunRequest
result = MiniOrk().run(RunRequest(
kickoff="kickoff.md",
recipe="docs",
mode="dry-run",
provider_policy=ProviderPolicy.codex_only(),
auto_init=True,
))
auto_init=True bootstraps .mini-ork/ on first use, matching mini-ork init.
Set it to False when your application manages initialization itself.
What RunResult carries¶
RunResult is a transparency contract, not just a return code:
| Field | Meaning |
|---|---|
command |
The exact argv that ran |
output |
Full combined output |
events |
Line-by-line events, for streaming adapters |
plan_path |
The plan written by the planner |
retained_home |
The .mini-ork home the run used |
run_id, task_class, verdict, returncode |
Result identity and outcome |
init_ran, init_output |
Whether bootstrap ran |
Recursive delegation¶
A run can spawn child runs. spawn is parent-centric: you name the parent, and
mini-ork enforces recursion limits before approving a child.
from mini_ork import MiniOrk, SpawnRequest
child = MiniOrk().spawn(SpawnRequest(
parent_run_id="run-root-123",
kickoff="child-task.md",
recipe="code-fix",
allow_child_spawn=True,
mode="dry-run",
))
print(child.spawn_id, child.child_workspace, child.spawn_status)
Limits, with defaults:
| Variable | Default | Bounds |
|---|---|---|
MINI_ORK_RECURSIVE_MAX_DEPTH |
2 | Delegation depth |
MINI_ORK_RECURSIVE_MAX_CHILDREN |
4 | Children per parent |
MINI_ORK_RECURSIVE_MAX_DESCENDANTS |
16 | Total descendants |
MINI_ORK_RECURSIVE_MAX_PARALLEL |
4 | Concurrent children |
Children run under .mini-ork/runs/<parent>/children/<child>/worktree/ and
share the parent's state database. The parent owns merge and publish.
Defining recipes in code¶
RecipeBuilder targets the same recipes/<name>/ contract the CLI reads. Code
and YAML are two front-ends to one format.
from mini_ork import ExtensionRegistry, NodeSpec, RecipeBuilder
registry = ExtensionRegistry()
recipe = (
RecipeBuilder("invoice-audit", "invoice_audit", "Audit invoices")
.keywords("invoice", "tax", "vat")
.node(NodeSpec(name="planner", type="planner", model_lane="planner"))
.node(NodeSpec(name="verifier", type="verifier",
verifier_ref="verifiers/check.sh"))
.edge("planner", "verifier", "verifies")
.build()
)
registry.recipe(recipe)
@registry.verifier("invoice-total")
def verify_invoice_total(artifact_path, plan_path):
return artifact_path.exists() and plan_path.exists()
Use code when a host application owns its recipes; use YAML when operators edit them without shipping code.
Prompt optimization with GEPA¶
GEPA (Genetic-Pareto reflective prompt evolution) mutates a prompt by reflecting
on an evaluator's natural-language feedback. mini-ork wraps the external gepa
framework (pip install gepa) in recipe mode (optimize a recipe's node prompts
against real runs) or external mode (optimize any seed prompt against any
critic, with no recipe and no state database).
The critic is a command in any language. It reads a JSON object on stdin and prints its verdict as the last non-blank line of stdout:
Log freely above that line. The feedback is forwarded verbatim into GEPA's reflection — that diagnosis is the signal GEPA turns into targeted mutations.
python -m mini_ork.gepa.run_gepa \
--seed-file seed_prompt.json \
--eval-cmd "node dist/eval-c4.js" \
--trainset fixtures/*.json \
--reflection-lm anthropic/opus \
--budget 30 --out out/gepa-c4
The seam is three composable pieces:
from mini_ork.gepa import (
SubprocessRunBackend, # cross-process / cross-language critic
CallbackRunBackend, # in-process: eval_fn(candidate, task) -> (score, feedback)
GEPARunBackendAdapter, # composes a RunBackend into gepa's adapter
)
Fail-loud. A non-zero exit, unparseable stdout, a missing score, or a
score outside [0, 1] raises EvaluatorError and halts the run. A broken
evaluator is never silently scored 0.0 — that would poison the Pareto front
with a fake gradient.
Next¶
- Extending mini-ork — registration APIs.
- Testing — the suite and its conventions.
- Verification stack — what
ExecOutcomefeeds.