Harness laws: inference, local tiny models and reasoning depth #
Shared rules (what a law secures, adversary, H1 to H6): harness.md.
S6. The inference layer #
Between harness and wire APIs; decides what a request may ask of a provider.
-
Quirks rulebook: family, model and host settings as data. Rule = layer, target id, directives with values. Compile rejects unknown directive, wrong value type, two rules of one layer+target giving one directive different values. Lookup: host > model > family. No rule → Unknown (no guess; wire API default applies). Explicit
model.compatbeats both. Rule order changes no answer. -
One target per layer:
- Family: the one family whose pattern matches the id (or id and name) and whose revision range, if any, holds. Revision (omp taxonomy): from first digit after the prefix in the bare name; a size (
8b) is not one; none → 0.qwen3.8-27b→qwen-3.8(>=3.8);qwen3-8b,qwq-32b→qwen(<3.8). - Two families match →
QuirksError::FamilyOverlap; none wins by id or order. Shipped-rulebook test rejects overlap on any compiled-catalog model. Out-of-catalog overlap → no family: family directives Unknown; error logged once per model id. - Model: model id.
- Host: host naming the wire API (API one host speaks), else naming the provider id, else longest URL fragment in base URL (tie: smaller host id). One host; signals never combine: provider
openrouteron a loopback base URL (local proxy) getsopenrouter, notloopback.openrouter(provider match) adds Anthropic-formatcache_controlmarks foranthropic/models;openrouter-url(custom provider, openrouter.ai base URL) gives other OpenRouter settings, no marks.
- Family: the one family whose pattern matches the id (or id and name) and whose revision range, if any, holds. Revision (omp taxonomy): from first digit after the prefix in the bare name; a size (
-
Forced tool call = tool + retry count. Each request ends with a soft prompt naming the tool. First request sets native forced choice only if supported and free (no prompt-cache invalidation); retries set it if supported. Support per request (
maps_forced_tool_choice, wire API's predicate). Manual extended thinking: Anthropic and Claude on Bedrock reject forced choice → unsupported, no escalation to an unsent flag; a wire API given one there drops it and warns. No call → retry while retries remain, thenForcedToolError::NotCalled(names tool). Nothing forced → no change. -
≤
strict_capstrict schemas per request, highestpriorityfirst (default0; ties: earlier tool). Rest loose: client validates and repairs arguments. None dropped. Strict-required tool past cap → error; several → names the highest-priority one. -
Repetition guard, text and thinking deltas of every stream:
- Chunk = one complete non-blank output line; lines over 4096 bytes (
MAX_CHUNK_BYTES) split there on a character boundary, so no-newline loops (abab…) are cut. - Window = last
kchunks (retry.provider.streamRepeatLimit, default 64); periodic with periodpif each chunk after the firstpequals the onepearlier. Period 1 tok / 4(at least 1) → abort: cycle seen 4 times; period 1 = one chunkktimes; default cuts ≤ 16-line blocks seen 4 times. Otherwise not cut. - State: per
p, count of consecutive chunks up to the newest equal to the onepearlier; periodic at ≥k - p. Holds ≤k / 4chunks,k / 4counters, one partial chunk per block, any stream length. - Cut → retryable
stream_repeating; message gives period and last line; failure info records them (RepeatedLines,repeated_lines). Harness retries with a new request ending in an unlogged note (attempt, period, last line) (harness.md H4.7); a greedy model (elpis-fast, refuses temperature ≠ 0) loops on the same request. - Example:
qwen3.8-27bthinking looped 1,359 lines on a 7-line cycle, no line twice in a row; a one-chunk-k-times check misses it; this guard cuts at line 77. - No text and no tool call = no answer: model error, retried with a note (harness.md H4.7).
lengthstop, no answer → length note (harness.md H4.8). Noted retries (cut stream, no answer) spend aMAX_NOTES(8) note, not a retry, and go at once.
- Chunk = one complete non-blank output line; lines over 4096 bytes (
-
Compaction:
- Threshold = limit (
contextWindow - reserveTokens) − margin (BackgroundMargin: eukhe session 10% of window,eukhe-core; harness library default 32,768 tokens). Over threshold → background compaction; over limit → request blocks until done. - Ends mid-run → summary placed at next run boundary; wire
compaction_endsent then with it, or as a skip if stale behind a later compaction. Queued summary survives run end (aborted run keeps queued writes). Summary write leaves queue unsettled, or a snapshot replaces events that could settle it →compaction_endunsuccessful. Summary queued (not placed, not stale) → no new background compaction. - Keeps newest
compaction.keepRecentTokens(eukhe default 40,000; system entries count), ≤ half the threshold so history remains; small windows keep less. - Check uses a local input-token estimate.
compaction.providerTokenCount(default off): ask providercount_tokensfirst (+1 request per generation); no counter or failure → warning, local estimate.
- Threshold = limit (
-
Provider operations beyond chat; eight capabilities:
count_tokens,list_models,embed,generate_image,tokenize,usage,web_search,compact.- Directive named after the operation = endpoint dialect (
"list_models": "openai") orfalse; compile rejects a dialect lacking the operation. - Scope-
capabilitieshosts (openai,google,llama-cpp) set only capability directives andprompt_progress; wire lookup skips them (localopenaiprovider keeps loopback settings). llama-cpp:"prompt_progress": true→openai-completionssends"return_progress": true; stream emitsprompt_progress(total,cache,processed,time_ms) per progress chunk before first token.model.compat.supportsPromptProgresswins.- Missing operation → typed
Unsupported { capability, provider }before auth or request; never another provider or local estimate (callers request the estimate explicitly). eukhe provider caps [<provider>]: support, no request.eukhe provider models <provider>: provider's model list.
- Directive named after the operation = endpoint dialect (
-
Service tiers (
service_tiers): entry = provider, wire API, tiers, optional family. Model honorsdefault+ tiers of entries matching provider, API, and family if named. Shipped:openrouter/openai-completions:flex,priority(OpenRouter routes to tier endpoints, bills the serving tier).openai/openai-responses,openai-codex/openai-codex-responses:auto,scale.- Families
gpt-priority,gpt-priority-explicit-cache: +flex,priorityonopenai; +priorityonopenai-codex(ChatGPT backend: no flex).
Readers (
quirks::service_tiers):/tier,/fast, connection state (availableServiceTiers), image route, completionsservice_tier. -
Thinking ladder (S10, ladder 0):
thinking_laddergives a family athinkingLevelMap. Shipped:qwen-3.8:off(wirenone),low,medium,xhigh.qwen:off(template toggle),high(omp's llama.cpp rules for Qwen templates).grok-4.3:offasnone; nominimal.grok-4.5: nooff,minimal.grok-4.6(idsgrok-4.6,grok-4.7): nooff,minimal; +xhigh.
Defaults (
thinking_defaults) fill missing entries:reasoning=offtohigh;plain(no reasoning) =off. Stored xAI subscription moves xAI models toopenai-responses; there, nothinkingLevelMap→ model's rulebook ladder, else all levels null. -
Tool calls read at stream end, any stop reason. Reply with tool calls runs them (vLLM, llama.cpp, Ollama stop with
stopplus calls) (harness.md H4.1).lengthreply runs none (arguments may be cut); each getstruncated: repeat the call with complete arguments; write large content as a minimal version first, then extend it with edits (harness.md H4.1). Tool-call markup in text of a call-less reply (five families: Hermes and Qwen JSON, Qwen3-Coder XML, omp "qwen" form, Kimi section tokens, DeepSeek DSML; not in quoted code) → rebuilt if every leaked call parses and names an offered tool: markup leaves the text, stop reason becomestoolUse, calls run as tool calls; else retried with a leak note, ≤LEAK_NOTES(2) in a row, then answers as text (harness.md H4.8.3; lawsrun_calls_never_answer,rec_leak_rebuilt_is_calls). Code:eukhe-durable/src/harness/generation/leak.rsscan,generation.rsclassify,generation/round.rsstart_tool_round. -
Provider default model:
provider_defaults(provider id → model id,quirks::provider_default_model). Read by startup model chain, fallback model template, onboarding after Prime Inference sign-in. Unlisted → no default. -
Prices missing from catalog rows:
service_tier_prices: row = provider, wire API, tiers (responseservice_tier, else request's), multiplier, optional model id. Cost = base × multiplier of provider+API+id row, else provider+API row without id; none → base. Shipped:openaionopenai-responses,openai-completions:flex0.5;priority,fast(API's Fast-mode value) 2,gpt-5.52.5.openai-codex:flex0.5;priority2,gpt-5.52.5.cache_write_1h_prices: row = regex on lowercased model id + input-rate multiplier; first match prices 1h cache writes; none → model's cache write rate. Shipped:claude2 (Anthropic's price; Bedrock and gateways reselling Claude at Anthropic list rates charge it too).
Read by
models::calculate_cost,models::apply_service_tier_pricing(quirks::cache_write_1h_multiplier,quirks::service_tier_multiplier). -
Every other model fact a wire API reads is rulebook data (S6.10); wire APIs never test the model id.
adaptive_thinking(claude-adaptive,claude-adaptive-4-6):thinking.type: adaptive+ effort, Anthropic Messages and Bedrock (models::adaptive_thinking); explicitcompat.forceAdaptiveThinkingwins.thinking_block_binding(claude-adaptive): Bedrock sendsthinking.block_binding(not GovCloud).prompt_caching(true:claude-adaptive,claude-adaptive-4-6,claude-cached; false:claude-uncached): Bedrock sendscachePointblocks; unnamed models cache only withAWS_BEDROCK_FORCE_CACHE=1.explicit_cache_breakpoints(gpt-explicit-cache,gpt-priority-explicit-cache): OpenAI Responses sendsprompt_cache_breakpoint,reasoning.context: "all_turns".google_thinking_level(true:gemini-3-plus,gemini-latest,gemma-4; false: Gemini 2.5 families): Google sendsthinkingLevel; unnamed →thinkingBudget.thinking_budgets(gemini-2.5-pro128, 2048, 8192, 32768;gemini-2.5-flash128, 2048, 8192, 24576;gemini-2.5-flash-lite512, 2048, 8192, 24576): Google per-level budget, Gemini API and Vertex. UserthinkingBudgetswins, clamped to the rule'sminimal..high. No rule → user budget, else-1(dynamic). Unordered rule budgets (notminimal≤low≤medium≤high) → error, never a clamp (rulebook compiler refuses).thinking_defaults.budgets(1024, 2048, 8192, 16384): budget when neither user nor rule gives one, every token-budget wire API.
Family
exceptregex excludes matches (families stay disjoint). Bedrock effort = model's thinking-level map (nativexhigh:xhigh: "xhigh"), else Bedrock default.
Model bend/infer.bend. Laws: laws/infer.bend (S6), laws/infer.bend and laws/effort.bend (S6.9). Proofs: proof/infer/ and proof/effort/. Free inputs: rulebook, query, directive, model replies, provider support, tools, stream, context sizes.
| Law | Says |
|---|---|
quirk_unknown_rejected |
Unknown-directive rule → no compile (S6.1). |
quirk_clash_rejected |
Same layer/target/directive, different values → no compile (S6.1). |
quirk_unknown_when_none |
No rule applies → Unknown (S6.1). |
quirk_known_when_some |
Applying rule always answers (S6.1). |
quirk_highest_wins |
Compiled: highest applying layer answers (S6.1). |
quirk_order_free |
Compiled: swapping two rules changes no answer (S6.1). |
quirk_compile_order_free |
Swapping two rules keeps compile outcome (S6.1). |
quirk_ladder_explicit_first |
Explicit map beats rulebook ladder (S6.9). |
quirk_ladder_rule_second |
No explicit map → rulebook ladder (S6.9). |
quirk_ladder_default_last |
Neither → no entries; default ladder gives all (S6.9). |
quirk_ladder_offered_exact |
Reasoning model offers a level iff ladder gives a wire string, or no entry and default lists it (S6.9). |
quirk_ladder_plain_exact |
Non-reasoning offers exactly plain default (S6.9). |
quirk_ladder_shipped_is_s10 |
Shipped defaults: offered = S10 ladder of chosen map; S10 laws hold (S6.9). |
force_bounded |
≤ 1 + retries attempts (S6.3). |
force_first_flag |
First attempt flags only if supported and free (S6.3). |
force_retry_flag |
Retries flag if supported (S6.3). |
force_reports |
Reports a call iff the model called the tool within its attempts (S6.3). |
strict_within_cap |
≤ cap strict tools (S6.4). |
strict_keeps_every_tool |
Each tool strict or loose, multiplicity kept (S6.4). |
strict_by_priority |
Strict priorities ≥ loose ones (S6.4). |
guard_cuts_repeats |
One chunk k times in a row → cut (S6.5). |
guard_cuts_loops |
Periodic k-chunk window, period ≤ k / 4 (at least 1) → cut (S6.5). |
guard_keeps_others |
No such window → not cut (S6.5). |
compact_starts_early |
Turn growth ≤ margin → compaction before any turn over limit (S6.6). |
Code:
- Rulebook:
eukhe-ai/src/quirks/mod.rs(compile,Rules::lookup,Rulebook::query,Rulebook::family,Rulebook::check_catalog,Quirks,ModelKey,ThinkingDefaults),quirks/revision.rs,quirks/tiers.rs(service_tiers,supports_service_tier),quirks/prices.rs(service_tier_multiplier,cache_write_1h_multiplier),quirks/rulebook.json(shipped),eukhe-ai/src/models/ladder.rs(thinking_level_map, ladder functions). - Resolvers (rulebook as detected input):
api/openai_completions/compat.rs(detect_compat,is_custom_qwen_template_model),api/anthropic_messages.rs(get_anthropic_compat,manual_thinking),api/openai_responses.rsget_compat,api/azure_openai_responses.rs,api/openai_codex_responses/request.rs,api/bedrock_converse_stream.rs(strict default,manual_thinking),api/google_shared/stream.rs(validated function calling),api/mistral_conversations/payload.rs,models/thinking.rsadaptive_thinking. - Strict, forced:
api/constrained_sampling.rs(strict_budget,StrictPlan),api/simple_options.rsmaps_forced_tool_choice;eukhe-types/src/pi_ai/tool.rsTool::priority,eukhe-types/src/pi_ai/settings.rsToolChoice(Required,Tool(name); mapped in every wire API);eukhe-durable/src/harness/generation/force.rs(ForceEscalation,ForceReply,Forcing,ForcedToolError,soft_prompt),ConversationStreamOptions::force_tool. - Guard:
eukhe-ai/src/utils/stream_repeat.rs(RepetitionGuard,MAX_CHUNK_BYTES,StreamRepeat),api/lazy.rswatched_lazy_stream. - Compaction:
eukhe-durable/src/harness/generation/decide.rsthreshold_for,types/settings.rs(BackgroundMargin,ThresholdTokens),eukhe-durable/src/harness/generation.rs(threshold_compaction,provider_tokens: provider count, logged fallback);eukhe-core/src/settings/cvar/table.rsCOMPACTION_PROVIDER_TOKEN_COUNT,eukhe-core/src/durable/settings.rs(WindowPercent(10.0), repeat limit);eukhe-daemon/src/worker/durable_host/translator.rs(summary_write,end_lost_placements, held ends at a snapshot). - Capabilities:
eukhe-ai/src/capabilities/(Capability,Dialect::endpoint,capabilities_of, one module per dialect),models/capabilities.rs(Modelsoperations),Directive::Endpoint,eukhe-cli/src/provider_command.rs.
Tie (bridge tests, transcripts byte for byte):
harness::generation::infer_bridge::the_rust_quirks_rulebook_equals_the_bend_modelvsbridge/bridge_quirks.bend: compile, lookup on generated rulebooks, each also with two rules swapped; directives = first of each value type inDirective::ALL; queries may name no family or host.the_rust_inference_units_equal_the_bend_modelvsbridge/bridge_infer.bend: escalation; guard on lines in random deltas (random chunks, 1-4 chunk cycles, same with one chunk changed, observed 7-line loop vs windows 27, 28, 32, 64); compaction threshold. Code compares strictly (>), so for whole token counts model threshold = limit − margin + 1.- Strict plan and thinking ladder (section 6) run as their compiled models (
bend/ffi_infer.bend,bend/ffi_effort.bend); no Rust copy.
Tests:
quirks::tests: shipped rulebook compiles, no catalog overlap;families_that_overlap_on_a_catalog_model_are_rejected,a_model_two_shipped_families_match_names_no_family; errors, precedence, order, host/family queries, former hand-written checks;the_shipped_qwen_families_split_at_the_revision_3_8.quirks::revision::tests;quirks::tiers::tests(the_table_keeps_every_answer_of_the_former_predicate: every compiled-catalog model and tier vs the former hardcoded predicate, kept only in the test);models::ladder::tests.openai_completions::tests::tool_choice(a_custom_qwen_3_8_model_sends_the_rulebook_ladder),openai_completions::tests::service_tier,openai_completions::tests::host_quirks(a_custom_provider_at_openrouter_ai_sends_no_cache_marks,the_openrouter_provider_at_a_loopback_url_keeps_the_openrouter_settings).constrained_sampling::tests(the_strict_budget_keeps_the_highest_priorities_and_the_earlier_of_equals,a_strict_plan_sends_tools_past_the_cap_loose_and_drops_none),generation::force::tests(manual_extended_thinking_has_no_native_forced_choice),anthropic_forced_tool_choicethe_forced_choice_predicate_matches_the_wire.utils::stream_repeat::tests:a_loop_with_no_newline_is_cut_in_bounded_chunks,a_long_line_is_chunked_on_character_boundaries,the_observed_thinking_loop_is_cut_at_its_77th_line,the_observed_thinking_loop_fails_as_repeating_by_default,a_cycle_longer_than_a_quarter_of_the_window_is_not_cut,a_single_differing_line_restarts_the_window,the_guard_keeps_a_quarter_of_the_window,the_guard_cuts_where_the_model_does(all streams of 3 chunk values ≤ 8 chunks,k≤ 12).generation::tests::threshold; translatora_summary_write_that_leaves_the_inbox_unsettled_ends_its_compaction,a_snapshot_ends_the_compactions_whose_end_it_may_have_replaced; wire testscargo test -p eukhe-ai --test providers.
Not modelled (tests tie them): model → targets map (S6.2); soft prompt, wire forms of forced choice; delta → chunk split.
S6 tool-call argument repair #
Before validation (as omp), the client repairs arguments that are not strict JSON or spell a declared type differently.
- Streaming preview closes incomplete text; final parse does not:
JSON.parse; else escape raw control characters and invalid string escapes, retry; else relaxed dialect: single quotes, unquoted keys, comments,True/False/None, stray/trailing commas,0x/0bnumbers, unquoted values (src/main.rs). Relaxed rejects truncation, trailing characters, non-finite numbers, unquoted values that can hide the next field. - Unparsable or non-object →
{ __parseError, __rawJson }(error, first 512 characters); no text key survives, so half-formed calls never run. Result:invalid_argumentserror with parse error and raw text. - JSON-string key (
"\"path\"") → plain key, unless the plain key is present. - Alternate spellings → declared type: JSON string for object/array; single string for string array; number/boolean for string. String booleans/numbers → value, as before.
- Each repair →
argument_repairedwarning on the result. Unknown key in a closed schema stays an error. - Unavailable tool →
tool_unavailablenaming ≤ 3 offered tools: small edit distance, nearest first; then names ending with the called name's last_or__segment.
Code: eukhe-ai/src/utils/json_parse.rs (parse_json_with_repair, parse_tool_call_arguments), json_parse/relaxed.rs, per-wire-API final parse (anthropic_messages/events.rs, bedrock_converse_stream/events.rs, mistral_conversations/consume.rs, openai_completions/streaming.rs, openai_responses_shared/stream.rs); eukhe-durable/src/harness/tool/arguments.rs (repair_arguments, ArgumentError::NotJson), tool/result.rs (tool_unavailable_message).
Tests: utils::json_parse::tests, json_parse::relaxed::tests, tool::arguments::tests, tool::result::tests.
S6 the built-in model catalog #
Feeds /model, registry, daemon get_model_catalog; user models.json on top. Two local sources; no fetch:
- Bundled
models.bundled.jsonbeside the executable; release builds it fromPrimeIntellect-ai/prime-agent-catalogat the commit inscripts/release/catalog-pin.json, checking each sha256. Strict parse; entry transports must be compiled in (transport pinning). - Compiled:
eukhe-aipi-ai tables (providers/data/*.json) + Prime Inference entries.
Catalog = union; model = provider + id.
- Every model of either source, exactly once; neither hides the other's.
- In both: bundled gives api,
baseUrl, data (name,reasoning,input,cost,contextWindow,maxTokens), its optional fields; compiled fills missing optional fields (thinkingLevelMap,compat,featured,headers) only on the same api. - In one: unchanged.
- Order: bundled, then compiled-only in compiled order. Fields come only from each source's entry, so read order changes nothing.
- Bundle missing or damaged → compiled catalog.
Example: Claude Haiku 5.5 (compiled, not pinned) is in /model. Move the pin to the catalog repo's newest commit:
make catalog-pin # validates, records the commit and both sha256
git diff scripts/release/catalog-pin.json # review, then commit
No code touches ~/.eukhe/models/provider-model-catalog.v1.json (older builds' fetched catalog).
Laws: laws/catalog_union.bend (C1; model bend/catalog_union.bend; proofs proof/catalog_union/).
Code: eukhe-models/src/effective.rs (effective_catalog), chain.rs (ModelCatalog::models), bundled.rs (load_bundled_models), pinning.rs.
Union runs as the compiled model (fast, pinned to union by law union_fast; entry bend/ffi_catalog_union.bend, request laws catalog_union_ffi_*): effective_catalog projects both sources (key and api interned, each field = place of its source entry), calls it, assembles the models back.
Tests: chain::tests::a_bundle_without_a_compiled_model_keeps_it; eukhe-daemon tests/model_catalog_e2e.rs (a_bundle_without_a_compiled_model_still_lists_it: pinned snapshot as bundle, Anthropic login → Claude Haiku 5.5 listed).
S6 models.json and the saved default model #
One models.json loader for every view: registry (eukhe model list, /model, daemon get_model_catalog, onboarding, auxiliary model, memory compactor, RLM, usage) and session model collection.
- Each
providers.<id>loads alone; bad entry (wrong type, missingbaseUrl/apiKey, invalid model) drops only that provider, diagnostic namesproviders.<id>. - Unreadable/unparsable file → one diagnostic, no custom models.
- Diagnostics:
eukhe model list(stderr),get_model_catalogmodelsJsonErrors, each session's settings-error row at open.
Unpinned sessions start on the saved default (defaultProvider/defaultModel), never silently replaced:
- Not in catalog or outside a set
enabledModels→ next startup default; one display-onlyeukhe.default_modelrow names saved default, reason, replacement. - Provider lacks credentials → typed
DefaultUnusable+ login hint; no substitute provider; session opens modelless; row names saved default and reason. - No replacement → session opens modelless with the row.
- Memory compactor (no
memory.model) fails with saved default and reason.
Laws: default_honoured_or_reported, default_quiet_when_honoured, default_quiet_when_unset, default_unkeyed_never_moves (model bend/default_model.bend; proofs proof/default_model/).
Code: eukhe-core/src/models/custom.rs (ModelsConfig::load), models/registry.rs (compose), durable/models/compose.rs, models/resolver.rs (saved_default_miss, resolve_default), durable/models.rs (resolve_session_model, resolve_open_model). resolve_default runs resolve of bend/default_model.bend as the compiled Bend core (entry bend/ffi_default_model.bend).
S9. Local tiny models #
On-machine models per role (as omp): titles tiny, typed judgments judge, translate, text to speech speech, speech to text dictation.
Catalog eukhe-tiny/src/catalog.rs pins each file: Hugging Face repo, commit, size, sha256. Models: lfm2.5-230m, lfm2.5-350m (text, ONNX q4); whisper-base, whisper-small (speech to text); kokoro (Kokoro-82M, text to speech, misaki lexicon for phonemes).
Download: user only, eukhe tiny-models download <id> / eukhe tiny-models download all; eukhe tiny-models list shows installed, missing, incomplete. Nothing else downloads.
- Files stage in
<agent dir>/models/tiny/.staging-<id>-<pid>; sha256 computed while writing. - Wrong size or sha256 → stop, staging removed, earlier install untouched. Staging not removable → fails with its own error plus a warning naming the directory.
- All correct → one rename to
<agent dir>/models/tiny/<id>: complete or absent.
Worker: one process per model+backend, shared by all eukhe processes. Socket, bind lock, log in <agent dir>/run/tiny/ (<id>-onnx.sock, .bind.lock, .log), made 0700 by worker and client even if present. Hidden eukhe tiny-worker <id> --backend onnx --agent-dir <dir>; agent dir from the starter's command line, never environment (daemon with --agent-dir and its worker agree).
- Client connects, sends
ping; worker answers launch tag<build id>|onnx|<device>|<dtype>. - Same tag → use it (joined).
- Other tag →
shutdown: worker removes socket, answersbye, refuses new connections/requests, finishes other processes' in-flight requests in full, exits; client starts a new one (replaced). New build or device/dtype change never hits a stale worker. - No live socket → client starts one (started), detached, own process group; it holds the bind lock only while checking and binding; a live worker answers → it exits, client uses the live one.
- Exits after 15 idle minutes (
EUKHE_TINY_WORKER_IDLE_MSfor tests), socket removed first. - JSON-line requests:
load;chat(messages, prefill, stop strings, max tokens);transcribe(audio, language);speak(text, voice, speed). One answer each, in order; client builds prompts, parses output. Worker dies mid-request → typedCrashed+ log path. Lines ≤ 256 MiB each side: longer request →bad_request; longer answer → out of protocol; bytes discarded.
Engine eukhe-tiny-onnx: ONNX Runtime via ort, loaded at run time from providers.tinyRuntimeLibrary, else ORT_DYLIB_PATH, else libonnxruntime.so (nixpkgs onnxruntime ≥ 1.24). Nix wrapper and dev shell set ORT_DYLIB_PATH to nixpkgs (--set-default: user value wins). Build downloads nothing, links no runtime. CPU only. Own tokenizer (tokenizer.rs): byte-level BPE from tokenizer.json with added tokens, pre-tokenizer regex, ranked merges; other settings → load fails.
Roles (each names a chain):
- Chain:
modelRoles.<role>(if set), thenretry.fallbackChains.<role>; explicit[]= primary only; unset → role's catalog models, then session provider model (text roles).translate:lfm2.5-350mbeforelfm2.5-230m(see Translation). - Available: local if installed; provider model if the session's usable model.
- First available wins; none →
Unavailable; never outside the chain.
Uses:
- Titles: first user message of a top-level session starts one in background (role
tiny;titles.generate: falsedisables); turn never waits; failure → none; user names kept.modelRoles.tinyunset, no local model → one extra session-provider request. Scripted (faux-script test seam) sessions: no title. - Judge:
localprovider exposes each text catalog model as classifierlocal/<id>onclassify. Code-modemodels.classify()without model (null/undefined) uses rolejudge: first chain model that is an available classifier (installed local, or provider classifier with configured auth); none →Unavailablenaming role and chain. - CLI:
eukhe translate <lang> <text>,eukhe say <text> [--out f.wav],eukhe transcribe <file.wav>→translate,speech,dictation. - First paint never waits for a model load.
Translation: user turn Translate from English to <Language>.\nEnglish: <text>, assistant prefill <Language>:. One-line text stops at first line end. Empty reply, or same letters and digits as input (case-insensitive) → TranslationError::Empty/TranslationError::Unchanged, prints nothing (failing tiny models echo). Letterless input exempt.
Eval: 10 short English sentences (hello, good morning, my friend, ... See you tomorrow.) → German, French, Spanish, Portuguese; greedy; stop at first line end. Echoes = replies equal to input (of 40); chrF = mean (0 to 100) vs one reference:
| Variant | 230m echoes | 230m chrF | 350m echoes | 350m chrF |
|---|---|---|---|---|
system "translator" + user Translate this text to <Language>: (old) |
9 | 34 | 0 | 64 |
| instruction in the system prompt only | 24 | 12 | not run | not run |
| language code, not name | 27 | 13 | not run | not run |
| old + one example pair | 2 | 36 | 0 | 41 |
old + prefill <Language>: |
2 | 52 | 0 | 59 |
old + one example pair + prefill <Language>: |
1 | 20 | not run | not run |
user Translate this text to <Language>: + prefill <Language>:, no system prompt |
16 | 26 | 2 | 53 |
user Translate from English to <Language>.\nEnglish: <text> + prefill <Language>: (new) |
1 | 53 | 0 | 65 |
new without "from English" (Text:) |
4 | 47 | 0 | 62 |
| new + the "translator" system prompt | 0 | 49 | 0 | 59 |
| new + one example pair | 0 | 59 | 0 | 59 |
- New recipe: best on
lfm2.5-350m, near best onlfm2.5-230m. - No example pair: needs one per target language.
lfm2.5-230mis poor (hello→ Germanwillkommen), so built-intranslateprefers installedlfm2.5-350m:Hallo,Vielen Dank.,Où est la gare ?, errors (Gute Morgensinn, mein Freund).- Source English only; eval tested English only.
Dictation (into the TUI draft):
stt.enabled(defaultfalse) enables keys; read from the session at each take-starting press;false→ key shows how to enable.app.stt.pushToTalk,app.stt.toggle: no default key; bind inbinds.cfg(S4.5), e.g.bind f8 "action app.stt.toggle". Push-to-talk records while held (terminal resends on auto-repeat; gap > 300 ms, 800 ms after first press, = release). Toggle records until next press.- Recorder: first on
PATHofpw-record,parec,arecord(macOS:sox,ffmpeg); raw 16 kHz mono 16-bit stdout → temp WAV. Failure → take fails with its stderr; no next program. dictationrole reads the file likeeukhe transcribe(local only); text inserted at cursor; empty → "Dictation heard no speech".- Status
[REC] recording, then[STT] transcribing. Esc mid-take stops recorder, removes file, shows "Dictation cancelled". Failure → error row.
No cpal: alsa-sys links libasound via pkg-config at build time; CI (ubuntu-24.04) lacks libasound2-dev; Nix would need alsa-lib, pkg-config. Recorder programs need nothing at build. Client input: no Bend model.
Code: eukhe-tui/src/dictation.rs (Recorder, Transcriber, SubprocessRecorder, run_take, wav_bytes), session_ui/dictation.rs (handle_dictation_key, apply_dictation_update), status_line.rs; eukhe-cli/src/tiny_command.rs (RoleTranscriber, transcribe_file).
Tests: eukhe-tui dictation::tests (fake recorder/transcriber: toggle, hold release, recorder failure, abort stops recorder and removes file; /bin/sh subprocess recorders: first found, stderr, no audio, no program), keybindings::tests::dictation_actions_bind_only_through_binds_cfg, status_line::tests::the_dictation_phase_leads_the_line, tests/dictation_headless.rs (TUI on mock daemon: toggle, push-to-talk, Esc, failure, stt.enabled off).
Model bend/tiny.bend. Laws: laws/tiny.bend (S9). Proofs: proof/tiny/. Free inputs: chain, available set, needs of any processes and tags, worker exits, any order.
| Law | Says |
|---|---|
tiny_pick_in_chain |
Only available chain models (roles 3). |
tiny_pick_first |
First available chain model (roles 3). |
tiny_pick_unavailable |
None available → Unavailable (roles 3). |
tiny_every_need_served |
Each need: exactly one of started, joined, replaced (worker 1-4). |
tiny_worker_current |
Worker has the tag of the last need since the last exit (worker 3). |
tiny_same_tag_joins |
Need with the running tag joins (worker 2). |
Code: eukhe-tiny/src/roles.rs (chain, pick, Candidate, FallbackChain), election.rs (ensure, request, probe, Launcher, Got), worker.rs (launch_tag, run_worker, bind, serve), client.rs (TinyClient, TinyError), catalog.rs (CATALOG, installed, install_state), download.rs download, tasks.rs (prompts, output parsing); eukhe-tiny-onnx/src/text.rs, whisper.rs, kokoro.rs, runtime.rs, tokenizer.rs; eukhe-core/src/tiny/ (settings, resolve, title.rs, judge.rs judge_classifier), eukhe-core/src/durable/codemode.rs; eukhe-durable/src/codemode/models.rs ClassifierDefault; eukhe-daemon/src/worker/title.rs; eukhe-cli/src/tiny_command.rs, tiny_models_command.rs.
Tie: pick runs as the compiled model (entry bend/ffi_tiny.bend, request laws tiny_ffi_*; a candidate crosses as the index of its first equal one). Each choice of one process's election (join, replace or start from its ping; a shutdown's answer; a launch's outcome; the check after a start; a started worker's seat) runs as the compiled model too (entry bend/ffi_tiny_elect.bend, laws tiny_*_spec, tiny_elect_*); Rust connects, pings, shuts down, launches and reports. eukhe-tiny bridge::the_rust_election_equals_the_bend_model (real election: ensure, real serve, in process, fake engine, generated needs and exits) vs bridge/bridge_tiny.bend stays for the interleaving only: several processes and workers on one socket over time.
Tests:
client::tests: real workers, temp run dir, started on the client's command line without agent dir variable, otherHOME: started, joined, replaced, idle exit, crash, not installed, client's agent dir served.worker::tests: shutdown lets another client's in-flight request finish; existing run dir made 0700.protocol::tests: over-limit line skipped, next read.download::tests(mock HTTP): bad sha256, wrong size, broken transfer install nothing; partial install replaced only on success; unremovable staging keeps the download's own error.eukhe-coretiny::tests(role resolution;translatepreferslfm2.5-350m),tiny::judge::tests(judgemodel for model-less classify).eukhe-tinytasks::tests: translate request; echo →TranslationError::Unchanged.eukhe-durablecodemode::tests::calls:models.classify(null, …)asks the default classifier.eukhe-daemontests/session_title_e2e.rs: supervisor on temp socket and agent dir, mock provider; first prompt titles;titles.generate: falsetitles nothing.eukhe-tiny-onnxtokenizer::tests: ids/text vs ids Hugging Facetokenizersonce gave for the same files and texts; random text round-trips.- Real-model (need models + runtime library, else print skip reason):
eukhe-tiny-onnxtext::tests(title fromlfm2.5-230mand, if present,lfm2.5-350m, whose graph has anum_logits_to_keepinput),whisper::round_trip_tests(Kokoro speaks, Whisper reads back),tokenizer::testson model files;eukhe-coretiny::smoke_tests(title,Models::classifyjudgment onlocalvia a real worker).
Not modelled: two processes starting a worker at once (bind lock orders; second uses the first's worker).
S10. Reasoning depth #
Thinking level = how much the model thinks before answering. Ladder order: off, minimal, low, medium, high, xhigh, max.
Ladder. All ladders and wire values are rulebook data (S6.9); code holds none. Offered levels keep ladder order:
- Chosen ladder: model's explicit
thinkingLevelMap(models.json, catalog); else, reasoning model of a custom provider: family's rulebook ladder; else none. Built-in providers keep catalog ladders. Customqwen3.8-27b, no map →off,low,medium,xhigh. - No reasoning → plain default (shipped:
offonly); map ignored. - Reasoning, per entry:
nullremoves; wire string keeps, sent as is; missing → default ladder (shipped:offtohigh, soxhigh/maxneed entries), sent in the wire API's own form. - So reasoning models may offer
offonly (restnull) or nothing (allnull). - Daemon serves offered levels (
availableThinkingLevels; each catalog model's levels to the picker); clients compute no ladder.
Clamp → offered level:
- Offered → unchanged.
- Else first offered above; none → first offered below.
- None offered →
off.
Cycle (cycle_thinking_level, forward/backward), one offered step:
- < 2 offered (no reasoning, or
offonly) → no change; daemon answersnull. - Else from clamp of current. Forward: next, last → first. Backward: previous, first → last.
- Forward visits each offered level once per lap in ladder order; forward and backward cancel.
Session (main conversation stores its level; may be unset):
- Open settles: stored, else default (
defaultThinkingLevel, elsemedium); clamped if the session model is in the catalog, else kept. - Model switch clamps current level to the new model; set clamps the request (kept without catalog model); cycle sets the step's result.
- Sent: stored level, else
off; each provider clamps before sending. - Shown (daemon summary, connection state
thinkingLevel): stored clamped to the model (unclamped without catalog model) = sent. Unset showsdefault, neveroff.
Model bend/effort.bend. Laws: laws/effort.bend (S10). Proofs: proof/effort/. Free inputs: model with or without reasoning, any map (entries missing, null, wire string); level; stored level and default; switches, sets, cycle steps in any order.
| Law | Says |
|---|---|
effort_offered_plain |
No reasoning → off only, any map (ladder 1). |
effort_offered_null |
null entry never offered (ladder 2). |
effort_offered_ext |
xhigh/max only with a wire entry (ladder 2). |
effort_offered_kept |
Reasoning model offers each kept level: wire entry, or missing below xhigh (ladder 2). |
effort_offered_asc |
Strictly ascending, no repeats (ladder). |
effort_clamp_offered |
Something offered → every clamp offered; switches land on one (clamp 2, session 2). |
effort_clamp_keeps |
Offered level unchanged (clamp 1). |
effort_clamp_idem |
Clamp idempotent, even with none offered (clamp 3). |
effort_cycle_still |
< 2 offered → no change (cycle 1). |
effort_cycle_next |
Forward → next offered (cycle 2). |
effort_cycle_wrap |
Forward from last → first (cycle 2). |
effort_cycle_prev |
Backward → previous offered (cycle 2). |
effort_cycle_wrap_back |
Backward from first → last (cycle 2). |
effort_cycle_inverse |
From offered, backward cancels forward (cycle 3). |
effort_cycle_inverse_back |
From offered, forward cancels backward (cycle 3). |
effort_unset_default |
Unset shows default, never off (session 4). |
effort_settle_offered |
Something offered → open settles on offered (session 1). |
effort_stored_is_sent |
After open and any actions, stored = sent (session 1-3). |
effort_shown_is_sent |
After open and any actions, shown = sent (session 4). |
Code: eukhe-ai/src/models/ladder.rs (thinking_level_map, get_supported_thinking_levels, registry_thinking_levels, registry_clamp_thinking_level, clamp_thinking_level, cycle_thinking_level), provider clamps in eukhe-ai/src/api/; eukhe-core/src/durable/open.rs settle_main_agent; eukhe-daemon/src/model_switch.rs (switch, set), setting_switches.rs handle_cycle_thinking_level, worker/summary.rs thinking_level, rpc/model_commands.rs.
Tie: offer, clamp and cycle run as the compiled model (bend/ffi_effort.bend, called from eukhe-ai models/ladder.rs); no Rust copy of the rule.
Tests: eukhe-core durable::open::tests (open_settles_an_unset_thinking_level_on_the_clamped_default); eukhe-daemon setting_switches::tests (cycle_thinking_level without reasoning → null; steps wrap both ways; unset shows default).
Not modelled: settings order for the default level; level → per-provider wire value.