Abjects is an LLM-mediated distributed object system where objects communicate via message passing, negotiate protocols using an LLM, and self-heal when communication breaks down. Everything in the system is an object (Abject) - including the Registry, Factory, LLM service, and UI server.
Tech Stack: TypeScript, Vite, WASM (sandboxed objects), Canvas (X11-style UI)
pnpm conjure # Gather dependencies
pnpm awaken # Awaken the backend (ws://localhost:7719)
pnpm scry # Scry into the abyss (http://localhost:5174)
pnpm whisper # Start P2P signaling server (:7720)src/
index.ts # Public API re-export barrel
core/ # Types, contracts, message builders, base Abject class, capabilities
runtime/ # Runtime orchestrator, MessageBus, Mailbox, Supervisor
objects/ # System objects: Registry, Factory, LLMObject, ObjectCreator, ProxyGenerator, UIServer
objects/capabilities/ # Capability objects: HttpClient, Storage, Timer, Clipboard, Console, FileSystem
protocol/ # Negotiator, Agreement management, HealthMonitor
llm/ # LLM provider interface and implementations (Anthropic, OpenAI, Ollama)
network/ # Transport abstraction, WebSocket, MockTransport
sandbox/ # WASM loader, capability-enforced imports, WorkerRuntime
ui/ # App shell, Canvas Compositor
workers/
object-runtime.worker.ts # Web Worker for WASM object execution
Always use require/ensure/invariant from src/core/contracts.ts. Contracts are never disabled - correctness over performance.
require(condition, message)- Preconditions at function entryensure(condition, message)- Postconditions before returninvariant(condition, message)- Class state consistency incheckInvariants()- Helpers:
requireDefined,requireNonEmpty,requireNonEmptyArray,requirePositive,requireNonNegative
Call checkInvariants() after state mutations. Override it calling super.checkInvariants() first.
Every system service follows this pattern:
- Extend Abject with a manifest in the constructor:
constructor() { super({ manifest: { name: 'MyObject', description: 'What it does', version: '1.0.0', interfaces: [{ id: 'abjects:my-object' as InterfaceId, name: '...', description: '...', methods: [...] }], requiredCapabilities: [], providedCapabilities: [...], tags: ['system'], }, }); this.setupHandlers(); }
- Register handlers in
setupHandlers()usingthis.on('methodName', handler) - Use
this.send()for fire-and-forget,this.request<T>()for request/reply (30s default timeout) - Override
onInit()for async initialization,onStop()for cleanup - Export a well-known ID constant:
export const MY_OBJECT_ID = 'abjects:my-object' as AbjectId
- Handlers receive
AbjectMessage, extract payload via type assertion:const { key } = msg.payload as { key: string } - Returning a value from a request handler auto-creates a reply message
- Method
'*'is a wildcard/catch-all handler - Unhandled requests get a
METHOD_NOT_FOUNDerror reply
- Interface IDs:
'abjects:module-name'(e.g.,'abjects:registry','abjects:http') - Well-known IDs:
UPPER_SNAKE_CASEwith_IDsuffix (e.g.,REGISTRY_ID,FACTORY_ID) - Capability IDs:
'abjects:category:action'(e.g.,'abjects:storage:read') - Tags: lowercase strings in arrays (e.g.,
['system', 'core'],['capability', 'http'])
- Target: ES2022, Module: ESNext, Strict: true
- Imports use
.jsextension:import { Abject } from './abject.js' noEmit: true(Vite handles bundling)- Libs: ES2022, DOM, DOM.Iterable, WebWorker
- One class per file (except
types.tswhich has all type definitions) - Interfaces declared in same file as implementing class
- Well-known IDs and factory functions exported from same file as class
- Public API re-exported from
src/index.ts
Global objects are singletons spawned once during bootstrap in server/index.ts.
- Create file in appropriate directory
- Extend
Abjectwith full manifest (include completeInterfaceDeclarationwith method params, returns, descriptions) - Add handlers for every method in the interface
- Use contracts for all preconditions/postconditions
- Override
checkInvariants()callingsuper.checkInvariants()first - Export well-known ID constant
- Add to
src/index.tsexports - Register its constructor and spawn it in
server/index.tsmain()
Per-workspace objects are spawned automatically for every workspace by WorkspaceManager. They run in worker threads when workers are enabled (the default). You must register the constructor in both the main thread AND the worker.
- Create file in
src/objects/ - Extend
Abjectwith full manifest, handlers, contracts, well-known ID (same as global) - Register constructor in
server/index.ts:runtime.objectFactory.registerConstructor('Name', () => new MyAbject()) - Register constructor in
workers/abject-worker-node.ts: import +constructors.set('Name', () => new MyAbject()) - (Optional) Mark worker-eligible in
server/index.tsworkerEligiblearray if it should run in a worker thread - Add to spawn list in
src/objects/workspace-manager.ts:INFRA_OBJECTS— non-UI Abjects (always spawned, including for inactive workspaces)UI_OBJECTS— Abjects with show/hide windows (only spawned for active workspaces)
- Export from
src/index.ts
CRITICAL: Forgetting the workers/abject-worker-node.ts registration causes silent spawn failures when workers are enabled. Always register in both places.
- Create in
src/objects/capabilities/ - Define capability ID constants in
src/core/capability.ts - Set
providedCapabilitiesin manifest, tag with['capability', '<name>'] - Follow existing patterns (see
http-client.tsfor domain allow/deny,storage.tsfor IndexedDB)
- Create in
src/llm/ - Implement
LLMProviderinterface (or extendBaseLLMProvider) - Include both
complete()andstream()methods - Add configuration to
LLMObject.configure() - Export from
src/index.ts
- Worker context:
object-runtime.worker.tshas its ownrequire()- can't import fromcontracts.ts - Object initialization: All objects must be
init(bus)before use;factory.spawnInstance()handles this - Mailbox bounds: Default max queue size is 1000; sending to a full mailbox throws
ContractViolation - API keys: Set via
ANTHROPIC_API_KEY/OPENAI_API_KEYenvironment variables - Compositor: Needs a real
HTMLCanvasElement - Sequence numbers: Per-sender, tracked in module-level state in
message.ts; useresetSequence()in tests - Import extensions: Always use
.jsin imports even though source files are.ts - Bootstrap: Global system objects must be registered and spawned in
server/index.ts. - Worker constructors: Per-workspace Abjects must have their constructors registered in BOTH
server/index.tsANDworkers/abject-worker-node.ts. Missing the worker registration causes silent spawn failures.
Bootstrap happens in server/index.ts:
Appcreates Canvas, Compositor, UIServer, RuntimeRuntime.start()creates MessageBus, initializes Registry and Factory on the busmain()spawns: LLMObject, HttpClient, Storage, Timer, Clipboard, Console, FileSystemmain()spawns: ProxyGenerator, Negotiator, HealthMonitor, ObjectCreatormain()spawns: Workspaces, P2P, and remaining system objects
When adding a new global system object, register its constructor and spawn it in server/index.ts.
Per-workspace objects are spawned by WorkspaceManager — add them to INFRA_OBJECTS or UI_OBJECTS in workspace-manager.ts, and register their constructors in both server/index.ts and workers/abject-worker-node.ts.
- uuid: Message ID generation (v4)
- ajv: JSON schema validation
- assemblyscript: WASM compilation toolchain (devDependency)
- vite: Build tool and dev server
- typescript: Language compiler