An instruction to an LLM is a request; a constraint should be a
guarantee. Atlas draws that line explicitly. Soft preferences
(“prioritise interpretability”) go in goal and shape the
agent’s choices. Hard requirements go in constraints, and
any constraint that can be tested in code is verified - by
Atlas, not the agent - against every final model.
Enforcement happens at three points:
- Up front: constraints appear in the agent’s task as numbered hard requirements, with an instruction to design for them from the start (pick a monotone model form; don’t bolt monotonicity on afterwards).
-
During the build: the agent has a
check_constraintstool to verify its own work before declaring the build done. -
After the build: Atlas independently runs every
machine check against every model in the results. Violations are
formatted into a repair prompt and sent back - the agent must refit
until the checks pass or
max_fix_roundsis exhausted, and the outcome is reported either way.
Three ways to write a constraint
library(atlas)
res <- atlas(mtcars, "mpg", n_models = 2, constraints = list(
# 1. built-in helpers: machine-checked, model-agnostic
uses_wt = con_uses("wt"),
mono_hp = con_monotone("hp", "decreasing"),
# 2. custom check: function(model, data) -> TRUE or a violation message
non_neg = constraint(
"predicted mpg must never be negative",
check = function(model, data) {
p <- predict(model, newdata = data)
if (all(p >= 0)) TRUE else "some predictions are negative"
}
),
# 3. plain string: enforced via instructions only
no_leak = "qsec is measured after the fact; never use it as a predictor"
))Prefer a check whenever the rule is testable; reserve
strings for rules only the agent can honour. A check may also
stop() - errors count as violations, with the error message
as the detail.
The built-in helpers
Both helpers are model-agnostic: they need only
predict(model, newdata = <data.frame>) to work, and
Atlas tells the agent every final model must support exactly that.
con_uses(var) asserts the model actually uses a
variable. The check scrambles the column and compares predictions: if
they don’t move, the variable isn’t contributing. No formula inspection,
so it works identically for a glm, a random forest, or a
wrapped model object.
con_monotone(var, direction) asserts predictions are
monotone in a variable, ICE-style: for a sample of observed rows, the
variable is swept across its range with everything else held fixed, and
every step must move in the stated direction (within
tol).
Monotonicity is non-strict, and that is a feature: a model that
ignores the variable has a flat - hence monotone - response, and passes.
So con_monotone() alone means “if the model
responds to hp, the effect must be decreasing”. Pair it
with con_uses() to also force the variable in:
# conditional: monotone if used, but may be dropped
constraints = list(mono_hp = con_monotone("hp", "decreasing"))
# mandatory and monotone
constraints = list(uses_hp = con_uses("hp"),
mono_hp = con_monotone("hp", "decreasing"))Custom check patterns
Any property you can compute from (model, data) can be a
guarantee:
# a performance floor
min_r2 <- constraint(
"in-sample R-squared must be at least 0.7",
check = function(model, data) {
r2 <- cor(predict(model, newdata = data), data$mpg)^2
if (r2 >= 0.7) TRUE else sprintf("R-squared is %.2f", r2)
}
)
# bounded predictions
in_range <- constraint(
"predictions must stay within the observed outcome range",
check = function(model, data) {
p <- predict(model, newdata = data)
if (all(p >= min(data$mpg) & p <= max(data$mpg))) TRUE
else "predictions leave the observed range"
}
)
# parity across a group
fair_gap <- constraint(
"mean absolute error must be within 1 unit across am groups",
check = function(model, data) {
err <- abs(data$mpg - predict(model, newdata = data))
gap <- abs(diff(tapply(err, data$am, mean)))
if (gap <= 1) TRUE else sprintf("MAE gap across am is %.2f", gap)
}
)Excluding features, and catching leakage
Constraints govern how variables are used; sometimes the
right answer is that a variable must not exist at all - it won’t be
available at prediction time in deployment, or it leaks the outcome.
That is exclude, and it is deliberately blunt: excluded
columns are dropped from the data before the agent ever sees
it, which no instruction can match for certainty.
res <- atlas(claims, "severity",
exclude = c("settlement_amount", # known only after the outcome
"claim_ref")) # an ID; useless in deploymentThe surviving predictors are screened automatically with
atlas_leakage_screen(): a fast univariate check that flags
any column which alone explains almost all of the outcome (R² ≥ 0.95 by
default) - the classic signature of a column derived from, or recorded
after, the target. Flagged columns are listed in the agent’s brief with
an order to confirm each one with you before relying on it. The screen
is also useful standalone, before any modeling:
atlas_leakage_screen(claims, "severity")
#> variable r2 flagged
#> settlement_amount 0.998 TRUE
#> exposure 0.310 FALSEBeing univariate, the screen catches copy-and-derived-column leakage,
not leakage that only emerges in combination - that is what the agent’s
own vigilance instructions and your exclude list are
for.
Constraints the agent suggests
Some monotone constraints are so obviously right that forgetting them
is the only failure mode - price should not fall as floor area grows.
During exploration, Atlas is instructed to think about which predictors
should plausibly have monotone effects, and to include any such
suggestions - variable, direction, one-line rationale - in the plan it
asks you to approve. Each suggestion you approve is added through the
agent’s add_monotone_constraint tool and becomes a real,
machine-checked constraint: verified, repaired on violation, and
persisted with the session. Nothing is added without your approval.
Constraints from natural language
extract_constraints() turns a plain-language brief into
constraint objects, mapping to machine checks wherever it recognises a
checkable rule:
cons <- extract_constraints(
"weight must be in the model; predictions can never go up as horsepower
rises; qsec is measured after the fact, never use it",
data = mtcars
)
cons
#> $uses_wt
#> [machine-checked] The variable `wt` must be used as a predictor.
#> $mono_hp
#> [machine-checked] Predictions must be monotonically decreasing in `hp`.
#> $no_qsec
#> [prompt-only] Never use qsec as a predictor.
res <- atlas(mtcars, "mpg", constraints = cons)Parsing intent is a judgment call, so the function returns the list
for you to inspect and edit rather than feeding it straight into a
build. Pass data to validate column names; anything
unmappable (an unknown column, a monotone rule without a direction)
falls back to prompt-only enforcement instead of becoming a broken
check.
Reading the compliance table
res$constraints has one row per model × machine-checked
constraint, plus one row per prompt-only constraint:
-
passed = TRUE/FALSE- the check ran;detailexplains any failure. -
passed = NA- prompt-only: enforced through instructions, not verifiable.
print(res) surfaces failures loudly, and a session’s
current state can be re-checked at any time with
res$session$check().