Skip to content

Latest commit

 

History

History
393 lines (309 loc) · 10.8 KB

File metadata and controls

393 lines (309 loc) · 10.8 KB

Adapter Guide

An adapter is a class that bridges your data source — a WebSocket feed, a REST API, a database, or a static mock — to the pixel-ops simulation engine. The engine knows nothing about where data comes from; it only receives typed events through the adapter interface.

What adapters are and why they exist

pixel-ops is data-source agnostic. The PixelOpsAdapter base class defines a small, stable contract: implement connect() and disconnect(), call this.emit() with typed events. This keeps the simulation engine decoupled from your infrastructure so the same component can render an LLM agent dashboard, a CI/CD control room, or a game server monitor without any changes to the core library.

Extending PixelOpsAdapter

import { PixelOpsAdapter } from 'pixel-ops';

class MyAdapter extends PixelOpsAdapter {
  connect(): void {
    // Called once after the Pixi canvas is ready.
    // Establish connections and start emitting events here.
  }

  disconnect(): void {
    // Called when the PixelOps component unmounts.
    // Close connections, cancel timers, release resources.
  }
}

The two abstract methods you must implement:

Method When it is called What to do
connect() After canvas initialisation Open WebSocket, start polling, begin emitting
disconnect() On component unmount Close connections, clear timers

Inside connect() and anywhere in your subclass, call this.emit(eventName, payload) to push events into the engine.

All available events

agent:status

Updates an agent's operational status. If the agent ID has not been seen before, a new agent entity is created in the simulation.

this.emit('agent:status', {
  id: 'agent-1',          // string — must match an id in config.agents
  status: 'working',      // 'working' | 'idle' | 'error' | 'celebrating' | 'sleeping' | 'disabled'
  room?: 'dev-office',    // optional: move the agent to this room
  task?: 'Running tests', // optional: task label shown in speech bubble
});
Field Type Required Description
id string Yes Must match a configured agent ID
status string Yes One of the six status values
room string No Built-in room ID to move the agent to
task string No Short description shown in the speech bubble

agent:move

Moves an agent to a specific room without changing its status.

this.emit('agent:move', {
  id: 'agent-1',
  room: 'break-room',
});
Field Type Required Description
id string Yes Agent ID
room string Yes Built-in room ID (see configuration.md)

notification

Pushes a message into the Comms Terminal log.

this.emit('notification', {
  level: 'info',           // 'info' | 'warning' | 'error' | 'success'
  message: 'Deploy complete',
  agentId?: 'agent-1',    // optional: associates the notification with an agent
});
Field Type Required Description
level string Yes Severity level
message string Yes Human-readable message
agentId string No Agent ID to associate with the notification

metric

Records a named numeric metric. Metrics are stored in the simulation state and can be read by custom systems.

this.emit('metric', {
  key: 'requests_per_second',
  value: 142,
});
Field Type Required
key string Yes
value number Yes

economy:transaction

Records an income or expense transaction in the economy ledger. Requires EconomySystem to be registered.

this.emit('economy:transaction', {
  amount: 50,
  type: 'income',
  reason: 'API call completed',
});
Field Type Required
amount number Yes
type 'income' | 'expense' Yes
reason string Yes

research:progress

Advances a research topic. Requires ResearchSystem to be registered.

this.emit('research:progress', {
  topicId: 'adv-monitoring',
  progress: 42,
  name?: 'Advanced Monitoring',
});
Field Type Required
topicId string Yes
progress number Yes — absolute progress value, not a delta
name string No

reputation:change

Adjusts the campus reputation score. Requires ReputationSystem to be registered.

this.emit('reputation:change', {
  delta: 10,
  reason: 'Customer satisfied',
  agentId?: 'agent-1',
});
Field Type Required
delta number Yes — positive or negative
reason string Yes
agentId string No

Event buffering behaviour

When connect() is called, the canvas may have listeners registered, or it may not yet. The adapter handles this transparently: if you call this.emit() before any listener is attached, the event is placed in an internal buffer (capped at 10,000 events). Once the engine registers its listeners, it calls flush() which replays the entire buffer in order.

This means it is safe to call this.emit() immediately at the start of connect() — you do not need to wait for any "ready" signal.

connect / disconnect lifecycle

PixelOps mounts
    → Pixi canvas created
    → Engine + systems initialised
    → Adapter event listeners registered
    → adapter.connect() called
    → adapter.flush() called (replays buffered events)
    → Game loop starts

PixelOps unmounts
    → adapter.destroy() called internally
        → adapter.disconnect() called
        → All listeners cleared
        → Buffer cleared

Call adapter.destroy() is handled by the component automatically on unmount. Do not call it manually unless you are managing the component lifecycle yourself.

Example: WebSocket adapter

import { PixelOpsAdapter } from 'pixel-ops';

interface AgentEvent {
  agentId: string;
  status: string;
  task?: string;
}

