Skip to content

Actor integration

If you author a Matrix actor and want users to converse with it through Chat (or Director's Chat tab), you need to satisfy a small contract. This page is the contract.

The minimum: be promptable

The user-facing path always lands in system.agents. system.agents then needs to recognize your actor as a target. The way that works:

  • Your actor declares static promptable = true (or returns promptable: true from $introspect).
  • Your actor's $introspect includes a cognitiveState block with at least one of purpose, systemPrompt, memoryScope, regime.

That's enough for Chat's UI to enable the input and offer your actor as a target. The actual prompt goes to system.agents, which uses the snapshotTree describing your actor as conversation context.

A richer integration:

ts
class MyActor extends MatrixActor {
  static description = 'What this actor does, in one sentence.';
  static promptable  = true;
  static cognitiveState = {
    purpose:      'concise statement of intent',
    systemPrompt: 'optional system prompt for cognitive interactions',
    memoryScope:  'session' | 'principal' | 'global',
    regime:       'explore' | 'execute' | ...,
  };

  static accepts = {
    'my-op': { ... },
    // declared ops, with description for each
  };

  static emits = {
    'my-event': { ... },
  };

  static skills = {
    'do-the-thing': {
      description: 'Plain-English skill description.',
      ops: ['my-op'],
    },
  };
}

Chat's snapshotTree carries mount, tag, accepts, emits, skills, tokens, cognitiveState, and children to system.agents. The richer your introspect, the better the model's answer.

What Chat actually sends

Going to system.agents $prompt (verified against RemoteActorConversationAdapter.prompt):

ts
{
  prompt:        request.text,
  requestId:     request.requestId,
  traceId?:      request.traceId,
  activityTo:    request.activityTo,
  callerSnapshot:request.callerSnapshot,
  callerRealm:   <transport.addressRoot or daemonRoot>,
  callerMount:   <chat root, default 'chat'>,
  sessionId?:    request.sessionId,
  principalId?:  request.principalId,
}

For the surface (Director's Chat tab), the payload also includes callerRoot, callerRuntimeRoot, callerSurfaceMount, and snapshotTree.

Your actor doesn't see the $prompt directly — system.agents does. But your actor will see:

  • Tool calls from the model (if the model decides to invoke your declared ops).
  • $activity frames (relayed via activityTo) if Chat asks system.agents to send activity to a specific address — your actor doesn't get those by default.

If your actor is NOT promptable

You can still be a Chat target — the user just won't be able to converse with you the same way. Instead, Chat treats you as a "discussion topic": the prompt goes to system.agents about you, and the snapshotTree describes you. The model then answers questions about your declared interface using the snapshot.

This is how Director's "ask about this actor" works for non-promptable mounts.

The alternative path: be the agent yourself

If you want to be the cognitive agent (own sessions, own memory, own inference routing), don't use Chat's system.agents indirection. Mount your actor at a name like my-app.agent and have Chat target it directly.

To do that:

  1. Implement $prompt and emit $activity frames yourself (or delegate to system.inference directly).
  2. Implement $sessionList, $sessionDelete, session_state.read, session_state.upsert.
  3. Configure Chat with serviceRefs.conversation.target = '<your-mount>' and mode: 'remote-actor'.

This bypasses system.agents. It's a heavier lift but useful for purpose-built agents (e.g., a SmithersAgent that owns its own session lifecycle).

Cancellation contract

If a user cancels mid-conversation, Chat sends $cancel { requestId }. Your actor (or system.agents) should:

  1. Stop emitting further $activity frames for that requestId.
  2. Free any resources tied to the request.
  3. Optionally emit a $activity frame with phase: 'cancelled' so Chat knows.

Honoring cancel is a strong recommendation, not a hard requirement — Chat will time out the stream eventually.

See also

Source: projects/matrix-3/packages/chat/src/app/services/adapters/RemoteActorConversationAdapter.ts:18-51 (the prompt payload), chat-component/src/surface/ConversationSurfaceActor.ts:339-435 (surface variant with snapshotTree).