Skip to content

Matrix actor integration

A Matrix actor that wants to evaluate Omega Lisp does so through @open-matrix/lisp-actor, which wraps the narrow @omega/core facade. Going through the narrow facade keeps the actor from pulling in the REPL, the driver registry, and the PiAI bindings.

The narrow facade

ts
// projects/omega-lisp/packages/core/src/index.ts
export type { Expr, Val, State, Frame, Env, Store, Runtime } from "...";
export type { EffectBackend, OpCall, Resumption } from "...";
export { COWStore, compileTextToExpr, stepOnce, runToCompletion } from "...";
export type { KernelRuntime } from "./kernel";
export { createKernelRuntime, createKernelPrimitives } from "./kernel";

This is enough to:

  1. Compile source text to Expr.
  2. Run a State to completion (or step once for debugging).
  3. Provide an EffectBackend so effects route to the host's chosen handler.

It is NOT enough to use the standard library packages (solve, streams, hof etc.), which require the installPrims path from the full omega-lisp package.

@open-matrix/lisp-actor

This sister package (referenced from AgentActor.ts:55-60):

ts
let OmegaSandbox: IOmegaSandboxConstructor | null = null;
try {
  const mod = await import('@open-matrix/lisp-actor') as
    { OmegaSandbox?: IOmegaSandboxConstructor };
  OmegaSandbox = mod.OmegaSandbox ?? null;
} catch { /* lisp-actor not available */ }

OmegaSandbox is the Matrix-friendly wrapper. Its public surface:

ts
interface IOmegaSandbox {
  eval(code: string): Promise<{
    ok: boolean;
    display?: string;
    value?: { tag?: string };
    error?: unknown;
  }>;
  serialize(): SerializedReplState;
}

interface IOmegaSandboxConstructor {
  create(options): Promise<IOmegaSandbox>;
  hydrate(state: SerializedReplState, options): IOmegaSandbox;
}

The sandbox handles persistence: serialize() returns the env + history snapshot; hydrate() reinstates them. This is exactly what Matrix's session_state(key='repl_state') round-trip uses.

How AgentActor uses it

AgentActor (in @open-matrix/agents) imports the sandbox dynamically. When the actor receives $eval for a session that already has REPL state, it hydrates; otherwise it creates. The session's repl_state is updated after each turn.

A minimal embedded actor

ts
import { MatrixActor } from '@open-matrix/core';
import { compileTextToExpr, runToCompletion, COWStore } from '@omega/core';

class MyEvalActor extends MatrixActor {
  static accepts = {
    'eval': { description: 'Evaluate omega-lisp code', code: 'string' },
  };

  async on_eval(payload: { code: string }) {
    const expr = compileTextToExpr(payload.code);
    const state = /* build initial State with COWStore + Env */;
    const final = runToCompletion(state);
    return { ok: true, value: /* extract from final */ };
  }
}

This is the bare minimum. Production actors layer on:

  • The standard primitives (installPrims from the full omega-lisp package).
  • An EffectBackend that routes effects to actor ops.
  • A persistence path for the REPL state across calls.

Why two packages?

Splitting @omega/core from omega-lisp is deliberate: the agents package and the lisp-actor wrapper want to bind to the small, stable kernel surface. The full omega-lisp package depends on driver implementations, pi-ai, and the help system — none of which a Matrix actor host has any business pulling in.

See also

Source: projects/omega-lisp/packages/core/src/index.ts:1-24 (narrow facade), projects/matrix-3/packages/agents/src/AgentActor.ts:31-62 (dynamic import + sandbox surface).