earl_sdk (Python 3.11+)Correction (2026-09-23), scoped (2026-09-25). This correction is about EARL modifying its own code. Where this document describes EARL "authoring", "self-modifying" or repairing its own mechanism, read: EARL's governed loop replaces one bounded expression by random search under a grammar the lab supplied, accepted by a lab-frozen gate; nothing in that apparatus learns from its attempts, and self-repair has not been demonstrated. Autonomous code authorship is not an earned claim. Record:
earl/genesis/g9/CORRECTION-2026-09-23-CODE-AUTHORSHIP.md; the original text is preserved as written.It does not describe
repair.develop, the code-repair developer preview, which is a different and narrower capability: EARL diagnoses, constructs and verifies supported repairs in the Rust code you designate, from taught operator meanings plus the requirement you supply, and applies them only on your approval. Its scope and limits are its own — see code-repair.md.
The SDK is generated from the platform's exported OpenAPI document and wraps
every /api/v1 operation. It adds no behaviour the API lacks. Source:
platform/sdk/python. Install: pip install -e platform/sdk/python
(standard library only; [crypto] for faster Ed25519).
client = EarlClient(base_url, token, transport=None)
| method | returns | notes |
|---|---|---|
capabilities() |
the manifest | cached; refresh=True to reload |
capability(name) |
one manifest entry | mode, platform_status, qualification {integration, semantic}, recorded_status; gap when not yet available |
require_available(name) |
the entry | raises NotAvailableInThisRelease unless mode == "qualified" |
by_status() |
{status: [names]} |
every capability the platform lists, grouped by its platform status: available, gated, present_not_exercised, research (nothing omitted; see the register) |
require_status(name, *statuses) |
the entry | raises NotAvailableInThisRelease unless the entry's platform_status is one of statuses; the message names its status and its gap (what stands between it and the next status) |
issuer_public_key() |
hex | the key records are pinned to |
create_workflow(name, domain_id) |
Workflow |
runs init on the domain's latest successful training run |
workflow(id), workflows() |
tenant-scoped | |
record(record_id) |
typed outcome | |
evidence_package(record_id) |
the signed package | NotFound if the record has none |
verify_record_online(record_id) |
verdict dict | the platform's own check |
verify_offline(outcome_or_record, pin_public_key=None, evidence_package=None) |
verdict dict (check result["valid"], not .valid) |
genuinely no network. Pass pin_public_key obtained out of band to pin the issuer; pass evidence_package to check one you already hold. pin_issuer=True opts in to fetching the key, which does use the network |
| method | capability | body | outcome |
|---|---|---|---|
evaluate(text, mode="verify", expectations=None, idempotent=True) |
evidence.evaluate | claims, one per line | Result (every claim VERIFIED), SemanticUncertainty (any UNCERTAIN/CONTESTED/HALLUCINATED), OperationalError (store or verifier unavailable) |
replay(record_id) |
evidence.evaluate | the comparison in result.comparison |
|
ask(question, intent="explain", idempotent=False) |
language.ask | a plain-English question | Result (an answer, a taught No, or "cannot establish"; outcome.answer, outcome.interpretation, result["claims"]), SemanticUncertainty (a clarification or a declined request; reasons has language_<mode>), OperationalError (path unavailable, or faithfulness_gate_failed with the answer withheld). A pronoun is resolved only from the workflow's previous answered question (result["antecedent_resolution"]). From 2026-10-02 the answer draws on the facts the workflow holds as well as the domain's, less any an operator rejected (result["basis"]), and each claim stated as given carries its sources. See natural-language-questions |
prove(query) |
reason.prove | Result only on status: proved |
|
decide(text, action, actor=None) |
decision.decide | one decision from evidence and authority: result["decision"] ALLOW / REVIEW / DENY with evidence, authority and reasons; Result only for ALLOW; advisory |
|
query(line) |
reason.query | explicit grammar | Result when derivable |
tick(n=1) |
reason.tick | Result with ticks[] |
|
plan(goal) |
goals.plan | Result with steps |
|
observe() |
self.observe | Result with census, read-only |
|
beliefs(query=None) |
memory.beliefs | a plain dict (not recorded) | |
submit(text, tag="developer") |
memory.beliefs | Result with propositions/reinforced; SemanticUncertainty no_belief_formed. With self_training on (rc27), staged_for_induction |
|
learning_controls() / set_learning_controls(self_training=, outcome_learning=, deduction=, reason=) |
learning.controls | the workflow's switches, each off by default; a change is a signed record; switching on needs an rc27 engine (409 otherwise) | |
learning() |
learning.runtime | self-trained words (with source documents and, from rc30, source_records: the submit records they came from), training window, staged documents, the learner's outcome ledger, from rc31 outcome_patterns (each learned pattern carrying reported outcomes: signature, outcomes carried, correlations, per-word confirmed/refuted) and from rc32 forgotten_words; read-only, recorded |
|
unforget_word(word, reason) |
learning.runtime_induction (gated) | a forgotten word, a reason | engine rc32 (build 2): undoes a forget; the word is not restored at once and can be learned again when it recurs in staged text. Allowed with the switch off |
forget_word(word, reason) |
learning.runtime_induction (gated) | a self-trained word, a reason | engine rc32: the word leaves the self-trained vocabulary with its associations and is never minted again; the record carries the reason and lists beliefs that mention the word (kept). Allowed with the self_training switch off |
report_outcome(record_id, succeeded, confidence=1.0, tokens=None) |
learning.outcomes (gated) | the outcome of an earlier result of this workflow; words default to that result's request; the next tick() attributes it (learning_after.outcomes) |
|
deduce(confirm=None, include_untrusted=None) / rollback_deduction(snapshot) |
reason.deduce (gated) | preview: derivations, digest, and trust.derivations (each premise's sources and trust); confirm=digest writes the trusted conclusions plus those named in include_untrusted (never one with an unresolved or rejected premise), after a snapshot; trust lists written and withheld; rollback is byte-identical |
|
premise_verdict(proposition, "verified"|"rejected", reason) |
reason.deduce (gated) | an operator's verdict on a held premise; a signed record | |
beliefs(query=None, provenance=False) |
memory.beliefs | a derived belief carries derived_from and trusted; provenance=True adds every belief's sources and trust |
|
authority_check(action, evidence_package_id) |
authority.check | advisory; result.decision, enforced: false |
|
records() |
record | typed outcomes in sequence | |
adopt_engine(tag), rollback_engine() |
runtime.version | A1 | |
adopt_successor(...) |
— | always raises NotAvailableInThisRelease (A2 reserved) |
|
repair_connect/designate/diagnose/proposals/adopt/rollback/status |
repair.develop | developmental; see below | |
Workflow.idempotency_key(op, *inputs) |
deterministic key |
goals.allocate is exposed in mode developmental: reachable, recorded, and explicitly incomplete until
the episode harness, the repair demonstration and the reproduction script pass. The SDK guards it with
client.require_developmental("goals.allocate"); require_available refuses it.
| method | operation | outcome |
|---|---|---|
wf.allocate(items, tick=) |
POST /v1/workflows/{id}/goals/allocate — the workflow's Allocate faculty orders items (each item_id plus up to nine observations: priority, deliv_staleness, time_since_contact, deliv_quality, comms_up, pending, dist_to_cell, plat_energy, plat_health) |
Result with order, scores, ties, defaulted_inputs, faculty; SemanticUncertainty invalid_allocation_request naming every violation |
wf.report_outcomes(outcomes, tick=) |
POST …/goals/outcomes — after-action truths (item_id, true_fresh, true_quality) for the request at tick |
Result with outcomes_recorded |
wf.allocation() |
GET …/goals/allocation — the faculty identity and the history summary |
Result |
The adapter between your items and the faculty is allocate-adapter/1 (named in every record): it carries
the observations verbatim and authors nothing. An outcome may carry attended: false for an item you did not
attend but whose truth you learned later; the recorded proxy uses attended outcomes only, the counterfactual
replay of a candidate uses every reported truth. Contract: earl/genesis/g9/SHIP-2-ALLOCATE-CONTRACT.md. The
result applies to the Allocate faculty; it is not evidence that planning, memory or reasoning self-repair.
repair.develop is exposed in mode developmental: EARL diagnoses, constructs and verifies supported
repairs in the Rust code you designate — operator_substitution (a wrong comparison or arithmetic
operator) and guard_construction (a missing leading guard) — over a contained copy of your project, and
applies them only on your confirmation. No external LLM writes the patch. Guarded with
client.require_developmental("repair.develop").
| method | operation | outcome |
|---|---|---|
wf.repair_connect(dir_or_targz_or_bytes) |
POST /v1/workflows/{id}/repair/connect — upload the crate; Workflow.pack_project packs a directory, excluding target/ and .git |
Result with files, engine (which repair binaries the bound engine carries) |
wf.repair_designate(target_file=, target_function=, repair_kind=, designated_files=, acceptance_test_cmd=, contract=/guard=) |
POST …/repair/designate — your scope and your statement of intent |
Result echoing the designation |
wf.repair_diagnose() |
POST …/repair/diagnose — locate, construct, verify; proposes, adopts nothing |
Result with outcome, diff, verification; or SemanticUncertainty for a non-repair |
wf.repair_proposals() |
GET …/repair/proposals |
every diagnose record, newest last |
wf.repair_adopt(record_id, confirm=True) |
POST …/repair/adopt — refused without confirm; preservation re-checked and recorded first; project snapshotted byte for byte |
Result with adopted, snapshot_sha256 |
wf.repair_rollback() |
POST …/repair/rollback — restores that snapshot |
Result with byte_identical |
wf.repair_status() |
GET …/repair/status |
plain dict: connected, designation, proposals, adoptions |
Read result["outcome"] from a diagnose: repair_proposed (verified, adoptable), candidate_rejected (built
and checked, failed your tests, not adoptable), verification_unavailable (the platform could not
build or run your tests — a platform toolchain fault, not your code) or non_repair with a reason. Two limits
belong in every description of this: the requirement is supplied by you, not inferred, and autonomous
initiation is not available — you start every repair. Full guide: /api/docs/guides/code-repair.
development.episode (mode developmental, INCOMPLETE) runs EARL's governed self-modification mechanism on
that faculty, over your workflow's allocation history, under the platform's authority, containment and
preservation rules. Guarded by client.require_developmental("development.episode").
| method | operation | outcome |
|---|---|---|
wf.register_acceptance(cases) |
POST /v1/workflows/{id}/development/acceptance — your acceptance cases for the order ({"name", "items", "expect_first"} or "expect_top"); judged by the platform, never by EARL |
Result |
wf.start_episode(purpose) |
POST …/development/episode — ONE governed episode over a contained copy of the image; nothing is installed |
Result: result["episode"] is the full ledger — outcome (succession, qualification_rejected, unconfirmed, limitation_open, config_adapt, no_decline, cooldown), the decline, every candidate with origin (fresh/mutant), disposition and reason, the qualification measures, the G6 class, the successor's spec when one qualified; result["contained"] |
wf.episodes() / wf.candidates(episode_record_id) |
GET …/development/episodes, GET …/development/episodes/{record_id}/candidates — every episode; every candidate as its own signed record |
lists of records |
wf.episode_world(episode_record_id) |
GET …/development/episodes/{record_id}/world — the allocation state exactly as the episode saw it, with the derived seed (the reproduction input) |
dict |
wf.adopt_candidate(episode_record_id, confirm=True) |
POST …/development/adopt — the authority action: refused without confirm, without a qualified successor, after the live image changed since the episode, or when preservation is not established; the preservation record (acceptance cases before/after, read-only probes identical) is written first and separately; the live image is snapshotted byte for byte; then the engine installs EXACTLY the recorded spec (form hash and generation verified) |
Result with adopted, preservation_record_id, snapshot_sha256; SemanticUncertainty when the engine refused the spec |
wf.reject_candidate(episode_record_id, reason) |
POST …/development/reject — your decision not to adopt, recorded |
Result |
wf.rollback_development() |
POST …/development/rollback — restores the latest adoption's snapshot byte-identically |
Result with byte_identical |
wf.lineage() |
GET …/development/lineage — generation, every succession with form hashes and the episode it was bound to |
Result |
The evidence faculty's cooldown applies across episodes (1,440 application ticks after the previous one):
episodes cannot be re-run without new evidence until a derived seed happens to succeed. Scope statement,
carried in every record: this applies to the Allocate faculty exposed as goals.allocate; the Transmit,
Recover and Narrow surfaces are not exposed; evidence evaluation, extraction, memory, planning and
observation do not execute the self-modification mechanism. The reference application that exercises all of
this end to end is platform/sdk/examples/journal_entry_review/ (demo_development.py).
Every call returns a signed record; typed(record) maps outcome_class to a
class. The classes are disjoint. Handle all three.
| class | outcome_class |
meaning | retry? |
|---|---|---|---|
Result |
result |
the mechanism produced a result | — |
SemanticUncertainty |
semantic_uncertainty |
it ran and declined; .reasons lists why (relation_unasserted, relation_direction_unasserted, relation_predicate_unasserted, negated_claim_grounds_only_affirmative_evidence, not_derivable, incomplete_derivation, no_belief_formed, ...) |
no — more evidence |
OperationalError |
operational_error |
it could not complete; .code (belief_store_unavailable, runtime_image_rejected, engine_binary_missing, engine_unavailable, adjudication_failed), .message |
after fixing |
Suppressed |
suppressed |
reserved for governed language (SUPPRESS); no M1 capability produces it |
never treat as uncertainty |
Common properties: record_id, sequence, operation, capability,
result, identity (binary sha256, build sha, KB fingerprint, engine tag),
evidence_package_id, signed_record, claims (list of ClaimVerdict:
text, verdict, demotion_reason, asserted_by, endpoints,
disposition, verified).
| exception | when |
|---|---|
NotFound |
404 — not yours or not there (never distinguished) |
WorkflowBusy |
409 — another call holds the workflow |
Unauthorized |
401/403 |
EarlApiError |
any other API error (status, detail, method, path) |
NotAvailableInThisRelease |
capability not qualified in the manifest, or A2 |
VerificationFailed |
reserved for callers that want verification to raise |
Semantic uncertainty and operational failure are not exceptions.
verify_offline(outcome) checks the earl-workflow-record/1 signature and,
when the record links one, the earl-evidence-package/1, pinned to the
manifest issuer. Pure-Python RFC 8032; cryptography used when installed.attest_input(record, text) confirms the record's request hashes the exact text you sent.python platform/backend/tools/export_openapi.py
python platform/sdk/python/tools/generate_from_openapi.py
The backend suite fails when _generated.py is stale, when any /v1 operation
is unwrapped, or when the client hard-codes a path.
Read capability-limitations.md. In one line:
integration is qualified on every offered capability (rc4); evidence.evaluate
is not qualified for unattended approval decisions; the SDK release is not
qualified overall.
| call | what it does |
|---|---|
client.create_application(name, description="", organization_id=None) |
creates the application in your organisation with its own service identity |
client.applications() / client.application(id) |
list / read (404 outside your organisation) |
app.create_credential(name) |
mints an API key (earl_ak_…), returned once under api_key; stored hashed |
app.revoke_credential(credential_id) |
revokes a key; calls with it are refused from then on |
app.connect_domain(domain_id) |
connects a trained domain of your organisation |
app.bind_workflow() |
creates (or returns) the application's persistent workflow on the connected domain |
app.archive() / app.unarchive() |
hides the application and its workflow from lists and revokes its keys (records are kept; ?include_archived=true lists them); unarchive restores visibility, not keys |
app.workflow() / app.sdk_snippet() / app.next_steps |
the bound workflow, a ready-to-run snippet, what is still missing |
An API key authenticates every /api and /api/v1 call as the application's service user, inside its
organisation; records carry that identity. Every record also carries execution_path (the runtime the
capability ran on) beside the engine identity.
client.domains())How EARL learns a domain, from the SDK. DomainTrainer drives the platform's documented routes with the client's credentials:
| method | route | notes |
|---|---|---|
create_domain(name, description, foundational_kb_package_id=) |
POST /domains |
returns the domain id |
add_document(domain_id, filename, text) |
POST /domains/{id}/documents/text |
markdown text; the response carries advisory corpus_warnings |
start_tuning_run(domain_id) |
POST /domains/{id}/tuning-runs |
over the domain's documents |
set_vocabulary(run_id, module_text) / build_kb(run_id) |
vocab-draft, approve, build-vocab-kb | raises with the contract's errors when refused |
set_curriculum(run_id, curriculum) |
lesson-draft, approve | curriculum.json as a dict |
run_school(run_id) |
POST /tuning-runs/{id}/run-school, then polls |
a SchoolOutcome: mastered / total, gaps per lesson, and engine (release, git commit, binary sha256, matches_manifest) |
school_record(run_id) |
GET /tuning-runs/{id}/school-record |
per lesson status, score, attempts and gaps; the ledger of which lesson taught each belief |
accept_school(run_id) / train(run_id) |
accept-school, train-domain, then polls | an unmastered school is accepted only with a logged reason |
set_policy(domain_id, document) |
PUT /domains/{id}/policy |
dispositions, rules, and the authority section decide uses |
store_check(domain_id) |
GET /domains/{id}/store-check |
facts, contradictions, noise facts |
set_language_faculty(domain_id, enabled) |
PUT /domains/{id}/language-faculty |
gated (rc27): later school and training runs use earl-learn / earl-learn-train with the bundle's WordNet tables |
language(run_id) |
GET /tuning-runs/{id}/language |
what the language faculty kept with the brain: rows of lexicon.tsv, word_classes.tsv, constructions.tsv, patterns.tsv, story_experience.tsv, for the school and trained brains |
train_directory(dir, name=, description=, foundational_kb_package_id=) |
all of the above | a domain/ directory (corpus/*.md, vocabulary.py, curriculum.json, optional policy.json); stops when school leaves lessons unmastered |