Host embedding guide¶
How to run mklang machines from an outer host — scripts, services, MCP clients, or an orchestrator — without treating the CLI as a black box. This is host surface documentation: it does not add language syntax.
Related: CLI reference, Stability, Best practices (§5 tools, §11 security, §14 surfaces), SPEC §7 (checkpoints), SPEC §11 (threat model), ADRs 0007 (checkpoints), 0008 (HITL), 0011/0013 (MCP), 0018 (truncation), 0019 (live events), 0020 (tool envelopes), 0023 (config layers), 0024 (fs tools).
Wire shape of outcomes:
schema/run-result.schema.json.
1. Entry points (pick one)¶
| Surface | When to use | Commission |
|---|---|---|
Library (mklang.host + mklang.run) |
In-process Python host | prepare_path / prepare_source → engine.run → host.build_output |
CLI mklang run / resume |
Shell, cron, CI, n8n “Execute Command” | path or registered name; JSON on stdout |
MCP mklang-mcp |
Agentic clients | tools run / resume; events as mklang.event |
All three share the same prepare → run → output seam. Prefer the library when you already own a Python process; CLI when the host is polyglot; MCP when the client is an agent runtime.
config + .env → prepare (load machine, tools, hooks, LLM)
→ inject inputs / host defaults (today, now)
→ engine.run(... suspendable / escalate_suspend / on_truncate)
→ build_output (+ checkpoint file or MCP handle on suspend)
Minimal library sketch¶
from mklang import host
from mklang.engine import run
cfg = "config/runtime.yaml" # or resolve via host paths
p = host.prepare_path(cfg, provider=None, path="machines/hello.mkl")
ctx = dict(p.machine.context)
host.inject_host_defaults(ctx) # only fills declared empty today/now
# apply user inputs into ctx here (e.g. host.set_path(ctx, "ticket.body", …))
res = run(
p.machine,
ctx,
p.registry,
p.llm,
p.prov.tiers,
tools=p.tools,
hooks=p.hooks,
suspendable=False,
escalate_suspend=False,
on_truncate="halt", # or "report"
# Control-flow taint (SPEC §6 / ADR 0030): refuse an effectful tool reached
# through a decision a judge made over external data. Default "report".
on_untrusted_flow="halt",
tool_effects={"my_plugin_lookup": "read"}, # unclassified tools are effectful
)
out = host.build_output(res)
# out["status"] in {"done", "halt", "suspended"}
CLI equivalent:
mklang run machines/hello.mkl --format json --on-truncate halt --untrusted-flow halt
# exit 0 done | 1 halt | 3 suspended | 2 usage/host error
2. Run outcomes and exit codes¶
status |
Meaning | CLI exit | Host action |
|---|---|---|---|
done |
Finished; result is the machine result |
0 | Consume result / trace / usage |
halt |
Aborted; not resumable from this result alone | 1 | Log error (+ at); fix inputs/machine; do not invent success |
suspended |
Resumable (budget or HITL escalate) | 3 | Persist checkpoint; later resume |
error |
MCP prepare/validation failure payload | (tool result) | Fix request; not an engine halt |
Common error strings (non-exhaustive): budget-exhausted, escalated,
gate-fail, output-truncated, no-gate-matched, untrusted-control-flow
(only under on_untrusted_flow="halt"), call-failed, call-depth-exceeded,
tool observation failures as judged by fail gates. A halt inside a sub-machine
reaches the caller prefixed — call-failed: <sub reason> — so match on the
prefix, not on equality.
Mapping tip for outer orchestrators: treat exit 3 / suspended as
waiting (human or budget), not as failure. Treat exit 1 / halt as failure
unless your product policy retries after changing inputs.
3. Checkpoints and HITL¶
Enabling suspend¶
| Goal | Flags / knobs |
|---|---|
| Pause on budget exhaustion | CLI --checkpoint PATH or suspendable=True + save frames |
Pause on escalate gates |
CLI --hitl (implies checkpoint; default path under XDG state) or escalate_suspend=True |
| Resume | mklang resume CHECKPOINT --set human.reply=… or MCP resume with handle/path |
Checkpoints are plaintext JSON of the blackboard and frames (mode 0600 is
a floor, not encryption). Do not put API keys or long-lived secrets in
machine context: — they will be snapshotted. See SPEC §11.
Resume injection¶
Inject only what the human (or outer workflow) decided, e.g.
--set human.reply=approve. Injected paths are tainted like other untrusted
context (SPEC §6). Prefer a dedicated key (human.reply) over overwriting
business fields.
MCP handles vs files¶
- Default MCP suspend returns an opaque single-use in-memory handle in
checkpoint. - Pass
checkpoint_pathfor a durable file (cross-process); result may include bothcheckpoint(handle) andcheckpoint_file(path). - Prefer durable paths for multi-process HITL (dashboard → worker).
4. Secrets, config, and tools¶
| Concern | Rule |
|---|---|
| Provider API keys | Host .env / environment (api_key_env in runtime.yaml); never in .mkl |
| Tool secrets (Tavily, …) | Same layering; runtime.yaml tools: is non-secret backend selection |
| Config resolution | Explicit --config / MKLANG_CONFIG > project > user > system > bundled (install) |
| FS workspace | --workspace / MKLANG_FS_ROOT / tools.fs.workspace; writes need --allow-write or config grant |
| Tool plugins | Entry points mklang.tools — host registers callables; machines only name them |
Capability policy: a machine may request a tool; only the host grants it. Unknown third-party tools should default to conservative risk metadata (BP §5.5).
5. Timeouts, budgets, truncation¶
| Knob | Effect |
|---|---|
Machine budget: |
Step/loop guard (fan-out charges branch count) |
CLI --max-tokens / cost_budget |
Shared produce+judge token budget; with checkpoint → suspend; else halt |
--on-truncate report\|halt |
Produce cutoff (ADR 0018): annotate vs halt output-truncated |
--untrusted-flow report\|halt |
Control-flow taint (ADR 0030): annotate vs halt untrusted-control-flow |
| Provider HTTP timeouts | Adapter/host concern; console implements cooperative cancel + close() |
Production hosts that must not silently ship partial rewrites should use
--on-truncate halt (or the library equivalent) and surface error clearly.
6. Wire shape (JSON)¶
Canonical core fields from host.build_output(res):
{
"status": "done",
"error": null,
"result": "…",
"usage": { "input_tokens": 0, "output_tokens": 0 },
"trace": [{ "step": 1, "state": "…", "…": "…" }]
}
Surface extensions:
| Field | CLI | MCP |
|---|---|---|
at |
yes when set | yes when set |
checkpoint |
path on suspend | handle (and path semantics differ) |
checkpoint_file |
— | durable path when requested |
warnings |
stderr, not always in JSON | array on result |
Validate example payloads against
schema/run-result.schema.json.
The schema allows additionalProperties so surfaces may grow without a
language bump; pin schema_version in your host if you re-emit a stricter
envelope.
7. Recommended host checklist¶
- [ ] Resolve config once; fail fast if the provider key env var is missing
- [ ]
check/lintmachines in CI before live commission - [ ] Scenario-test with
mklang test(scripted LLM) for control flow - [ ] Decide suspend policy (checkpoint path + HITL) before irreversible tools
- [ ] Map
done/suspended/haltto your orchestrator’s success/wait/fail - [ ] Keep secrets out of blackboard and checkpoint files
- [ ] Prefer
--on-truncate haltfor fidelity-critical pipelines - [ ] Log
usageand correlation ids next to the outer job id - [ ] Treat tool observations and resume injections as untrusted (SPEC §6)
8. Out of scope (deliberate)¶
- HTTP webhook server / cron daemon inside the language core
- Built-in WhatsApp / Supabase / Gmail connectors (host plugins only)
- Encrypting checkpoints at rest (host responsibility if required)
- Guaranteeing identical gate traces across providers (measure via gate divergence; do not assume)
For product-level trigger and queue infrastructure, keep an outer orchestrator and call mklang for logic — that is the intended split between host platform and language runtime.