Skip to content

Querying

Cross-session query APIs enable you to build admin dashboards, usage analytics, multi-tenant management, and monitoring tools. List sessions by user or agent type, query usage metrics across conversations, and browse memory entities — all with consistent pagination and filtering.

Shared Pagination Contract

All query methods return paginated results with a consistent structure:

typescript
interface PaginatedResult {
  total: number;        // Total count across all pages
  offset: number;       // Current offset
  limit: number;        // Requested page size
  hasMore: boolean;     // Whether more results exist
}

The limit parameter defaults to 50 and has a maximum of 200. Use offset for offset-based pagination:

typescript
// First page
const page1 = await stateStore.listSessions({ limit: 50, offset: 0 });

// Next page
const page2 = await stateStore.listSessions({ limit: 50, offset: 50 });

// Check if more pages exist
if (page1.hasMore) {
  // Load more...
}

Listing Sessions

The listSessions() method on SessionStateStore provides powerful filtering for browsing conversations. Use it to build admin dashboards, analytics views, or debug tools.

Basic Usage

typescript
import { RedisStateStore } from '@helix-agents/store-redis';
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);
const stateStore = new RedisStateStore(redis);

// List all sessions
const result = await stateStore.listSessions();
console.log(`Found ${result.total} sessions`);

for (const session of result.sessions) {
  console.log(`${session.sessionId}: ${session.agentType} - ${session.status}`);
}

Filtering Options

typescript
interface ListSessionsOptions {
  userId?: string;                           // Filter by user
  agentType?: string;                        // Filter by agent type
  status?: SessionStatus | SessionStatus[];  // Filter by status
  parentSessionId?: string | null;           // Children of parent, or null for root sessions
  tags?: string[];                           // Must have all specified tags (AND logic)
  metadata?: Record<string, string>;         // Must match all key-value pairs (AND logic)
  dateRange?: {
    from?: number;                           // Inclusive lower bound (epoch ms)
    to?: number;                             // Inclusive upper bound (epoch ms)
    field?: 'createdAt' | 'updatedAt';       // Which timestamp to filter (default: createdAt)
  };
  orderBy?: {
    field: 'createdAt' | 'updatedAt';
    direction: 'asc' | 'desc';
  };
  offset?: number;                           // Pagination offset (default: 0)
  limit?: number;                            // Page size (default: 50, max: 200)
  includeMessageCount?: boolean;             // Include message count (default: true)
}

Example Queries

typescript
// Find active sessions for a specific user
const userSessions = await stateStore.listSessions({
  userId: 'user-123',
  status: 'active',
});

// Find failed sessions in the last hour
const recentFailures = await stateStore.listSessions({
  status: 'failed',
  dateRange: {
    from: Date.now() - 3600000,
    field: 'updatedAt',
  },
});

// Find root sessions (no parent) for a specific agent type
const rootResearchers = await stateStore.listSessions({
  agentType: 'researcher',
  parentSessionId: null,
});

// Find sessions with specific tags
const taggedSessions = await stateStore.listSessions({
  tags: ['production', 'high-priority'],
});

// Find sessions with custom metadata
const customerSessions = await stateStore.listSessions({
  metadata: {
    environment: 'production',
    customerId: 'cust-456',
  },
});

// Sort by most recently updated
const recent = await stateStore.listSessions({
  orderBy: { field: 'updatedAt', direction: 'desc' },
  limit: 20,
});

SessionSummary Fields

Each summary includes lightweight metadata without the full state:

FieldTypeDescription
sessionIdstringSession identifier
agentTypestringAgent type name
statusSessionStatusCurrent session status
userIdstring?User ID if set
tagsstring[]?Tags if set
metadataRecord?Metadata key-value pairs if set
parentSessionIdstring?Parent session ID if this is a sub-agent
stepCountnumberTotal LLM call count
resumeCountnumberNumber of resume operations
messageCountnumberTotal message count
createdAtnumberCreation timestamp (epoch ms)
updatedAtnumberLast update timestamp (epoch ms)

To get the full state including customState and output, call loadState(sessionId).

Querying Usage

The queryUsage() method on UsageStore enables cross-session usage queries for cost tracking, analytics, and monitoring.

Basic Usage

typescript
import { RedisUsageStore } from '@helix-agents/store-redis';
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);
const usageStore = new RedisUsageStore(redis);

// Query all usage entries
const result = await usageStore.queryUsage({});
console.log(`Found ${result.total} usage entries`);

for (const entry of result.entries) {
  console.log(`${entry.kind} - ${entry.timestamp}`);
}

Filtering Options

typescript
interface QueryUsageOptions {
  sessionId?: string | string[];             // Filter by session(s)
  agentType?: string;                        // Filter by agent type
  userId?: string;                           // Filter by user
  kinds?: Array<'tokens' | 'tool' | 'subagent' | 'custom'>; // Filter by entry kind
  dateRange?: {
    from?: number;                           // Inclusive lower bound (epoch ms)
    to?: number;                             // Inclusive upper bound (epoch ms)
  };
  orderBy?: {
    field: 'timestamp' | 'sessionId';
    direction: 'asc' | 'desc';
  };
  offset?: number;                           // Pagination offset (default: 0)
  limit?: number;                            // Page size (default: 50, max: 200)
}

