Skip to main content

@tokenring-ai/iterables

Overview

The @tokenring-ai/iterables package provides a pluggable system for defining and using named iterables in TokenRing. Iterables are reusable data sources that can be used with the /foreach command to batch process items across various data types and sources.

This package implements a provider-based architecture where different iterable types can be registered to handle various data sources (files, JSON, CSV, APIs, database queries, etc.). It integrates seamlessly with the Token Ring agent system to provide state persistence and checkpoint recovery during batch operations.

Key Features

  • Named Iterable Management: Define, list, show, and delete named iterables with persistent state
  • Provider Architecture: Register custom iterable providers for different data sources
  • Chat Commands: /iterable and /foreach commands for managing and processing iterables
  • Template Interpolation: Support for variable interpolation in prompts using {variable} syntax
  • State Persistence: Iterables are persisted across sessions using the agent's state system
  • Checkpoint Recovery: Automatic checkpoint creation and restoration during batch processing
  • Error Handling: Graceful error handling with recovery during batch operations
  • Streaming Processing: Items are processed one at a time to minimize memory usage

Core Components

IterableService

The central service that manages all iterable operations:

class IterableService implements TokenRingService {
readonly name = "IterableService";
description = "Manages named iterables for batch operations";

// Provider registry
registerProvider: (provider: IterableProvider) => void;
getProvider: (type: string) => IterableProvider | undefined;

// Iterable management
define(name: string, type: string, spec: IterableSpec, agent: Agent): Promise<void>;
get(name: string, agent: Agent): StoredIterable | undefined;
list(agent: Agent): StoredIterable[];
delete(name: string, agent: Agent): boolean;

// Iterable generation
generate(name: string, agent: Agent): AsyncGenerator<IterableItem>;
}

IterableProvider Interface

Interface for implementing custom iterable providers:

interface IterableProvider {
readonly type: string;
readonly description: string;

getArgsConfig(): {
options: Record<string, { type: 'string' | 'boolean', multiple?: boolean }>;
};

generate(spec: IterableSpec, agent: Agent): AsyncGenerator<IterableItem>;
}

IterableState

State slice that persists iterable definitions across sessions:

class IterableState extends AgentStateSlice<typeof serializationSchema> {
readonly name = "IterableState";
serializationSchema = z.object({
iterables: z.array(z.object({
name: z.string(),
type: z.string(),
spec: z.any(),
createdAt: z.date(),
updatedAt: z.date()
}))
});
iterables: Map<string, StoredIterable> = new Map();

constructor({iterables = []}: { iterables?: StoredIterable[] } = {});
serialize(): z.output<typeof serializationSchema>;
deserialize(data: z.output<typeof serializationSchema>): void;
show(): string[];
}

Type Definitions

StoredIterable

interface StoredIterable {
name: string;
type: string;
spec: IterableSpec;
createdAt: Date;
updatedAt: Date;
}

IterableItem

interface IterableItem {
value: any;
variables: Record<string, any>;
}

IterableSpec

interface IterableSpec {
[key: string]: any;
}

IterableMetadata

interface IterableMetadata {
name: string;
type: string;
description?: string;
createdAt: Date;
updatedAt: Date;
}

Services

IterableService

The main service for managing iterables. Implements TokenRingService interface.

Provider Registration

Register custom iterable providers with the service. The provider's type property is used as the key:

app.addServices(new IterableService());
iterableService.registerProvider(fileProvider); // Provider must have type: 'file'

Define Iterables

Create named iterables with specific types and specifications:

await iterableService.define('files', 'file', {
pattern: '*.ts',
directory: 'src'
}, agent);

List Iterables

Retrieve all defined iterables:

const iterables = iterableService.list(agent);
// Returns: StoredIterable[]

Get Iterable Details

Retrieve a specific iterable by name:

const iterable = iterableService.get('files', agent);
// Returns: StoredIterable | undefined

Delete Iterables

Remove a defined iterable:

const deleted = iterableService.delete('files', agent);
// Returns: boolean (true if deleted, false if not found)

Generate Iterable Items

