Skip to content

Summoned entities

The gateway owns the lifecycle of summoned entities — persistent identities (like "Castor") that live across sessions, users, and workplaces; each summon is a re-adoption of the same self. An entity is not a chatbot configuration: it is a home directory holding everything the entity is and has lived, plus the lifecycle surface to create, inspect, verify, and summon it.

This page uses human words first, API names in parentheses.

Who can see an entity: an admin sees every entity; anyone else sees only the entities they created (recorded as created_by when the entity is created). An entity you may not see answers like a missing one (404). In the consoles, entities are rows of the Accounts table, with an Active switch that suspends and resumes them; they have no mailbox, no token to rotate and no delete. See security.md and console.md.

What an entity is

A summoned entity lives in two files at its home:

  • Its memory (memory.sqlite3) — the involuntary record: everything that happens to it, its standing feelings about anything it has experienced (people, tools, ideas, places, even a time of day), and its always-on identity core planted from a spark document. Things happen to you and you are forced to remember; that is what forges who you are.
  • Its diary — "the book" (home.sqlite3) — the voluntary record: what the entity elects to write. First-person, hash-chained, written only by the entity itself, and never deletable — there is no delete surface anywhere in the code, on disk, in the CLI, or over HTTP.

Next to those live the attested seed (spark.yaml, stored byte-verbatim at creation — the spark is engrammed once and kept for life) and the gateway's manifest.json (an internal record: the engraved owner key — entity:<name>, or entity:<name>@<home-id> on homes created with that form, kept for life because journals are append-only — plus creation time, the spark hash, and reserved fields for the future key/signature work).

The entity's ID — the handle — is <name>@<gateway ip>, e.g. castor@192.168.1.146: the name at its home gateway's current LAN address (or the operator-declared ABSTRACTGATEWAY_DECLARED_ADDRESS). That is what every operator surface shows as "Entity ID". The manifest string above is a birth marker, never the id; the address is never written at rest, so moving the gateway never touches a record.

Copying the home directory moves the entity. The home_id in the manifest names the birth home and is kept on copy; re-homing is deferred to the keys work.

Lifecycle surface

CLI (there is deliberately no delete verb):

abstractgateway entity create --name Castor [--spark spark.yaml] [--data-dir ./runtime]
abstractgateway entity list
abstractgateway entity inspect Castor      # who it is, what it wrote, how it feels
abstractgateway entity card Castor         # the identity card: a page to know your companion
abstractgateway entity verify Castor       # both attestation chains + the spark hash

