Best practices¶
Canonical checklist for writing, running, and hosting mklang machines.
How to author a correct file: Authoring.
How to tune reliability and cost: Patterns.
What the language guarantees: SPEC (cookbook §10, threat model §11).
This page answers: what should I always do, never do, and where does each rule live?
1. Layer discipline (do not mix layers)¶
| Layer | Owns | Examples |
|---|---|---|
Language (.mk) |
Control flow, prose contracts, portable structure | states, gates, tiers, tool: names, parse: list |
| Host runtime | Bindings, budgets, clocks, truncation policy, LLM adapters, produce/judge prompt assembly, ops logging, FS data roots for tools | tools={…}, hooks, on_truncate, context.today / now fill, llm/prompts.py, process loggers, plugin FS tools |
| Surface | UX, consent, compact observations, chrome vs content rendering, session audit | CLI flags, MCP tools, console brain, Markdown log, transcript/session paths, workspace .mk only |
Rules
- Side effects live only in
tool:states (host callables). Never inexecutionor generative prompts. - The
.mknever names a provider or model — onlytier:(ADR 0003). - Host tools are opaque names +
(dict) → str. Do not promote search/bash/FS into language syntax. - Generic bash / filesystem stay out of core (console: workspace
.mkonly; production I/O = plugins or external host).
2. Authoring checklist (every machine)¶
Before shipping a .mk:
- [ ] Schema header +
mklang: "0.3"when using 0.3 faces (parse: list, …). - [ ] Every non-terminal state ends with
when: otherwise(last). - [ ] At least one path reaches
END;budget≥ shortest path (+ headroom). - [ ] Every
{{path}}root iscontext:, a stateoutput:, HITLhuman.*, or fan-outitem/index. - [ ] Exact policy (amounts, allowlists, formats) uses
hook:, not prose alone. - [ ] Real I/O uses
tool:+ top-leveltools:declarations for documentation. - [ ] Time-sensitive machines declare
today: ""(andnow: ""for wall-clock) incontext:and useToday is {{today}}/Current local time is {{now}}in prompts. - [ ] Irreversible actions sit behind
escalate(and HITL in production). - [ ]
mklang checkclean;mklang lintclean (use--strictin CI). - [ ] Scenario tests cover happy path and escape hatches (
mklang test). - [ ] Sticky policy lives in
execution(system channel); turn data and{{…}}live inprompt(user channel) — see §3.
3. Prompt assembly (system vs user)¶
The reference interpreter builds LLM calls from language faces. There is no
system: keyword in the language (that would be a 0.4 ADR). Map faces to
channels:
| Face / artifact | LLM channel | Interpolated? | Put here |
|---|---|---|---|
structure |
system (produce) | No | Output contract / shape for this state |
execution |
system (produce) | No | Sticky operational policy (never side effects) |
prompt |
user (produce) | Yes {{…}} |
This turn’s task + data (history, today/now, observations) |
when: conditions |
judge user | No (prose) | Gate selection only |
Host JUDGE_SYSTEM |
judge system | fixed | Choice protocol {"choice": n} — not authorable |
Rules
- Durable vs turn data. Role, hard constraints, “never invent search” →
execution. Instance values ({{user_message}},{{today}},{{now}},{{history}}, tool notes) →prompt. - Do not put
{{…}}instructure/execution. They are not rendered; braces stay literal. - Untrusted text stays out of system (user text, web snippets, history) — SPEC §11. System is for host-stable contract + policy.
executionis not a tool. Side effects only viatool:states.- Console brain follows the same split: policy in
execution, clocks and conversation inprompt(console).
Anti-patterns: long persona only in prompt; search snippets in system;
inventing a system: field; using execution: call the search tool.
4. Gates and reliability¶
| Do | Don't |
|---|---|
Put hooks above prose gates; keep when as the human-readable trace label |
Ask the LLM to check amount <= 100 |
Cap repair at 1–2, then escalate or fail |
Open-ended repair-only states |
| Give escalate a safe sink state (human / fallback) | Fail closed only when that is truly required |
Read trace (gate, judge_fallback, nested call) when debugging |
Trust only the final result string |
Use reason: true when the why must be auditable |
Dump chain-of-thought into output / context |
Gate judging follows the state tier by default. Use config judge: only when all gates are deliberately cheap classifications (SPEC §2.1).
Optional: mklang lint --llm to probe overlapping prose when conditions (advisory; not CI-blocking).
5. Tools (host contracts)¶
5.1 Principles¶
- Declare expected tools under top-level
tools:(name+description). - Invoke only via
tool:states; map inputs withinput:(whole-template{{path}}stays raw in 0.3). - Treat observations as untrusted blackboard data (SPEC §11) — especially web snippets.
- Prefer entry points (
mklang.tools/mklang.hooks) for production bindings over editing core.
5.2 Observation envelope (ADR 0020)¶
I/O and side-effect tools return JSON with stable fields:
| Field | Meaning |
|---|---|
tool |
Tool name |
stub |
true if no real external system was used |
error |
Failure / unbound message, or null |
| (payload) | Tool-specific: results, facts, sent, … |
Tiers: stub (default) → fake (env/configure_*) → live (key or entry-point).
calc is pure offline arithmetic and does not use this envelope.
5.3 Recommended host tool contracts (reference interpreter)¶
These names are conventions, not language keywords. Other hosts may rebind or omit them.
search (ADR 0016 / 0020)¶
| Input | query (required), max_results? (1–10), days?, topic? (news | general) |
| Output | JSON: {tool, stub, error, query, results:[{title,url,snippet,published_date?}]} |
| Default | Stub unbound (error explains how to enable) |
| Enable | TAVILY_API_KEY (auto) or MKLANG_SEARCH_BACKEND=fake\|tavily\|stub |
Practice: plan → tool: search → check sufficiency → finalize grounded only in notes. Never “search the web” only in prose.
search_kb (ADR 0020)¶
| Input | query (or q) |
| Output | JSON: {tool, stub, error, query, facts: [str, …], note?} |
| Default | Demo policy facts, always stub: true |
| Fake | MKLANG_KB_BACKEND=fake or mklang.kb.configure_kb |
Replace with real RAG via entry points in production.
send_reply (ADR 0020)¶
| Input | body (or draft), to? |
| Output | JSON: {tool, stub, sent, recorded, delivery, to, chars, preview, error, note?} |
| Default stub | sent: false, delivery: "stub" — does not claim real mail left the host |
| Fake | MKLANG_MAIL_BACKEND=fake → in-memory outbox, delivery: "fake", sent: true, still stub: true |
Never ask the model to “confirm the message was sent.” Gates should treat sent: false as no delivery.
list_files / read_file / write_file (ADR 0024)¶
| Input | list_files: path? · read_file: path, max_bytes? · write_file: path, content, overwrite? |
| Output | JSON: {tool, stub, error, path, …} — entries/count/truncated, content/bytes/truncated, bytes/written/existed |
| Default | Live reads confined to the workspace (MKLANG_FS_ROOT or cwd); writes refused without a grant |
| Enable | Workspace: --workspace / MKLANG_FS_ROOT / cwd · Writes: --allow-write / MKLANG_FS_WRITE=1 · Offline: MKLANG_FS_BACKEND=stub |
Relative paths only; .., absolute paths, and dotfiles are refused; writes are
capped, suffix-allowlisted (never .mk), atomic, mode 0600. See §13 for the
class model and rules.
calc¶
| Input | expr (or query): arithmetic expression |
| Output | Decimal string, or error: … (not the I/O envelope) |
Safe subset only (no eval of Python). Use for ReAct demos and numeric observations.
5.4 What not to bake into the language¶
| Temptation | Keep as |
|---|---|
| Web search, HTTP, email, payments | Host tool: |
| Shell / arbitrary FS / git | Host plugin (sandboxed), never core |
Console write_machine / run_machine |
Console surface only |
“Current date/time” as $now keyword |
Declared context.today / context.now + host fill |
6. Web, time, and knowledge cutoff¶
Live or news-like questions fail in predictable ways if the machine relies on model training data.
| Practice | Detail |
|---|---|
Use tool: search |
research_web.mk, research_compress.mk, news_search.mk |
Declare today: "" |
Host fills ISO YYYY-MM-DD when still empty after inputs (CLI / MCP / console) |
Declare now: "" for wall-clock |
Host fills local ISO datetime with offset (e.g. 2026-07-17T14:32:05+02:00) — use for “what time is it?”, not for news recency alone |
| Prompt with calendar / clock | Today is {{today}} / Current local time is {{now}}; include year in queries when time-sensitive |
| Recency inputs | Prefer days + topic: news for news machines |
| Ground finalize | Cite titles/URLs/published_date from notes only |
| Forbid fill-in | Explicitly ban inventing facts or answering from pre-training when notes are empty/thin |
| Honest failures | If stub says search unbound, tell the operator how to enable it — do not fabricate hits |
Language note: there is no primitive that “disables knowledge cutoff.” Discipline is host clock + tools + prose + gates.
7. Output cutoff and context budgets (anti-cutoff)¶
| Layer | What happens | Practice |
|---|---|---|
| Produce length stop | Trace/events get truncated: true (ADR 0018) |
Prefer adequate max_tokens in runtime params; use --on-truncate halt for strict runs |
| Default policy | report continues with partial text |
Do not treat partial finalize as complete without checking trace |
parse: list + truncate |
Halts parse-list-truncated |
Keep planner outputs short and well-structured |
Produce {{…}} budget |
Long values end with …[truncated] (ADR 0017) |
Compress notes before the next loop (research_compress.mk) |
| Judge CONTEXT | Head+tail + …[context_truncated]… |
Put critical facts in state output, not only deep context |
| Console observation | Compact JSON: truncated, result_truncated, …[truncated] on clipped result |
Brain/user must report cuts — never invent the missing tail |
Continue-stitching after length stop is deferred (not default).
8. Memory and composition¶
| Situation | Practice |
|---|---|
Growing accumulate lists |
Explicit compress generative state; do not rely on silent host summary |
| Plan → map | Planner uses parse: list; executor over: "{{steps}}" |
Pass lists into call/tool |
Whole-template input: { x: "{{list}}" } (0.3 raw resolution) |
| Reuse architecture | Prefer std_* (call: std_refine, …) over copy-paste |
| Host-dependent patterns | ReAct / router / hooks stay authored examples, not pure stdlib |
9. Budgets and cost¶
- Step
budget: worst-case path × loops + fan-out width; leave repair headroom. - Fan-out: charges
max(1, len(branches))at runtime; static check counts fan-out as 1. - Token cost:
--max-tokens/cost_budgetshared withcallchildren. - Tiers: default
balanced; cascadefast→ escalate →reasoningfor mostly-easy work. - Sample diversity: temperature and/or
{{index}}in the prompt; do not assume all reasoners sample.
10. Testing and CI¶
| Layer | Command | Role |
|---|---|---|
| Schema + semantics | mklang check |
Blocking shape/graph |
| Static smells | mklang lint (--strict in CI) |
Typos, dead gates, unread outputs |
| Prose gate overlap | mklang lint --llm |
Advisory only |
| Path pinning | mklang test … --script … |
No API keys; escape hatches |
| Language contract | pytest + conformance/ |
Interpreter semantics |
| Live smoke | MKLANG_LIVE=1 pytest tests/test_live.py |
Opt-in providers |
Keep machine.test.yaml beside the machine. Cover escalate, repair exhaustion, empty tool results, and search-unbound paths for web machines.
Runs, the console, and lint --llm sit behind an upfront provider-key gate:
they fail fast naming the exact .env variable to set (local is exempt), so
keyless environments stick to check / lint / test.
11. Security (SPEC §11) — operational minimum¶
- Treat customer text and search snippets as injection-capable.
- Prefer hooks + HITL before irreversible tools.
- Checkpoints hold the full blackboard in plaintext (mode
0600is a floor, not encryption). - Do not put secrets in
.mkor context; keys stay in host env /.env. - Console: tool consent once per session; workspace confinement for authored
.mkfiles. - Do not confuse run trace / live events / ops logging (§12) or turn arbitrary disk into a language feature (§13).
12. Observability: trace vs events vs process logging¶
Three channels — do not merge them into one API.
| Channel | Purpose | Consumer | Persistence |
|---|---|---|---|
| Run trace | Semantic record: state, gate, policy, tokens, truncated |
Authors, tests, checkpoints | RunResult.trace, checkpoint JSON |
| Live events | In-flight progress (on_event) |
Console activity tree; MCP mklang.event (ADR 0019) |
Ephemeral; console may append to transcript.jsonl |
| Process / ops logging | Host diagnostics: adapter HTTP, retries, config, plugin errors | Operators, developers | stderr / host log file / future OTel |
Rules¶
- Trace is the source of truth for “what the machine did.” Events are a live shadow of the same story, not a second semantic model (ADR 0019).
- Ops logging is host-only — never a face of the
.mk, never deposited on the blackboard as “memory,” never a gate condition. - No
tool: login core. Business audit that must be a side effect is a named host tool with ADR 0020 envelope + consent — not free-form logging. - Observer isolation. A failing log sink or event listener must not abort
the run (same rule as
on_event/ MCP forwarder). - Secrets. Never log API keys. Prefer no full produce/judge bodies at default levels; DEBUG only when explicitly enabled.
- Levels (when host logging exists).
DEBUGadapters/raw HTTP;INFOcoarse host lifecycle;WARNINGstub tools, truncation,judge_fallback;ERRORhalts. Do not INFO-log every state (events already cover that). - Console separation. Conversation pane ≠ ops log. UI stays Rich/Markdown; diagnostics go to stderr or a host log path.
- MCP. Keep
mklang.eventfor run vocabulary only. Host stack traces use a different logger name (e.g.mklang.host), not the event stream. - OTel (optional, later). Spans are a projection of the trace for
platforms; they do not replace
RunResult.trace(ROADMAP).
Anti-patterns¶
print()in the engine; log-spam every token at INFO.- Putting log lines or file tails into context so gates “read the log.”
- Overloading MCP logging notifications with host debug (breaks clients that
treat
mklang.eventas the run UI feed).
13. Filesystem: four classes, not one tool¶
Generic bash/FS stay out of core. When you need disk, pick the class:
| Class | Examples | Where it lives | Controls |
|---|---|---|---|
| 1. Host-owned paths | runtime.yaml, checkpoints, console session dir |
CLI / host config | Operator-chosen paths; checkpoint mode 0600 |
| 2. Workspace authoring | write_machine / read_machine (.mk only) |
Console surface (ADR 0015) | Resolve under workspace; reject escape; confirm overwrite |
| 3. Machine data I/O | Read CSV, write a report | Builtins mklang.fs (ADR 0024) |
Workspace confinement, size/type limits, write grant, stub off-switch |
| 4. Arbitrary FS / shell | rm, bash, git |
Never core; explicit sandboxed plugin | Default off; high friction |
Current host layout (documentation SSOT)¶
This section is the documentation source of truth for current host-owned paths; ADR 0021 records the decision and rollout history, while surface guides should link here instead of maintaining a separate path policy.
| Root | Current location | Contents |
|---|---|---|
| Config | $XDG_CONFIG_HOME/mklang (default ~/.config/mklang) |
runtime.yaml, .env |
| Data | $XDG_DATA_HOME/mklang (default ~/.local/share/mklang) |
user machines/ |
| State | $XDG_STATE_HOME/mklang (default ~/.local/state/mklang) |
console/sessions/<id>/ and checkpoints |
| System | /etc/mklang, /usr/share/mklang/machines |
system config and machines |
Console sessions always live under
$XDG_STATE_HOME/mklang/console/sessions/<id>/.
mklang init --user creates these roots and seeds the user machines/ with a
commented hello.mk sample plus its hello.test.yaml scenario (keyless first
run via mklang test).
Local vs global (ADR 0023): runtime.yaml resolves first-hit-wins
(project → user → /etc → bundled) for every entry point, mklang-mcp
included; .env layers per key — real environment > project .env > user
.env; mklang doctor shows which layer won.
MKLANG_CONFIG_DIR, MKLANG_DATA_DIR, and MKLANG_STATE_DIR override the
corresponding user roots; MKLANG_CONFIG selects one runtime config file
directly. The implementation authority is mklang.paths; changes to it must
update this table and the console guide in the same commit.
Rules for class 3 (data tools)¶
Implemented by mklang.fs (list_files / read_file / write_file, §5.3;
ADR 0024). The reference posture is the coding-tool workspace model: reads are
live by default under --workspace / MKLANG_FS_ROOT / cwd, disk writes need
an explicit grant (--allow-write / MKLANG_FS_WRITE=1 / console consent).
- Names only in the
.mk—tool: read_file, not path syntax in the language. - Relative paths in tool input; host joins to the configured workspace
root. Refuse path escape after
resolve(same idea as console_workspace_path);.., absolute paths, and dotfile segments never resolve. - ADR 0020 envelope —
{tool, stub, error, …};MKLANG_FS_BACKEND=stubforces the offline refusal tier (reads default to live per ADR 0024 — the one sanctioned amendment to stub-by-default). - File bodies are untrusted observations (SPEC §11). Do not put them in the produce system channel; treat like web snippets.
- No recursive delete / shell in core. Destructive ops only as explicit plugins with strong confirmation.
- Audit lightly — log tool name + relative path + byte count at INFO; not full file contents.
- Console stays non-IDE — do not register general FS tools on the default
brain; keep workspace
.mkonly unless the operator opts into plugins.
Memory & planning mapping¶
The class model gives machines the same memory layering native coding agents use — each level has exactly one home:
| Agent memory level | mklang home | Class |
|---|---|---|
| Working memory (in-run) | Blackboard context + accumulate (SPEC §4.6) — never on disk |
— |
| Session state / resume | Checkpoints (0600) and console state.json — outside workspace |
1 |
Project memory (AGENTS.md, CLAUDE.md) |
Non-dotted files in the workspace, read via read_file |
3 |
| Plans / reports the machine produces | write_file under the workspace, behind the write grant |
3 |
| Global config / memory hierarchy | XDG roots + precedence (ADR 0021/0023) | 1 |
For machine authors: keep durable machine memory in non-dotted data files in
the workspace (e.g. memory/notes.md); update it with read → write_file
overwrite: true. Host state (checkpoints, sessions) is unreachable from
class-3 tools by construction — the dotfile ban makes the "data lake"
anti-pattern below structural, not just conventional.
Anti-patterns¶
- Language face
file:/$pathwithout ADR + conformance. execution: write the result to disk.- Widening
write_machineto arbitrary extensions/paths. - Using session/transcript directories as a machine “data lake.”
- Absolute paths from the model without canonicalize + root check.
14. Surfaces quick reference¶
| Surface | Best practice |
|---|---|
| CLI | init once, doctor when in doubt, then check → lint → test → run; --on-truncate halt for strict research; --hitl for human gates (auto-checkpoints; --checkpoint to choose the path); ops log on stderr when enabled |
| MCP | Commission by name/path/source; stream run events as mklang.event only; durable checkpoint_path for multi-process HITL |
| Console | Prefer RUN of workspace/search machines for live facts; honor truncation fields; enable Tavily for web; Markdown chrome/content (console rendering); workspace .mk only — no generic FS/bash |
Console cancellation and shutdown (documentation SSOT)¶
Ctrl+Grequests cooperative cancellation between states and keeps the console open; an active provider response is allowed to finish.Ctrl+Cand/quitclose the surface. During an active run, shutdown sets the cancellation signal, releases pending human input, invokes the optional providerclose()hook to interrupt in-flight I/O, waits for the backing worker thread, and then tears down Textual.- Provider plugins remain compatible without
close(), but network-backed adapters should implement it so console shutdown cannot wait for an SDK timeout. Shutdown hooks must be idempotent and must suppress late UI/session callbacks after teardown begins.
15. Anti-patterns (quick list)¶
execution: use the search toolon a generative state.- Asking the model to confirm a side effect it cannot perform.
- Prose-only money/policy thresholds.
- Answering “what happened this week?” without
tool: searchandtoday. - Silent acceptance of truncated produce / clipped console result as complete.
- Unbounded
accumulatewithout compress. budgetsized for the happy path only.- Naming
claude-…/gpt-…inside the.mk. - Putting PII into checkpoints without a retention policy.
- Expecting stdlib pure machines to perform host I/O.
- Treating a stub
send_replyas real delivery (sentmust be true andstubfalse for live). - Interpolating user/LLM text into Rich markup in a TUI (
[b]…[/b]) — use Markdown renderables for agent prose and plain/fenced text for everything else (Console). - Putting sticky role/policy only in
prompt(user) instead ofexecution(system), or putting{{user_message}}/ history intostructure. - Mixing ops logging with run trace/events, or logging secrets/full prompts at default levels (§12).
- Generic filesystem/bash in core, or treating console workspace as full disk access (§13).
16. Language vs host: what may become language later¶
Candidates for a future 0.4 (need ADR + conformance) — not current practice requirements:
| Candidate | Why it might become language |
|---|---|
parse: json / object |
Structured composition beyond lists |
Machine/state on_truncate policy |
Portable anti-cutoff in the document |
| Context zones / pin (ADR 0017 L2) | Trusted vs untrusted blackboard |
Per-gate hitl: |
Finer HITL than run-level |
| Budget split (steps vs fan-out width) | Clearer volume caps |
Until then: use host policy + patterns + this checklist. Do not invent ad-hoc syntax outside the schema.
Related¶
| Doc | Role |
|---|---|
| Getting started | First-run path: install → init → key → console |
| Install | pipx / AUR install, host layout, completions |
| Authoring | Recipe + skeleton + faces → LLM channels |
| Patterns | Tiers, reliability, clocks, execution usage |
| Stdlib | Ready std_* architectures |
| Console | TUI, rendering, brain clocks, consent, workspace FS |
| SPEC §4–§6 | Faces + produce/judge semantics (+ non-normative host notes) |
| SPEC §8 | Trace / observability |
| SPEC §11 | Threat model (injection, checkpoints at rest) |
| ADR 0015 | Console scope (not an IDE) |
| ADR 0019 | mklang.event vs ops log |
| ADR 0020 | Tool envelope for I/O (incl. future FS tools) |
| ROADMAP | OTel maybe; no bash/FS in core |