@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:
/iterableand/foreachcommands 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
valueandvariables valueis the raw item datavariablesare 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
CommandFailedErrorif name is missing - Throws
CommandFailedErrorif--typeis missing or invalid - Throws
CommandFailedErrorif 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
CommandFailedErrorif name is missing - Throws
CommandFailedErrorif 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
CommandFailedErrorif name is missing - Throws
CommandFailedErrorif 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
CommandFailedErroris thrown with the error message - The final state is restored after all items are processed in the
finallyblock
Error Handling:
- Throws
CommandFailedErrorif remainder is empty - Throws
CommandFailedErrorif iterable name is not prefixed with @ - Throws
CommandFailedErrorif prompt is missing - Throws
CommandFailedErrorif 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:
- Registers chat commands (
/iterableand/foreach) - Adds the IterableService to the application
- Initializes the IterableState for each agent
Integration
Integration with Agent System
The package integrates with the Token Ring agent system by:
- State Management: Registers
IterableStateas an agent state slice for persistence - Command Registration: Registers chat commands with
AgentCommandService - Service Registration: Implements
TokenRingServicefor 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
- Provider Naming: Use clear, descriptive provider names that reflect their purpose
- Spec Structure: Design provider specs to be flexible and extensible
- Variable Names: Use intuitive variable names in provider implementations
- Error Handling: Always handle errors gracefully in provider implementations
- Checkpoint Management: The framework handles checkpoints automatically - don't manually manage them in provider code
- Iterable Lifecycle: Define iterables before processing them with
/foreach - State Persistence: Understand that iterables persist across sessions
- Naming Conventions: Use meaningful names for iterables that describe their content
- Sequential Processing: Items are processed one at a time to minimize memory usage
- 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 commandstest/integration.test.ts- Integration tests for full workflowstest/IterableProvider.test.ts- Provider-specific teststest/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 providerszod(^4.3.6) - Schema validation
Development Dependencies
vitest(^4.1.0) - Testing frameworktypescript(^5.9.3) - TypeScript compiler
Related Components
@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.