Recommended configurations & flows¶
Operating guidance for building good machines. The cookbook (SPEC.md §10)
maps architectures to constructs; this page is about configuring them well.
Canonical checklist (do / don't / layers / tool contracts / anti-patterns): Best practices. Correct-file recipe: Authoring.
Tiers — route by cost, escalate for quality¶
Use fast for |
Use balanced for |
Use reasoning for |
|---|---|---|
| classify, route, extract | most generative states | validation, synthesis, critical gates |
| high-volume fan-out branches | drafting | final answers, policy judgements |
- Default
default_tier: balanced. Override per state; don't reach forreasoningreflexively — it's the expensive tier. - Gate judging follows the state's tier by default (SPEC §2.1): a
reasoningstate's high-stakes gates (refund thresholds, legal matters, human escalation) are judged by the reasoning model, not silently downgraded. Thejudge:config key is an opt-in global override that forces one model for all gate judging — a cost/latency optimization that also downgrades your most critical gates, so reach for it only when your gates really are uniform, cheap classifications. - Speculative cascade beats a flat
reasoningmachine on cost: draft atfast, and let anescalategate promote only the low-confidence cases to areasoningstate. Same answers, a fraction of the tokens.
Reliability — gates are the safety net¶
- Use code-hook gates for exact checks (
hook: name→ host(ctx, output) -> bool). Amounts, equality, allowlists: do not ask the LLM. Put hooks above prose gates; keepwhenas the trace label. Seeexamples/hook_gates.mkand ADR 0006. Custom tools/hooks: package entry pointsmklang.tools/mklang.hooks(see CONTRIBUTING). - Never put real I/O in generative states. Searching, sending mail, charging a
card: use
tool:states (host callables). Do not writeexecution: use tool Xon a generative state — the model cannot call tools there and will invent observations. Do not ask the model to "confirm the message was sent." - Honest stub observations (ADR 0020). Reference I/O tools return JSON with
tool,stub,error. Defaultsend_replyhassent: false; defaultsearchis unbound until Tavily/fake is enabled. Readstub/errorbefore treating an observation as live data (Best practices §4). - Web search is a host tool, not a model skill. Builtin
searchis a structured stub until a backend is bound.TAVILY_API_KEYalone auto-enables Tavily; or setMKLANG_SEARCH_BACKEND=fake|tavily|stub. Never put "search the web" only in generativeprompt/execution— the model will invent hits. Usetool: search(seeexamples/research_web.mk,machines/news_search.mk). Optional tool inputs:days,topic(news|general); results may includepublished_date. Snippets are untrusted (SPEC §11). executionfor sticky policy. The reference interpreter putsstructure+executionon the system channel andprompton user. Prefer durable guardrails inexecution(no inventing search, honesty on truncation) and keep{{…}}data inprompt(Best practices §3).- Host clock convention (
today/now). Declare empty keys incontext:; CLI / MCP / console fill them only when declared — they never invent undeclared keys. This is host convention + authoring discipline, not a language primitive.
| Key | Fill | Use for |
|---|---|---|
today: "" |
ISO date YYYY-MM-DD |
News/recency, knowledge-cutoff framing |
now: "" |
Local ISO datetime with offset | Wall-clock (“what time is it?”) |
Prompts should say Today is {{today}} / Current local time is {{now}},
prefer recent sources, and forbid filling gaps with pre-training knowledge
older than that date.
- Watch for output cutoff. When a produce hits max_tokens, the runtime sets
truncated: trueon the trace step and on livestate-doneevents (ADR 0018). Default policy isreport(annotate and continue); use--on-truncate haltorrun(..., on_truncate="halt")(also on MCP/console) for strict runs. Prefer raising tiermax_tokensparams over relying on auto-continue (continue stitching is deferred, not the default). Consolerun_machineobservations propagate produce truncation and mark clipped results with…[truncated]+result_truncated— never treat a cut observation as complete. - Bound growing blackboards (working memory vs archive). Long
accumulate/ research loops explode prompts. Prefer an explicit compress generative state that rewrites a key shorter before the next loop — seeexamples/research_compress.mk. The host does not summarize for you: it only caps judge CONTEXT (head+tail marker), per-value produce interpolation (high default,…[truncated]), and console brain history (last N turns / chars). Transcript and fullSession.historystay the audit archive; only the prompt view is windowed (ADR 0017). - Treat
{{context}}as untrusted when it may contain customer or web text. Prose gates and prompts interpolate raw values; injection can steer transitions (SPEC §11). Prefer hooks + HITL before irreversible tools. - End every non-terminal state with an
otherwisegate. Without it, a run canhaltwithno-gate-matched— and if the judge returns garbage, the runtime hard-halts withjudge-unparseableunlessotherwiseis eligible (soft fallback is recorded asjudge_fallbackin the trace).mklang checkwarns when the catch-all is missing. - Guarantee a reachable
END.mklang checkerrors if none exists. - Fix
unresolved-interpolationlint before shipping.mklang lintflags any{{path}}whose first segment nocontext:key, stateoutput:, or (inside a fan-out)item/indexprovides — a typo ({{kb_answr}}) otherwise renders to an empty string and silently degrades the prompt. Under--strictit fails the run. If a host injects extra context keys at run time, declare them incontext:with placeholder values so the reference to them resolves and the lint stays quiet. (When the root is an inlinecontext:map —ticket: {body: …}— the second segment is checked too, so{{ticket.bod}}is caught. Deeper tails, and roots whose shape isn't statically known (state outputs, runtimehuman/item/index), are not verified against prosestructure.) - Cap
repair. Arepair: Nwith a modestN(1–2) plus a followingescalate/failgate prevents an endless self-correction loop. - Give escalation a safe sink. Route hard cases to a terminal
human_reviewstate rather than failing — it's a graceful degrade, not a crash. With--hitlthe run actually pauses on a fired escalate (checkpoint under$XDG_STATE_HOME/mklang/checkpoints/, or wherever--checkpointpoints) andmklang resume --set human.reply="…"feeds the decision to the handler (ADR 0008). - Size
budgetto the worst case. Roughly: longest path × loop iterations, plus the width of any fan-out (asample: Ncosts N steps). Leave headroom; hitting the budget is ahalt. - Let
mklang check/lintcatch impossible budgets. Ifbudgetis below the shortest path (in states) fromentryto a gateto: END, validation reports errorbudget-infeasible— a guaranteedbudget-exhaustedbefore the first provider call.budget < shortest + 2is a warning (no headroom for a single repair). Fan-out states count as 1 in this static check (branch width is data-dependent), so a machine can still exhaust budget on a wide map even after the check passes (SPEC §7). - Map-reduce: size
budgetagainst data cardinality. A fan-out chargesmax(1, len(branches))steps (SPEC §7), sobudgetis also a volume cap — anoveron 30 items withbudget: 25haltsbudget-exhaustedbefore the reducer. Setbudget ≥ expected branches + machine overhead, or bound the list before the fan-out. If the item count is unknown at authoring time, size for the worst case. - Use
--max-tokens(cost budget) on longcalltrees. The remaining budget is shared with sub-machines so a runaway child cannot burn tokens unbounded. - Fan-out concurrency is a
ThreadPoolExecutorwithmax_workers=5today — fine for modestsample/overwidths; very wide maps should stay small or wait for the async roadmap item.
Testing — pin the gates before you spend a token¶
- Write scenario tests for every gate you would be embarrassed to see misfire.
mklang test machine.mk --script machine.test.yamlruns the machine against a scripted LLM (produce texts + judge picks) and scripted tools/hooks — no provider, no API key, fully deterministic. Each scenario is a named case in the conformance format (llm/tools/hooks/run+expect), and the runner shares its matcher with the conformance suite, so a green scenario means the interpreter would route your machine exactly that way. - Cover both the happy path and the escape hatches. The value is in the
branches you hope never fire: the escalate-to-human path, the repair loop giving
up, the empty-tool-result fallback. Script the judge pick that steers into each
and assert the
traceskeleton (state→to,policy) lands where you think. Seeexamples/triage.test.yaml(happy path + KB-empty escalation). - Keep scenarios next to the machine (
triage.mk→triage.test.yaml) and run them in CI — a.mkedit that reroutes a gate fails the scenario, not a customer.
Reasoning & observability¶
- Use
reason: trueon states whose why matters (diagnosis, judgement, synthesis). The chain-of-thought lands in the trace, not the context — you get auditability without polluting downstream prompts. On reasoner models it maps to native thinking for free. - Read the trace, not just the result. Every transition records the gate that
fired and (for fan-out/
call) the nested detail. When a run goes wrong, the trace says exactly where and why.
Composite flows (which pattern when)¶
| Situation | Reach for |
|---|---|
| One high-stakes answer, want robustness | Self-consistency (sample → vote) |
| Many similar items (docs, tickets, rows) | Map-Reduce (over → reducer) |
| Quality-critical prose | Reflexion (repair loop, or a critic) |
| Distinct request types → distinct handling | Router-of-experts (classify → call) |
| Mostly-easy workload, occasional hard case | Speculative cascade (fast → escalate) |
| Open-ended tool-using task | ReAct (think → tool state → accumulate loop) |
| Explore several partial solutions, keep the best | Tree-of-Thought (sample → select → loop) |
Provider notes¶
- The
.mknever names a model — only tiers. Pick models inconfig/runtime.example.yaml(defaultactive: deepseek); keys come from.env(DEEPSEEK_API_KEY, …). Switching provider is a one-lineactive:change ormklang run --provider …. - Diversity for
samplecomes from temperature; keep the sampling state on a model that honors it (most chat models do — some reasoner models ignore it). Each sample branch also sees its own{{index}}(0-based), so you can drive diversity explicitly — "you are branch {{index}}, take a different approach" (Tree-of-Thought, debate), not temperature alone. reason: trueyields a captured chain only on models that expose thinking (Anthropic adaptive, DeepSeekdeepseek-reasoner, o-series). On plain models the model still reasons internally; the trace just won't hold the scratchpad.- Per-tier
paramsare applied to each generation:effort/thinkingon Anthropic,reasoning_efforton OpenAI/xAI, etc. They're best-effort — a param a provider doesn't support is dropped and the call retried, so mixing providers never breaks. Put them underproviders.<name>.params.<tier>in the runtime config. Prefer setting a healthymax_tokenson balanced/reasoning tiers so produce is less likely to hit a length stop (ADR 0018); truncation is still traced when it happens.
Layer boundaries (quick)¶
Put in the .mk |
Keep on the host / surface |
|---|---|
Gates, tiers, tool: / hook: names |
Tool/hook implementations, API keys |
parse: list, compress states |
on_truncate default, search backend |
Declared context.today: "" / now: "" |
Filling today (date) / now (local datetime) |
| Scenario tests next to the machine | Console consent, MCP sessions, bash/FS plugins |
| Trace / live events / ops logs mixed | Keep channels separate (Best practices §12) |
| Generic read/write disk in core | Class-3 host tools with root + stub only (§13) |
See Best practices §1 and §13 for the full layer map and what may become language 0.4 later (not current syntax).