Pluggable Interfaces

Interoception follows Cadence's philosophy: build for sophistication, implement simple. The two main interfaces you need to implement are Embedder and StateProvider.

Embedder

interface Embedder {
  embed(text: string): Promise<number[]>;
  embedBatch(texts: string[]): Promise<number[][]>;
  readonly dimensions: number;
}

The embedder converts text to vectors. The sensor calls embedBatch for efficiency — it deduplicates all texts before embedding.

OpenAI Example

import type { Embedder } from "@peleke.s/interoception";
import OpenAI from "openai";

function createOpenAIEmbedder(model = "text-embedding-3-small"): Embedder {
  const client = new OpenAI();
  return {
    dimensions: 1536,
    async embed(text) {
      const res = await client.embeddings.create({ model, input: text });
      return res.data[0]!.embedding;
    },
    async embedBatch(texts) {
      const res = await client.embeddings.create({ model, input: texts });
      return res.data.map((d) => d.embedding);
    },
  };
}

Test/Stub Embedder

For testing, use a deterministic embedder:

const stubEmbedder: Embedder = {
  dimensions: 3,
  async embed(text) {
    // Simple hash-based deterministic embedding
    const hash = [...text].reduce((h, c) => h + c.charCodeAt(0), 0);
    return [Math.sin(hash), Math.cos(hash), Math.sin(hash * 2)];
  },
  async embedBatch(texts) {
    return Promise.all(texts.map((t) => this.embed(t)));
  },
};

StateProvider

interface StateProvider {
  getGoals(): Promise<string[]>;
  getRecentContext(): Promise<string[]>;
  getGoalRelevantMemories(): Promise<string[]>;
  getAllMemories(): Promise<string[]>;
}

The state provider exposes your agent's internal state. Each method returns an array of strings that the sensor will embed.

What Each Method Provides

Method Returns Used By
getGoals() Current goals/objectives Goal drift, Memory retention
getRecentContext() Recent conversation, observations Goal drift, Contradiction pressure, Semantic diffusion
getGoalRelevantMemories() Memories relevant to current goals Memory retention
getAllMemories() All available memories (Available in MetricInput)

Implementation Tips

  • Return meaningful strings: the quality of coherence readings depends on the quality of state text
  • Keep it current: return the agent's current state, not historical state
  • Be selective: return the most relevant items, not everything. 5-20 items per method is typical
  • Async is fine: all methods are async — query databases, APIs, or caches as needed

Example with a Memory Store

import type { StateProvider } from "@peleke.s/interoception";

function createStateProvider(agent: MyAgent): StateProvider {
  return {
    async getGoals() {
      return agent.activeGoals.map((g) => g.description);
    },
    async getRecentContext() {
      const turns = await agent.conversation.getRecent(10);
      return turns.map((t) => t.content);
    },
    async getGoalRelevantMemories() {
      const goals = agent.activeGoals.map((g) => g.description);
      return agent.memory.searchByRelevance(goals, 10);
    },
    async getAllMemories() {
      return agent.memory.getAll();
    },
  };
}

Design Principles

Both interfaces follow the same pattern:

  • Consumer implements — interoception defines the shape, your code fills it in
  • Async by default — real implementations will hit APIs, databases, or models
  • No defaults — there's no "default embedder" because the right choice depends on your use case
  • Composable — wrap, cache, or layer implementations as needed

See Also