Process items from an iterable using an async generator:

for await (const item of iterableService.generate('files', agent)) {
console.log(item.value);
console.log(item.variables);
}

Attach to Agent

Initialize the service with an agent and register state:

const service = new IterableService();
service.attach(agent);
// Initializes IterableState for the agent

Provider Documentation

Provider Interface

All iterable providers must implement the IterableProvider interface:

interface IterableProvider {
readonly type: string; // Unique identifier for this provider type
readonly description: string; // Human-readable description

getArgsConfig(): {
options: Record<string, {
type: 'string' | 'boolean';
multiple?: boolean;
}>;
}; // Configuration schema for this provider

generate(spec: IterableSpec, agent: Agent): AsyncGenerator<IterableItem>; // Generate items
}

Provider Implementation Example

import Agent from "@tokenring-ai/agent/Agent";
import {IterableItem, IterableProvider, IterableSpec} from "@tokenring-ai/iterables";
import * as path from 'path';

class FileIterableProvider implements IterableProvider {
readonly type = 'file';
readonly description = 'File-based iterable provider';

getArgsConfig() {
return {
options: {
pattern: { type: 'string' },
directory: { type: 'string' },
recursive: { type: 'boolean' }
}
};
}

async *generate(spec: IterableSpec, agent: Agent): AsyncGenerator<IterableItem> {
// Implementation that yields IterableItem objects
const pattern = spec.pattern as string || '*.txt';
const directory = spec.directory as string || '.';
const files = await findFiles(pattern, directory, spec.recursive as boolean);

for (const file of files) {
yield {
value: file,
variables: {
file,
basename: path.basename(file),
ext: path.extname(file),
directory
}
};
}
}
}

Provider Registration

Plugin Registration

Register providers through the plugin system. The plugin automatically adds the IterableService:

// In plugin.ts
import IterableService from './IterableService.ts';

export default {
name: packageJSON.name,
version: packageJSON.version,
install(app, config) {
app.addServices(new IterableService());
// Register additional providers here if needed
}
};

Programmatic Registration

Register providers directly with the service:

const service = new IterableService();
service.registerProvider(new FileIterableProvider());

KeyedRegistry Pattern

The IterableService uses a KeyedRegistry to manage providers. Each provider is registered with its type property as the key:

class IterableService implements TokenRingService {
private providers = new KeyedRegistry<IterableProvider>();
registerProvider = this.providers.register;
getProvider = this.providers.getItemByName;
}

Provider Guidelines

getArgsConfig()

Return an object with options defining accepted arguments:

getArgsConfig() {
return {
options: {
// String argument
name: {type: 'string'},

// Boolean flag
enabled: {type: 'boolean'},

// Multiple values
tags: {type: 'string', multiple: true}
}
};
}

generate()

  • Return an AsyncGenerator<IterableItem>
  • Each item must have value and variables
  • value is the raw item data
  • variables are exposed for prompt interpolation
  • Access spec parameters directly: spec.paramName

Variables Best Practices

  • Provide intuitive variable names
  • Include both raw and formatted versions (e.g., {date} and {dateFormatted})
  • Document available variables in provider description
  • Keep variable names consistent across similar providers

Example: Database Provider

export default class SqlIterableProvider implements IterableProvider {
type = "sql";
description = "Iterate over SQL query results";

getArgsConfig() {
return {
options: {
query: {type: 'string'},
database: {type: 'string'}
}
};
}

async* generate(spec: IterableSpec, agent: Agent): AsyncGenerator<IterableItem> {
const dbService = agent.requireServiceByType(DatabaseService);
const db = dbService.getDatabase(spec.database || 'default');

const rows = await db.query(spec.query);

for (let i = 0; i < rows.length; i++) {
yield {
value: rows[i],
variables: {
row: rows[i],
rowNumber: i + 1,
totalRows: rows.length,
...rows[i] // Flatten columns as variables
}
};
}
}
}

Usage:

/iterable define users --type sql --query "SELECT * FROM users WHERE active=1"
/foreach @users "Send email to {email} for user {name}"

RPC Endpoints

