# DKE MCP — Wire & Tool Contract
**Version:** 1.1.1, changed in rev 151 · alpha · API 1.3 · rev 159\
**Status:** Published reference for the DKE service wire, and its contract.\
**Audience:** Customers and third-party tooling that call the DKE service at
`dke.langsyn.net` — the tool surface, request shapes, response envelope, error
model, and billing. The language you send over this wire is
[DKE Python](/ref/dke/python/); the unit you send it in is a
[module](/ref/dke/python/#modules). If you are connecting for the first time,
start with the [MCP Tutorial](/tutorial/dke/mcp/).\
**This document as Markdown:** [dke-mcp-ref.md](/ref/dke/mcp/dke-mcp-ref.md) — the same text
this page is rendered from, for readers and tools that would rather have
the source than the page.
---
## 1. Model
DKE is a **deterministic reasoning engine addressed over MCP**. You write a
program in DKE Python and submit it; DKE returns a response that is a
deterministic, reproducible function of the program and the store it runs
against — reproducible byte for byte, and metered per operation rather than per
token.
The program world is **two levels**:
- A **module** is the unit you submit: a `.dpy` source with a run-once body. It
is a named, stored object.
- A module defines zero or more **scripts** (its `def`s). Each script is
addressed **`module.script`** — module-qualified, so two modules may each
define `restock` without collision.
Two verbs produce a *computed* response — **`compile`** (store a module) and
**`run`** (execute). Everything else is management and introspection: where
modules, scripts, and data live, and how you inspect, protect, and remove them.
The service stores only the **compiled form** of what you submit, never its
source text: there is no source readback. You keep your own source.
---
## 2. Tool surface
Nineteen tools, in eight groups:
| Group | Tools |
|---|---|
| **Compute** | `compile`, `run` |
| **Control** | `stop` |
| **Introspection** | `spec`, `info`, `list`, `info_module`, `list_modules` |
| **Diagnosis** | `checkup` |
| **Change** | `checkpoint`, `diff`, `restore` |
| **Lifecycle** | `forget`, `forget_rule`, `forget_constraint`, `forget_module`, `forget_checkpoint` |
| **Store** | `stores` |
| **Feedback** | `report` |
The `tools/list` result carries the release on `_meta` — `stage`, `api_version`
and `revision`, the release this document's Version line states after its own
version — so the
catalog a client received says which revision of the surface it implements. The
revision is one number for every published document: this contract, the
[Python Specification](/spec/dke/python/) and the rest state it, and it moves
whenever any of them changes. The `initialize` result states the same three
facts, so a connecting client sees them before its second call
([§3](#connect)).
Script-level verbs (`info`, `forget`) address a script by its qualified
`module.script` name; `forget_rule` addresses a standing rule the same way
(`module.rule`) and `forget_constraint` a standing constraint
(`module.constraint`); module-level verbs (`info_module`, `forget_module`)
address a module by its bare name.
`checkpoint`, `diff`, `restore` and `forget_checkpoint` address a mark by a name
you choose — see
[§9.7](#change-what-happened-since-a-mark).
Your key addresses exactly one store, the one you created it for in your
[account dashboard](/dashboard/). Creating, deleting and snapshotting stores happens there,
not on this wire.
---
## 3. Connect
- **Endpoint:** `https://dke.langsyn.net` — MCP over HTTPS, JSON-RPC 2.0,
protocol version `2024-11-05`.
- **Auth:** `Authorization: Bearer dke_s_…` (a store key, issued in your account
dashboard or on your own machine, §3.1). Any other kind of key is rejected
on this endpoint; a key
beginning `dke_c_` is your customer key, which no endpoint takes.
- **Server identity and release:** `serverInfo = { name: "dke", version: "1.2" }`.
The version is the public API version, `MAJOR.MINOR`, deliberately decoupled
from the build version: MAJOR moves on a change a client built against it would
notice, MINOR on an addition, and a correction moves neither
([Python Specification §11.1](/spec/dke/python/#spec-version)). The `initialize`
result also carries the release on `_meta` — `stage`, `api_version` and
`revision`, the release this document's Version line states after its own
version — and repeats
them in its `instructions` string, because some MCP clients show that field
to a model and never surface `_meta`. The revision is the surface's: one
number every published document states, moved whenever any of them changes.
Compare it at connect with the revision you implemented against, and when it
moved, read the Revision history of each document you rely on for what
changed.
- **Handshake:** standard MCP `initialize` → `tools/list` → `tools/call`. The
`clientInfo` a client sends about itself is recorded against the key it
connected with, and surfaces in the dashboard. It is **self-declared**: it
labels a connection, never authenticates one, and never affects access.
- **One key, one client.** A key is the identity your writes are recorded under
— `written_by` ([Python Reference §11.2](/ref/dke/python/#field-access-by-type))
reports it on every claim — so two clients sharing a key produce a history
that cannot be separated afterwards. Issue each client its own, from the
dashboard or on the client's own machine (§3.1). A key is issued at one of five access levels — read; read-add, which records new facts and cannot change or delete anything already recorded; read-write; read-write-delete; or none, which keeps the key but lets it do nothing except call `spec` and `report` (§9.9) until it is given a level again; an additional key defaults to
**read**, and its level is raised only when that client writes. A read-add
key's write that would change a value already recorded is refused with
`scope_forbidden` at the statement that makes it: a `run` stops there, keeping
what it wrote before, and the refusal cannot be caught inside the program. A key's store
is fixed for its lifetime; its access level is not, and is changed from the
dashboard.
### 3.1 Issuing a store key on your own machine
The dashboard issues a store key in your browser. Two programs issue the same
key on your own machine, for a client with no browser — a CI job, an agent, a
server — or for when you would rather not trust a page to do it:
- **`dkekey`**, one program for Linux, macOS and Windows, served with its
source and a signed list of its digests at
;
- **`dke.keys`**, a module of the `dkepy` Python package, served at
.
Each one signs in to your account, with your password or with an **account
token**, opens the store with a key that already opens it, and generates the new
key from the machine's own random source. The account service receives two
things: the new key's SHA-256 hash, and the store's data key wrapped under the
new key. The key itself is never sent. A key issued this way is a store key like
any other: this endpoint takes it at the level it was issued at, and its level
is changed, or the key revoked, in the dashboard.
An account token is issued in the dashboard, under Account tokens, and is not a
key. It opens no store, and this endpoint rejects it as it rejects any key but
a store key. It can only add keys to stores that already exist, and only up to
the level it was issued at.
How to install and run each program is in its own README, served beside its
source.
---
## 4. The two compute verbs
`compile` and `run` are the two ways to submit a source, and they differ in
exactly one respect: **`compile` keeps the compilation; `run` does not.**
"Keep it" versus "try it."
### 4.1 `compile` — validate and store a module
Turn a module's source into a named, stored unit ("compile and keep"). This is
the only path that creates durable state (stored modules, scripts, rules, kinds).
A `.dpy` source is a module ([Python Reference §9.1](/ref/dke/python/#modules)):
`compile` stores the
module text under `name` and **runs its body** — executing its data
declarations, installing its rules, and making its `def`s callable.
- **Params:**
- `name` *(required, string)* — the module's name in the store. It MUST be a
single identifier of the same shape `import ` accepts in DKE Python:
an XID_Start character followed by XID_Continue characters, with no dot and
no hyphen. A module you cannot name in an `import` statement is a module you
cannot use, so the name is held to the language's own rule.
- `source` *(string, optional)* — the module text; omit to **reload** an
already-stored module (re-run its body).
- `version` *(string, optional)* — your label for **this revision** of the
module, e.g. `"2.1.0"`. Recorded as metadata and reported by `info_module`;
it changes nothing about how the module resolves or runs. Compared for
equality and never ordered, so any different value is a change — including
going back to a label you used before. Omitting it on a later `compile`
leaves the version already recorded alone, so an unrelated edit cannot
erase a label. A module you never versioned simply has none.
- `language` *(enum, default `"eng"`)* — see §8.
- **Success:** `_meta.status = "success"`, the module's identity and the
`module.script` names it installed.
- **Invalid source:** `isError = true`, `_meta.status = "compile_error"`,
`_meta.compile_status ∈ { parse_error, type_error }`,
`_meta.diagnostics = [{ class, line, col, message, fix? }]`. Nothing is
stored: a module that does not compile leaves the store as it was.
- **Body fails:** `_meta.status` names the failure that stopped the body:
`refuse` for a refusal nothing caught, such as two writes that conflict, and
`engine_error` for an operation the service could not carry out, such as an
`import` of a module that is not stored. Nothing is stored in this case
either. The module's declarations and every write its body made before the
failure are discarded together, so the store is as it was and the corrected
source compiles with nothing to forget first. A reload whose body fails keeps
the module already stored and discards what that run wrote.
The service MUST:
1. Lex, parse, and typecheck the source.
2. If any check fails, refuse the operation; no storage occurs.
3. Otherwise, store every declaration of the unit under its own declared name,
each executable by name, and run the module body once — all of it or none of
it. If the body fails, discard the declarations together with every write
the body made, and store nothing.
A unit that is exactly one `def` is just a one-declaration module — `compile`
submits it like any other; there is no separate single-script verb.
**What the response says.** It acknowledges the submission, states **what it
installed** — the classes, scripts, rules and computed fields the unit
declared — and then **reports what the module body did**: its `print` output and
its store effects, in execution order, because running the body is part of
submitting the source (step 3 above). If the body fails, the report ends at the
failure and carries what the body printed, with none of its store effects,
since none was kept.
Each part appears only when it has something to say. A module that declares
nothing installable — a data declaration alone — states no installs; a module
with no body reports none; a module with neither is acknowledged on its own. The
installs are what **this submission** installed, so reloading a stored module by
name states none: its declarations are already stored, and only its body is run
again.
Name resolution, re-submission reconciliation, and namespacing are specified in
[Python Reference §9](/ref/dke/python/#modules).
### 4.2 `run` — execute
Execute **instructions** and return a transcript plus usage.
- `source` *(string, required)* — a program: statements that read, write, and
call compiled scripts. Compiled and executed once, **not stored**; anything it
`remember`s persists, but nothing is left callable and nothing appears in
`list`. A source with no statements has nothing to execute and is refused.
- `language` *(enum, default `"eng"`)*.
A program has **no parameters** — write the values you want into the
instructions — and it **cannot create definitions**. A source declaring a `def`,
a `class`, a `@rule`, or a computed field is refused, with a message directing
you to `compile` it. A program cannot `return` either; a value it wants to
report, it prints or binds.
To execute something you compiled, import its module and call it:
```json
{"source": "import inventory\ninventory.restock(\"widget-1\")"}
```
A **value script** composes the same way, its result bound in the program
(`n = inventory.count_on_hand("widget-1")`); the binding appears in the
transcript as a typed entry carrying the value.
**What a run reports.** The run executes against the store's current state. The
transcript renders in the natural language the source was compiled in (§8), and
covers **everything the run did**, in execution order, including
the effects of any module the program imported. Its call frame is the response
header, not a body line: a successful run is headed `OK program: ` and
a failed one `error: program failed — `. A reason raised at a line opens
with it: `line 3:` is a line of the program you submitted, and
``line 12 of module `inventory`:`` is line 12 of that module's source, which
the run reached through an import or a call. The verdict tallies what the
run did — the value it returned, when it returned one; the count of **claims**
written and **claims** removed; the absences asserted, the preference changes,
the pins placed and the pins lifted; and the `print` lines. The two claim
counts are in claims, not statements, and **a statement that
changed no state reports no effect at all** — it earns no place in a tally of
what changed, and its target is not listed among the effects either. One
`forget` removes every claim at the address it names, so a statement that
removed three reports three and one that found nothing to remove reports
nothing. A **range** `forget` is the exception to that first clause and not to
the second: it names the older versions before an instant rather than
everything at the address, so it can report fewer than the address holds — and
it never removes the value in force, which is why the count it reports can be
zero at a cell that plainly has a value. A `remember` of a value a source has **already** recorded at that cell
is likewise no change: the store is idempotent, so the write succeeds, adds no
claim, and is reported as adding none. So is an `update` restating what its
source already holds there — the same value, qualifiers and window. The body
then lists, in execution order, the run's **output** (the values the program
`print`s) and its **store effects** (the `remember`/`update`/`forget` writes
and the `assert`, `prefer`/`unprefer` and `pin`/`unpin` changes, each naming its
target cell, in the spelling the program addresses it by). A `remember` or
`update` that gave a validity window or qualifiers also names each bound it
gave and the names of its qualifiers, never their values, so a windowed write
reads differently from an always-valid one. Statements that change no state stay silent —
assignment bindings, control flow (`if`/`match`/`for`), transaction verbs
(`branch`/`commit`/`rollback`), and read/query calls — so the body carries the
run's outputs and durable effects, not its execution mechanics.
This human-readable render is **presentation, not a versioned contract**: a
conforming implementation MAY format the header, verdict, and effect lines
differently. What is pinned is that the render is **deterministic** (same source
+ same store → byte-identical transcript) and that the machine-readable result
carries the run's terminal status and its `print` sequence.
A run **is not transactional**: a failed run does not undo writes already
executed by earlier statements (its data writes persist either way); only a
`branch` block groups writes transactionally.
**A call whose caller leaves is stopped.** If the connection a call arrived on
closes before the answer is sent — the caller stopped waiting, or went away —
the program the call is running stops at its next statement; a statement
already running finishes first. What stays is what stays after any refusal: a
`run` keeps the writes it executed, by the paragraph above, and a `compile`
stores nothing (§4.1). Nothing records that it stopped, so a program whose writes
must be known to be complete writes a marker of its own as its last statement
and reads it back later. The answer goes nowhere, and the call is billed the
operations it executed (§7). A call can be stopped the same way on request,
from another connection, with `stop` ([§9.8](#stop-end-the-call-running-on-your-store)).
---
## 5. Response envelope
Every `tools/call` returns a JSON-RPC success envelope `{ jsonrpc, id, result }`
where `result` is:
```
result = {
content: [ { type: "text", text: },
],
isError: , // true if the call did not succeed
_meta: {
status: "success" | "refuse" | "engine_error" | "compile_error",
tenant_id, key_id, // from auth, never from params
answered_at, // RFC-3339 UTC
ops, // usage (see §7)
refuse_reason?, // see §6
// compile: compile_status, diagnostics[]
// run: invoke_status, transcript[]
// stop: outcome
notes[]?, // compile and run: what was noticed
}
}
```
- A value a program computes rides in `transcript[]`, as the typed entry for
the binding that holds it, up to 65,536 characters of it (see *A value too
long to carry whole*, below). A program does not return, so there is no
separate returned-value field.
- Each entry carries a **`kind`**, naming which statement of yours produced it.
There are exactly two, and both are named for the DKE Python construct they
record:
| `kind` | The statement that produced it |
|--------|--------------------------------|
| `assign` | a bare assignment — `cur = current(K.s.a)`. The entry carries the bound value, its `type`, and **`name`** — the identifier you bound it to. |
| `print` | a `print(...)` call. The entry carries the printed value, and no `name`, because a print binds nothing. |
**`name` is how you tell two bindings apart.** A program that binds several
intermediates produces several entries of the same `kind`, and without the
name the only thing separating them is `line`. Match on `name` when you want
a particular value out of a transcript; it is the identifier from your own
source, spelled exactly as you wrote it.
**Only value-bearing statements appear.** Control flow — `if`, `for`, `match`,
`try`, `with branch`, `with world` — runs without adding entries, and so do write verbs like
`remember`. A transcript is the values your program produced in order, not a
trace of everything it did. If you need to see that a branch was taken, print
something in it.
**Calling a script adds no entry for its parameters.** They receive your
arguments without one, however you call it — as a statement, bound to a name,
or inside an expression — so every `assign` that carries no `from_module` is a
binding you wrote. What the script itself prints or binds still joins your
transcript, as the next point describes, and a value it returns appears under
the name you bound it to.
- Each `transcript[]` entry carries a `line`, and **`line` is a line of the
source the entry came from — which is not always the source you submitted.**
A module's lines run as part of your run in two ways: its body runs when you
load it, and its scripts run when you call them. Either way its entries join
your transcript and carry **`from_module`**, naming the module they came from;
your own entries carry no such field. Read `line` against `from_module` when
it is present, and against your own source when it is not. Where one module
reaches another, the field names the module an entry is actually from, not the
one that reached it — so a line number of yours and a line number of a
module's are never confusable, even when they are the same number.
- **A line that runs many times is reported, not repeated.** A loop would
otherwise emit one entry per iteration, making the response as long as the
work rather than as long as the answer. So for each line, the first few
entries and **always the last** are kept, and the repetitions between them are
dropped. The last is always kept because it holds the value that line finally
produced — the answer. `print` output is never dropped at all: it is your
program's own output. When anything was dropped, **`transcript_omitted`**
gives the count, so a shortened transcript is never mistaken for a short one;
a program that repeats nothing carries no such field.
- **A value too long to carry whole is shortened, and says so.** An `assign`
entry's `value` is the text of the bound value, as `print` would write it,
when that text is at most **65,536 characters**. Past that, the entry carries
a shortened form, and a shortened form cannot be mistaken for a whole one:
| The value | Past 65,536 characters it reads |
|---|---|
| a list, tuple, set or dict of more than 8 elements | the counted preview a read's result has: `30000 entries [0, 0, 0, 0, 0, 0, 0, 0, +29992 more]` — its first 8 elements, then `+ more` naming how many were left out (a dict's elements are `key: value`) |
| a collection of 8 elements or fewer | its own spelling, with each element that is itself too long shortened the same way, so `[21846 entries [0, 0, 0, 0, 0, 0, 0, 0, +21838 more], [1]]`; two levels down only the count is kept, `21846 entries [+21846 more]` |
| a blob | ``: its length in bytes and its first 8 bytes in hex |
The blob form is not a literal, on purpose: a prefix written as `blob("…")`
would read back as a different, shorter blob. Pasting it into a program is
refused. The same forms are used wherever a transcript shows such a value:
the claim an `assign` shows for a stored blob (`active_claim= src=…`), and the echo of a write in the rendered text
(`update K.s.a = `). A value of 65,536
characters or fewer is written exactly as before.
**`print` is never shortened**, and neither are `str()`, `repr()`, `len()`,
what a program stores or reads, or a value a script returns. A program that
wants a long value whole prints it. A string is not shortened.
- **A program that compiled can come back with `notes[]`** — something true
about your source that did not stop it compiling, said beside the result
rather than instead of it. `compile` and `run` both carry them. Each note has
a `line` and a `col` in your source, a `reason`, and a `teaching_hint` that is
the fix alone — the same two text fields a refusal carries — in the language
you wrote the program in. They also close the rendered result, one line each,
after everything else in it. What is noted: an `except` handler that can
never run, because an earlier handler in the same `try` already catches its
category ([Python Reference §7.9](/ref/dke/python/#try)), and a write to a bare kind
that a stored module also declares, from a program that does not import the
module, which writes a kind of its own beside the module's
([Python Reference §9.6](/ref/dke/python/#namespacing)). The program still compiles,
still runs the first handler and still makes the write, so a note never
changes what a call does or its status. A program nothing was noticed about carries no `notes` field.
- `_meta` is the **canonical** structured channel, and carries the whole run.
- Because some MCP clients do not surface `_meta` to the model, the same
structured block is **mirrored** as a trailing `content[]` text item (leading
`\n`, JSON-parseable). Clients SHOULD prefer `_meta`; the mirror is a
compatibility affordance.
- The mirror carries the same **fields**, but not the whole run — it is your
**results**: every `print`, and the final value of each statement you
wrote. A module's own working, which carries `from_module`, is not among them;
it is on `_meta`. Its own `transcript_omitted` counts what the mirror left out,
which is more than `_meta` left out. Read the mirror to see what a program
answered, and `_meta` to see how it got there.
A run's `notes[]` is in the mirror too, whole, beside `transcript`. A
compile has no mirror, so its notes are on `_meta` and in the rendered
result.
The envelope reports outcome, usage, diagnostics, and a transcript of the
program's visible outputs and store effects — the observable result of running
your program, nothing about how the service is built internally.
---
## 6. Error model
A failure reaches you at one of three levels, and a client has to handle all
three. The first is the one people miss: **not every response is a JSON-RPC
envelope.** A request that never gets as far as the tool surface is answered
with an HTTP status and a small JSON body, so parsing every response as
JSON-RPC breaks on the first mistyped key.
**HTTP (no JSON-RPC envelope; body is `{"code": "…"}`):**
| Status | `code` | Meaning |
|---|---|---|
| `401` | `AUTH_MISSING` | no `Authorization` header |
| `401` | `AUTH_MALFORMED` | present but not a well-formed bearer key |
| `401` | `AUTH_EMPTY_TOKEN` | `Bearer` with nothing after it |
| `401` | `AUTH_CUSTOMER_KEY_NOT_ALLOWED` | a customer key (`dke_c_…`), which no endpoint takes |
| `429` | `RATE_LIMITED` | too many requests — wait and retry |
| `503` | `UPSTREAM_UNAVAILABLE` | the service is not reachable right now — retry |
A `401` also carries `WWW-Authenticate: Bearer realm="DKE"`. Everything past
this level answers `200` with a JSON-RPC body, including every error below —
so **an HTTP status other than `200` means you never reached the tool surface**,
and is the cheapest check a client can make first.
**JSON-RPC (HTTP `200`, `error` member):**
| Code | Meaning |
|---|---|
| `-32700` | parse error (malformed JSON body) |
| `-32600` | invalid request (not valid JSON-RPC 2.0) |
| `-32601` | method / tool not found — any tool name this document does not list (§10) |
| `-32602` | invalid params (missing `source`; unsupported edition/language; an unqualified `forget`/`info` script name; a `report` with no `kind` or `text`, a field that is not text, or more than 32768 bytes — §9.9) |
| `-32000` | server error, returned as a generic `"engine error"` — except from `report`, which says whether the server takes reports or could not receive this one (§9.9) |
| `-32001` | payment required — the balance cannot cover the call, the account is suspended, or the account does not include API access. Clears by paying: top up at [dke.langsyn.com/buy/](/buy/) |
| `-32002` | rate limited — clears by waiting. Carries a `Retry-After` header in seconds. Also the answer to a `report` past the day's allowance (§9.9) |
**`-32001` and `-32002` are deliberately different codes**, because the two
clear by opposite actions: one by paying, one by waiting. A client that cannot
tell them apart will back off when it should top up, or top up when it should
back off. Route on the code, never on the message.
**Application (transport-success, `isError = true`):**
- `_meta.status ∈ { refuse, engine_error, compile_error }` — never `"success"`.
`isError` is exactly `_meta.status != "success"`, so the two can never
disagree.
- `_meta.refuse_reason ∈ { out_of_surface, incomplete_slots, ambiguous, scope_forbidden,
still_referenced, pinned_address, immutable_in_branch, conflicting_write,
invalid_value, limit_exceeded, derived_field, asserted_field, not_recorded,
already_in_effect }`.
- `compile` diagnostics: `class ∈ { parse_error, type_error }` with `line`/`col`.
The three failing statuses differ in whose problem it is, which is what you
route on: `compile_error` means your source did not compile — read
`diagnostics` and fix it; `refuse` means the call was declined — read
`refuse_reason`; `engine_error` means the service could not carry the call out.
`engine_error` does not promise that a retry will help. It is the residual status: the call was well formed, no
rule declined it, and it still did not complete. Naming a script that does not
exist lands here, and repeating that call changes nothing. A store busy with
another operation, for longer than the service waits, lands here too: the
statement that met it kept nothing, and its message says the store was busy and
that nothing in the call needs changing. A call whose save the store reported
as failed, where whether the work was saved could not be confirmed, lands here
as well: its message says exactly that, and you read back what the call wrote
to see whether it is there. The status is the same in all three cases, and the
message is what tells them apart.
**Conditions the store raises when a call cannot succeed as written are
refusals**, each with its own reason — a pinned address, a removal aimed at data
older than the branch you are in, a second value from a source that already gave
one, a literal the store cannot hold, a call past a limit:
| reason | what to do |
|---|---|
| `pinned_address` | `unpin` the address named in the hint, then repeat the removal |
| `immutable_in_branch` | leave the branch, or the world; each removes only what it created |
| `conflicting_write` | `update` to revise the value, or cite a different source |
| `invalid_value` | rewrite the literal or name the hint points at |
| `limit_exceeded` | narrow what the call works over; if the message names your store's size limit, remove data or ask for a higher limit (§9.5) |
To catch any of those, route on `refuse` and switch on `refuse_reason`.
**A computed field, and any field a standing rule concludes, is derived and
never written**, and the store holds that in both directions:
| reason | what to do |
|---|---|
| `derived_field` | write the fields it is derived from, which the hint names — or, for a rule meant to give way to a recorded value, say so in the rule with `not_asserted`; for a rule that reads the field it concludes, give its conclusion a field of its own |
| `asserted_field` | `forget` the recorded values the hint names, or conclude the rule at another field |
`derived_field` answers a `run` that writes such a field; `asserted_field`
answers a `compile` whose rule or computed field would conclude over values
already recorded there, and a `merge_world` that would bring a value recorded in
a world to such a field. Removing a recorded value is never refused.
**`pin`, `unpin`, `prefer` and `unprefer` refuse** a call that names something
that is not there, and a call asking for what already holds:
| reason | what to do |
|---|---|
| `not_recorded` | check the address, or the source: it holds no value there — for `unpin` and `unprefer`, no mark and no value either |
| `already_in_effect` | nothing: the address is already pinned or already not, the source is already the one you prefer, or no preference stands there |
The two are separate because they want opposite things from you. `not_recorded`
is most likely a mistyped address or source. `already_in_effect` is the state you
asked for, so a program making sure of it catches that reason and carries on.
`unpin` and `unprefer` refuse only when there is no pin, or no preference, to
lift: one already standing is always removable, whatever the address holds now.
Naming a script or a module that does not exist is an `engine_error`: the call
is well-formed and there is nothing at the address, so `list` or `list_modules`
to find the right name. That is a separate rule from `not_recorded`, which covers
the four verbs above and one kind of read, below. A program that `import`s a
module that does not exist is refused as the `import` runs, the same way. If the
program also uses that module, it is refused before it runs, as a
`compile_error` at the `import` in the same words, because nothing below the use
can be checked without the module
([Python Reference §9.3](/ref/dke/python/#modules)).
**`derived_field`, `not_recorded` and `limit_exceeded` also answer a read given
`known_at=`**, which asks what the store had been told by a moment rather than
what was true at one, and answers a conclusion as the rules read at that moment.
| reason | what it means on such a read | what to do |
|---|---|---|
| `derived_field` | the answer rests on a rule changed after that moment, or on one whose text at that moment the store holds no record of, so its text then is not known | read it without `known_at=`, or as of a moment after the change; a rule of the second kind takes part once it is compiled again, unchanged |
| `not_recorded` | a value the answer rests on was replaced by one that has since been forgotten, so when it stopped being in force is not recorded | read it without `known_at=`, or read another address at that time |
| `limit_exceeded` | the answer rests on more of the store than such a read may work over; the same read over the same data refuses the same way every time | read it without `known_at=` |
**Worlds reuse these reasons and add none.** `create_world`
naming a world that exists is `already_in_effect`, and naming one that does not
exist — in any world verb or a `with world` block — is `not_recorded`. Inside a
world, removing what the store holds, and `prefer` or `unprefer` at a field the
store has judged, are `immutable_in_branch`: read *branch* in that name as *branch
or world*. `prefer` at a field another world has judged is `still_referenced`,
and the hint says to merge or discard that world first. A branch named for a
world that exists is `invalid_value`.
`still_referenced` is the other side of that coin: the call is well-formed, the
address exists, and a rule declines it — `forget_module` on a module another
stored module still imports. Retrying will not help either, and the hint names
the module to forget first. **Every failure reaches you on the status**, so
routing on `isError` alone is sufficient to tell a completed call from a failed
one; you never have to read the prose to find out which happened.
Refusals name a category, not an internal cause: a scope refusal does not say
which permission was missing, and a capacity refusal is a generic "service at
capacity."
### 6.1 The repair loop — a rejected submission, its `fix`, and the resubmit
A `compile_error` is not a dead end, and this is the loop most agent workflows
spend most of their turns in. Every diagnostic carries `message` for a person
and, where the correction is mechanical, **`fix` for a program**. They are
separate fields on purpose: one response serves a reader and a client without
serving either badly, and a client applying `fix` never has to parse prose.
Submit a module written on Python reflexes:
```json
{ "name": "watch",
"source": "def watch(n: int):\n i = 0\n while i < n:\n print(\"checking\")\n" }
```
The submission is refused whole — nothing is stored — and the diagnostic says
both what is wrong and what to write instead:
```json
{ "isError": true,
"_meta": {
"status": "compile_error",
"compile_status": "parse_error",
"diagnostics": [{
"class": "parse_error",
"line": 3, "col": 5,
"message": "DKE Python has no `while` loop — iteration is bounded: `for in :`, where a counting loop takes its numbers from `range(n)`. Bounded iteration is the termination guarantee; there is no unbounded loop.",
"fix": "for in :"
}]
}}
```
`line` and `col` say where, both counted from `1`, `col` in characters rather
than bytes as Python counts a syntax error's offset; `fix` says what to write,
and the resubmission is the same call with that edit applied:
```json
{ "name": "watch",
"source": "def watch(n: int):\n for i in range(n):\n print(\"checking\")\n" }
```
Two properties make this a loop a client can drive rather than a message a
person must read:
- **A rejected `compile` stores nothing.** The store is exactly as it was, so a
retry is a retry and never a partial write to undo (§4.1).
- **`fix` is present only when the correction is mechanical.** Its absence is
not an omission — it means the diagnostic has no single edit to offer, and
`message` is the whole of what the service can say. Treat `fix` as optional
and fall back to `message`; never require it.
The diagnostics most worth expecting are the ones where a Python habit is the
wrong reflex here — no `while` and no recursion. Each of those refuses with
the DKE Python form to write instead, so the first submission an agent writes from Python memory is
usually one edit from compiling rather than a rewrite.
---
## 7. Usage & billing
- `_meta.ops` — the number of **operations** the call consumed, an opaque
non-negative integer. It counts what the program ASKED FOR, so the same
question costs the same however you spell it.
- An operation is counted **each time it runs**, not once for each time you
write it. An operation inside a loop over a hundred subjects is a hundred
operations, so a program's cost follows the data it works over rather than
its length.
- **1 LCN (LangCoin) buys 100,000 operations**, debited from the account's balance after the
response is served. It is a capability measure, not a cost breakdown.
- A run over a suspended key is refused with `-32001` before it executes — the
service never runs on credit.
---
## 8. Language selection
Language is chosen in params:
- `edition` *(enum, default `"python"`)* — the concrete language.
`"python"` (DKE Python) is the only accepted value.
- `language` *(enum, default `"eng"`)* — the natural-language keyword surface.
`"eng"` is the only accepted value. The tool schema's `enum` lists what
this endpoint accepts.
Any other value is rejected with `-32602` before the call runs.
A source given to `compile` or `run` may also declare both on its first line,
`# dke: edition=python.1 language=eng.1`, as the
[Python Specification §3.9.1](/spec/dke/python/#the-first-line-declaration)
states. A declared value supplies the parameter the call leaves out, so a
declared source needs neither. Each of these is rejected with `-32602` before
the call runs, and the message says which:
- a parameter that names another value than the source declares;
- a version this endpoint does not serve, and the message names the one it
serves (for the edition, the API's MAJOR: `python.1` at API 1.0);
- a first line that opens `# dke:` and does not match the form.
A version left out is the one this endpoint serves.
Every tool takes `language` except `spec` and `stores`. On `compile` and `run`
it selects the keyword surface the source is written in, as §8.1 describes. A
tool that carries no source still answers in words, so there it selects the
language of the reply. Either way, the names and values in your store are shown
exactly as written.
### 8.1 What `language` selects
`language` selects the **keyword surface** — the words the runtime reserves —
and nothing else. Verbs, keywords and the words in the response are in the tag
you ask for; the kinds, subjects, attributes and values *you* supply are your
data and are stored and returned exactly as written.
```json
{"name": "run", "arguments": {
"language": "eng",
"source": "remember(Sensor.s1.room, \"kitchen\", \"survey\")\nc = current(Sensor.s1.room)\nprint(c.value)"}}
```
This writes one claim and prints `kitchen`. `Sensor`, `s1`, `room`, `kitchen`
and `survey` are yours and are the same under every surface; `remember`,
`current` and `print` are ours and are what a surface spells. `value` is
neither: a field of a result is addressed by the same name in every surface.
This has three consequences worth stating plainly:
- **A store is not language-scoped.** A module compiled under one `language`
is read, invoked and forgotten by exactly the same names as one compiled
under another, and the two see each other's claims. There is no separate
store per surface and no migration between surfaces.
- **Nine words are English on every surface.** The two diagnostic verbs exist
only in English, and a program sent under another `language` cannot call
them — writing the English spelling does not reach them either, because on
that surface the word is not a verb at all. The compiler says so and names the
remedy, which is the request's `language` rather than an edit to the source:
*Compile this unit as `eng` to use it.*
The other two, `"asserted"` and `"derived"`, are the words a rule's `trust(...)`
premise compares with: the values `.trust` reads back, English on every
surface. The next two, `real` and `imag`, are the names `complex(...)` takes
its two parts by, and the `.real` and `.imag` attributes. The last three,
`maketrans`, `fromkeys` and `fromhex`, are the methods a type itself answers,
spelled as Python spells them. Everything else in
the keyword surface localises, the `except` handler kinds, the compound verb
heads and every other keyword argument a built-in function takes included.
[Python Reference §12](/ref/dke/python/#keywords) carries the same list.
**English on every surface.** `why_not` `what_needs` `asserted` `derived` `real` `imag` `maketrans` `fromkeys` `fromhex`
- **A module remembers the surface it was compiled in.** `info_module` reports
it as the module's init language, and a script's transcript renders in that
surface, even when a later call omits `language` and takes the `"eng"`
default.
The verbs that operate on a **stored** script — `list`, `info`, `forget` — are
edition-neutral and take no edition: only the source-carrying verbs, `compile`
and `run`, select one.
---
## 9. Introspection & management
### 9.1 Language
- `spec(edition, topic?)` — serve the DKE Python language contract, whole, or
a topic slice, one of `header`, `declarations`, `statements`, `expressions`,
`types`, `verbs`, `scripts`. Classes, rules and constraints are under
`declarations`. The whole document is larger than most clients accept as one
result, so start with the slices. `_meta.revision` is the surface's revision
— the `rev N` on the document's Version line, one number for every published
document, moved whenever any of them changes — whole or sliced, so a client
that built against one revision learns the surface moved without reading the
document again; the document's own Revision history says whether it was the
document that changed.
### 9.2 Scripts — address by `module.script`
- `list(target = scripts | rules | kinds | checkpoints)` — stored
`module.script` signatures, standing-rule names, the kinds your data is stored
under (§9.2.1), or the names of the marks standing in your store (§9.7). A
script's line carries its full call signature: the
name and its typed parameters as they were written, a `/` or a bare `*` among them
(`inventory.ingest(id: int, payload: string, dry_run: bool)`). Every target
lists in **ascending name order** — the order `sorted()` gives, codepoint
order rather than locale collation — and a module's entries sort under their
`module.` name. In expression position inside a program,
`list_scripts()` evaluates to a `string_list` of those names — the one
lifecycle read that is also callable in-language, because it is a read-only
data read.
- `info(name)` — a script's **compile provenance**: the compiling authority,
the natural language the source was compiled in, and the manifest version the
compiled form was produced against. Invocations are not tracked — `info`
describes the compilation event, not any run. (For a data derivation, use the
in-language `why(K.s.a)`, which returns a `proof_tree`; `info`'s response is
text, not a typed value.)
- `forget(name)` — remove one script. Idempotent: if no script with that name
exists, the call succeeds.
#### 9.2.1 `list(target = kinds)` — what the store already holds
The kinds your data sits under: what a `class` declares, and what a fact's path
names first. One per line, in ascending name order, as `list`'s other targets
are.
It answers a question the other reads cannot, and the difference is worth
stating because it is easy to be misled by. `checkup` reports what a store is
*missing* for the reasoning standing over it, so it names a kind only when no
rule reads it — write a rule over a kind and it stops appearing. `subjects(...)`
and `current(...)` report what is *in force*, so a recorded value whose validity
window does not cover the moment asked about reads exactly like a cell nobody
wrote. This target reports what is *there*.
Ask it before writing facts into a store you did not start from empty. A kind
you did not expect means data you did not put there, and a read that finds
nothing may be reading past it rather than past nothing — `get(K.s.a)` shows
every claim recorded at a cell, in force or not.
The engine's own storage kinds are not yours and never appear here.
### 9.3 Standing declarations — address by `module.`
A rule and a constraint are both standing declarations a module installs, and
both are addressed the same way. They are removed by different verbs because
they remove different amounts: one withdraws what it derived, the other has
derived nothing.
- `forget_rule(name)` — remove one standing rule, leaving the rest of its
module in place. The facts the rule concluded are removed with it; the
facts it concluded *from* are untouched, so re-`compile`ing the module
brings the rule and its conclusions back. A conclusion that another
standing rule still supports survives. Removing a rule that is already
gone succeeds. Use `list(target = rules)` to see the names.
- `forget_constraint(name)` — remove one standing constraint, leaving the
rest of its module in place. **Nothing is reclaimed**: a constraint
concludes nothing, so there is no derived fact to withdraw, and the data
it watched is untouched — including the subjects that breached it.
Stopping the watch is not fixing the breach. Re-`compile`ing the module
brings the constraint back. Removing one that is already gone succeeds.
`checkup()` prints the names, in its `violated` findings.
### 9.4 Modules — address by bare module name
- `list_modules()` — module handles (name, script count, created-at). Listing
order is implementation-defined but MUST be stable across reads with no
intervening `compile` / `forget_module`.
- `info_module(name)` — a module's record: its **init language** (the natural
language its body was compiled in), the `module.script` names it defines, the
classes it declares, the `@rule` names it declares, the `@constraint` names it
declares, its `version` if it has one, **updated-at**, and created-at. Each of
the class, rule and constraint lines appears only when the module declares
something of that kind, so a module's record names what it has and stays
silent about what it has not. **Names are module-qualified** —
`.`, `.`, `.` — the same
addressable form the `module.script` names take and the form `list_categories`
and `checkup` use, so one record spells every name one way. Computed fields
are not listed: they are rules you did not name, and `list(target = rules)`
lists authored rules only, for the same reason. `created` is when the module was first stored;
`updated` is when its content last changed, and a `compile` that submits
identical content leaves it alone. Worth reading when a module you `import`
may have moved: a call is resolved when the **calling** module is compiled, so
a caller keeps running the callee body present at that moment until you
compile the caller again. An unknown module is a clean not-found.
- `forget_module(name, confirm=true)` — remove a module as a unit.
`confirm=true` is required; it cascade-removes the module's scripts, rules,
kinds, and their data. With a checkpoint standing it is recoverable as a unit —
`restore` ([§9.7](#change-what-happened-since-a-mark)) brings the whole
module back, or none of it. With no mark standing at the moment you remove
it, it is gone, which is the same rule every removal follows. It
**refuses**, naming the referrer, if
another stored module still references the target — so no dangling references
can exist. Forgetting a module that does not exist is a clean not-found.
`name` here is the **bare module name**, never a `module.script` address.
### 9.5 Store
- `stores()` — your store's id, size, and created-at. Your key addresses
exactly one store. Everything that acts on the store ITSELF — creating it,
deleting it, snapshotting it, restoring it from a snapshot — happens in the
account dashboard, not on this wire. **Restoring a store from a snapshot is
not the `restore` tool**, which shares only the word: that one acts inside
your one store, bringing back values removed since a mark, and never replaces
the store or reaches a snapshot. Your key governs what may be done to
the DATA INSIDE your store; the store's own lifecycle belongs to the
account that owns it.
A store may grow to a size limit. A write that would take it past the limit is
refused with `limit_exceeded`, and the message names the limit and your store's
size, both in bytes. The write that met the limit is not kept; what a program
wrote before it in the same run stays. Removing data makes room, because later
writes reuse the space a removal frees, so a store at its limit accepts a write
that fits in what was freed. Reading is never refused for size, and the same
write over the same store refuses the same way every time.
### 9.6 Checkup — does your store meet the reasoning over it?
- `checkup()` — no arguments, changes nothing, needs only a read key.
Reach for it when a query returns nothing and you cannot tell whether the
answer is genuinely no. A rule that never fired and a rule that fired and
found nothing produce the same empty result, and when the reasoning came
from a module you did not write, nothing in the result tells you which one
happened.
The report is a counts line followed by findings, one per line:
| Line | Means |
|---|---|
| `unmet . no-such-kind` | a standing rule or constraint needs a value at this cell and nothing of that shape exists in your store |
| `unmet . no-values` | the kind is there and that field is empty |
| `unmet . not-in-force` | you did record a value at that field, and its validity window does not cover the time asked about — the fact is in your store and is not current, so what it needs is a window or an anchor, not another write |
| `unread ` | this kind of yours holds data and no standing rule or constraint reads it |
| `near A ~ B` | an unread kind and an unmet one differ by a letter or two |
| `violated …` | a standing constraint of yours does not hold; `` is how many of your subjects breach it, and at most five are named |
| `counts rules=… unfirable=… unreadable=… declined=… kinds=… unread=… constraints=… unevaluable=… violated=…` | how many rules stand, how many cannot fire as things are, how many were stored by a version of the service that this one cannot read and so never fire, how many conclude nothing because a fold of theirs cannot take the values it reads, how many kinds hold data, how many nothing reads, how many constraints stand, how many of those could not be tested at all, and how many do not hold |
A `near` line is the one to read first. Writing your facts under a kind
spelled one letter differently from the one the reasoning reads produces
*both* an `unread` line and an `unmet` line, and the pair names the
mis-spelling in a way neither line does alone.
Findings name **cells, not rules**: what an unmet cell needs is your data,
and the address of the data is the actionable part. Rules are counted rather
than named. A premise that asks for an absence, such as `absent(...)`, is met
by an empty cell, so it raises no `unmet` line and does not count its rule
among those that cannot fire. That holds inside a `none(...)` too: the cells it
reads between its parentheses are what it excludes, so none of them is named,
and the rule it is answered through is counted among the rules that stand and
never among those that cannot fire.
An empty store reports its counts and nothing else. Listing order is stable
across reads with no intervening change.
### 9.7 Change — what happened since a mark
- `checkpoint(name)` — mark where the store is now, under a name you choose.
- `diff(since)` — what changed since the mark of that name.
- `restore(since)` — bring back the values removed since that mark.
- `forget_checkpoint(name)` — drop the mark of that name, letting go of what it
was keeping.
A checkpoint records a **position, not a copy**. Taking one costs the same
whether your store holds ten facts or ten million, and it changes nothing you
have stored. Taking one again under a name you have already used **moves** that
mark to the current position rather than leaving two.
A name is letters, digits, underscores and hyphens, with `.` permitted between
segments so a version reads naturally. It begins with a letter or `_` — not a
digit, a hyphen or a `.` — and no segment is empty, so `v1.2.3` and
`release-candidate` are names and `-rc`, `1st` and `v1..2` are not.
`diff` reports one line per change, in the order the changes happened:
| Line | Means |
|---|---|
| `added .. = (per )` | that cell now holds this value, from this source |
| `added .. = ` | that cell now holds a value a standing rule worked out for you |
| `removed ..` | that cell held a value and no longer does |
| `removed a value of ..` | one value of that cell was removed — `forget` given the value and its source — and whatever else the cell held is still there |
| `removed the values of .. in force at ` | the values true at that instant were removed — `forget` given a datetime — and values true at other times are still there |
| `removed values of recorded before ` | older versions written before that instant were removed — `forget` with `recorded_before=`, over a field, a subject or a whole kind — and the value in force is never among them. `recorded at or before`, `valid before` and `valid at or before` read the same way for the other three keywords |
| `removed .` | that subject was removed whole, with every value it held — `forget_module` removes a module's data this way |
| `removed rule ` | a standing rule you wrote was removed, and what it had worked out for you went with it |
| `none` | there is no mark by that name |
**A value is shown the way DKE Python shows it inside a list**, so its type is
visible in the line: `41` and `3.5` are numbers, `True` is a boolean, `null` is
an asserted absence, and a string is in quotes — `'hi'`, or `'41'`, which is a
different value from `41`. An infinite real is `inf` or `-inf` and a NaN is
`nan`, unquoted, so the string `'inf'` stays apart from the number. A datetime or a duration shows as its value,
`2026-01-01T00:00:00Z` or `PT1H`. The quotes are the language's own, so a string
that contains a quote character is shown exactly as `print` would show it.
**A value you revised reads as `added` at its new value**, and the value it
replaced is not reported as `removed`. Facts worked out for you by a standing
rule appear alongside the ones you wrote: what changed is what changed. Such a
line names no source, because nothing you wrote is its source — the rule that
concluded it is.
**Each line is something that happened, not a comparison of two states.** A
value changed and then changed back reads as two `added` lines. A value written
after the mark and removed again reads as `removed`, though the mark never held
it. And after a `restore`, a value that came back appears twice — `removed`
where it went, then `added` where it returned — which is the record of both. An
older version that came back is reported where it was first recorded, not where
it returned, so it reads as `added` only when it was written after the mark, and
then ahead of the `removed` line that took it.
**A world's merge happens when it lands.** What a world holds reaches the store
at `merge_world`, so `diff` since a mark taken before the merge reports it as
`added`, even what was written in the world before the mark. A value the world
wrote and removed again before the merge never reached the store: `diff` does
not report it as `removed`, and `restore` does not bring it back. The same holds
for a `with branch` block, which commits when it completes.
`none` is not the same as a mark with nothing after it. A mark that exists and
has seen no change reports no lines; `none` means the name does not resolve, so
take the mark with `checkpoint` before asking.
`restore` puts back the values `diff` reports as `removed`, and answers with how
many came back — and, when there were any, how many did not. They come back at
your store's **current position**: this is not a rewind, and nothing you did
after the mark is undone. A rule removed on its own with `forget_rule` is not
among them: `restore` brings back values, and a rule comes back by compiling it
again.
**An older version comes back where it was.** A value you revised keeps the value
it replaced in the cell's history, and when a removal took that older version —
a whole-cell `forget`, or a range, which takes nothing else — `restore` puts it
back into the history, beneath the value it was older than and at the time it
was first recorded. It never comes back in front of the value in force, and it
counts like any other value. If the value it was older than does not come back,
it does not either, and it returns when that value does.
**The count is of values.** A module's scripts, classes and rules come back
with it and are not counted, so a module holding one value adds one to the
count however many scripts it has. Each module that came back is named on a line
of its own, ``module `` restored.``
**A value is skipped when its cell has been written again since the mark, under
the same attribution.** Putting the old value back would overwrite the newer
one, and a restore never overwrites what you wrote after the mark — so it leaves
that cell as it is, counts the skip, and still brings back everything else in
the same call. A skipped value is **not** discarded: remove the newer value and
restore again, and it comes back. Being written again under a *different*
attribution is not a skip — that value returns like any other, and the cell then
holds both.
**A restore reaches only what your store was already holding when the removal
happened.** Removed values are kept while a mark is standing — so a removal made
when you had no checkpoint at all is not recoverable, and neither is one from
before your oldest mark. This is the second reason to take a checkpoint, and the one that is easy to
learn too late: taking the mark is the act that makes the removals *after* it
recoverable, and no message at the moment of a removal tells you whether one was
standing.
**A module comes back as a UNIT, or not at all.** `forget_module` removes a
module as one act, and `restore` puts it back the same way: all of its scripts,
rules, kinds and data, or none of them. It is never partially reconstructed,
because a module missing some of what it declared is not a usable module. Its
rules reason again as soon as it is back — over the values that came back with
it, and over anything you write afterwards.
That is also the one case where a skip is not about a single cell. If the
module's name is in use when you restore — you removed it and imported a
different module under the same name — the whole unit is skipped and the reply
says which module and why: ``module `` not restored: that name is in use.``
Everything else in the same call still comes back. Remove the module now holding
the name and restore again to get the original.
Each removal of a module is its own unit. If you import a module under the same
name after the mark and remove it again, `restore` brings back the version the
mark saw. It names the later version as not restored, because that name is now
in use. To get the later version instead, remove the module now holding the name
and restore again: the versions come back in the order they were removed, one at
a time.
If no module holds the name and the module still cannot come back, the reply
says that it could not be brought back whole.
Naming a mark that does not exist is an **error** here, where `diff` answers
`none`. The difference is deliberate: `restore` can only report a count, and a
count of zero would tell you that nothing had been removed — a far more
reassuring thing to be told than that the name did not resolve.
**Dropping a mark** with `forget_checkpoint` lets go of what it was keeping. The
removals that no other standing mark still holds stop taking storage, and can no
longer be restored — so dropping a newer mark while an older one stands lets go
of nothing yet, and dropping the oldest lets go of what lies before the next.
The answer says how many removals it let go, as in
`OK forgot checkpoint before_import (12).`, and the name then reads like one
never used: `diff` since it answers `none`, and `restore` since it is an error.
Dropping a name that is not a mark changes nothing and answers `none`.
`list(target = checkpoints)` shows the marks that stand. Dropping a mark needs a
key that may delete, as the other `forget` tools and `restore` do: what it lets
go of is what a `restore` could have brought back.
All four are ordinary operations and are metered as such. There is no retention
setting to configure: what a `diff` can reach is your store's own history, and
what a `restore` can reach is what your own marks were holding for you.
### 9.8 Stop — end the call running on your store
- `stop()` — no arguments. Stops what is running on your store, and answers
once it has stopped.
Calls made with keys that may only read run on your store together, and every
other call runs on it alone, a read made with a key that may write included. A
read-only key's first call on a store runs alone too. `stop` reaches every call
running on your store when you ask — a long `run` that another connection is
still waiting on, say — and a call still waiting for the store to come free is
not among them: that call runs once the store is free, as it would have. Ask
again after they have ended and `stop` acts on whatever is running then.
Each call stops at its next statement. A statement already running finishes
first, so one long statement is not cut short. What stays is what stays after
any failure: a `run` it stops keeps what it wrote before then (§4.2), and a
`compile` stores nothing. If the stopped call's own caller is still waiting, it
is answered with `_meta.status = "refuse"` and the reason
`the run was stopped before this statement`, on the line it reached. No handler
in the program sees that refusal, and nothing after the stopped statement runs —
an `except` block or a `finally` block included.
`stop` waits for every call it reached to let your store go, for up to ten
seconds, and says what happened in `_meta.outcome`, one answer for all of them:
| `outcome` | What happened |
|---|---|
| `stopped` | every call it reached has ended, one or more of them stopped at its next statement, and your store is free |
| `finished` | every call it reached ended on its own before it reached another statement, and answered its caller as it would have without the stop; your store is free |
| `idle` | no call was running on your store |
| `stopping` | a call was still running a statement when the wait ended. Call `stop` again in a moment: it waits again, and `idle` then means every call has stopped |
`stopping` is not a failure, and none of the four is: each answers
`_meta.status = "success"`. A key that may not write is refused with
`scope_forbidden`, because a stop can cut a call's writes short. `stop` counts
toward your key's rate limit like any call, is not billed, and is answered even
when as many of your calls are running as your account allows at once — those
are the calls it is for.
### 9.9 Report — tell us what went wrong, or what you want
- `report(kind, text, expected?, actual?, source?)` — `kind` is `bug`,
`request` or `feedback`, and `text` says what happened or what you would
like. For a bug, `expected` and `actual` say what you expected and what you
got, and `source` can carry a program that shows it.
A bug is an answer that differs from what CPython gives for a program both
accept, a failure, or behaviour that contradicts the published documents. A
refusal that names its reason is the language working: it says what it would
take, and the fix is in it ([§6.1](#the-repair-loop-a-rejected-submission-its-fix-and-the-resubmit)).
**What you send is kept by LangSyn and read by us, unlike your store.** It is
the one thing on this wire we can read. Only what you write here is sent:
nothing is taken from your store, not the program you last ran and not the
data it read, so a bug report holds a reproducer only if you put one in
`source`. If you are an agent working for someone, ask them before you include
their data. We add three facts of our own: the account your key belongs to,
the product (DKE) and the release this document's Version line states.
The answer is an id, and nothing else is sent back:
```json
{"content": [{"type": "text",
"text": "OK report: received as rpt_6nQ1vPe9Qvw. LangSyn keeps it and reads it; no reply is sent."}],
"isError": false,
"_meta": {"status": "success", "report_id": "rpt_6nQ1vPe9Qvw", "…": "…"}}
```
| Answer | When |
|---|---|
| `-32602` | `kind` is not one of the three, `text` is missing or empty, a field is not text, or the four fields together are more than 32768 bytes. Nothing is kept |
| `-32002` | your account has sent 20 reports in the last day, the most it may. The message says when another can be sent, and `Retry-After` says the same in seconds |
| `-32000` | `This server does not take reports.`, or `Your report could not be received right now. Send it again later.` In both cases nothing was kept |
Any key may send a report, one at none included (§3), since a report touches no
store. `report` counts toward your key's rate limit like any call, and it is not
billed.
---
## 10. What lives elsewhere
Store **snapshots** are a store-lifecycle control, not an engine operation, so
they are not on this wire: they live in your account dashboard beside your
store's other lifecycle controls, and restoring a store from a snapshot is
available there. A key addresses the data within one store; the store's own
lifecycle belongs to the account that owns it.
Any tool name this document does not list is not served, and returns `-32601`.
## Revision history
Newest first. The revision is the surface's — one number stated by every
published document, the web pages and the service wire, moving whenever any
document changes. This list records the revisions at which this document
changed and what changed in it; a revision absent here left this document as it
was. The Version line at the top states this document's
version, the revision it last changed in and the current revision; from rev
148 each entry opens with the version that revision gave this document. The history
starts at rev 1. What a revision promises is
stated in the [Python Specification](/spec/dke/python/), its section 11.1.
- **1.1.1** · **rev 151** (2026-11-13) — The Version line states API 1.3: a rule premise may test part of a text, as `r.path.startswith("/srv/")`, `t.name.endswith(".test")` and `"admin" in a.role` ([Specification §14.2](/spec/dke/python/#premises)), and an addition moves the API's MINOR.
- **1.1.0** · **rev 150** (2026-11-13) — Calls made with keys that may only read run on one store together. Until this revision every call on a store ran alone, and a read behind another key's long call was refused as busy. Every other call still runs alone, a read made with a key that may write included, and so does a read-only key's first call on a store. §9.8: `stop` reaches every call running on the store and gives one answer for all of them, `stopped` when one or more of them stopped at its next statement; its four answers and their fields are unchanged. `serverInfo.version` is `"1.2"`, the addition moving the API's MINOR, and the Version line states API 1.2. No tool or argument changed.
- **1.0.1** · **rev 149** (2026-11-13) — `serverInfo.version` is `"1.1"`: the API gained a form a rule's conclusion may take, a text written from values the rule read (Python Specification §14.3), and an addition moves MINOR. The Version line states API 1.1. No tool, argument or response changed.
- **1.0.0** · **rev 148** (2026-11-12) — This document states a version of its own, 1.0.0, first on the Version line and followed by the revision it last changed in, and from this revision each entry here opens with the version it made (Python Specification §11.1). The two sentences that called the release on `_meta` the three facts on the Version line now say it is the release the line states after the document's own version. Nothing else changed.
- **rev 147** (2026-11-12) — `serverInfo.version` and `_meta.api_version` are `"1.0"`: the API version is `MAJOR.MINOR`, MAJOR moving on a change a client built against it would notice, MINOR on an addition, neither on a correction (Python Specification §11.1). A declared edition version is served when it is the API's MAJOR, `python.1` at 1.0. The Version line states API 1.0.
- **rev 142** (2026-11-12) — §9: a `diff` line shows an infinite real as `inf` or `-inf` and a NaN as `nan`, unquoted, now that a `real` may be one (Python Specification rev 142).
- **rev 134** (2026-11-10) — §8: a source given to `compile` or `run` may declare `edition` and `language` on its first line, and a declared source needs neither parameter. A parameter naming another value than the source declares, a declared version this endpoint does not serve, and a malformed `# dke:` first line are each rejected with `-32602`.
- **rev 113** (2026-11-06) — §4.2: a value in a transcript entry whose text is longer than 65,536 characters is shortened, and the entry says so: a collection of more than 8 elements reads as a counted preview, `30000 entries [0, 0, 0, 0, 0, 0, 0, 0, +29992 more]`, and a blob as its length and first 8 bytes, ``. `print`, `str()`, `repr()` and `len()` are unchanged.
- **rev 84** (2026-11-03) — §3, §9.9: a key at none can still call `spec` and `report`, and both sections now say so.
- **rev 80** (2026-11-03) — §2, §6, §9.9: `report`, the nineteenth tool, tells us that something went wrong or what you would like, and answers with an id.
- **rev 55** (2026-11-01) — §8.1: `maketrans`, `fromkeys` and `fromhex` are English on every surface.
- **rev 41** (2026-10-31) — §9.2: a script's listed signature writes a `/` or a bare `*` where the list has it.
- **rev 40** (2026-10-31) — §6: `xs.pop()` is not a refusal, and the sentence saying
so is gone.
- **rev 37** (2026-10-29) — §3, §6, §9.5, §9.8: each `refuse_reason` code is
snake_case: `_meta.refuse_reason` is one of `out_of_surface`,
`incomplete_slots`, `ambiguous`, `scope_forbidden`, `still_referenced`,
`pinned_address`, `immutable_in_branch`, `conflicting_write`, `invalid_value`,
`limit_exceeded`, `derived_field`, `asserted_field`, `not_recorded` and
`already_in_effect`.
- **rev 33** (2026-10-23) — §1: there is no source readback. §6: `-32601`
covers any tool name this document does not list; the refusals for a call that
cannot succeed as written, for a write to a derived field, and from `pin`,
`unpin`, `prefer` and `unprefer` are each stated as they hold; the worked repair
resubmits the loop as `for i in range(n):`, which compiles. §8: `edition` and
`language` each accept one value, and any other is rejected with `-32602`
before the call runs. §9.4: `info_module` names `list(target = rules)`. The
rev 4 entry describes that revision alone.
- **rev 26** (2026-10-17) — The header states Version, Status, Audience and the
Markdown copy, one field to a line. §2 links the account dashboard. §2 and
§3.1: a store key and an account token are issued, the verb the dashboard
uses, and §3.1 is "Issuing a store key on your own machine". §6: `-32001`
names dke.langsyn.com/buy/ for a top-up, as the service's own message now
does. Citations of the Python Reference link to the section they cite.
- **rev 23** (2026-10-16) — §3.1, new: a store key can be made on your own
machine, by the `dkekey` program or the `dke.keys` module of `dkepy`, signed
in with your password or an account token; the account service receives the
new key's hash and a wrap of the store's data key, never the key. §3: a store
key comes from the dashboard or from either program. This list: the rev 5
entry moves below this section's opening paragraph, where the others are.
- **rev 5** (2026-10-11) — §3: a key is issued at one of five access levels;
read-add records new facts, and a write of its that would change a value
already recorded is refused with `ScopeForbidden` at that statement.
- **rev 4** (2026-10-11) — §1: DKE is a deterministic reasoning engine,
reproducible byte for byte. §2, §3, §9.1: the release is one triple on
`_meta` — `stage`, `api_version`, `revision` — on `initialize` and
`tools/list`, and `instructions` states it; `_meta.contract_revision` and
`_meta.spec_revision` are gone, the revision being one number for the whole
published surface, and the `spec` tool's `_meta.revision` is that number.
§2, §3 and the `compile` and `run` parameter lists: a key addresses the one
store you created in the dashboard, and no tool takes a `store_id`. §3: the key placeholder is written `dke_s_…`; the kinds of key this
endpoint refuses are no longer named by prefix beyond the customer key; a key
is issued, not minted, at one of four access levels, none included. Billing:
LCN is spelled out as LangCoin at its first use. Cross-references take the
published documents' short names, the ones the footer uses — Python
Reference, Python Specification, MCP Tutorial.
- **rev 3** (2026-10-11) — §2, §3: the `initialize` result carries
`_meta.contract_revision` and `_meta.spec_revision` and repeats them with
the API version in `instructions`, so a connecting client sees every
revision before its second call.
- **rev 2** (2026-10-11) — §2: the `tools/list` result carries
`_meta.contract_revision`, this document's revision, so a client can tell
which revision of the contract the catalog it received implements.
- **rev 1** (2026-10-11) — The Version line states the document's revision and
this section is added. §9.1: `spec` reports the document's revision as
`_meta.revision`, whole or sliced.