SWE-bench Verified, live, four arms: 13% cheaper than no compaction, and the highest reward. See the numbers

The settings form (field descriptors)

The dashboard's Settings page edits a tenant's configuration as fields, not as YAML text. This page documents the mechanism behind that — where the fields come from, what the server accepts, and what a component author has to do when they add a knob.

Why it is not hand-written

The first version of the fields form was hand-written twice: once as a Go struct, once as a table of controls in JavaScript. It reached 18 keys of about a hundred and one component of fifteen; the other fourteen were hand-edit-only. Worse, the two copies had already drifted:

Nothing in the tree could notice any of that. So the fields are declared beside the config struct that reads them, and every layer — the form, the API, validation, the delete list — is a walk over those declarations.

The descriptor

// components/registry.go
type Field struct {
    Key     string   // DOTTED path inside the block: "trigger.min_request_tokens", "model.source"
    Type    string   // bool | int | float | enum | string | strings
    Default any      // what an ABSENT key means (nil = the type's zero)
    Options []string // the permitted set, for enum
    Hint    string   // one line of prose, from the doc comment already on the struct
    Secret  bool     // a credential: write-only, never echoed back
    Min     int      // smallest accepted number — see "0 is not one thing" below
}

Each component declares its list from the same init() that registers it, next to the config struct it describes:

func init() {
    components.RegisterFields("dedup", dedupConfig{}, []components.Field{
        {Key: "min_tokens", Type: components.FieldInt, Default: 100, Min: 1,
            Hint: "Only replace a repeated tool output above this many tokens…"},
        markerModeField(),
    })
}

The zero-value config struct is passed in on purpose: the anti-drift test reflects over its yaml tags and fails when the declared keys and the struct's keys disagree in either direction. Nested blocks (model:, trigger:) need no nesting in the descriptor — they are dotted paths, and components.TriggerFields("trigger") / modelFields("model") declare the shared ones once.

Descriptors are form metadata only. No constructor reads them, and no component behaves differently because its fields are declared.

Coverage

97 keys across 14 components today (99 across 15 with -tags cg_skeleton), plus pipeline and mode. What is deliberately not on the form: preset:, store.* and observe.* — deployment-shaped keys that are not per-component and are set on the account page, which still takes a whole document.

Two different notions of "default"

Answers Lives in
Field.Default What does an absent key mean? (min_tokens 300, cold cache off) the descriptor, beside the struct
the recommended prefill What should the page offer somebody switching this on for the first time? (cold sweep on, hot path off, a cheap model, caps 2/20) config.RecommendedComponents(), served at /api/options

They are separate on purpose. Conflating them is how a form ends up writing its own opinion over an operator's deliberate value.

0 is not one thing

Field.Min carries real semantics that used to live in two hand-written maps of field names:

The API contract

GET /api/options serves the descriptors next to the component names:

{
  "components": ["agentdiet", "cacheinject", "cachesplit", "…"],
  "component_fields": {
    "extract_llm": [
      {"key": "strategy", "type": "enum", "default": "code",
       "options": ["auto", "code", "single", "rlm", "deterministic"], "hint": "…"},
      {"key": "min_tokens", "type": "int", "default": 300, "min": 1, "hint": "…"},
      {"key": "trigger.min_request_tokens", "type": "int", "hint": "…"},
      {"key": "model.api_key", "type": "string", "secret": true, "hint": "…"}
    ],
    "cachesplit": []
  },
  "recommended": {"extract_llm": {"min_tokens": 8000, "…": "…"}, "extract_llm_sweep": {"min_tokens": 1000, "…": "…"}}
}

Fields appear in declaration order. default, options, min and secret are omitted when they are the zero value; an omitted default means "the type's zero". Every name in components has an entry in component_fields, empty for a component that takes no configuration.

The form itself is posted to PUT /api/me (or PATCH /api/tenants/{id}) as:

{"config": {
  "pipeline": ["format", "dedup", "extract_llm", "cachesplit"],
  "mode": "sync",
  "components": {"extract_llm": {"min_tokens": 8000}, "extract_llm_sweep": {"min_tokens": 800}}
}}

and read back from the same shape on tenant.effective_config. The keys inside a component are the dotted YAML keys, so the page cannot post a field name the server does not read.

What the server does with it

Strict per-component blocks

config.LoadBytes has always rejected unknown keys at the top level. Since this change each component's block is decoded with components.Decode, which is KnownFields(true) — a typo is an error at load time rather than silence. That is what makes a descriptor-driven form trustworthy: the declared keys and the accepted keys are provably the same set. It also means a document with a mistyped component key loads but does not build, and the settings page reports it and refuses to save over it, exactly as for a top-level typo.

components.cachesplit takes no configuration and now says so: its constructor used to receive the raw block and discard it.

The rules the tests pin

Each of these came from a production failure, and each has a test named after it in config/form_test.go.

Rule
R1 ParseForm is best-effort — strict load, else a non-strict fallback that records ParseError. Saving from a guess is refused server-side.
R2 Enablement comes from pipeline membership, never block presence.
R3 Only stated keys reach the form, so a meaningful zero is displayed and preserved.
R4 A key written by merging into an existing block is never deleted as its parent.
R5 Keys the form does not know about survive the round trip; comments and order do not.
R7 The delete list is derived from the declared keys, so the enable and disable paths cannot disagree. The hand-written list named a whole nested block, so disabling deleted sibling keys that enabling had carefully preserved.
R8 Enum options are read from the same constant the engine parses (extract.Modes), never retyped.
R11 The descriptor carries the component default; the recommended prefill is a separate policy layer.

Adding a knob

  1. Add the field to the component's config struct with its yaml tag and a doc comment.
  2. Add one components.Field to that component's RegisterFields call, with the hint taken from the comment.

That is the whole change. Skip step 2 and TestEveryComponentDeclaresExactlyItsConfigurableKeys names the key you forgot; get the type or the enum options wrong and it names that too. The generalized round-trip test then perturbs your field off its stored value, saves, and asserts it reached the document, that nothing else in the document moved, that re-parsing reads it back, and that the result still builds.

The page that draws it

dash/ui/app.js renders the whole form from the descriptors and knows no field names. One function does it:

renderComponentFields(name, fields, values, disabled, { recommended, redraw, opts })

It returns one <details> per component — data-testid="cfg-<name>" — and inside it one control per declared field, in declaration order, keyed x-<component>-<key with dots as dashes>. The type picks the control:

type control posted as
bool checkbox true / false
int, float <input type=number min=…> number
enum <select>, the declared options in order plus one empty "— default (x) —" string
string, strings <input type=text> (password when secret) string / list
anything else no control, and a line saying the server is newer than this page nothing

A component with no declared fields draws no section at all, and hint is rendered verbatim under every control: it is the only thing on the page that explains why a configured component did nothing.

The replaced version hand-wrote sixteen control calls for one component, with its own copy of the default table (XLLM_DEFAULTS) and its own enum lists — which is where R8 came from. TestSettingsFormSpeaksTheSameFieldNamesAsTheServer is now INVERTED for that reason: it walks every declared field and fails if app.js names one, with a four-entry allowlist that each say why. Mentioning a name is no longer evidence of anything; not mentioning one is.

What the page must get right

The page is checked by driving it under jsdom against the real descriptors: all 97 fields render, all 25 enums offer exactly their declared options, all 53 numeric inputs carry their declared min, an unstated key is not posted, a min: 1 field refuses 0, a secret planted in the payload appears in neither the DOM nor the request body, and an edited value survives the save and the reload that follows it.