This package does not define any RPC endpoints.

Chat Commands

/iterable - Manage Named Iterables

The /iterable command provides subcommands for managing iterables:

/iterable define <name> --type <type> [options]

Create a new iterable with specified type and configuration.

Syntax:

/iterable define <name> --type <type> [provider-options]

Arguments:

  • <name>: Unique name for the iterable
  • --type <type>: The iterable provider type to use (required)
  • [provider-options]: Provider-specific options (e.g., --pattern, --file)

Examples:

/iterable define files --type file --pattern "**/*.ts"
/iterable define projects --type json --file "projects.json"

Error Handling:

  • Throws CommandFailedError if name is missing
  • Throws CommandFailedError if --type is missing or invalid
  • Throws CommandFailedError if provider is not found

/iterable list

Show all defined iterables with their types.

Example:

/iterable list

Output:

Available iterables:
- @files = file
- @users = json

/iterable show <name>

Display detailed information about a specific iterable.

Syntax:

/iterable show <name>

Arguments:

  • <name>: Name of the iterable to show

Example:

/iterable show files

Output:

Iterable: @files
Type: file
Spec: {
"pattern": "**/*.ts",
"directory": "src"
}
Created: 2024-01-01T00:00:00.000Z
Updated: 2024-01-01T00:00:00.000Z

Error Handling:

  • Throws CommandFailedError if name is missing
  • Throws CommandFailedError if iterable is not found

/iterable delete <name>

Remove a defined iterable permanently.

Syntax:

/iterable delete <name>

Arguments:

  • <name>: Name of the iterable to delete

Example:

/iterable delete old-projects

Error Handling:

  • Throws CommandFailedError if name is missing
  • Throws CommandFailedError if iterable is not found

/foreach - Process Iterables with Prompts

The /foreach command processes each item in an iterable with a custom prompt:

/foreach @<iterable> <prompt>

Process each item in an iterable with a template prompt.

Syntax:

/foreach @<iterable> <prompt>

Arguments:

  • @<iterable>: Name of the iterable to process (prefixed with @)
  • <prompt>: Template prompt to execute for each item

Variable Interpolation:

  • {variable} - Access item properties
  • {variable:default} - Fallback values for missing properties
  • {nested.property} - Access nested properties with dot notation
  • {nested.property:default} - Combine nested access with fallbacks

Examples:

/foreach @files "Add comments to {file}"
/foreach @users "Welcome {name} from {city}"
/foreach @projects "Review {name}: {description:No description}"
/foreach @data "Process {nested.value:default}"

Common Use Cases:

  • Code analysis and refactoring across multiple files
  • Data processing and transformation
  • Content generation for multiple items
  • Batch operations on structured data

Important Notes:

  • The command maintains checkpoint state between iterations and restores it after processing each item
  • If an error occurs during processing of an item, a CommandFailedError is thrown with the error message
  • The final state is restored after all items are processed in the finally block

Error Handling:

  • Throws CommandFailedError if remainder is empty
  • Throws CommandFailedError if iterable name is not prefixed with @
  • Throws CommandFailedError if prompt is missing
  • Throws CommandFailedError if error occurs during item processing

Configuration

This package does not require any plugin configuration. The package configuration schema is empty:

import {z} from "zod";

const packageConfigSchema = z.object({});

No configuration is required by default. The plugin automatically:

  1. Registers chat commands (/iterable and /foreach)
  2. Adds the IterableService to the application
  3. Initializes the IterableState for each agent

Integration

Integration with Agent System

The package integrates with the Token Ring agent system by:

  1. State Management: Registers IterableState as an agent state slice for persistence
  2. Command Registration: Registers chat commands with AgentCommandService
  3. Service Registration: Implements TokenRingService for integration with the app framework

Plugin Installation

Install the plugin in your application:

import TokenRingApp from "@tokenring-ai/app";
import iterablesPlugin from "@tokenring-ai/iterables";

const app = new TokenRingApp();
app.use(iterablesPlugin);

Service Registration

The plugin automatically registers the IterableService:

// In plugin.ts
import IterableService from "./IterableService.ts";