export class WebSocketAdapter extends PixelOpsAdapter {
  private ws: WebSocket | null = null;
  private readonly url: string;

  constructor(url: string) {
    super();
    this.url = url;
  }

  connect(): void {
    this.ws = new WebSocket(this.url);

    this.ws.addEventListener('open', () => {
      this.emit('notification', { level: 'success', message: 'Connected to agent feed' });
    });

    this.ws.addEventListener('message', (event) => {
      let data: AgentEvent;
      try {
        data = JSON.parse(event.data);
      } catch {
        return;
      }

      this.emit('agent:status', {
        id: data.agentId,
        status: data.status as any,
        task: data.task,
      });
    });

    this.ws.addEventListener('close', () => {
      this.emit('notification', { level: 'warning', message: 'Disconnected from agent feed' });
    });

    this.ws.addEventListener('error', () => {
      this.emit('notification', { level: 'error', message: 'WebSocket error' });
    });
  }

  disconnect(): void {
    this.ws?.close();
    this.ws = null;
  }
}

Usage:

const adapter = new WebSocketAdapter('wss://your-backend.example.com/agents');

<PixelOps adapter={adapter} config={config} />

Example: REST polling adapter

import { PixelOpsAdapter } from 'pixel-ops';

interface AgentApiResponse {
  id: string;
  state: 'active' | 'idle' | 'failed';
  currentTask?: string;
}

export class RestPollingAdapter extends PixelOpsAdapter {
  private intervalId: ReturnType<typeof setInterval> | null = null;
  private readonly apiUrl: string;
  private readonly pollIntervalMs: number;

  constructor(apiUrl: string, pollIntervalMs = 5000) {
    super();
    this.apiUrl = apiUrl;
    this.pollIntervalMs = pollIntervalMs;
  }

  connect(): void {
    // Fetch immediately, then on interval
    this.poll();
    this.intervalId = setInterval(() => this.poll(), this.pollIntervalMs);
  }

  disconnect(): void {
    if (this.intervalId !== null) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }
  }

  private async poll(): Promise<void> {
    try {
      const res = await fetch(this.apiUrl);
      const agents: AgentApiResponse[] = await res.json();

      for (const agent of agents) {
        this.emit('agent:status', {
          id: agent.id,
          status: agent.state === 'active' ? 'working' : agent.state === 'failed' ? 'error' : 'idle',
          task: agent.currentTask,
        });
      }
    } catch {
      this.emit('notification', {
        level: 'warning',
        message: 'Failed to fetch agent status',
      });
    }
  }
}

Example: Static demo adapter

Use this when you want to show the simulation without a real backend — useful for screenshots, onboarding flows, or development.

import { PixelOpsAdapter } from 'pixel-ops';

export class StaticDemoAdapter extends PixelOpsAdapter {
  private timers: ReturnType<typeof setInterval>[] = [];

  connect(): void {
    // Set initial states
    this.emit('agent:status', { id: 'worker-1', status: 'working', task: 'Training model' });
    this.emit('agent:status', { id: 'worker-2', status: 'idle' });
    this.emit('agent:status', { id: 'research-1', status: 'working', task: 'Researching embeddings' });
    this.emit('economy:transaction', { amount: 1000, type: 'income', reason: 'Initial credits' });

    // Simulate activity over time
    this.timers.push(
      setInterval(() => {
        const statuses = ['working', 'idle', 'error'] as const;
        const status = statuses[Math.floor(Math.random() * statuses.length)];
        this.emit('agent:status', {
          id: 'worker-1',
          status,
          task: status === 'working' ? 'Processing batch' : undefined,
        });
      }, 4000),
    );

    this.timers.push(
      setInterval(() => {
        this.emit('notification', {
          level: 'success',
          message: `Task completed in ${(Math.random() * 2 + 0.5).toFixed(1)}s`,
          agentId: 'worker-2',
        });
        this.emit('economy:transaction', {
          amount: Math.round(Math.random() * 20 + 5),
          type: 'income',
          reason: 'task-complete',
        });
      }, 6000),
    );
  }

  disconnect(): void {
    for (const t of this.timers) clearInterval(t);
    this.timers = [];
  }
}

TypeScript: importing event types

All event payload types are exported from the main package:

import type {
  AgentStatusEvent,
  AgentMoveEvent,
  NotificationEvent,
  MetricEvent,
  EconomyTransactionEvent,
  ResearchProgressEvent,
  ReputationChangeEvent,
  AdapterEventMap,
} from 'pixel-ops';

You can also use AdapterEventMap directly for type-safe generic utilities:

import { PixelOpsAdapter, type AdapterEventMap } from 'pixel-ops';

function sendEvent<K extends keyof AdapterEventMap>(
  adapter: PixelOpsAdapter,
  event: K,
  payload: AdapterEventMap[K],
) {
  // adapter.emit is protected, so call from within the subclass
}