Example Queries

typescript
// Get token usage for a specific user
const userTokens = await usageStore.queryUsage({
  userId: 'user-123',
  kinds: ['tokens'],
});

// Get failed tool calls across all sessions
const failedTools = await usageStore.queryUsage({
  kinds: ['tool'],
});
const failures = failedTools.entries.filter(e => 'success' in e && !e.success);

// Get usage for multiple specific sessions
const sessions = await usageStore.queryUsage({
  sessionId: ['session-1', 'session-2', 'session-3'],
});

// Get usage in a date range
const lastWeek = await usageStore.queryUsage({
  dateRange: {
    from: Date.now() - 7 * 24 * 60 * 60 * 1000,
    to: Date.now(),
  },
  orderBy: { field: 'timestamp', direction: 'desc' },
});

// Get custom metrics for a specific agent type
const apiCalls = await usageStore.queryUsage({
  agentType: 'researcher',
  kinds: ['custom'],
});

Cost Tracking Example

Aggregate token usage across sessions for cost reporting:

typescript
async function calculateMonthlyCost(userId: string): Promise<number> {
  const startOfMonth = new Date();
  startOfMonth.setDate(1);
  startOfMonth.setHours(0, 0, 0, 0);

  const result = await usageStore.queryUsage({
    userId,
    kinds: ['tokens'],
    dateRange: { from: startOfMonth.getTime() },
  });

  const PRICING: Record<string, { input: number; output: number }> = {
    'gpt-4o': { input: 0.0025, output: 0.01 },
    'gpt-4o-mini': { input: 0.00015, output: 0.0006 },
  };

  let totalCost = 0;

  for (const entry of result.entries) {
    if (entry.kind === 'tokens') {
      const pricing = PRICING[entry.model];
      if (pricing) {
        totalCost += ((entry.tokens.prompt ?? 0) / 1000) * pricing.input;
        totalCost += ((entry.tokens.completion ?? 0) / 1000) * pricing.output;
      }
    }
  }

  return totalCost;
}

Listing Memory Entities

The listEntities() method on MemoryStore returns distinct entity/source pairs with memory counts. Use this to browse users or contexts that have stored memories.

Basic Usage

typescript
import { RedisMemoryStore } from '@helix-agents/memory-redis';
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);
const memoryStore = new RedisMemoryStore({ redis, dimensions: 1536 });
await memoryStore.initialize();

// List all entities
const result = await memoryStore.listEntities();
console.log(`Found ${result.total} entity/source pairs`);

for (const entity of result.entities) {
  console.log(`${entity.entityId} (${entity.sourceName}): ${entity.memoryCount} memories`);
}

Filtering Options

typescript
interface ListEntitiesOptions {
  sourceName?: string;                       // Filter by source name
  metadataFilter?: Record<string, unknown>;  // Filter entities with matching metadata
  orderBy?: {
    field: 'entityId' | 'memoryCount';
    direction: 'asc' | 'desc';
  };
  offset?: number;                           // Pagination offset (default: 0)
  limit?: number;                            // Page size (default: 50, max: 200)
}

EntitySummary Fields

Each summary provides aggregate information about an entity:

FieldTypeDescription
entityIdstringEntity identifier
sourceNamestringMemory source name
memoryCountnumberTotal memory count
oldestMemoryAtstringOldest memory timestamp (ISO)
newestMemoryAtstringNewest memory timestamp (ISO)

Example Queries

typescript
// List entities for a specific source
const userPrefs = await memoryStore.listEntities({
  sourceName: 'user-prefs',
});

// Sort by memory count
const mostActive = await memoryStore.listEntities({
  orderBy: { field: 'memoryCount', direction: 'desc' },
  limit: 10,
});

// Filter by metadata
const approvedEntities = await memoryStore.listEntities({
  metadataFilter: { approved: true },
});

Store Compatibility

Not all stores support all query methods. Durable Object stores throw errors for cross-session queries because DOs are scoped to a single session.

StorelistSessionsqueryUsagelistEntities
InMemoryStateStore--
InMemoryUsageStore--
InMemoryMemoryStore--
RedisStateStore--
RedisUsageStore--
RedisMemoryStore--
PostgresStateStore--
PostgresUsageStore--
D1StateStore--
D1UsageStore--
CloudflareMemoryStore--
DOStateStore--
DOUsageStore--

Attempting to call a query method on a DO store will throw an error explaining that the method is not supported in the DO environment.

Contract Tests

The framework provides contract test suites to validate store implementations:

typescript
import { memoryStoreContractTests, usageStoreContractTests } from '@helix-agents/core/testing';

// Test your memory store implementation
memoryStoreContractTests({
  name: 'MyMemoryStore',
  createStore: async () => ({ store: new MyMemoryStore() }),
  clearStore: async (store) => await store.clear(),
});

// Test your usage store implementation
usageStoreContractTests({
  name: 'MyUsageStore',
  createStore: async () => ({ store: new MyUsageStore() }),
  clearStore: async (store) => await store.clear(),
});

These suites include tests for listEntities() and queryUsage() to ensure consistent behavior across implementations.

Next Steps

Released under the MIT License.