export default {
name: packageJSON.name,
version: packageJSON.version,
install(app, config) {
app.addServices(new IterableService());
}
};

Command Registration

The plugin registers the following commands with AgentCommandService:

// In plugin.ts
import agentCommands from "./commands.ts";

export default {
install(app, config) {
app.waitForService(AgentCommandService, agentCommandService =>
agentCommandService.addAgentCommands(agentCommands)
);
}
};

State Persistence

Iterables are persisted across sessions using the agent's state system:

agent.initializeState(IterableState, {});
agent.mutateState(IterableState, (state) => {
state.iterables.set(name, iterable);
});

Checkpoint Recovery

The /foreach command uses checkpoint recovery to ensure consistent state:

const checkpoint = agent.generateCheckpoint();

try {
for await (const item of iterableService.generate(iterableName, agent)) {
// Process item with interpolated prompt
const interpolatedPrompt = interpolate(prompt, item.variables);
await runChat({ input: interpolatedPrompt, chatConfig, agent});

// Restore state before next iteration
agent.restoreState(checkpoint.state);
}
} finally {
// Restore final state
agent.restoreState(checkpoint.state);
}

Variable Interpolation

The /foreach command supports variable interpolation using the following implementation:

function interpolate(template: string, variables: Record<string, any>): string {
return template.replace(/\{([^}:]+)(?::([^}]*))?}/g, (match, key, defaultValue) => {
const value = getNestedProperty(variables, key);
return value !== undefined ? String(value) : (defaultValue || match);
});
}

function getNestedProperty(obj: any, path: string): any {
return path.split('.').reduce((current, prop) => current?.[prop], obj);
}

Interpolation Features:

  • Simple variables: {variable}
  • Default values: {variable:default}
  • Nested properties: {user.name}
  • Mixed: {nested.value:fallback}

Usage Examples

Basic Iterable Definition and Processing

import TokenRingApp from "@tokenring-ai/app";
import IterableService from "@tokenring-ai/iterables";
import Agent from "@tokenring-ai/agent";

const app = new TokenRingApp();
const service = new IterableService();
app.addServices(service);

// Register a provider
service.registerProvider({
type: 'static',
description: 'Static iterable provider',
getArgsConfig: () => ({ options: {} }),
async *generate(spec, agent) {
yield { value: 'item1', variables: { name: 'Item 1' } };
yield { value: 'item2', variables: { name: 'Item 2' } };
}
});

// Define an iterable
await service.define('items', 'static', {}, agent);

// Process the iterable
for await (const item of service.generate('items', agent)) {
console.log(item.value); // 'item1', 'item2'
console.log(item.variables.name); // 'Item 1', 'Item 2'
}

// List all iterables
const iterables = service.list(agent);
console.log(iterables); // [{ name: 'items', type: 'static', ... }]

Custom Provider Implementation

import Agent from "@tokenring-ai/agent/Agent";
import {IterableItem, IterableProvider, IterableSpec} from "@tokenring-ai/iterables";
import * as path from 'path';

class FileIterableProvider implements IterableProvider {
readonly type = 'file';
readonly description = 'File-based iterable provider';

getArgsConfig() {
return {
options: {
pattern: { type: 'string' },
directory: { type: 'string' },
recursive: { type: 'boolean' }
}
};
}

async *generate(spec: IterableSpec, agent: Agent): AsyncGenerator<IterableItem> {
const pattern = spec.pattern as string || '*.txt';
const directory = spec.directory as string || '.';
const files = await findFiles(pattern, directory, spec.recursive as boolean);

for (const file of files) {
yield {
value: file,
variables: {
file,
basename: path.basename(file),
ext: path.extname(file),
directory
}
};
}
}
}

// Register the provider
service.registerProvider(new FileIterableProvider());

// Use the provider
await service.define('sourceFiles', 'file', {
pattern: '*.ts',
directory: 'src',
recursive: true
}, agent);

Batch Processing with /foreach

// Define a JSON iterable
await service.define('users', 'json', {
file: 'users.json',
arrayPath: 'data'
}, agent);

