Harness laws: settings and directors #
The shared rules (what a law secures, the adversary, H1 to H6) are in harness.md.
S4. Settings as console variables #
Each setting is a console variable (a cvar). One table declares each cvar
once: its id, its type, its scope, two flags and its default. The id is
the dotted path of the key in settings.json (retry.enabled).
- The registry holds each id once. A second declaration of an id, or a
default that does not have the type of its cvar, stops eukhe at
startup (
RegistryError). - The scope is Global or Local. A Global cvar has one value for every agent: only the global config sets it. A Local cvar can have a different value in each agent.
persist: an agent can write its sets of the cvar to its cfg.replicate: clients see the value of the cvar.
The value of a cvar comes from layers. A higher layer wins:
default < config < each agent of the family, root first:
its profile < its own sets
- The config is
settings.json(global, then project) and thenconfig.cfg(global<agent dir>/config.cfg, then project.eukhe/config.cfg). The project files obey the project trust rules ofsettings.json. A project file never sets a Global cvar. - The profile of a subagent is
subagent.cfg, thenagents/<name>.cfgfor a named agent. The root session is the agentmain. - The sets of an agent are its runtime sets (
/set,/toggle, the session switches). They are session state: the conversation documenteukhe.cvar.agent, committed on each change. So the state tree shows them, and a rewind or a fork restores them.
In each layer, the last valid set of a cvar wins. A set is valid when its
cvar is declared and its value has the type of the cvar. In a profile or
an agent's sets, the cvar must also be Local. A set that is not valid is
rejected with a typed error (CvarError): Undeclared, BadValue,
Global, NotBool.
A subagent starts from the family of its parent: the spawn carries the parent's layers. Its own profile and sets go on top of them.
The session switches (transport, steering mode, follow-up mode, auto
retry, auto compaction, service tier) are sets of the session's own
agent. They do not change another session. A session cannot set a Global
cvar: /set --global writes it to the global config.cfg. A toggle
in /set --global flips the value of the global layer only (the global
settings.json, then the global config.cfg). A project file never
changes what it flips. The read, the change and the write of a cfg file
hold the lock of the file, so two sessions that write at the same time
do not lose a set. A cfg file that exists but eukhe cannot read is a
settings error, and eukhe does not write it. The errors of a profile
file and of an alias in the agent state that does not parse are
settings errors too. The eukhe.settings service shows the settings
errors of a session as an eukhe.settings_error row of its main
conversation (display-only: the model does not see it). It shows the
errors at open, and after a reload it shows each new error. It shows
each distinct error one time for each session that eukhe opens.
The command language is the same in the cfg files, in the /set slash
command family and in key binds. A line is a list of commands, separated
by ;. A word in double quotes keeps its spaces.
| Command | Does |
|---|---|
set <id> <value> |
Sets the cvar. |
toggle <id> |
Sets a Bool cvar to the opposite of its value in the agent, with the values the agent inherits. |
alias <name> "<cmds>" |
Gives a name to a list of commands. |
bind <key> "<cmds>" |
Gives the commands that a key press runs. |
action <id> |
Runs a built-in client action (in a bind). |
<name> |
Runs the alias <name>. |
- An expansion replaces each run of an alias with its commands. It
nests at most one level per alias of the table and one more. A deeper
nest repeats an alias, so it is a cycle (
ExpandError::Cycle). A run of an alias that does not exist isExpandError::Unknown. An expansion that fails runs nothing. - The key binds are in
<agent dir>/binds.cfg. The first bind of a key wins: the user's binds come before the default binds. A key with no bind runs nothing; an empty bind (bind <key> "") turns off a default. The default actions that share a key share one bind; the screen that gets the key runs its own action. A key press sends itsset,toggleandaliascommands to the session and runs itsactioncommands in the client. eukhe writesbinds.cfgone time fromkeybindings.jsonwhenbinds.cfgdoes not exist; the binds it writes give each key the actions thatkeybindings.jsongave it. Abinds.cfgthat eukhe cannot read, or an import that eukhe cannot write, shows a warning at startup and at/reload. /cvar savewrites the agent's sets ofpersistcvars to itsagents/<name>.cfg. Read back as the profile, they give the same values.
Model: bend/cvar.bend (registry, value, written, toggle,
expand, press). Laws: LAWS.bend section (S4). Proofs:
PROOF.bend section cvar. Every input is free: any declarations (with
duplicate ids and wrong defaults), any config, any family of agents with
any profiles and sets (wrong types, Global cvars, undeclared ids), any
alias table and any binds.
| Law | Says |
|---|---|
cvar_registry_unique |
The registry holds each id once (S4.1). |
cvar_registry_typed |
Each default of the registry has its type (S4.1). |
cvar_value_typed |
Each value has the type of its cvar in each agent; an undeclared cvar has no value. |
cvar_inherits |
A subagent with no profile and no sets has its parent's values. |
cvar_set_wins |
The last valid set of a subagent wins over its parent's value. |
cvar_global_uniform |
A Global cvar has one value in each agent: the config on the default (S4.2). |
cvar_written_flagged |
An agent writes to its cfg only persist cvars and replicates only replicate cvars (S4.3). |
cvar_persist_roundtrip |
The written cfg, read back as the profile, gives the same value of each persisting cvar (S4.6). |
cvar_toggle_twice |
Two toggles give back the value. |
alias_expands_fully |
An expansion leaves no alias to run, or it reports a cycle or an unknown alias (S4.4). |
alias_self_cycle |
An alias that runs itself is a cycle (S4.4). |
alias_unknown_reported |
A run of an alias that does not exist is reported (S4.4). |
alias_runs_body |
An alias of plain commands runs exactly its commands (S4.4). |
bind_press_plain |
A key press runs only plain commands (S4.5). |
bind_unbound_runs_nothing |
A key with no bind runs nothing (S4.5). |
bind_first_wins |
A key press runs the commands of the first bind of the key (S4.5). |
Code: eukhe-types/src/cvar.rs (the command language: parse_list,
parse_script, Aliases, expand, Binds::press);
eukhe-core/src/settings/cvar.rs (Registry, Cvars::value,
Cvars::execute, Sets::written, document_sets) and
settings/cvar/table.rs (the declarations); settings/cfg.rs (the
config layer: settings.json, then the config.cfg files under the
trust rules); settings/manager.rs and settings/getters.rs (the
getters, all through Cvars); eukhe-core/src/durable/cvar_agent.rs (the
session's agent: CvarAgent, the eukhe.cvar.agent document,
run_cvar_line, save_agent_cfg) and durable/settings.rs
(EukheSettings: the family of the session over its config). The daemon
commands cvar { line, global } and cvar_save run them
(eukhe-daemon/src/setting_switches.rs); a refused command answers its
CvarError with data: { kind, id?, message }. A spawn carries the parent's
family in the create config (cvarFamily). The TUI sends /set,
/toggle, /alias and /cvar as cvar lines
(eukhe-tui/src/session_ui/cvar.rs).
Ties: the bridge test
settings::cvar::bridge::the_rust_cvars_equal_the_bend_model compares
transcripts with bend/bridge_cvar.bend byte for byte. It runs 300
generated scenarios: registries (with duplicate ids and wrong
defaults), values over a config and a family (wrong types, Global
cvars, undeclared ids, profiles and sets), toggles of the last agent of
a family (a subagent toggles a value that it inherits), the written sets
(persist and replicate), expansions (cycles, unknown aliases) and
key presses. The scenarios reach every rule.
Regression tests: a_switch_changes_only_its_own_session (daemon: two
workers over one agent dir; the switches of one do not change the other
or settings.json); a_setter_never_clobbers_a_global_settings_file_that_failed_to_parse
(a setter refuses to write over a settings.json that did not parse);
cvar_rejects_a_global_cvar_from_a_session;
a_subagent_inherits_its_parents_family_under_its_profile;
a_fork_starts_from_the_agent_state_at_its_fork_point;
an_unreadable_cfg_file_is_reported_and_never_overwritten;
concurrent_global_sets_both_land;
a_global_toggle_flips_the_global_value_not_the_projects;
profile_and_agent_alias_errors_reach_the_manager;
settings_errors_show_as_a_row_of_the_session;
unreadable_binds_cfg_and_failed_import_are_warnings; the
keybindings::tests of the binds.cfg import and of the presses.
S4b. Config documents #
The config documents are settings.json (global and project),
mcp-connections.json and auth.json. No input of these documents is
lost in silence.
- A writer reads the document under its lock, applies its change, and
writes the result. When the read fails (the file cannot be read, it is
not valid JSON or not an object, or its version is not known), the
writer writes nothing and returns the error. Only a file that is not
there reads as empty. Each key that the change does not touch keeps
its value. This applies to
eukhe mcp addandeukhe mcp remove, to each settings setter, to the connection records and toAuthStoragesetandremove.setandremovereturn the error to the caller. - A settings setter writes the document that is on disk when it writes,
not the copy that it loaded. A key that failed to load stays, and a
key that another writer added stays. The setter refuses a value that
its setting does not hold. When a higher layer sets the same setting
(the global
config.cfg, the projectsettings.jsonor the projectconfig.cfg), the setter says which file sets it (Persisted::Shadowed), and the client shows it. The daemon shows a default model or thinking level that it could not save as a settings error row. - Each leaf of a
settings.jsonapplies or is named by a settings error. A leaf is a value that is not an object, or the whole value of a cvar. A value that its setting does not hold isBadValue; a key that no setting declares isUndeclared. These are the sameCvarErrors as forconfig.cfg, and they show in the same row. One bad leaf drops only that leaf. The known top-level keys come from theSettingstype. - A malformed limit fails closed. A
providerMaxInFlightentry that is not a whole number limits its provider to 1 request; a value that is not a map limits each provider to 1.idleEvictionMinutesand the session archive rules are a whole number,offornone(NatOrOff); another value is a settings error. - An untrusted project does not change a limit, a system prompt or a
credential:
maxToolRounds,subagentMaxConcurrency,subagentStallMs,toolBlockingBudgetMs,autonomous,directors,thinkingBudgets,retry.maxRetriesandretry.failover.maxRetriesare withheld with the keys that run code, and so are the projectSYSTEM.mdandAPPEND_SYSTEM.md. The trust warning names them.trustedProjectscomes from the global layer only (the globalsettings.json, then the globalconfig.cfg). - An
mcpServersentry is a unit: a project entry replaces the global entry of its name, and it never takes a URL or a credential from it.kernelTransportis a unit too. - The sessions directory has one precedence for each reader (the CLI,
the dashboard, the gateway and the daemon,
settings::resolve_session_dir): the--session-dirflag, thenEUKHE_SESSION_DIR(or the legacyEUKHE_CODING_AGENT_SESSION_DIR), then thesessionDirsetting, then<agent dir>/sessions.
Model: bend/config.bend. Laws: LAWS.bend section config. Proofs:
PROOF.bend section config.
| Law | Says |
|---|---|
rmw_refuses_broken |
A document that could not be read or parsed is never written (S4b.1). |
rmw_keeps_others |
A write keeps each key that the change does not touch (S4b.1). |
rmw_applies_change |
The key that the change sets has the value that it sets. |
settings_save_preserves |
A setter writes the document on disk with its key changed, whatever copy it loaded (S4b.2). |
leaf_applied_or_reported |
Each leaf applies or is named by a diagnostic (S4b.3). |
leaf_not_both |
No leaf both applies and is named. |
limit_malformed_fails_closed |
A malformed in-flight limit limits to 1, never "no limit" (S4b.4). |
limit_number_applies |
A whole-number entry is its provider's limit. |
untrusted_changes_nothing_guarded |
An untrusted project changes no limit, prompt or credential (S4b.5). |
trusted_project_applies |
A trusted project's item applies. |
project_entry_never_merges |
A project MCP entry applies whole (S4b.6). |
global_entry_kept |
A global entry that the project does not name stays whole. |
The laws of the saved default model are in inference.md
("S6 models.json and the saved default model") and in
LAWS.bend section config.
Ties: the bridge test
settings::bridge::the_rust_config_code_equals_the_bend_model compares
transcripts with bend/bridge_config.bend byte for byte. It runs 360
generated scenarios: each writer over a missing, valid, invalid,
unreadable and other-version file; setters after another writer changed
the file; documents with good, wrong-typed and undeclared leaves;
providerMaxInFlight values; projects under each trust; and global and
project mcpServers entries.
Regression tests: add_refuses_a_settings_file_that_does_not_parse;
a_setter_keeps_a_field_that_failed_to_load_and_a_concurrent_edit;
another_version_is_refused_and_never_rewritten;
set_over_an_unreadable_auth_file_is_refused_and_writes_nothing;
a_bad_leaf_drops_only_itself_and_is_named;
a_setting_a_cfg_file_shadows_says_so;
a_project_mcp_entry_never_takes_global_credentials.
S5. The Director stack #
After each final answer, the agent loop asks a stack of Directors what to do. A Director is one layer of the stack. A layer has a kind and a budget: the loop steps that it can still ask for. The kinds are the goal, the autonomous run, a reminder, and a forced tool call.
- The top layer decides first. Each layer gives one verdict:
Defer: the next layer down decides. The layer stays.Again: the loop continues. This costs the layer 1 unit of its budget. A layer with no budget yields.Yield: the loop stops and gives control back to the user.Stack{l}: the layer putslon top of itself, and the loop continues. This costs 1 unit of budget. A layer with no budget yields. A push needs room in the full stack: the layers above the layer (also the layers that deferred or left), the layer, and the layers below must be fewer than the cap. With no room, the loop fails.Leave: the layer removes itself. The next layer down decides.Abort: the loop stops with a failure.
- When no layer decides, the loop gives control back to the user.
- The loop of one run has a global budget of steps. Each continuation costs 1 step. When no step is left, a continuation yields.
- The verdict of each kind comes from the session state:
- Goal: an active goal mints a continuation (
Again), unless its progress check stops it (8). A paused, budget-limited, complete, or failed goal yields. A cleared goal leaves. An empty reply never gets to the goal: the harness fails it as a model error (harness.md H4.7), and the goal fails with that error. - Autonomous: it defers while a goal drives continuations, so the goal
keeps its priority. A disabled run leaves. Else its gates and limits
continue the run (
Again) or stop it (Yield). - Reminder: its text goes first in the continuation of the step. Then it leaves. When the step does not continue (no layer below continues, or no step is left), the reminder defers: it stays on the stack until a continuation comes. A reminder never makes its own continuation, so it waits for the goal or the autonomous run.
- Forced tool call: each request forces its tool (
stream.forceTool, S6) until a response calls the tool. Then the layer leaves at the next final answer. Each response to a forced request without the call is a miss. The miss after thetriesretries fails the generation withForcedToolError::NotCalled(S6.3), with the real number of requests. That miss removes the layer and appends a display row with the error (eukhe.director, not in the model context) in one commit, so the next request is not forced. When a run ends before a request forced the tool, the layer continues the run (Again) while it has budget. The continuation only names the forced call; the soft prompt of the forced request asks for the call. With no budget, the layer aborts withForcedToolError::NotCalledand the same display row.
- Goal: an active goal mints a continuation (
- A step evaluates the layers from the top until one decides. The autonomous gates run before the commit of the step. The step commits the stack, the goal and autonomous changes, the goal context row, and its decision in one commit. A rerun of the same generation repeats the decision.
- A run that has a new first input starts a new loop: the global budget
(
directors.maxSteps, default 200) comes back, the goal layer gets that budget, and the autonomous layer gets its remaining continuations (autonomous.maxContinuationsminus the used ones). The goal and the autonomous run come on the stack (the goal above the autonomous run) when they drive continuations and have no layer. They go below the pushed layers (reminders and forced calls), so the pushed layers decide first.directors.stackCap(default 8) caps the pushed layers only. The stack keeps 2 more places, one for the goal and one for the autonomous run, so these always have room: a stack holds at mostdirectors.stackCap + 2layers, and each decision gets that cap. A push past the cap is refused with an error. With no layer, a final answer writes nothing. - Reminders and forced calls come on the stack from these entry points:
the
/remind <text>and/force <tool> [--tries <n>]session slash commands (the TUI sends them through the daemon), and the kernel host requestdirectors.push(goal.remind(text)andgoal.force_tool(tool, tries=1)in the Python REPL). A forced call gets 1 unit of budget andtries1 when not given. - The stack is the conversation document
eukhe.directors. It is rewindable: a fork or a branch move starts from the stack as of its fork entry, so a rewind removes the Directors added after that entry and brings back the ones removed. A resume reads the stored stack. The document is a node of the state tree (S1), under its conversation.
Model: bend/director.bend. Laws: LAWS.bend section (S5). Proofs:
PROOF.bend section director. Every verdict is free: each layer of each
step says what the adversary picks.
| Law | Says |
|---|---|
dir_stack_bounded |
A decision never grows the stack past its cap (S5.1). |
dir_loop_stack_bounded |
So the loop never does, whatever its steps (S5.1). |
dir_loop_ends |
The loop ends (gives control back or fails) after at most n + 1 steps, with n its global budget, whatever every layer says (S5.3). |
dir_end_is_final |
An ended loop stays as it ended (S5.3). |
dir_encode_sound |
The stack in the tree is a sound tree (S5.8). |
dir_resume_restores |
A resume reads back the stack that the tree holds, exactly (S5.8). |
dir_rewind_restores |
A rewind of a sound tree to the tree of a stack restores that stack, exactly (S5.8). |
Code: eukhe-core/src/durable/directors/stack.rs decide, Loop::tick,
Layer, LayerKind, Verdict; directors/mod.rs DIRECTORS_DOC,
DirectorState, DirectorLimits (bound), push_director,
DIRECTOR_NOTICE_CUSTOM_TYPE; durable/goals/hooks.rs (the on_yield
step: begin, judge, commit_step; the forced tool of
before_request; the call or the miss of after_response,
mark_forced_response); durable/goals/host.rs (directors.push);
durable/session_commands.rs (/remind, /force);
skills/goal/src/goal/__init__.py (remind, force_tool);
eukhe-durable/src/harness/generation.rs (the before_request hooks may
set the forced tool of a request, RequestMessages::force_tool);
eukhe-core/src/settings directors.
Tie: the bridge test
durable::directors::bridge::the_rust_director_stack_equals_the_bend_model
runs decide, Loop::tick, and the persisted eukhe.directors value
against bend/bridge_director.bend on 300 generated scenarios (stacks,
verdict lists per step, caps, global budgets; the model's decide,
tick, run, encode, and decode), and compares the transcripts byte
for byte. The scenarios reach every rule of the model (a push with room,
a push that fits only the layers below, a full stack, no budget, a
leave, all layers defer, no step left, a tick after the end). Regression
tests: durable::directors::tests (the goal layer until the goal
completes, the global budget, a reminder, a reminder that waits on the
stack for a continuation, a forced tool call, a forced call that is never
made and a next run that is not forced, the goal on a full stack, the
stack after a reopen and in a fork); durable::session_commands::tests
(/remind and /force on the stack, a /force that fails once);
durable::goals::tests. Not modelled: the map from the session state to
the verdicts (S5.4), begin (S5.6), the reminder that defers when no
continuation follows, and the kind data of a layer (the model keeps a
number for the kind). Their tests tie them.
S5.8. A goal without progress stops #
-
At each continuation of an active goal, the progress check looks at the work of the session: the number of its distinct acts (a tool call with its result, by tool name, arguments, result text and error flag; or an accepted progress note). Call ids, timestamps, and the text and thinking around a call are no work. The check compares the work with the work at the last continuation of the same run and goal:
- The work grew: the count of continuations in a row with no new work goes back to 0, and the goal continues.
- The work did not grow: the count grows by 1. Under the cap, the goal
continues, and its continuation tells the model how many
continuations in a row did no new work and that the goal pauses at
the cap. At the cap (
directors.stallSteps, default 3;0takes the default), the goal pauses with the reason (No progress: <n> goal continuations in a row made no tool call with a new result), the run yields, and a display row (eukhe.director, not in the model context) tells the user why and how to go on. - The first continuation of a run, or of a new goal, starts the count: a new user input or a resume gives the goal the full cap again.
Before this check, an active goal with no token budget got up to
directors.maxSteps(200) continuations when the model only answered (for example "waiting for the build"), each a request with the full context. The default 3: the first continuation can be an answer that only states a plan, the second carries the note that asks for one concrete step, and the third shows that the model did not act on it. (An earlier strike counted replies with no text and no tool call. Such a reply is now a model error, H4.7, so that strike was removed. This check counts work, not replies.)
Model: bend/progress.bend (work, check, goal). Laws
(LAWS.bend section progress): goal_stops_without_progress (continuations in a
row whose acts the session did before, at least as many as the cap, stop
the goal from any count) and goal_progress_continues (a continuation
with an act the session never did before clears the count). Proofs:
PROOF.bend section progress.
Rust: eukhe-core/src/durable/children/work.rs (WorkLedger),
durable/goals/state.rs (stall_check), durable/goals/hooks.rs
(goal_check, the work read in on_yield, the pause and its row in
commit_step), durable/directors/mod.rs (DEFAULT_STALL_STEPS).
Tie: the bridge test
durable::goals::hooks::bridge::the_rust_progress_check_equals_the_bend_model
runs the ledger over real transcripts and goal_check against
bend/bridge_progress.bend. Regression test:
durable::goals::tests::a_goal_without_progress_pauses_at_the_stall_cap.