Reference
What you import and what you declare, exactly. The entry points, the primitives a plugin is made of, who calls what for whom and where, the events the log keeps, schedules, and a page for every plugin.
On this page 23
- Install
- Entry points
- Product packages
- Who, for whom, where
- Who may call:
allow - A plugin
- Collections
- Actions
- Processors
- Events a plugin declares
- Views
- Resources and adapters
- Routes and
who - Rules:
guardandlabels - Workers
- Startup checks
- Contexts
- The runtime
- Events
- Schedules
- Models and providers
- The web's look and words
- Plugins
This page lists what Copilotz gives you to import, and what a plugin may declare. It says what each piece is, not why it's built that way: the architecture says how it runs. Every plugin Copilotz ships has a page of its own, read from the plugin itself.
Install
npm install copilotz # Node 24+ (npm, pnpm, yarn)
bun add copilotz # Bun
deno add jsr:@copilotz/copilotz # Deno (or `deno add npm:copilotz`)On Deno, map the name in deno.json ("copilotz": "jsr:@copilotz/copilotz@^1.0.0-rc.34"), and the imports read the same everywhere. 3 pieces need a package of their own, installed beside it: esbuild to bundle the page's code on Node (bundle), @electric-sql/pglite for PostgreSQL in the process, and @types/react for the front ends' types. A product's plugins are packages of their own too, at Copilotz's version (npm install @copilotz/work, deno add jsr:@copilotz/work): see Product packages.
Entry points
The same names on npm (copilotz/…) and JSR (@copilotz/copilotz/…).
| Import | What it has |
|---|---|
copilotz | the runtime: createRuntime, definePlugin, defineAction, the access helpers (people, inPerson, grant…), connect (the client)… |
copilotz/stores/memory | memoryStore(): in memory, the reference every store is held to |
copilotz/stores/sqlite | sqliteStore({ path }): one file, on node:sqlite or bun:sqlite |
copilotz/stores/postgres | postgresStore({ url, schema, tenant }) (a server, or PGlite in the process), postgresPool, copyTenant, exportTenant, dropTenant |
copilotz/stores/latency | withLatency(store, ms): a store with round trips, to measure |
copilotz/blobs/files | fileBlobs({ dir }): large values as files |
copilotz/blobs/http | httpBlobs({ url, headers }): large values in a bucket (S3, GCS, R2…) |
copilotz/blobs/gcs | gcsBlobs({ bucket, prefix }): large values in Google Cloud Storage, as the instance's service account |
copilotz/plugins/models | models: language models, embeddings, transcription and images, with the models.dev catalog |
copilotz/plugins/agents | chat and its tool, history, memory, memoryExtraction, skills, routines, openapi, webTools |
copilotz/plugins/agents/web | their web front ends: chatWeb, memoryWeb, skillsWeb, routinesWeb |
copilotz/plugins/agents/terminal | their terminal front ends: chatTerminal, memoryTerminal |
copilotz/plugins/access | access (spaces, identity, contacts), googleSignIn, emailSignIn, email, devSignIn, accounts |
copilotz/plugins/access/web | spacesWeb, contactsWeb, accountsWeb |
copilotz/plugins/access/terminal | accessTerminal |
copilotz/plugins/billing | usage: the ledger, and the port an app's pricing plugs into (spending) |
copilotz/plugins/billing/web | usageWeb |
copilotz/plugins/billing/terminal | usageTerminal |
copilotz/plugins/compute | computers (people's machines, where agents work), cloud, apps, and their hosts |
copilotz/plugins/compute/web | computersWeb, cloudWeb, appsWeb |
copilotz/plugins/compute/terminal | computersTerminal |
copilotz/plugins/compute/downloads | computerDownloads: the computer program's release, served by the app (Node) |
copilotz/plugins/interfaces | terminalPlugin (the terminal's data), api, whatsapp, voice, mcp, webLines (site chats), maintenance |
copilotz/plugins/interfaces/web | webShell (the web's root View), apiWeb, whatsappWeb |
copilotz/plugins/interfaces/terminal | terminalShell: the terminal's root View |
copilotz/plugins/<theme>/contract | each theme's plugins' contracts (access, agents, models, interfaces, compute): shapes, and the little code with them, for plugins built on them |
copilotz/web | the web host in the browser (web), its toolkit (Button, Markdown…), what a widget asks (display, surface, remind) |
copilotz/web/words | the words alone, without the toolkit: defineWords, wordsOf, formatDate… |
copilotz/web/server | the page's server half: webPage (the page, as a plugin), widgetsOf |
copilotz/web/widget | the widget host: an app's web Views inside an outside assistant (MCP Apps, ChatGPT) |
copilotz/web/theme | themeOf(plugins): the app's theme, for the page's first paint |
copilotz/web/node | listen (serves runtime.fetch on Node and Bun, sockets included), bundle (the browser's code, with esbuild) |
copilotz/terminal | the terminal host, terminal({ runtime, me }), and its toolkit |
copilotz/exporters | otlp, exposition: metrics out, the OpenTelemetry way |
copilotz/testing | one test API for Deno, Node and Bun: test, tempDir, serve, spawn, testWho, ticketFor, openTo… |
copilotz/testing/words | testing a front end's words (pseudoWords, englishLeft), and listing them (wordsList) |
copilotz/describe | describe(plugin): what a plugin declares, as JSON |
The browser loads only copilotz/web, the front ends under …/web, and what they import. Every other entry point is for the process that runs the runtime, or the terminal.
Product packages
What a product does its own way, rather than what every app needs, is a package of its own, released with Copilotz at its version: @copilotz/work@1.0.0-rc.34 goes with copilotz@1.0.0-rc.34. Each has the same entry points, its plugins (@copilotz/work), their front ends (@copilotz/work/web) and their shapes, for plugins built on them (@copilotz/work/contract), with the same names on npm and JSR.
| Package | What it has |
|---|---|
@copilotz/work | boards (kanban) and documents (shared Markdown with versions and merges); boardsWeb, documentsWeb |
@copilotz/directory | directory: many orgs behind one address; directoryWeb, the org switcher, the org's page, staff's console |
@copilotz/billing | credits (plans, credits, budgets), stripe, stripeWebhook; creditsWeb |
@copilotz/team | team (agents people make), tiers (the models an org's agents think with); teamWeb, tiersWeb |
They import Copilotz by its entry points alone, as an app's own plugins would. Their collections, Actions, events, Views and resources keep the names they had inside Copilotz, so an app's data stays as it is.
Who, for whom, where
Every call runs as an actor. The actor says who acts, for whom an agent works, and where the work is. The runtime carries it into everything the call causes: nested calls, tools, the Processors its events wake, its timers, and the Views observed for it. Every event records it.
{ kind: "agent", id: "north", for: { kind: "person", id: "ana" }, in: "launch" }| Field | What it says |
|---|---|
kind, id | who acts |
for | whom an agent works for: a person or a contact |
in | where the work is: an opaque id, as id is. To the access plugin it's a space. None: nowhere in particular |
via | whose work trusted code was doing when it acted as someone else (ctx.as) |
key, scope | a person acting through a developer key, and what the key may use |
origin | what set a person's work going, such as a routine |
The kinds the runtime makes or checks:
| Kind | Who | Made by |
|---|---|---|
person | someone with an account, signed in or through their key | identity; a key's by api |
agent | an agent, working for someone (for) | chat, for each turn |
contact | someone outside the org: a WhatsApp customer, a caller, an MCP chat | contacts |
system | trusted code, which no rule restricts; a named grant is one too | the runtime: no actor, ctx.as(), grant |
maintenance | the maintenance key, for support's SQL | the maintenance plugin's routes |
anonymous | an HTTP request nobody is signed in to (id: "") | runtime.fetch, when no who answers |
Each way in says where its work is:
| Way in | Who, for whom | Where (in) |
|---|---|---|
| The web | the person | the page's space: a call says it |
| A chat's turn | its agent, for whoever started it | the chat's space |
| the customer (a contact), or a linked person | the number's space, or the space they're in | |
| Voice | the caller (a contact), and its agent for it | the line's space |
| A web line, an MCP line | the visitor or conversation (a contact) | the line's space; a tool, where its chat is |
| A developer key | its person, through it | the key's space |
| The terminal | its person | where they work (/space, /work) |
| A routine | whoever made it, set going by it (origin) | the routine's space |
| Handing work to a space | the same person | the new space |
A host calling for someone says where too: runtime.call(action, input, { actor: { ...ana, in } }), or a client's call(action, input, { in }). What a page says is recorded, never proved, and a person's own reach isn't narrowed by it. An agent's reach is: its person's reach, within the spaces the agent is a member of, within in and what in draws on. In the person's own Personal space, their agents reach all they do, and what they write elsewhere for them there is theirs (no labels from there). Tools that take a space (in) work there: check it with workIn from the agents' contract. The access plugin keeps that rule (design).
From copilotz:
personOf(actor): the person whose work this is, the one acting or the one found alongfor, thenvia. Read it instead ofctx.actor.id, which for an agent is the agent's.principalOf(actor): the person or contact the work is for.whereOf(actor): itsin, if it has one.onBehalf(actor): whether a person's work is done for them rather than by them: through a key, set going by a routine, or by code acting as them for an agent.
Who may call: allow
An Action's allow(actor, input) says who may call it. The runtime asks it wherever an actor calls.
| Where the call comes from | allow asked | Without an allow |
|---|---|---|
outside: runtime.call with an actor, the REST API, the socket, a View's handler | yes | refused |
code acting for an actor: ctx.call in an Action, a Processor or a tool | yes | runs: the Action is internal |
| a timer an actor set | yes | runs |
a read calling an unrecorded Action (ReadContext.call) | yes | runs |
| the system, a grant, or code with no actor | no | runs |
For an agent, it's asked twice: of the agent, then of whom it works for (allows). So a tool never calls what its agent's person couldn't. A key also calls only what its scope lists, from outside (inScope). Which documents a call then reaches is the rules' business, not allow's.
The helpers say what they mean for an agent:
| Helper | Lets in |
|---|---|
people | a person, their key, their routines, and an agent working for a person |
inPerson | the person themselves: never a key, an agent, a routine's run, or code acting for them |
contacts | a contact, and an agent working for one |
peopleOrContacts | both |
Say inPerson for what only the person decides: consent, keys, who's in a space, money, publishing. Say people for the rest. Copilotz's own Actions do the same, and each plugin's page shows who may call each of its Actions. chat.send and chat.attach have a rule of their own: people and contacts writing as themselves, never an agent.
import {
type Actor,
createRuntime,
defineAction,
definePlugin,
inPerson,
people,
personOf,
} from "copilotz";
import { memoryStore } from "copilotz/stores/memory";
const notes = definePlugin({
name: "notes",
collections: { note: { schema: { type: "object", properties: { text: { type: "string" } } } } },
actions: {
"note.add": defineAction({
input: { type: "object", properties: { text: { type: "string" } }, required: ["text"] },
allow: people, // a person, their key, or an agent working for one
run: (ctx, { text }) =>
ctx.db.collection("note").insertOne({ text, by: personOf(ctx.actor) }),
}),
"note.clear": defineAction({
input: { type: "object" },
allow: inPerson, // the person themselves, never on their behalf
run: (ctx) => ctx.db.collection("note").deleteMany({}),
}),
},
});
const runtime = await createRuntime({ store: memoryStore(), plugins: [notes] });
const ana: Actor = { kind: "person", id: "ana", in: "launch" };
const north: Actor = { kind: "agent", id: "north", for: ana, in: "launch" };
for (const [who, actor] of [["ana", ana], ["north for ana", north]] as const) {
for (const action of ["note.add", "note.clear"]) {
const outcome = await runtime.call(action, { text: "Call Bruno" }, { actor })
.then(() => "ok", (error) => error.code);
console.log(`${who}, ${action}: ${outcome}`);
}
}
await runtime.close();It prints:
ana, note.add: ok
ana, note.clear: ok
north for ana, note.add: ok
north for ana, note.clear: forbiddenGoing beyond is a named grant. Trusted code that must do what its actor couldn't acts as the system, for one reason: ctx.as(grant("availability")). What it does is recorded as the grant, via whose work it was and in where. The owner counts its ops in copilotz.elevated.ops, by grant. A model never holds one: your code decides what leaves it. Copilotz's own are chat.hand-over, chat.hand-back and web.budget.
run: async (input, ctx) => await ctx.as(grant("availability")).call("slots.all", { in: ctx.in }),Queries and events have an allow too, asked only from outside. A query declared as a bare function is the system's. No actor emits an event without allow from outside, and a key emits none.
A plugin
A plugin is a plain object. definePlugin only types it.
| Field | What it declares |
|---|---|
name | its name |
requires | the plugins it builds on, by name: the runtime doesn't start without them |
collections | its collections, and indexes it adds to another's ({ usage: { indexes: [["tags.app"]] } }) |
actions | what can be done |
events | the events it emits, beyond its collections' changes and its Actions' lifecycle |
processors | what happens when events happen |
views | how its data shows, by name; null removes an earlier plugin's View |
resources | configuration, by kind and name |
adapters | implementations of a port, by port and name |
schedules | Actions time calls (see Schedules) |
check | a startup check of the merged configuration it understands |
guard | the rules: who reaches which documents |
labels | what reads reveal, carried to what the same job writes |
workers | kinds of worker: programs that do its work but aren't plugin code (a computer) |
routes | its HTTP, by URLPattern pathname |
who | who a request is, by a credential it knows |
Plugins merge in order. Collections, Actions, events, Processors and kinds of worker are unique: a second of one name stops the runtime from starting. Resources and schedules patch: a later plugin's entry merges into an earlier one's (JSON Merge Patch: objects merge, null removes, the rest replaces). A later View, adapter or route replaces an earlier one of its name. Every plugin's guard restricts together, the first who that answers wins, and the last labels is the one.
Collections
Documents with a JSON Schema, read and written with MongoDB's collection API. defineCollection types its documents and queries from the schema.
| Field | What it does |
|---|---|
schema | the JSON Schema every write is checked against |
queries | named reads: a function, or { allow, run } to let actors run it from outside |
vectors | fields holding embeddings, kept as the store's vectors and searched with nearest |
indexes | lists of fields ([["at"], ["tags.tenant", "at"]]), each a scalar its schema types |
private | fields only trusted code reads: an actor's queries and Views get the documents without them |
records | true: a ledger, appended to and never changed, whose documents are their own record |
durable | false: kept in the owner's memory, never in the store, with no events |
events | false: its writes record no events |
ctx.db.collection(name) has insertOne, insertMany, find (a cursor), findOne, countDocuments, updateOne, updateMany, replaceOne, deleteOne, deleteMany, aggregate, nearest(vector, { filter, limit }) and query(name, params).
- Filters:
$eq,$ne,$gt,$gte,$lt,$lte,$in,$nin,$exists,$elemMatch,$and,$or,$nor,$not. - Updates:
$set,$unset,$inc,$push(with$each),$addToSet,$pull;upsert. - Finds:
sort,limit,skip, andprojection({ name: 1 }keeps fields,{ data: 0 }leaves them out), applied before any large value is loaded. - Aggregates:
$match,$group(with$sum,$count,$avg,$min,$max),$sort,$skip,$limit, run in the store. - Unanswered writes:
{ writeConcern: { w: 0 } }sends a write with the next op or the commit, so it costs no round trip of its own.
Plugin code runs any named query, as whoever it acts for, under the rules. From outside, runtime.query("message.history", params, { actor }) runs only a query whose allow lets them in.
Actions
What can be done, by name. defineAction types run's input from the schema.
| Field | What it does |
|---|---|
input | the JSON Schema a call's input must match |
allow | who may call it (see Who may call); none: it's internal |
run | (ctx, input) => result: what it does |
description | what it does, for people and for tools that list Actions |
record | false: unrecorded. It runs inline in its caller's job, with no events and no commit of its own |
private | fields its calls are recorded without, as dot paths, in its input and its result |
Every recorded call is on the record (action.started, then action.completed or action.failed), retryable by id, and everything it causes is its caller's, where its caller is. Queries, Views and startup checks may call unrecorded Actions, read-only.
Processors
What happens when events happen: ordered per key, at least once, from a durable cursor.
| Field | What it does |
|---|---|
on | the event types it handles |
where | a MongoDB filter on the event's data: the owner never dispatches the events it skips |
key | (event) => key: events of one key run in order, and different keys run at once |
run | (ctx, event) => …, as the event's actor, where the event was |
Events a plugin declares
events: { "booking.confirmed": { schema, description?, allow? } }. Code emits one with ctx.emit(type, data), in the job's commit. Emitting a type nobody declared fails, so every event is in some plugin's contract. allow says who may emit it from outside (runtime.emit, a View's handler). The runtime's own events (<collection>.created, action.…, processor.…) can't be declared.
Views
How data shows in one place, found by name: (item, ctx) => what it shows. Text for a model, Ink elements in the terminal, React in the browser. A name says what it shows (booking, tool:book, context:today), and a place prefixes it where the same thing shows in several (llm.message, web.message, terminal.message). The shells ask for their own default when a name has no View (web.default, terminal.default).
- Reads inside a View return at once, as
{ status: "loading" }or{ status: "ready", value }. The runtime runs the View again until a pass reads nothing new. ctx.show(name, item, hints?)shows exactly the View it names. Observed, the shown View is a part that runs again on its own.ctx.callandctx.emitact from a handler, as the actor looking.runtime.render(name, item, { viewer?, hints? })renders once, as the system. Code renders withctx.render, as its actor: a turn renders its agent's prompt, thellm.threadView, that way.runtime.observe(name, item, { actor?, viewer?, hints?, part? })keeps one live: it runs again when a commit changes a document it read. Itsreadyis the first output, andclose()stops it.
How Views run is in the architecture.
Resources and adapters
- Resources are configuration as named plain objects, by kind:
agents,tools,provider,shared,theme. The runtime gives them no meaning. A plugin reads the kinds it understands, and a later plugin patches an earlier one's entries, so an app changes one agent'seffortwithout redefining it. - Adapters are implementations of a port, by port and name:
llm.openai,embeddings.hash,email.resend. A later plugin's adapter of a name replaces an earlier one's.
Routes and who
runtime.fetch(request) answers HTTP: its own paths (POST /connect, the socket at /oxian, the REST API at /api/) and plugins' routes. Hosts only listen: Deno.serve(runtime.fetch), or listen(runtime.fetch) from copilotz/web/node.
- A route is
(request, ctx) => Response, keyed by aURLPatternpathname (/hooks/whatsapp/:name). Itsctxhasruntime(trusted code),actor(who the request is),paramsandcookies. Routes deal with HTTP, and Actions do the work. A thrownfailure's code answers as its status: 400, 401, 403, 404, 429, else 500. whois(request, ctx) => actor, from a credential its plugin knows (a session's cookie, a key inAuthorization). The runtime asks each plugin's in order, and the first that answers wins. A request nobody'swhoanswers is{ kind: "anonymous", id: "" }.- Cookies are set and read by plain name through
ctx.cookies, and the runtime names them (__Host-<name>wherever they're Secure).
Rules: guard and labels
guard(actor, { collection, access, filter, direct? }, ctx)returns the filter an actor's documents must match. Reads and deletes reach only what matches it. Inserts, and what updates leave, must match it.truerestricts nothing, andundefinedmeans the collection has no rule. The owner asks it on every op an actor's work makes. A collection no rule speaks for never reaches an outsider's own reads and writes (REST, the socket).labelscarries what a job read (a document's space) to what it then writes, so an answer drawn from a space not everyone sees is marked. Work done for someone from their own Personal space carries nothing: it's theirs.
The access plugin declares both, for the collections its shared resource names.
Workers
workers: { computer: { admit, ready?, gone?, workloads } } declares a kind of worker that connects to the owner as executors do, presenting <kind>:<secret>. admit, ready and gone name Actions. Plugin code sends one jobs with ctx.workers.dispatch(kind, id, workload, input), or ctx.workers.call for one JSON answer. One that isn't connected fails with offline.
Startup checks
check(merged) gets the merged resources, adapters, views (their names) and actions, and call for other plugins' unrecorded Actions (models.check). What it throws stops createRuntime, so a wrong model fails at start, not on the first turn.
Contexts
What plugin code gets. Context is an Action's, a Processor's and a tool's.
| Field | What it is |
|---|---|
actor | who this runs for, carried into everything it causes. None: the system |
in | where the work is: its actor's in. None: nowhere in particular |
db.collection(name) | the collection API. Each write commits on its own |
call(action, input) | calls an Action as actor, asking its allow |
as(actor?) | this context acting as someone else, or as the system (as()), or as a grant. Trusted code only |
transaction(tx => …) | several writes, events and timers, atomic |
emit(type, data, { id? }) | commits a declared event |
schedule(action, input, when) | has time call an Action, and returns the timer's id; unschedule(id) removes it |
record(collection, doc) | appends to a ledger with the job's next write, kept even if the job fails |
stream(channel) | live frames for subscribers, never stored (tokens being written) |
render(name, item) | renders a View |
graph | link, unlink and edges between documents |
branch(key) | this context with its calls numbered apart, for work run at once (a step's tool calls) |
cancel(target), disconnect(actor) | stops a running call or delivery; ends an actor's browsers |
workers, fetch, metrics | workers by kind and id; the outside fetch (a test's fake, if given); counters, histograms and gauges |
resources, adapters | the merged configuration |
kept(key, work) | what a rule works out, kept until a commit changes what it read. Only the owner keeps it; here it just runs |
event, scheduled, retry, signal | the event a Processor handles; the timer that called; whether this ran before; when to stop |
ctx.as(other) records what it does as other, via the actor it acted for, and stays where the work is unless other says where (in, or in: undefined for nowhere). ctx.transaction holds the store's writer: an op in it that fails, a write the rules refuse included, ends it and rolls it back, and one with no op for 5 seconds rolls back too (one writer).
The others are narrower:
ReadContext(a query's, a rule's): reads, never writes, withactor,in,record,kept, andcallfor unrecorded Actions.ViewContext: reads that answer at once,show, and from handlerscallandemit, withactor,in,viewerandhints.ToolContext(chat's, incopilotz/plugins/agents): aContext, so itsinis where its chat is, plusagent,thread,trigger,callId(the idempotency key for what it does outside), andaskandanswerto wait for a person's reply.
The runtime
const runtime = await createRuntime({ store, plugins });| Option | What it sets |
|---|---|
store | where it keeps everything: memory, SQLite or PostgreSQL |
plugins | the app: a list of plugins, which may nest |
blobs | where large values live (the store, by default); blobThreshold: past how many bytes (16 KiB) |
cache | the owner's read cache ({ documents, queries }), or false |
executors | executors in this process (1); transports and key for others to connect |
metrics | where it records metrics (createMetrics) |
clock | what timers run on: the system's, or manualClock(start) in tests |
fetch | what ctx.fetch calls outside services with |
browsers | how much browsers may hold: sockets, tickets a minute, contacts' together |
| Method | What it does |
|---|---|
call(action, input, { id?, actor? }) | runs an Action; with id, a retry answers from the log |
query(name, params, { actor? }) | runs a collection's named query |
emit(type, data, { id?, actor? }) | commits a declared event |
observe(name, item, options) | keeps a View live |
render(name, item, { viewer?, hints? }) | renders a View once, as the system |
subscribe(listener) | hears committed events and live frames as they happen |
fetch(request) | answers HTTP |
bytes(collection, filter, field, { actor? }) | a large value's size and read(start, end), for host code serving a file |
idle() | resolves when no delivery or timer is pending and observed Views caught up |
close({ drain? }) | stops, giving running work drain ms (5 s) to finish |
cancel, disconnect, wake, stats, describe | stops a call; ends an actor's browsers; fires due timers; counts; what's declared |
Without actor, a call runs as the system. An actor's in says where.
The rest of copilotz:
- Running many:
runtimes(open), a pool of runtimes by id, each opened on first use and closed when idle (many orgs).createOwnerandcreateExecutorplace the owner and its executors apart (topology). - The client:
connect(url, { key?, over?, plugins? })reaches an app over its socket or its REST API (the REST API and the client). - Errors:
failure(code, message, { public? }),forbidden,invalid,isPublic. Only a public error's words reach a caller outside. - Links and addresses:
signLink,verifyLink,linksOf,clientAddress,callerOf… - Metrics:
createMetrics,noMetrics(metrics). - Time:
manualClock,systemClock. - Checks:
validator(schema), a JSON Schema check as the runtime runs them.
Events
The log records what happened, with no bookkeeping tables beside it. Every event has its type, data, cause, commit time (at) and actor, with for and in.
| Event | When | Its data |
|---|---|---|
<collection>.created, .updated, .deleted | a write changes a document, in its commit | _id, doc; an update also changed, removed |
action.started | a recorded call starts | action, input, startedAt, attempt |
action.completed, action.failed | it ends | action, result or error, ms, attempt |
action.retried | a call runs again | attempt, after: failed or lost |
processor.retried, processor.failed | a delivery failed; the last one is the dead letter | attempt or attempts, error |
| a declared event | code emits it | what its schema says |
- Collection writes: the document after the write, the last version for a delete, without private fields. A write that changes nothing records nothing.
sys.collections never record. - Action calls:
startedcosts no commit of its own: it lands in the call's earliest commit, ahead of all its effects. An Action'sprivatefields stay out of bothinputandresult. - Retries:
runtime.call(action, input, { id })is retryable. An id that completed answers its recorded result without running. One still running is joined. Otherwise the call runs again. An id reused for another action or input is refused (id_conflict). A call whose executor died fails withexecutor_lost. A nested call's id comes from its cause, its place, and its input, so a retried delivery finds the calls it already made. - Timers: an occurrence is the call
<timer>@<due time>, and a failed one runs again as that same call. - Causes: every event names its cause, so the log is a tree you walk from any event back to the request that started it.
- Large values: strings over 16 KiB are stored once, by the hash of their content, and documents and events hold a reference in their place. Plugin code never sees one, and a
$blobkey in what it writes is refused. - Outside effects (payments, emails): write the intent first, call out with its id as the idempotency key, then write the result.
The log and its Processors are in the architecture.
Schedules
Time is a caller of Actions. A timer is a document in sys.timers naming an Action, its input and when to call it.
schedules: {
"usage.close": { cron: "10 0 * * *", call: "usage.close", input: {} },
heartbeat: { every: "5m", call: "health.check" },
}- Declared: by a cron expression (in a
timezone, UTC by default) oreveryso long (30s,15m,2h,1d, or milliseconds), withretries(2). They patch like resources, andnullremoves one. At start the owner makes the timers what the plugins declare. A schedule calling an unknown Action, with input its Action refuses, or with a bad cron expression stops the runtime from starting. Declared ones run as the system. - From code:
ctx.schedule(action, input, { at | cron | every, id?, retries? })sets a timer with the call's writes, andctx.unschedule(id)removes it. In a transaction,tx.schedulecommits with the rest. Scheduling an id again replaces its timer. Without anid, a retried call sets the same timers again rather than more. A declared timer can't be changed from code. - As whom: a timer set by an actor runs as them, where they were, and its call asks
allow. - Once: the owner claims what's due in one commit and calls each occurrence as
<timer>@<due time>, so an occurrence called twice runs once. A recurring timer that missed several times fires once, then keeps its beat. - Retries: a failed occurrence tries again after 10 s, 30 s, 90 s…, up to an hour apart.
action.failedstays on record for Processors to answer. - The clock is a port:
createRuntime({ clock }).manualClock(start)moves only when a test says so (advance,set). A host whose clock only rings, like a Durable Object's alarm, callsruntime.wake().copilotz.timer.latenessmeasures how late occurrences were called.
Models and providers
A model is "provider/model", with its settings next to it.
resources: {
provider: {
openai: { apiKey }, // the catalog knows its protocol and address
deepseek: { apiKey }, // OpenAI-compatible, at api.deepseek.com
local: { adapter: "openai-compatible", baseURL: "http://localhost:11434/v1" },
chatgpt: { adapter: "openai", catalog: "openai", headers: () => oauthHeaders() },
},
model: { cheap: { model: "openai/gpt-5.4-mini", effort: "low" } }, // aliases
agents: {
lead: { instructions, model: "openai/gpt-6.1-sol", effort: "low" },
// A list is tried in order when a model fails before writing anything.
north: { instructions, model: ["chatgpt/gpt-6.1-sol", "anthropic/claude-opus-5-5"], effort: "low" },
},
history: { summarizer: "cheap" },
memory: { embedder: { model: "openai/text-embedding-3-small", dimensions: 512 }, extractor: "cheap" },
}- A
provideris an account or an endpoint:baseURL,apiKey(the provider's usual auth header),headers(added last; a function computes them per request, hearing whom the call is for),adapter(the protocol:openai,openai-compatible,anthropic,gemini, or a plugin's own),catalog(whose entries its models use,falsefor none),options,tags,silenceandretry. - The catalog is a snapshot of models.dev. It fills in what a provider leaves out: its protocol and address, and each model's prices, limits and how its reasoning is controlled.
- Settings sit next to a model, per port:
effort,maxTokensandtemperaturefor language models,dimensionsfor embeddings, andoptions(sent as they are) for all. Settings where a model is used win over an alias's, and a list entry's win over its list's. effortis one scale for every provider:none,minimal,low,medium,high,xhigh,max. A level a model doesn't take stops the runtime from starting.- Checked at start: chat checks every agent's model, history its summarizer, memory its embedder and extractor, so a missing provider or a wrong effort fails
createRuntime, not a turn.
Action inputs name models and never carry keys, because inputs are recorded. A plugin adds an adapter under adapters.llm, adapters.embeddings, adapters.transcription or adapters.image, named by the protocol it speaks. The models page lists its Actions.
The web's look and words
- The theme: the web host owns a vocabulary of tokens and its defaults, which are Copilotz's look. An app rebrands with a
themeresource ({ radius, font, logo, light, dark }), applied from the first paint. Front ends compose the host's toolkit (Button,Menu,Dialog,Markdown…), and their own CSS is only layout, on the theme's variables. The default fonts are served under/assets/copilotz/. A theme's own come from Google Fonts or from the app'spage.assets. - Words: every word the web front ends show is English by key (
chat.send), and an app changes any with awordsresource. The keys and their English are in WORDS.md. Blanks are{name}. Plurals are an object by the language's plural categories, chosen by{count}. Dates, numbers and currency follow the page's language,page: { lang: "pt-BR" }. - Your own front end says its words the same way:
const say = defineWords("acme", { … }), thensay("key", { name }).
Serving on the web shows the page, its theme and the sign-in page's words.
Plugins
Each plugin's page is read from the plugin itself: what it keeps, who may call each Action, where it shows, what it needs, and what it builds on. The index lists them all, and reference/plugins.json has the same as data.
- Models:
models. - Agents:
chat(web, terminal),history,memory(web, terminal) withmemoryExtraction,skills(web),routines(web),openapi,webTools. - Access:
spaces(web, terminal),identity,contacts(web),googleSignIn,emailSignIn,email,devSignIn. - Billing:
usage(web, terminal). - The doors:
webPage,webShell,webLines,terminalPlugin,terminalShell,api(web),mcp,whatsapp(web),voice,maintenance.
From the product packages:
@copilotz/work:boards(web),documents(web).@copilotz/team:team(web),tiers(web).@copilotz/directory:directory(web) and what each org gets of it,directory.member.@copilotz/billing:credits(web),stripe,stripeWebhook.
The compute plugins (computers, cloud, apps) and accounts have no page here. Installing a computer covers what an app serves for computers.