// Process users with a prompt
await chatService.executeCommand('/foreach @users "Send welcome email to {name} at {email}"', agent);

Complex Variable Interpolation

// With nested properties and fallbacks
/foreach @projects "Project: {name}, Status: {status:Unknown}, Owner: {owner.name:Unassigned}"

Error Handling

try {
await service.define('test', 'unknown', {}, agent);
} catch (error) {
console.error(error.message); // "Unknown iterable type: unknown"
}

try {
for await (const item of service.generate('nonexistent', agent)) {
// ...
}
} catch (error) {
console.error(error.message); // "Iterable not found: nonexistent"
}

Best Practices

  1. Provider Naming: Use clear, descriptive provider names that reflect their purpose
  2. Spec Structure: Design provider specs to be flexible and extensible
  3. Variable Names: Use intuitive variable names in provider implementations
  4. Error Handling: Always handle errors gracefully in provider implementations
  5. Checkpoint Management: The framework handles checkpoints automatically - don't manually manage them in provider code
  6. Iterable Lifecycle: Define iterables before processing them with /foreach
  7. State Persistence: Understand that iterables persist across sessions
  8. Naming Conventions: Use meaningful names for iterables that describe their content
  9. Sequential Processing: Items are processed one at a time to minimize memory usage
  10. Template Design: Use clear, descriptive variable names in prompt templates

Testing

The package includes comprehensive unit and integration tests using Vitest:

# Run all tests
bun run test

# Run tests in watch mode
bun run test:watch

# Run tests with coverage
bun run test:coverage

# Type check
bun run build

Test Files

  • test/commands.test.ts - Unit tests for chat commands
  • test/integration.test.ts - Integration tests for full workflows
  • test/IterableProvider.test.ts - Provider-specific tests
  • test/IterableState.test.ts - State management tests

Testing Provider Implementations

import {describe, it, expect} from 'vitest';
import IterableService from '../IterableService.ts';
import type {IterableProvider, IterableSpec} from '../IterableProvider.ts';
import Agent from '@tokenring-ai/agent/Agent';

class TestProvider implements IterableProvider {
readonly type = 'test';
readonly description = 'Test provider';

getArgsConfig() {
return { options: {} };
}

async *generate(spec, agent) {
yield { value: 'test', variables: { data: 'value' } };
yield { value: 'test2', variables: { data: 'value2' } };
}
}

describe('IterableService', () => {
it('should register and use a provider', async () => {
const service = new IterableService();
service.registerProvider(new TestProvider());

const mockAgent = {
initializeState: () => {},
getState: () => ({ iterables: new Map() }),
mutateState: () => {},
requireServiceByType: () => service
} as any;

await service.define('test-iterable', 'test', {}, mockAgent);
const items = [];
for await (const item of service.generate('test-iterable', mockAgent)) {
items.push(item);
}

expect(items).toHaveLength(2);
expect(items[0].variables.data).toBe('value');
});
});

Performance Considerations

  • Streaming processing: Items are processed one at a time to minimize memory usage
  • State checkpoints: Maintains state between iterations for consistency
  • Error isolation: Errors in one item can be handled without affecting others
  • Provider efficiency: Providers should implement efficient data access patterns
  • Persistence: Iterables are persisted across agent resets

Dependencies

Production Dependencies

  • @tokenring-ai/app (0.2.0) - Core application framework
  • @tokenring-ai/agent (0.2.0) - Agent system and state management
  • @tokenring-ai/chat (0.2.0) - Chat service integration
  • @tokenring-ai/utility (0.2.0) - Utility functions and providers
  • zod (^4.3.6) - Schema validation

Development Dependencies

  • vitest (^4.1.0) - Testing framework
  • typescript (^5.9.3) - TypeScript compiler
  • @tokenring-ai/agent - Agent system for state management and command execution
  • @tokenring-ai/chat - Chat service for prompt execution
  • @tokenring-ai/app - Base application framework
  • @tokenring-ai/utility - Utility functions including KeyedRegistry

License

MIT License - see LICENSE file for details.