Extending mini-ork¶
mini-ork is designed to be extended without editing executor core modules. Node types, routing policies, gate types, provider transports, subcommands, embedders and whole recipes all have registration APIs. This page shows each surface, the signature it expects, and how to load it safely.
Extension surfaces¶
| Surface | Registration call | Selected by |
|---|---|---|
| Node type | register_node_handler(type, fn, phase=) |
workflow.yaml node type |
| Implementer fan-out | register_implementer_submode(recipe, node, artifact, script) |
Recipe + node id |
| Routing policy | register_policy(name, fn) |
MO_ROUTING_POLICY |
| Gate type | register_gate_evaluator(type, fn) |
Gate row's gate_type |
| Provider kind | register_provider_kind(kind, builder) |
providers.yaml kind |
| Dispatch backend | register_dispatch_backend(model, fn) |
Model lane name |
| Subcommand | register_subcommand(name, handler) |
mini-ork <name> |
| Embedder | register_embedder_provider(name, factory) |
MO_EMBED_PROVIDER |
A recipe may ship a register.py next to its YAML. The loader executes it once
per process before the run; this is the supported place to hold recipe-local
registrations. See Recipe-local registration.
Node handlers¶
A node handler runs one node. The executor's preamble — routing, environment
publication, gates, prompt assembly — has already happened, so the handler
receives a fully prepared NodeDispatch:
from mini_ork.cli.execute_handlers import NodeDispatch, register_node_handler
def handle_invoice_check(nd: NodeDispatch):
# nd.prompt_ref, nd.lane, nd.run_dir, nd.recipe, nd.task_class, ...
out = nd.run_dir / "invoice-check.json"
ok = check_invoices(nd.plan_path, out)
return (0 if ok else 1), ("succeeded" if ok else "failed")
register_node_handler("invoice_check", handle_invoice_check)
The contract is (NodeDispatch) -> (rc, finish_reason). rc == 0 means the
node succeeded; any other value is a failure and the run's recovery logic takes
over. Return a specific finish reason rather than a generic one — it is what an
operator sees in the board.
Pass phase="early" for planner/reflector-style nodes that must run before
capability and watchdog gates and without prompt assembly. The default,
phase="main", gets the full dispatch treatment.
Handlers can replace core types
register_node_handler replaces an existing handler for that type. A
recipe-local register.py can therefore override a core node type for the
rest of the process. Only load recipes you trust; read their register.py.
Implementer submodes¶
When one implementer node fans out into many independent units, register the dispatcher as data instead of code:
from mini_ork.cli.execute_handlers import register_implementer_submode
register_implementer_submode(
recipe="docs-audit",
node_id="implementer",
results_artifact="units-results.json",
script="scripts/run_units.py",
)
The script is invoked by the implementer handler and must write the declared results artifact. This keeps fan-out composition out of the executor.
Routing policies¶
A routing policy chooses a lane for a node:
from mini_ork.dispatch.routing import RoutingContext, register_policy
def route_by_risk(ctx: RoutingContext) -> str:
if ctx.node_type in ("planner", "reviewer"):
return "opus_lens"
return "cheap_lens"
register_policy("by_risk", route_by_risk)
RoutingContext carries node_type, current_lane, root and task_class.
The policy must return a lane name that exists in providers.yaml; an unknown
lane should fail loudly rather than fall through to a default. Built-in policies
include workflow_default, frontier_only, cheap_only, static_hybrid,
learning_governed (the default) and trace_governed.
Gate evaluations¶
A gate type is a function from the gate's condition and context to a verdict:
from mini_ork.gates.gate_registry import register_gate_evaluator
def eval_file_exists(condition, context_json, db_path, mini_ork_root):
# condition is the gate's own string; return "pass" | "fail" | "defer"
import json, os
ctx = json.loads(context_json or "{}")
path = ctx.get("path")
return "pass" if path and os.path.exists(path) else "fail"
register_gate_evaluator("file_exists", eval_file_exists)
The type is Callable[[str, str, str, Optional[str]], str] — condition,
context JSON, DB path, engine root. Return exactly pass, fail or defer.
An unregistered gate type keeps the historical fail-safe: it defers. That is
the right default: an unknown gate must not silently pass.
Write the gate's context from the node that produced the artifact. A gate that
cannot read its evidence should defer, not guess. See
Verification and gates for how verdicts aggregate.
Providers and dispatch backends¶
providers.yaml entries name a kind. A new kind supplies a builder that
turns configuration into a ProviderSpec:
from mini_ork.dispatch.providers import ProviderSpec, register_provider_kind
def build_my_provider(name, cfg, **kwargs) -> ProviderSpec:
return ProviderSpec(
name=name,
# base URL, auth headers, transport, model mapping, ...
)
register_provider_kind("my_endpoint", build_my_provider)
A dispatch backend bypasses the standard transport for one model lane:
from mini_ork.dispatch.providers import register_dispatch_backend
def dispatch_via_queue(request, spec):
...
return DispatchResult(...)
register_dispatch_backend("my_lane", dispatch_via_queue)
The backend sees a DispatchRequest and ProviderSpec and returns a
DispatchResult. It is responsible for its own timeout and for reporting a
structured failure rather than raising into the executor.
Credentials belong in the vault
A provider builder should receive secret names and resolve them through the vault, not receive literal keys from YAML. See Security.
Subcommands¶
from mini_ork.cli.main import register_subcommand
def handle(rest: list[str], root: str) -> int:
...
return 0
register_subcommand("my-command", handle)
The handler receives the remaining argv and the resolved engine root, and returns a process exit code. Registration is per process; a durable CLI addition is a module referenced from the dispatcher's native-subcommand table, not a runtime monkey-patch.
Embedders¶
Semantic memory uses an embedder selected by name:
from mini_ork.memory.semantic import register_embedder_provider
def make_embedder():
from my_pkg.embed import MyEmbedder
return MyEmbedder()
register_embedder_provider("my-embedder", make_embedder)
Keep third-party imports inside the factory. The factory runs only when that
provider is selected, so import mini_ork stays cheap and does not require the
optional dependency.
Recipe-local registration¶
Place register.py beside a recipe's task_class.yaml:
recipes/invoice-audit/
task_class.yaml
workflow.yaml
artifact_contract.yaml
register.py # imports and registers the extensions above
prompts/
The loader imports it once per process, before the run, by file path. It does
not swallow errors: a syntax error, an import error or any exception fails the
run with a non-zero exit and the original exception chained as the cause. A
missing register.py is a silent no-op.
This is the recommended way to ship an extension with a recipe. It keeps the registration next to the workflow that needs it and avoids a global import side effect.
Whole recipes without code¶
Most new work needs no Python at all. A recipe is three YAML files plus prompts:
recipes/<name>/
task_class.yaml # when this recipe applies
workflow.yaml # nodes, edges, lanes, gates
artifact_contract.yaml # required outputs and their checks
prompts/<node>.md
Then:
Schemas and field meanings are in Schemas and Recipes and task classes.
Testing an extension¶
- Unit-test a node handler by constructing a
NodeDispatchwith atmp_pathrun dir; do not shell out to the whole executor. - Test a routing policy as a pure function over
RoutingContext. - Test a gate evaluator by passing a context JSON and asserting the returned
string is one of
pass,fail,defer. - Integration-test a recipe with
mini-ork validateand a dry run before a live run.
See Testing for fixtures, CI shards and the environment-hygiene rules the suite depends on.
Next¶
- Python SDK — the same primitives from an application.
- Development worktrees — ship the change.
- Architecture: run lifecycle