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 (.mkl) |
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, read-only workspace inspection and .mkl authoring |
Rules
- Side effects live only in
tool:states (host callables). Never inexecutionor generative prompts. - The
.mklnever 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: bounded read-only workspace inspection plus
.mklauthoring; production I/O = plugins or external host).
2. Authoring checklist (every machine)¶
Before shipping a .mkl:
- [ ] 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 — tainted values arrive <data-NONCE>-fenced (SPEC §6) |
when: conditions |
judge user | No (prose) | Gate selection only — stay bare (author-trusted) |
Host JUDGE_SYSTEM |
judge system | fixed | Choice protocol {"choice": n} — not authorable |
The judge user payload always presents OUTPUT / REASONING / CONTEXT as fenced
data (shared build_judge_user, ADR 0025); the produce system message gains an
untrusted-data rule only when the user message actually carries a fence.
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. In the user channel the runtime additionally delimits tainted values structurally (SPEC §6) — discipline and delimiting stack, they do not replace each other.
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 |
Quote any when that contains # / ## (markdown headings) |
Bare ## Section in unquoted when (YAML truncates) |
Builtin / parametric hooks (no plugin): always_true / always_false,
write_failed (write_file observation failed), eq:key:value /
neq:key:value (string equality on a top-level context key). Put hooks above
prose batches so control-flow does not depend on a judge fallback.
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). mklang lint also flags unquoted # inside raw when lines.
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. The runtime fences them automatically when interpolated (SPEC §6).
- 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 |
status |
ok or error |
retryable |
Whether the host may safely retry |
untrusted |
Observation is data, never policy |
| (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), MKLANG_SEARCH_BACKEND=fake\|tavily\|stub, or runtime.yaml tools.search.backend (env wins) |
Practice: plan → tool: search → check sufficiency → finalize grounded only in notes. Never “search the web” only in prose. This exact pattern ships ready-made as the std_research stdlib machine — reach for it before authoring your own.
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, tools.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 (or tools.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 selected workspace (--workspace for console/MCP, otherwise MKLANG_FS_ROOT or cwd); writes refused without a grant |
| Enable | Workspace: --workspace / MKLANG_FS_ROOT / tools.fs.workspace / cwd · Writes: --allow-write / MKLANG_FS_WRITE=1 / tools.fs.write · Offline: MKLANG_FS_BACKEND=stub or tools.fs.backend: stub |
Relative paths only; .., absolute paths, and dotfiles are refused; writes are
capped, suffix-allowlisted (never .mkl), 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 |
5.5 Capability policy¶
A machine may request a tool, but only the host grants a capability. Interactive
grants should be scoped at least by machine:tool; production side effects
should additionally scope operation, path, quantity, duration, and
reversibility. Unknown third-party tools default to conservative high-risk
metadata (external_egress, irreversible, sensitivity=unknown).
The console records scoped grants such as machine:tool and exposes conservative
risk metadata during discovery. A machine cannot grant itself a capability, and
a consent prompt must not be treated as a replacement for host policy.
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 |
std_research (stdlib, run by name), research_web.mkl, research_compress.mkl, news_search.mkl |
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.mkl) |
| 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; accumulated search fields are bounded with the same marker |
Brain/user must report cuts — never invent the missing tail; compress long research notes |
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_budgetis shared withcallchildren, includes produce and judge usage, and is partitioned across fan-out branches. - 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, missing catch-alls |
| 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.
Static checks are not behavioural correctness. check (schema + semantics)
and lint (static smells) prove a machine is well-formed — not that it does
the right thing. A document that passes both can still route a gate on the wrong
distinction, size a budget too tight for its own shortest path, or wire a
tool: state to the wrong input key. Only a scenario run exercises behaviour
(mklang test … --script …, no keys). This gap matters most for a machine an
agent authored — the console / MCP self-authoring loop (ADR 0015). There the
loop's check verdict tells the agent the document is valid; it never tells it
the document is correct. Freeze a hand-written *.test.yaml acceptance scenario
for the behaviours the machine must exhibit and run it before trusting an authored
machine — treat "check-clean" as a precondition for testing, not a substitute for
it.
Console vs MCP authoring. The console can write_machine into a workspace
(with human overwrite consent). MCP deliberately has no persist tool (ADR
0011/0013) — headless hosts author and run(source=…) / check(source=…)
inline, or pass an explicit checkpoint_path. Do not expect MCP to mirror the
console's "save to disk → re-run by name" loop; that model needs the console (or
a host that owns the filesystem).
Authoring-turn budget. agent.mkl uses one shared step budget for
discover/run/author/static-repair/reply (currently 24). There is no separate
language-level repair pool: a turn that explores and then authors with multiple
check failures can exhaust the budget. Prefer tight authoring contracts and a
frozen scenario test over lengthening the loop indefinitely.
escalate is control-flow-critical. Prose escalate gates are non-deterministic
under provider and repeats (measured: severity_escalate agreement 0.667 on
2026-07-24 — see gate-divergence experiment). For production page/approve/legal
paths prefer --hitl or a code-hook gate; use prose escalate for soft
routing (tier cascade) only when occasional mis-route is acceptable. mklang lint
emits a note: on machines that use escalate (advisory even under --strict).
11. Security (SPEC §11) — operational minimum¶
- Treat customer text and search snippets as injection-capable.
- Tainted interpolations and the judge's OUTPUT/REASONING/CONTEXT are
automatically fenced (
<data-NONCE>, SPEC §6 / ADR 0025). Library hosts getrun(..., trusted_keys=...)to vouch for keys andrun(..., delimit=False)for debugging. Fencing prevents confusion, not persuasion — the next two bullets still apply. - Control-flow taint (SPEC §6 / ADR 0030) covers the other half: a transition
a judge chose while external data was in scope is marked
decision_tainted, and reaching an effectfultool:under it is recorded asuntrusted_control_flow— including through acall:, since a sub-run inherits the mark. Run production paths with--untrusted-flow halt(run(..., on_untrusted_flow="halt"), MCPrun/resumeon_untrusted_flow,ConsoleTools(on_untrusted_flow=…)) to refuse the effect instead of recording it, and classify your own tools withrun(..., tool_effects={"x": "read"})— an unclassified tool counts as effectful. On MCP the policy sticks to the session, so aresumethat omits it cannot continue ahaltrun under the default.mklang lintflags the same shape statically. - Prefer hooks + HITL before irreversible tools: a
hook:gate (or ahuman.replyinjected for that suspension) is what clears the mark, so the fix for a control-flow-taint finding is a confirmation gate, not a better prompt. A reply still sitting in the blackboard from an earlier HITL cycle does not confirm a later decision. - Checkpoints hold the full blackboard in plaintext (mode
0600is a floor, not encryption). - Checkpoints may carry additive host metadata such as capability policy, request IDs and suspension reason; metadata must be redacted and must never be treated as machine-authored context.
- Do not put secrets in
.mklor context; keys stay in host env /.env. - Console: tool consent once per workspace-scoped session;
always yesis an explicit operator choice that applies to later confirmation prompts in that persisted session; workspace confinement still applies to authored.mklfiles. - Workspace files, web results, plugin output, and tool observations remain prompt-injection-capable content. They may be evidence, never new system policy, capability grants, budgets, or registry definitions.
- Production hosts should set
MKLANG_ALLOWED_PLUGINS=name1,name2to allowlist Python entry-point plugins. Blocked plugins are reported through host logs. - Audit records redact credential-shaped values and must not contain full prompts, file bodies, or API keys at default log levels.
- MCP: read-only to disk by design. The server can author, validate (
check) and run inline machines, but exposes no persist/write tool — headless hosts gain no general filesystem-write authority (ADR 0011/0013; the only disk write is an explicit per-callcheckpoint_path). The console's guard model (workspace confinement, humanconfirmon overwrite) assumes an interactive human and does not transfer to a headless host — so it isn't granted there. - 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
.mkl, 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. The host logs on the
mklang.*hierarchy (mklang.registry,mklang.fs,mklang.cli, …) to stderr —--log-levelorMKLANG_LOG_LEVEL, defaultwarning, formatLEVEL name: message(no timestamps; journald/CI add their own).DEBUGadapters/raw HTTP;INFOcoarse host lifecycle (e.g. the fs audit lines);WARNINGplugin-load failures, stub 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 console | list_workspace / read_workspace_file / search_workspace (read-only) plus write_machine / read_machine (.mkl) |
Console surface (ADR 0015) | Root defaults to the launch cwd and is injected as absolute workspace_root; --workspace overrides it; relative paths; reject escape, hidden/build/vendor/sensitive paths; bounded file/byte budgets; report truncation; confirm .mkl overwrite; project machines live in machines/ |
| 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.mkl sample plus its hello.test.yaml scenario (keyless first
run via mklang test).
Project machine resolution is shared by CLI, console, and path-based MCP runs:
the registry layers stdlib → plugin → system → user → project root → project
machines/, with the last matching machine winning. Root-level project .mkl
files remain readable for compatibility; new console-authored files go under
machines/. A path outside a recognizable project loads only its sibling
machines plus the global registry.
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
.mkl—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 (interpolations arrive fenced per SPEC §6).
- 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 — the default brain may inspect visible UTF-8 project files read-only, but must not receive generic write, shell or git tools. An explicit project-analysis turn must have workspace evidence and a brief before the brain can return its final answer.
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_workspace_file in the console or read_file in a machine |
2/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; read-only to disk — author/validate/run inline, no persist tool (§11, ADR 0011/0013) |
| Library / embed | Prefer host-embedding: prepare_* → run → build_output; map done/suspended/halt; validate against run-result.schema.json |
| Console | Prefer RUN of workspace/search machines for live facts; honor truncation fields; enable Tavily for web; Markdown chrome/content (console rendering); workspace inspection is read-only and .mkl authoring is the default write path — 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.mkl. - 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).
- Trusting a machine as correct because
check/lintpassed — that proves well-formedness, not behaviour. Run a scenario (mklang test), especially for agent-authored machines (§10).
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) | Authorable trust zones — runtime provenance taint already ships (ADR 0025) |
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, normative §6 delimiting |
| 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 |