Choose a generation model (API and CLI)
Sonnet 5 remains the default. To choose a model for an individual generation or refinement, use the optional API request fieldmodel or the CLI flag
--model (aethis-cli 0.41.0 or later):
Omitting
model preserves the server’s configured default, which is Sonnet 5
unless overridden by the server operator. An explicit selection applies to
that request; repeat it when refining with the same model.
From an existing CLI project, with the selected provider key in your environment:
Structured acceptance contracts
An expected outcome checks one part of a ruleset’s behaviour. A versioned acceptance contract can also require specific pending reviews, review-completion mappings and unanswered fields that must remain available. A candidate is accepted only when every supplied check passes. Invalid scenario inputs fail the check, even when ignoring those inputs would produce the expected outcome. This also applies to outcome-only scenarios in project test runs. A completed generation may retain a failing draft for inspection; use the acceptance verdict, rather than job completion alone, to determine whether it passed. Each test retainsname, field_values and expected_outcome. Its optional
expectations object supports:
The optional
expected_review_bindings catalogue maps each completion field to
its complete token-to-Boolean-or-null mapping. Each declared field must identify
exactly one review point, and no additional mapped point is allowed. false
and null are distinct: a false outcome rejects that review criterion; a null
outcome leaves it pending. Omitting the catalogue adds no catalogue assertion;
{} asserts that there are no mapped review points. A null catalogue is invalid.
For a prepared generic JSON contract file, use:
contract_version: 1, test_cases, and optionally
expected_review_bindings. The CLI replaces the scenarios and catalogue
atomically, then verifies their readback before requesting generation. An older
engine that drops the contract is rejected. Scenario assertions authored under
expect in tests/scenarios.yaml are also preserved without a separate file.
A review-binding catalogue belongs in the generic contract file; placing
expected_review_bindings or contract_version at the top level of
tests/scenarios.yaml is rejected with instructions to use
--acceptance-contract.
API callers send the same fields to the project’s /tests endpoint with
replace: true. Structured assertions require version 1. A legacy append or
replacement cannot silently erase an existing contract; replacing or clearing
it requires an explicit version 1 replacement. See the REST request
envelope for the body
shape.
Keep independent hold-out cases outside the generation feedback. Passing the
supplied contract establishes coverage of those checks, not complete fidelity
to every source clause or explanatory passage.
The TDD loop
Rule generation is test-driven. The engine generates rules from your source text. Your test suite validates the output. Failing tests become the basis for targeted guidance — feedback that points to the specific source clause the engine missed. The loop repeats until all tests pass.Step 1 — Create a ruleset
If you completed Phase 2 (field vocabulary), you already have confirmed field names and aproject_id — write your test cases using those names and skip to Step 2.
If starting fresh on a single-section domain, you need test cases before creating the ruleset. Two approaches:
- Inspect an existing ruleset in the same domain with
aethis_schemato learn the field naming convention, then write tests using those names. - Make your best guess at field names, create the ruleset with preliminary tests, then run
aethis_discover_fieldsto confirm. If names don’t match, add corrected tests viaaethis_create_rulesetwith a new project — the cost is low.
Step 2 — Generate and test
> instead of ≥), and the school type restriction isn’t compiled. Each failure includes a hint pointing to the likely cause.
Step 3 — Refine with guidance
Add guidance that references the specific source clause:aethis_refine adds the guidance, then makes the minimal edit to fix the failing tests — seeded from the section’s active ruleset and keeping the passing tests green, rather than re-authoring the whole section. Returns (second attempt — all passing):
Guidance variants
Add guidance without regenerating
Useful when you want to accumulate several pieces of guidance before triggering a generation run:Check accumulated guidance
Domain-level guidance
Add once, applies to all projects in the domain — no need to repeat cross-section principles on each ruleset:Guidance examples
Real guidance from the UK Free School Meals household criteria section (11 tests, 8 qualifying routes). Each hint addresses a specific compilation gap.OR logic across routes
OR logic across routes
Problem: The engine treats all qualifying criteria as AND conditions — a household must meet all routes to qualify, when it should be any one.Guidance:
Compound AND within an OR route
Compound AND within an OR route
Problem: The Universal Credit route passes when
receives_universal_credit is true, ignoring the income cap.Guidance:Unconditional boolean overrides
Unconditional boolean overrides
Problem: Looked-after children are being checked against benefit criteria when they should bypass them entirely.Guidance:
Negative conditions (absence of a benefit)
Negative conditions (absence of a benefit)
Problem: The Child Tax Credit route qualifies anyone receiving CTC, missing the requirement that they must not also receive Working Tax Credit.Guidance:
Diagnosing a specific failure
If a test is failing but the hint isn’t clear enough, useaethis_explain_failure to get a deeper diagnosis:
Test coverage strategy
Good coverage catches failures before production:- Boundary values — test just below, at, and above every threshold (age 3, 4, 15, 16)
- Every Enum value — one test per enum value for each Enum field
- Every code path — combinations of fields that exercise a distinct branch
- Five tests minimum per section — more is better; sparse test suites let edge-case bugs through
Date arithmetic: years_between
Source text frequently expresses age or duration thresholds in terms of a date of birth or a start date (“must be 18 or over”, “at least 5 years’ continuous residence”). Never ask for or store an age field directly — dates drift out of sync with a stored age the moment time passes, and a re-run of the same ruleset on a later day silently changes the answer. Compile the threshold from the date field instead:
years_between(start, end) returns the number of completed whole years between two Date fields — true “has the Nth anniversary been reached on or before end” logic, leap-year correct (29 February birthdays included). It is not days_between(start, end) / 365, which drifts by roughly a day every four years and silently produces off-by-one-year answers near a birthday. Both helpers compile to the same compiled rule evaluation as every other operator in this reference (see What you can express for the full operator table) — there’s no separate date-math code path to reason about at decide time.
Step 4 — Publish
Once all tests pass:name is auto-derived from the section ID (e.g. child_eligibility → "Child Eligibility"). To override it, pass name in the publish call:
ruleset_id is now ready for aethis_decide. Published rulesets are locked and versioned.
If you published the ruleset into a Rulebook, it remains in testing until
you promote that exact version to live. Promotion re-runs the go-live gates,
archives the prior live version of the same named ruleset, and cuts a new
Rulebook version atomically. Use aethis rulesets promote-to-live with the
rulebook, ruleset name, and exact candidate ruleset_id in that order.
The underlying request includes force_unsafe: false by default. If the
presence-polarity gate finds an unanswered-field default that can be more
favourable than an answered value, promotion fails with
non_conservative_presence_op; re-author the default so silence is the strict
reading, or make the field explicit. On this gate, force_unsafe: true is
restricted to internal Aethis keys and writes an audit event — external authors
cannot use it.
That restriction is specific to the promotion presence-polarity gate. The
force_unsafe flag on publish is a different gate with different rules: it
bypasses the server-side TDD gate, it is available to any key with authoring
scopes, and it records a publish_force_bypass audit event rather than refusing.
Do not carry the internal-only restriction across from one to the other.
After a successful promotion, inspect
presence_polarity_advisories. It is normally null; a non-null list means
the candidate went live through an audited internal override or the
advisory path for an existing first-party showcase member. Each entry names the
affected field, criterion, and semantic-risk message. The full REST
request and response are in REST API.
Sources: budget, duplicates, and lifecycle
Generation builds its context from every active source on the project, so source hygiene directly affects rule quality. Token budget. Every upload response includesestimated_tokens per file, a project_estimated_tokens running total, and the generation_token_budget it must fit (the model’s context window minus headroom reserved for the generation loop). Before any generation starts, the engine counts the exact prompt — if it exceeds the budget, the request is rejected with 422 token_budget_exceeded (including the count, the budget, and the largest sources) before any model cost is incurred.
Duplicates. Re-uploading identical content (or a same-named file) is flagged in the response under possible_duplicates — surfaced, never silently merged. Conflicting versions of the same guidance are a correctness hazard: resolve them by superseding the stale copy.
Lifecycle. GET /projects/{project_id}/sources lists every source with its status; PATCH /projects/{project_id}/sources/{source_id} sets it:
active— included in generation (the default)superseded— replaced by a newer upload (optionally point at it withsuperseded_by); kept for provenance, excluded from generationreference_only— kept on the project, excluded from generation
DELETE removes a source outright — but a source cited by any persisted ruleset’s provenance returns 409 rather than orphaning those citations. Prefer superseded when generated rulesets already cite it. The retained bytes behind a source stay downloadable at GET /projects/{project_id}/sources/{source_id}/raw (scope projects:read); see Provenance and citations.
Grounding report
Every generated rule cites the source passages it is grounded in. After generation, the job (and thegenerate-and-test response) carries a provenance_report:
totals— how many citations resolved against the uploaded sources (verified), cited passages that don’t exist (flagged), and rules or fields with no citation (uncited)coverage— per source: how many passages are cited by at least one rule, plus a sample of passages nothing cites
source_targets. See Provenance and citations.
Handling generation timeouts
Rule generation can outlive a client timeout. A timeout means the caller lost its wait for the result; it does not mean the server-side job stopped. Do not start another generation until you have inspected the job — a project admits only one active generation, and retrying blindly can duplicate work.- Check the project status with
aethis_generation_status(oraethis statusin the CLI). A current recovery envelope advertisesgeneration_contract_version: 1and returnstelemetry_availability, the server-authoritativeworker_lifecycle, andretry_readinessalongside the job details. If the marker is absent, do not use the cancellation workflow. - If
retry_readinessisblocked, keep polling;cleanup_pendingmeans the server still has ownership cleanup to complete. Retry only when it becomesready. Turn count, pass-rate progress, last tool, and heartbeat age are monitoring evidence, but an old heartbeat alone does not prove worker death. - If the job is
success, run the tests and continue to publish only when the test result is acceptable. If it isfailed, use its structured failure detail and source-cited guidance to correct the project before starting a new run.
Cancel only on explicit caller intent
If the caller decides to abandon an in-flight run, callaethis_cancel_generation({ project_id, job_id, confirm_job_id: job_id }) only
after showing the exact status job_id and receiving a fresh explicit reply
from the caller. Repeating the id in confirm_job_id binds the target and
prevents an accidental mismatch; it does not prove that a human approved the
action. The MCP host’s destructive-action approval and the caller’s fresh reply
are separate requirements. A missing or mismatched id makes no MCP cancellation
request. For non-interactive CLI automation, aethis cancel --yes still
observes and displays the target first. Cancellation marks that job failed and
releases its project ownership so another run can be admitted. It is never
performed automatically after a timeout, stalled progress indicator, or
disconnect.
The response outcome is cancelled for the first transition or
already_cancelled when the same exact cancellation is repeated. Both are a
successful, idempotent resolution of that job-bound request. Other terminal
jobs and a changed active job are conflicts, not successful cancellation.
The worker-stop result is intentionally separate from releasing ownership. A
worker may cooperate with the cancellation only later; on older workers it may
continue until it finishes or the instance is replaced. Read the returned
detail before assuming model work or provider spend stopped. Your
model-provider key is sent with a generation request only and is never
persisted or replayed by status/cancel requests.
Act on structured reasons, not guesses
For a failed job, inspectjob.error_detail.reason_code and its retryable
flag where present. The server returns sanitised provider detail — never the
provider key or unbounded provider error text.
After all sections are published
If your domain has multiple sections, compose them into a rulebook — a container that references published rulesets and defines outcome logic.Create a rulebook
Via the REST API:Ruleset references
Eachruleset_ref has:
Outcome logic
Defines how section outcomes compose. Each section is assigned a letter (A, B, C…) in the order listed inruleset_refs.
Supported operators: AND, OR, NOT, parentheses for grouping.
robot_hints — guidance for a conversational agent
A rulebook optionally carries robot_hints: a flat map of beat name → natural-language guidance string, for a conversational agent (like Lisa) driving an applicant through the composed rulebook. Set it on create or update:
Values are free-text strings — there’s no further schema on the guidance itself, only on the key set.
robot_hints is additive and back-compat: a rulebook authored before this field existed simply returns robot_hints: null on /schema (below), so an older consumer doesn’t need to change anything.
Activate and evaluate
/decide endpoint accepts either a ruleset_id (single section) or a rulebook_id (composed multi-section). When evaluating against a rulebook, the response includes section_results showing each section’s individual decision.
The rulebook /schema response merges {rulebook_id, sections, fields, robot_hints, engine_version} across every composed section:
robot_hints— the rulebook’s guidance map (above), ornullfor a rulebook authored before the field existed.engine_version— the sameaethis-core@X.Y.Zstring echoed on every/decideresponse and at/openapi.json→info.version, so a consumer can record which engine build resolved this schema without a separate call. Useful for pinning replay: if you cache a schema response,engine_versiontells you whether a later/decideran against the same compiled build.