← All guides

SDK reference — 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

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

Workflow

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

Developmental operations (SHIP-2, incomplete)

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).

Outcomes

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).

Errors (exceptions)

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.

Verification and attestation

Regenerating after an API change

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.

Qualification status

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.

Applications

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.

Domains: school and document training (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