Entities#
A world's cast as data: one folder per organisation, person, store, agency or product,
each holding one entity.json and the CSV tables that belong to it, every row of which
says where it was read. scripts/entities/ checks a collection,
compiles it onto a base world (new sites, catalog listings, accounts, follows,
mailboxes, bank accounts) and into a society spec whose runner then
operates the people, and imports a selection of CommandAGI's seed drive into the format.
The format follows CommandAGI's seed (seed/drive/public-record/companies/<org>/org.json
with modules and a section per module, People.csv, Roles.csv,
Inventory/listings.csv, and the provenance columns of its records.md § lineage), so a
folder can move from one to the other with its history intact.
python scripts/entities/entities.py validate worlds/robotics-co/entities
python scripts/entities/entities.py compile worlds/robotics-co/entities --out target/entities/halyard-market
.venv/bin/python scripts/entities/entities.py run worlds/robotics-co/entities # compiles, then runs the society offline
python scripts/entities/import_commandagi.py worlds/robotics-co/entities --clean --limit 30 --stores 6
.venv/bin/python scripts/entities/test_entities.py # offline checks
.venv/bin/python scripts/entities/bench.py 1000 10000 50000 --boot # how it scales
A collection#
worlds/robotics-co/entities/
entities.json the manifest: base world, directory, terms, society settings
org/halyard-robotics/entity.json
org/org-tensaco/entity.json People.csv
org/org-penske-automotive-group-inc-de/entity.json People.csv Finance/statements.csv
person/user-cole-mercer/entity.json Roles.csv
person/nina-petrova/entity.json
store/org-parts-digikey/entity.json Inventory/listings.csv
agency/agency-ca-sos/entity.json
product/halyard-r1/entity.json
The folder's kind and name are the entity's kind and entity; a mismatch is a problem.
Ids are lower case ([a-z0-9][a-z0-9._-]*), stable, and the only way entities name each
other. A file in the folder that is neither entity.json nor a table the format knows
(README.md, update.js) is ignored; an unknown .csv is a problem.
entities.json:
| Field | Meaning |
|---|---|
schema_version, id | 1; the id names the compiled society and is appended to the world's id. |
world | The base world (worlds/robotics-co, or a world.json). It is read, never edited. |
directory | Optional: {domain, service, title, tagline, page_size}. A static site with a page per entity and paged lists per kind. |
terms | Term id → {owner, license, attribution, share}: what an entity's terms list means. |
society | Spec fields passed through to the compiled society: seed, clock, network, operation, checks, outcomes, interventions, sweep. |
An entity#
{
"entity": "user-andre-whitfield", "kind": "person",
"modules": ["identity", "contacts", "devices", "accounts", "behavior"],
"terms": ["commandagi-seed"],
"identity": {"name": "Andre Whitfield", "kind": "ai-agent", "title": "Chief Revenue Officer", "timezone": "America/Los_Angeles"},
"contacts": {"emails": [{"address": "[email protected]", "kind": "work", "service": "google-mail"}], "hours": "08:00-18:00"},
"devices": [{"id": "phone", "profile": "ios", "apps": ["x-social"]}],
"accounts": {"x-social": "awhitfield"},
"behavior": {"rules": ["You are an AI agent, not a person: ..."],
"policy": {"kind": "scripted", "repost": 0.45, "like": 0.35, "reply": 0.0},
"activation": {"propensity": "hours(8-17:0.7, else:0.05)"}},
"sources": [{"fact": "name, title, work address, timezone and note", "url": "seed/drive/ppl/andre-whitfield/andre-whitfield.vcf",
"via": "file", "read_by": "script:import_commandagi", "observed_at": "2026-10-25"}]
}
Each name in modules has a section of that name, held to
schemas/<module>.json (JSON Schema 2020-12); a listed
module without a section, or a section whose module is not listed, is a problem. sources
(fact, url, via, read_by, observed_at, note) says where the entity's own fields
came from; terms names the terms they are held under.
| Module | Kinds | What it holds | Compiles to |
|---|---|---|---|
identity | all (required) | name, legal names, kind (human, ai-agent, operating ...), title, category, jurisdiction, headquarters, industry, ids, listings, the real-world website as recorded, about; extra keeps source fields with no place here | the directory page; a member's persona (name, title, location, backstory) |
contacts | all | emails (service: the mail site holding the mailbox), phones, hours | a member's mailbox (users, accounts, names on that mail site); anyone else's address as an external contact |
relations | all | [{rel, target, title, unit, since, ...}]: reports_to, member_of, customer_of, supplier_of, follows, maker_of, sold_by, owns, regulated_by, registered_with, partner_of, advises | follows (below), persona lines, orgs[].reports_to, the directory's Relations and Connected lists |
devices | person | [{id, profile, presentation, apps, open, viewport, files}], or {machine, existing: true} for a computer the world has | society devices: a computer, node, link, theme and presentation each, or a session on the existing machine |
accounts | all | {site: handle} or {site: {handle, verified, bio, followers, following, existing, operator}} | a member's: society accounts. Anyone else's social account: written into the site's state with its operator's actor (or none) |
finances | all | accounts at a bank-kind service ({service, id, kind, name, holder, balance_cents, limit_cents}, or state_patch for any other service), statements (a table) | the bank's accounts, owned by the holder's actor; reported figures on the directory page |
media | all | website (domain, title, real_url, or existing with its service), channels, posts ({site, id, text, at}) | a static site per new website (a home page with the org's people, products and contacts); posts as exogenous injections at their round |
storefront | store, org | service, domain, platform, existing, category, listings (a table), default_stock, real_url, mirrors | listings in a shop service: a new one per distributor, one shared per marketplace platform, or merged into a shop the world has |
obligations | all | [{agency, kind, form, name, due, recurrence, status, fee_cents, reference}] | the agency's directory page (filings on record); a member's persona (owes_...); listed as uncompiled for an agency with a portal service until the gov service has a compiler |
behavior | person | persona, rules, policy (llm, procedure, scripted, idle, with that kind's parameters), activation, intent | makes the person a society member; passed to the spec, with the persona filled in from identity and relations |
work | person | society work[] entries (procedure, as, joins, at/on, overrides) | passed through: the society runner runs them when their trigger fires |
product | product (required) | maker, summary, category, price_cents, stock, sold_at (stores), bullets, status | a listing in each sold_at store's shop; the maker's website and directory page |
agency | agency (required) | jurisdiction, level, service (its portal, when the world has one), handles, processing_days | the directory page with every obligation filed with it |
Tables#
schemas/tables.json lists the tables a folder may
hold, the kinds that hold each, and their columns. Every one ends with the provenance
columns source, via, read_by, observed_at: where the row was read, how, by whom (a
person, an agent, script:<name>) and when. A row with any of them empty is a problem.
| Table | Kinds | Columns (then provenance) | Read as |
|---|---|---|---|
People.csv | org, store, agency | person, name, title, relationship (+ unit, since, kind, shares, shares as of, last filed) | a member_of edge from each person |
Roles.csv | person | org, title (+ department, reports to, since, kind, relationship, ticker, shares, ...) | member_of the org, reports_to the reports to |
Customers.csv | org, store | customer (+ since, note) | a customer_of edge from each customer |
Inventory/listings.csv | store | id, title, price, currency (+ url, quantity, price_breaks, manufacturer, mpn, condition) | one shop product per row; the provenance is printed in its description |
Finance/statements.csv | org, store | statement, line_item, unit, period, value (+ filing) | the latest revenue, net income and assets on the directory page |
References in tables may be written the way CommandAGI writes them (../user-x,
cloud://home/org-x); they are read as the id after the prefix. An edge written at both
ends (a People.csv row and the person's Roles.csv row) is one edge.
Problems#
entitylib.problems(collection, world) returns every problem as one line naming the
file and the JSON Pointer, or the table and row, the way CommandAGI's recordProblems
does; validate prints them and compile refuses a collection that has any. From
test_entities.py's broken collection:
org/acme/entity.json: /identity/headcount: is not a field of identity (known: name, legal_names, ...)
org/acme/People.csv:2: person ghost: no such entity
org/acme/People.csv:3: source is empty (every row says where it was read)
org/acme/People.csv:3: observed_at 'yesterday' is not a date
person/roadrunner/entity.json: /entity: "road-runner" is not the folder's name roadrunner
person/roadrunner/entity.json: /storefront: is listed in modules but has no section
person/roadrunner/Notes.csv: not a table this format knows (known: People.csv, Roles.csv, ...)
person/wile/entity.json: /behavior/policy/kind: "chaotic" is not one of llm, procedure, scripted, idle
person/wile/entity.json: /accounts/x-social: @ACME is also acme's
person/wile/entity.json: /accounts/mastodon: wile would sign in to @acme, @wile on mastodon; a person holds one account per site
Across entities it checks dangling and mistyped references (a reports_to that is not a
person, a maker that is not an org), handles, machines, domains and post ids used twice,
one account per person per site (a site signs a person in to the first account whose
actor is theirs), and, against the base world: sites that do not exist, existing devices
and shops the world lacks, finance accounts at a service that is not a bank without a
state_patch, domains the world already serves, and a person whose actor would be a
base machine's user.
Compiling#
compile writes four files, each a pure function of the entity files and the base
world: two compiles are byte-identical (the test compares them).
overlay.json: what the entities add.services(new sites, each with a node in the internet zone at100.64.x.y, a link frompop-westand DNS names ondns-public),service_patches(RFC 7396 merge patches of the world's own services' initial state),internet_overlays(merge patches of the internet's sites, the same field a world declares) andnetwork(appended).compiler.apply_overlay(base, overlay).world.json: the base world with the overlay applied.society.json: a spec in the schema of docs/societies.md, whoseworldis thatworld.json. Members are the people withbehavior; their follow edges becomecompletegraphs, one per followee (follow-<site>-<handle>); accounts outside the society that members follow or that post are named withexisting: true;media.postsbecomeexogenousposts;reports_tobetween members becomesorgs[].reports_to; the rest of a member's roles, managers outside the society, customers, suppliers and obligations becomepersona.knowslines.index.json: counts by kind and by compiled thing, the edges, what each entity became (page:directory.example/org/org-tensaco,account:x-social/@TensaCo,listing:digikey/...,machine:...,society:...), and anything not compiled, with why.
Follows: a follows edge, a member_of edge (following one's employer) and a
customer_of edge (following a supplier) become a follow on every social site where both
ends have an account. A member's follows go to the society (so its counters move with the
spec's own edges); anyone else's are written into the site's follows with both
counters moved.
Importing CommandAGI's seed#
import_commandagi.py <collection> reads the seed drive (a local checkout; nothing is
fetched) and writes:
| From | To |
|---|---|
public-record/companies/<org>/ (--limit with officers on file, at an even stride through the sorted 5 686) | an org: org.json → identity (ids, SIC industry, listings, capital in extra, the README's summary as about); People.csv kept as is; Finance/*.csv (a column per period, its provenance in the last rows) → Finance/statements.csv, one row per figure carrying that column's provenance and filing; the README's last 10-K → an accepted report obligation to agency-us-sec; Products/<p>/README.md → a product |
public-record/people/<user>/ (every officer of those companies) | a person with Roles.csv; roles at companies outside the selection are dropped and counted in sources |
ppl/<person>/ (the AI-agent personas) | a society member: the vCard → identity and contacts (the work address mapped onto the simulated domain, with a mailbox on google-mail); Roles.csv → relations; the README's hours → activation, working style → temperament, sign-off line and AI-agent disclosure → rules; a phone with x.com; a scripted policy whose chances follow the role (communications, revenue and the CEO amplify; counsel, finance and people mostly read; engineers and scientists like and reply) |
customers/<org>/ | an org with People.csv, products, a website on <name>.example, and a post per product (its README's first line, quoted) |
stores/<org>/ (DigiKey and Mouser, then --stores minus two at a stride) | a store. DigiKey and Mouser: listings joined from the parts their read.mjs names and the prices CommandAGI's hardware bom.csv rows give (the stores' own listings.csv is not synced in the seed), each row's source naming the BOM row. Stores on Amazon and eBay sell in the internet's simulated marketplace of that name; Alibaba and AliExpress stores share one new site per platform |
What is kept and what is made up is written down. Every row keeps the seed's provenance
columns; a row or field the importer derived says read_by: script:import_commandagi
and its sources entry says from what. The x.com handles and domains are made up from
names and say so; real addresses stay as data (identity.website, real_url) and the
world sees only the simulated domain. The companies' posts state their latest reported
revenue, worded from the statement row they name. terms follow the source: sec-edgar
(public domain), wikidata (CC0), commandagi-seed, model-written for a row whose
via is model, importer-derived. The importer is deterministic (two runs into empty
folders write the same bytes) and owns only the folders whose sources name it:
--clean removes those first, and hand-written entities are never touched.
The demo: Halyard's market#
worlds/robotics-co/entities/ puts 430 entities around
Halyard Robotics: 414 imported (30 public companies with their 315 officers and 46
products, TensaCo and its 13 AI-agent executives with 2 products, DigiKey, Mouser and four
marketplace stores with 38 listings, the SEC) and 16 written by hand (Halyard with its
bank accounts, storefront and two state filings, its contract manufacturer, Nina from
sales on her existing workstation, the California Secretary of State and CDTFA, the R1,
and ten Oakland warehouse workers, makers, an analyst and a reporter on their phones).
Measured on this machine (entities.py run, 2026-10-28):
| entities | 430: 33 orgs, 6 stores, 3 agencies, 49 products, 339 people; 376 edges |
| load, validate, compile, write | 0.043 s, 0.032 s, 0.011 s, 0.033 s |
| compiled | 6 new sites (the directory with 439 pages, TensaCo's site, DigiKey, Mouser, Alibaba, AliExpress), 38 listings, 31 x.com accounts, 2 Northwind accounts, 13 mailboxes, 40 follows, 26 posts, 28 obligations listed |
| society | 24 members on 24 devices (23 new phones and Nina's existing workstation), all scripted |
| run | 2 rounds in 2.4 s: 27 decisions; likes 11 → 17, reposts 7 → 8; two runs reach the same state hashes |
Nina's Northwind login shows Halyard's operating account and card; directory.example,
digikey.example and tensaco.example answer in the browser (the test loads them).
Scaling to tens of thousands#
bench.py writes a synthetic collection of N entities shaped like the demo (a tenth
orgs, each with an eight-row People.csv and thirty figures; three quarters people with
a Roles.csv; a tenth products; one store in a hundred with fifty listings; 40 members),
then times every stage. With --boot, the extension boots the world, loads three pages,
hashes and inspects the state, and runs one society round.
| N | load | validate | compile | write | world.json | boot | state hash | inspect | society boot | 1 round, 40 members |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 000 | 0.09 s | 0.04 s | 0.02 s | 0.06 s | 1.7 MB | 0.03 s | 0.02 s | 0.05 s | 0.31 s | 3.0 s |
| 10 000 | 0.89 s | 0.32 s | 0.21 s | 0.57 s | 15.8 MB | 0.18 s | 0.11 s | 0.21 s | 1.22 s | 4.1 s |
| 50 000 | 4.98 s | 1.67 s | 1.06 s | 2.96 s | 78.8 MB | 0.91 s | 2.43 s | 1.93 s | 8.02 s | 9.8 s |
Compiling is linear: patches accumulate in place, one per entity. (The first version rebuilt a site's whole state for every account it added, and took 24.7 s at 20 000 entities; it now takes 3.0 s, and 1.06 s at 50 000 once the overlay's new services are shared with the world rather than copied.) Reading the files dominates past 10 000: about 100 µs an entity, most of it opening one JSON file and its CSVs.
What grows with N is the directory: one page per entity is most of the 79 MB, and the
state hash and inspect() (which the society runner calls every round to measure)
grow with it. For a world of tens of thousands, then:
- keep the directory, or leave it out of the manifest and let each org's own website and storefront carry what people need to see;
- make members the few hundred people who act, and leave everyone else as data: a member
costs a machine, a session and a slot (
people × slot < quantum, so 2 400 members fit one-hour rounds at a 1.5 s slot), where a non-member costs a page and a few edges; - keep a collection on disk as it is (one folder per entity reads in ~5 s at 50 000) and
compile once into
target/; the compiled world boots in under a second.
Gaps and hooks#
- The society runner reads its base world from a file, so
compilewrites the whole world (world.json) beside the overlay. Aworld_overlayfield in the society spec (applied byschema.load_basewithcompiler.apply_overlay's semantics) would let a spec nameworlds/robotics-coand the overlay instead of a 1 MB copy. - Obligations compile to directory pages and persona lines, not to an agency portal:
the
govservice is being written. Once its state has a register of filings, an agency'sservicecan receive them as a typed patch, as bank accounts are. - Accounts compile on social sites (and bank accounts, mailboxes, shops); an account on another kind of site (Slack, GitHub, a tracker) is listed as uncompiled, as the society does.
- Scripted members only engage with posts. A store's customers buying, or a person
paying a bill, need
procedurepolicies orwork; the format passes both through.