Skip to content

Querying ​

Cross-session query APIs enable you to build admin dashboards, usage analytics, 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 / retry entries¹
messageCountnumberTotal message count
createdAtnumberCreation timestamp (epoch ms)
updatedAtnumberLast update timestamp (epoch ms)

¹ Counted once by the winning run start of each resume() / retry() entry (a refused or losing call never counts). Cloudflare Workflows, Temporal and DBOS maintain it. A parent's re-spawn of a suspended sub-agent is not a resume() call and does not count, and neither does a DBOS resume() that attaches to the session's still-running workflow (no new entry). runtime-js, and the Cloudflare Durable Object runtime that runs its loop, have never counted it, so it reads 0 there.

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

Querying Runs ​

Every execute(), resume() and retry() creates a run in its run-start commit, so a session's runs are an exact history of its entries. Read them per session:

typescript
// The session's current (latest) run: the one that owns the session now
const current = await stateStore.getCurrentRun(sessionId);

// One run by id
const run = await stateStore.getRun(runId);

// Every run of the session, ordered by turn
const { runs, total } = await stateStore.listRuns(sessionId, { status: 'failed', limit: 20 });

A run's status is one of:

StatusMeaning
runningExecuting. Live.
suspended_client_tool, suspended_awaiting_children, suspended_step_partialWaiting on client-tool / approval submissions or sub-agents. Live.
completed, failed, interruptedEnded. Terminal.
supersededA later entry took the session over (a resume after a suspension, or a takeover from a dead owner). Terminal; not a failure.

isLiveRunStatus(status) (from @helix-agents/core) tells live from terminal. A terminal status never changes again. RunMetadata also records the run's boundaries, fixed by its run-start commit: startSequence (the stream head at the commit), startMessageCount and startUIMessageCount, and turn. These methods work on every built-in store, including DOStateStore inside the Durable Object (for its own session). From outside the object, DOStateStoreClient offers getCurrentRun only.

To read the session's checkpoints, use getLatestCheckpoint / listCheckpoints / getCheckpoint; each checkpoint names the commit that wrote it (kind) and its run (runId). See Checkpoints. To read a consistent view of a session for a UI (messages, state, status and stream cursor from one checkpoint), use a snapshot; see Resumable Streams.

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-6.1-sol': { input: 0.002, output: 0.01 },
    'gpt-6-luna': { input: 0.0001, output: 0.0005 },
  };

  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.