ECHO is the data model that applications and agents share. Plugins code against its objects instead of keeping private
stores. An agent keeps its memory in the same graph, and can inspect the schema of everything in it. Data from external
systems lands there as linked objects, so one query can reach all of it.
A conversation ends and its context goes with it. An agent that works across sessions needs somewhere durable to put what
it learned, and that memory is only useful if it connects to the work it is about.
In ECHO a memory is an ordinary object. It has a type, it can reference the issue it is about, and it comes back through the
same query an application uses. The org.dxos.skill.memory skill stores an agent’s memories this way.
Returns a Type.Type entity — a live, in-memory TypeSchema instance,
NOT a Schema.Schema. Use Type.InstanceType<typeof Foo> for the instance
type and Type.getSchema(Foo) to obtain the underlying Effect Schema.
The entity's id defaults to EntityId.deterministic(typename, version) so
constructing a type never reaches crypto.getRandomValues() — required for
Cloudflare workerd, which forbids RNG calls in global (module-evaluation)
scope. Pass { id } to override (e.g. with EntityId.random() from a
request handler).
Giving the type a nominal class name keeps its inferred type portable for
declaration emit: structural expansion (which can pull in unnameable union
members from referenced schemas) is replaced by a reference to the class.
The second call takes either a schema, or struct fields (wrapped in Schema.Struct) with an optional function
that pipes annotations onto the struct.
Defines a struct schema from a map of field schemas.
Details
Each field value is a schema. Use
optionalKey
or
optional
to
mark fields as optional, and
mutableKey
to mark them as mutable.
The resulting schema's Type is a readonly object type with the fields'
decoded types. The Encoded form mirrors the field schemas' encoded types.
Declared fields may be inherited and are copied to own properties in the
output. The __proto__ field is accepted only when it is an own property.
Parsing does not guarantee that output keys retain their input order.
@performance ― Async; executes the full query (index plus working set) on every call, not cached; O(n) in the data
the executor examines, not in the result count.
run();
13
Because memory is not a separate store, it replicates with the rest of the space, and any code that can query the space can
read it.
An agent has no compiled types. It works from what it can read at run time, so the schema has to be data, not only a
TypeScript declaration.
Every ECHO type reports its name, version and metadata at run time, and converts to JSON Schema, the format tool parameters
already use. org.dxos.echo#F-2.3 The schema goes the other way too: a type built from JSON Schema and
added to a database replicates like any object, so an agent can define a new type while it runs.
Persisted Type.Type entities carry typename in EntityMeta.key (the
canonical registry-provenance field); unnamed drafts fall back to the
entity's object id so the helper always returns a string. Any dxn: or
echo:/ prefix is stripped — typename is a bare identifier, not a URI.
@performance ― O(1) meta read with jsonSchema and id fallbacks.
getTypename(
classMemory
Memory);
2
const
const version: string
version =
import Type
Type.
constgetVersion: (input: Type.AnyEntity) =>string
Gets the version.
@example0.1.0
@example
0.1.0- (in-database, versioned by automerge heads)
The registry-provenance semver lives in EntityMeta.version; unversioned
drafts default to DRAFT_VERSION ('0.0.0'). In-database entities are
additionally versioned by their automerge heads, which are exposed as the
semver pre-release tag (<semver>-<heads>). In-memory declarations have no
heads and surface the bare semver. Read the registry semver alone via
Type.getMeta(input).version.
@performance ― O(h log h) in Automerge heads for database types (sorted into the suffix per call); O(1) for in-memory
types.
Construct a new object-kind type entity from raw metadata — for cases where
an Effect Schema isn't available (e.g. JSON-Schema arriving over the network
or from a UI editor). Parallel to
makeObject
but takes pre-built
jsonSchema instead of piping through an Effect schema.
The returned entity is in-memory; persist it with db.addType(entity).
@performance ― O(schema size); validates the JSON schema into a new in-memory type entity, with no database I/O.
Convert effect schema to JSON Schema.
NOTE: This handles custom annotations.
@param ― schema
toJsonSchema(
import Schema
Schema.
functionStruct<{
readonly name: Schema.String;
}>(fields: {
readonly name: Schema.String;
}): Schema.Struct<{
readonly name: Schema.String;
}>
Defines a struct schema from a map of field schemas.
Details
Each field value is a schema. Use
optionalKey
or
optional
to
mark fields as optional, and
mutableKey
to mark them as mutable.
The resulting schema's Type is a readonly object type with the fields'
decoded types. The Encoded form mirrors the field schemas' encoded types.
Declared fields may be inherited and are copied to own properties in the
output. The __proto__ field is accepted only when it is an own property.
Parsing does not guarantee that output keys retain their input order.
Persists a Type definition (clones/forks the entity) so it replicates to other peers.
Runs a conflict query first: if a type with the same typename + version already exists in
this space, the existing persisted entity is returned and no duplicate is created. This is
the only supported way to add Type entities —
add
rejects them.
@performance ― Async; O(t) in the space persisted types, which it queries in full for a duplicate before writing.
addType(
const Contact: Type.Type<TypeSchema>
Contact);
11
The database skill in plugin-space gives agents this through its QueryTypes and AddType operations. An agent can look
up what a space holds before it writes, instead of guessing at field names.
Mail, issues and calendars live in systems that do not know about each other. Syncing them into ECHO makes each record an
object, with a foreign key that records where it came from. A Ref can then link a GitHub issue to a Linear task, or to the
note that discusses both.
The foreign key is what makes a repeat sync safe. Look the record up by its key, then update the object you found or add a
new one, and the record does not end up in the graph twice.
@performance ― Async; executes the full query (index plus working set) on every call, not cached; O(n) in the data
the executor examines, not in the result count.
Perform mutations on an echo object within a controlled context.
All mutations within the callback are batched and trigger a single notification
when the callback completes. Direct mutations outside of Obj.update will throw
an error for echo objects.
This function also works with nested objects within echo objects (e.g., Template structs)
that are reactive at runtime.
@param ― obj - The echo object to mutate. Use Relation.update for relations.
@param ― callback - The callback that performs mutations on the object.
@example
const person = Obj.make(Person, { name: 'John', age: 25 });
A plugin declares its types with Type.makeObject and contributes them to the schema registry. Its UI, its operations and
the agents that use it all read and write the same objects. There is no plugin-specific API in between, so a second plugin
can select the first plugin’s objects by type.
The rest of this page is the argument for the model itself. Keeping state on a server means client code spends its time on
copies: caches, optimistic updates, retry queues and conflict handling. ECHO moves the state onto the device. Objects are
CRDT documents in a local database, so a write is finished when the call returns, and peers merge their changes when they
connect.
Returns a Type.Type entity — a live, in-memory TypeSchema instance,
NOT a Schema.Schema. Use Type.InstanceType<typeof Foo> for the instance
type and Type.getSchema(Foo) to obtain the underlying Effect Schema.
The entity's id defaults to EntityId.deterministic(typename, version) so
constructing a type never reaches crypto.getRandomValues() — required for
Cloudflare workerd, which forbids RNG calls in global (module-evaluation)
scope. Pass { id } to override (e.g. with EntityId.random() from a
request handler).
Giving the type a nominal class name keeps its inferred type portable for
declaration emit: structural expansion (which can pull in unnameable union
members from referenced schemas) is replaced by a reference to the class.
The second call takes either a schema, or struct fields (wrapped in Schema.Struct) with an optional function
that pipes annotations onto the struct.
Defines a struct schema from a map of field schemas.
Details
Each field value is a schema. Use
optionalKey
or
optional
to
mark fields as optional, and
mutableKey
to mark them as mutable.
The resulting schema's Type is a readonly object type with the fields'
decoded types. The Encoded form mirrors the field schemas' encoded types.
Declared fields may be inherited and are copied to own properties in the
output. The __proto__ field is accepted only when it is an own property.
Parsing does not guarantee that output keys retain their input order.
Schema for string values. Validates that the input is typeof"string".
@stability ― stable
@category ― models
@since ― 4.0.0
@stability ― stable
@category ― schemas
@since ― 4.0.0
String }),
3
) {}
4
5
class
classTask
Taskextends
import Type
Type.
constmakeObject: <Task>(dxn: DXN.DXN, options?: {
id?: EntityId;
}) =>Type.MakeObject<Task>
Factory function to create an ECHO object type.
Returns a Type.Type entity — a live, in-memory TypeSchema instance,
NOT a Schema.Schema. Use Type.InstanceType<typeof Foo> for the instance
type and Type.getSchema(Foo) to obtain the underlying Effect Schema.
The entity's id defaults to EntityId.deterministic(typename, version) so
constructing a type never reaches crypto.getRandomValues() — required for
Cloudflare workerd, which forbids RNG calls in global (module-evaluation)
scope. Pass { id } to override (e.g. with EntityId.random() from a
request handler).
Giving the type a nominal class name keeps its inferred type portable for
declaration emit: structural expansion (which can pull in unnameable union
members from referenced schemas) is replaced by a reference to the class.
The second call takes either a schema, or struct fields (wrapped in Schema.Struct) with an optional function
that pipes annotations onto the struct.
Defines a struct schema from a map of field schemas.
Details
Each field value is a schema. Use
optionalKey
or
optional
to
mark fields as optional, and
mutableKey
to mark them as mutable.
The resulting schema's Type is a readonly object type with the fields'
decoded types. The Encoded form mirrors the field schemas' encoded types.
Declared fields may be inherited and are copied to own properties in the
output. The __proto__ field is accepted only when it is an own property.
Parsing does not guarantee that output keys retain their input order.
Perform mutations on an echo object within a controlled context.
All mutations within the callback are batched and trigger a single notification
when the callback completes. Direct mutations outside of Obj.update will throw
an error for echo objects.
This function also works with nested objects within echo objects (e.g., Template structs)
that are reactive at runtime.
@param ― obj - The echo object to mutate. Use Relation.update for relations.
@param ― callback - The callback that performs mutations on the object.
@example
const person = Obj.make(Person, { name: 'John', age: 25 });
@performance ― Async; executes the full query (index plus working set) on every call, not cached; O(n) in the data
the executor examines, not in the result count.
run();
9
Nothing here waits on a network. db.add and Obj.update apply locally and return, and finished already contains the
task. org.dxos.echo.query#F-2
Here is one task, marking an item as done, written against a REST API and against ECHO. The REST version is not careless. It
is the minimum that handles a slow network, an offline user and a concurrent edit, and it still has two open questions in
its comments.
Returns a specified element from the Map object. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map.
@returns ― Returns the element associated with the specified key. If no element is associated with the specified key, undefined is returned.
Perform mutations on an echo object within a controlled context.
All mutations within the callback are batched and trigger a single notification
when the callback completes. Direct mutations outside of Obj.update will throw
an error for echo objects.
This function also works with nested objects within echo objects (e.g., Template structs)
that are reactive at runtime.
@param ― obj - The echo object to mutate. Use Relation.update for relations.
@param ― callback - The callback that performs mutations on the object.
@example
const person = Obj.make(Person, { name: 'John', age: 25 });
Note: Only accepts objects. Use Relation.update for relations.
@performance ― Synchronous; costs the mutations made in the callback plus one batched notification and an O(owning
fields) parent pass.
update(
const task: Task
task, (
task: {
done: boolean;
id: EntityId;
"~@dxos/echo/Kind": EntityKind.Object;
}
task) => {
task: {
done: boolean;
id: EntityId;
"~@dxos/echo/Kind": EntityKind.Object;
}
task.
done: boolean
done = true;
});
}
};
The ECHO version has no error branches for these cases because the failures do not reach it. There is no request to fail,
the local write cannot conflict with itself, and a concurrent edit from a peer merges as a CRDT change.
Each query reads one local engine, which answers from the objects this tab holds. A subscriber sees your own write in the
same call that made it, so a list does not need an optimistic copy of itself. org.dxos.echo.query#F-2
Queries also read the host index and EDGE, which hold objects this tab has not loaded. ECHO treats those answers as evidence.
It re-evaluates each remote entry against the local document and drops the entry when local state contradicts it, so a slow
server cannot bring back an object you deleted. org.dxos.echo.query#F-5
Every type carries a version in its DXN. An object migration declares how objects of an old version become the new one, so
data written by last year’s build still opens. org.dxos.echo#Migration
A Ref field stores a DXN and resolves lazily, across objects and across spaces. Relations are objects too, with typed
source and target endpoints, so you can query from either end. org.dxos.echo#F-4
Once persistence and merging are the database’s job, the code that remains is your application. Start with the
learning path, or look up a module in the reference.