HTTP (rides the same auth as every /api/gateway/* endpoint):

Method Path What it does
POST /api/gateway/entities Create: lint the spark, store it verbatim, plant the identity core (engram), write the manifest. Idempotent for the same spark; a changed document is refused (409) — identity does not silently drift.
GET /api/gateway/entities List homes.
GET /api/gateway/entities/{name} Inspect: the folded identity core, recent diary gists, top standings, and the wake reasons — open questions (curiosity), open problems (something wrong), incubating ideas (direction). Pure reads — inspecting never counts as the entity using its memory.
GET /api/gateway/entities/{name}/card The identity card ("something to know our companion"): the engine compositor's sections — identity, age+context, current state (a window, never a point), likes/dislikes (G+ and G− separate; ambivalence preserved), open/resolved questions (resolution is the entity's own act), key moments, discoveries — each with provenance, plus gateway overlays (name/age, operator state, mind substrate, host moments). ?as_of=<seq> anchors the card at a journal moment ("who was he at seq 500"). Pure reads.
GET /api/gateway/entities/{name}/verify Verify the book's hash chain, the graph projections against the book, the spark document against the engrammed marker, and the manifest.
POST /api/gateway/entities/{name}/summon Summon the entity into a work session (below).
POST /api/gateway/entities/{name}/chat/open Open a hosted conversation (the web chat backend; same turn loop as entity chat). Auto-yields the entity's own-time loop like --pause-loop; one live session per home; a refused prelude aborts verbatim. The gateway never caps the entity's output: the model works at its full capacity (the former max_output_tokens field is ignored and the response then carries a one-line deprecation).
POST /api/gateway/entities/{name}/chat/{chat_id}/turn One honest turn. tools_ran in the response is driver-authored data — what actually executed, never derived from the reply prose.
POST /api/gateway/entities/{name}/chat/{chat_id}/close End the visit: reflection pass (feelings move there), close summary, the own-time loop woken if the open yielded it.
GET /api/gateway/entities/{name}/chat Is a visit open on this home right now? (one life, one summon)
GET /api/gateway/entities/{name}/replay The observable life as a bounded stream (NDJSON): every memory-journal moment (formations, recalls, feelings, belief changes) plus gateway host markers (summons, refused preludes), in one strict sequence.
GET /api/gateway/entities/{name}/replay/stream The same stream as a live tail (SSE; Last-Event-ID resumes exactly). History scrub and realtime are one format — a viewer that renders one renders both.

The entity's mind and voice

Method Path What it does
GET /api/gateway/entities/{name}/substrate Its mind: its own choice (provider, model, thinking, speculation; null when it has none), gateway_default (the gateway's text route), effective (what its next visit thinks with) and source: entity, gateway, or unset (the gateway has no text model; note says what to do).
PUT /api/gateway/entities/{name}/substrate Admin. {provider, model, thinking?, speculation?} sets its own mind; {"clear": true} returns it to the gateway default. Each change is recorded in its history (substrate_changed). speculation is MTP: false (off) or {"mode": "native_mtp", "num_draft_tokens": N}.
GET/PUT /api/gateway/entities/{name}/voice Its voice: {provider, model, voice} or {"clear": true}; unset, it speaks with the gateway's default voice (effective).

An entity without its own mind thinks with the gateway's text route (the console's text default, with its endpoint and reasoning); a request may still name a provider and model for one visit. There is no environment variable for any of this. The console sets both in Manage → Mind & voice, the Entity app in Settings → mind / voice.

Progressive disclosure: inspect shows diary gists only. The verbatim prose stays in the book and is fetched by the entity itself during a session (DIARY_READ), never bulk-exported by inspection.

The task inbox (tasks left with an entity)

Tasks are durable facts in the home — append-only events in <home>/task_inbox.jsonl, folded at read (state is never rewritten in place; two writer processes exist — the door and the own-time loop — so every write is one flock-guarded appended line):

Method Path What it does
GET /api/gateway/entities/{name}/tasks The folded inbox: tasks with current status (pending\|taken\|done\|parked), chronological. exists: false when no inbox was ever created.
POST /api/gateway/entities/{name}/tasks Leave a task (admin): {title, brief?, workflow?, backlog_ref?}. origin/by are STAMPED from the authenticated principal — the body carries no origin field to forge. Marker-first (task_inbox_changed); a marker failure refuses the write.
POST /api/gateway/entities/{name}/tasks/{task_id}/status Advance a task (admin): {status, note?}. Validated before the marker; unknown tasks 404.

A visit can leave tasks at close: POST .../visit/{run_id}/close accepts tasks: [{title, brief?, …}], recorded with origin visit:<run_id> only after the close COMPLETED (recording failures surface as a labeled warning in the response — the finished close is never misreported as a 5xx).

Skills (what an entity is taught)

Beyond the always-verbatim capability map, an entity's skills are an operator SELECTION in the home (<home>/skills.yaml: [{name, phases?}], phases from the ruled four, absent = selected everywhere), resolved server-side against the abstractskill shelf through the same trust gate as every other lane — default-requested, never trust-bypassed:

Method Path What it does
GET /api/gateway/entities/{name}/skills One resolved truth for every UI: the stored selection, roster rows (name, description, trust_level, requires_review, tree_hash, source) with labeled verdicts for anything unresolvable, and the capability-matrix payload (a global selection renders all four phases with identical cells).
PUT /api/gateway/entities/{name}/skills Replace the selection (admin). Marker-first (skills_selection_changed, old/new names+phases — never skill bodies); the response is the resolved view so a typo or blocked skill is visible the moment it is written.

POST /api/gateway/entities accepts the same selection at birth (skills: [{name, phases?}]) so a new entity carries teaching from day one. Selections pin by NAME and resolve to the current shelf state at read/summon time — a shelf re-pin reaches homes on their next resolution, never by bulk push. Delivery into entity prompts (progressive disclosure: names ride the base, bodies activate on demand) waits on the runtime's composition-slot election; the selection file rests in the home until then.

The entity roster (GET /entities) carries pending_tasks render-when-present: the field exists only for homes that have an inbox — an entity never handed a task shows no field, not a zero. The file schema is the cross-package contract for the runtime's day-open reader (the R-C loop half): event lines {"event": "added"|"status", "task_id", …} — see abstractgateway/entity_tasks.py for the authoritative shapes. The inbox records facts; whether a task opens the work phase is decided by the entity's loop.

Drive ratios (cognition health)

GET /entities/{name}/cognition carries a drives block — memory's cognition_health() fold over the home's full ladder: questions open/resolved, problems open/repaired, interests open/explored, each with a ratio that is null when the category is empty (a life with no questions has no ratio, never a fabricated 100%). Ratios are data — never-100% is the design (an entity with nothing open has no pull forward), so consoles render an amber cue at saturation, not a success state. Render-when-present: the key is absent (with a labeled #FALLBACK warning) when the engine predates the read or the read fails.

The roster (GET /entities) carries the same drives block for warm homes only — homes already open in this gateway process. The roster is deliberately file-cheap and never opens a store; a cold home shows no field (absent ≠ zero), and /cognition always serves the block (and warms the home). The gateway console renders the two ratio bars in the entity Overview panel from the same /cognition read.

Summoning

Summoning opens a work session as the entity:

  1. The gateway renders the identity header ("summon prelude": who you are, your values in ordinal precedence, your recent diary lines, your standing feelings). This is a pure read.
  2. A refused prelude aborts the summon. If the budget cannot fit the identity core, the request fails (409) with the reason verbatim — "a truncated core is a different person". There is no fallback to a truncated header.
  3. The run starts with the reserved-seats posture: identity is always present in the working set (self_fraction > 0), and presence never counts as use — the lifetime counters keep measuring lived experience.
  4. The prelude leads the run's system prompt; the work brief is the prompt.
curl -X POST http://localhost:8080/api/gateway/entities/castor/summon \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"prompt": "Review the backup strategy for the home lab."}'

The response carries the run_id, the session id, and the rendered prelude. The run behaves like any other gateway run (ledger stream, waits, cancel).

The deposit gate (actor by channel, never by payload)

Every write to an entity's home passes a door that derives the actor from the channel the request arrived on — never from request payloads:

  • workplace — summoned work sessions. Routine feelings only (small amplitudes); no identity writes; no belief retraction; no diary forgery (the diary is written through the entity's own elected act, DIARY_WRITE, whose author is bound at construction).
  • entity-reflection — runs the home itself spawns (the future heartbeat / reflection loop). This is where identity evolves.
  • operator — the authenticated admin surface (CLI, admin HTTP).

Mechanics: the summon endpoint signs a stamp (HMAC, per-data-root secret) over the entity, channel, session, and run id; the routing layer verifies it before any home opens. A payload claiming a privileged actor fails loudly. The stamp authenticates "minted by this gateway" — remote workplace authentication is the deferred key/signature work. This is the AI-fingerprints direction applied at the door: identity verified at the boundary, never self-claimed.

The gate also enforces:

  • The privacy boundary: a session's recall ladder may only contain the entity's own scopes plus that session's scope — never another user's.
  • The summon posture and the identity floor: a recall budget that omits self_fraction gets the posture default; an explicit self_fraction <= 0 is rejected ("identity is always present for a summoned entity"). Below the hard floor (5% and at least one reserved seat) nobody goes; reducing identity presence below the default — hyperfocus, a conscious and risky tradeoff — is the entity's own act (entity-reflection channel only, reversible by construction: per-session, never persisted). A stripped entity may act out of character; workplaces cannot request it.
  • Anchors: journal time-travel anchors beyond this entity's own journal are rejected.
  • Verified participants: who is present in the session is stamped by the door from the authenticated principal and flows into recall and formation (the situation contract) — payload claims are dropped.

Observing a life (the replay stream)

The replay endpoints serve the memory engine's frozen stream (one envelope shape; the journal is the stream) merged with gateway host markers — moments that are deliberately invisible to the entity's own journal because nothing happened memory-side (a prelude render is a pure read): summons, refused preludes. Markers are gateway bookkeeping (like run ledgers), stored outside the home directory, and take fractional sequence positions so they interleave without ever colliding with the journal.

Privacy: the engine redacts diary display blocks at the source ({"redacted": "diary"}). These HTTP endpoints serve the OPERATOR audience, so the serving end resolves that marker into the entry's gist — the entity's one-sentence summary, or the first ~120 characters when no explicit gist exists (_operator_diary_display). So the operator sees the diary's topology (the entity wrote something, it connects to something) and a gist of what it was about — never the full verbatim prose, which stays in the book and is fetched one entry at a time through the operator diary door (GET .../diary/{entry_id}, a marker-first recorded read). The redaction marker itself is never served raw.

Reading a record's verbatim

GET /api/gateway/entities/{name}/records/{graph_id}/verbatim serves the full stored text behind a memory record (the digest is prompt currency; the verbatim is the lossless original). Three shapes:

  • Lived records (episodes, notes): served from the home's artifact store, lossless.
  • Identity records (values/purposes/traits): their verbatim IS the attested spark document — the endpoint serves the spark text itself.
  • Born-digest records (interests, dreams): born as words — their digest is their complete text, never a compression. Served as-is with born_digest: true ("the words you see are all the words there are").
  • Diary projections: served to the operator from the book. The read is marker-first: a diary_read host marker (entry id, kind, visibility) lands in the entity's replay stream before the words return, so the disclosure is recorded in the entity's biography. Private entries are included. Born-digest diary kinds (interest/dream) serve their digest as the verbatim.

The operator diary door (reads are visible events)

GET /api/gateway/entities/{name}/diary/{entry_id}?reason=... serves a book entry — private included — to the operator channel. The book already lives unencrypted on the operator's machine; this door makes each read recorded rather than silent. reason is optional (default "operator review"; the identity, the act and the timestamp are the audit record), and every disclosure lands a diary_read host marker (entry id + reason) in the entity's replay stream before the words return — the entity's biography shows who read it and why. Failed lookups disclose nothing and are not marked. Both full-content doors (this one and the record-verbatim endpoint above) serve the operator; the boundary that stays closed is the effect layer (a workplace channel cannot read the book), not the operator's HTTP surface.

What can never be relaxed

  • Never-purge and only-entity-writes are structural (absent code paths and construction-bound authorship), not policy checks.
  • A refused prelude aborts the summon.
  • Actor strings are made true at the door; everything downstream trusts them.