Skip to main content
This appendix provides a quick reference for Idyllic constructs used throughout the book. Idyllic is notation for exploring ideas—the principles transfer to any implementation.

Core Decorators

@field

Marks a property as persisted and client-synced state.
export default class MySystem extends AgenticSystem {
  @field count = 0;                          // Primitive
  @field items: string[] = [];               // Array
  @field config: Record<string, any> = {};   // Object
  @field status: stream<string>('');         // Streaming value
}
Behavior:
  • Automatically persisted across Durable Object hibernation
  • Changes broadcast to all connected clients
  • Supports all JSON-serializable types

@action()

Marks a method as callable from outside the system.
@action()
async processInput(input: string): Promise<string> {
  // Implementation
  return result;
}
Behavior:
  • Callable via WebSocket from clients
  • Typed parameters and return values
  • Automatically generates client types

@tool()

Marks a method as available to the LLM for invocation.
@tool({
  description: 'Search the web for information on a topic'
})
async searchWeb(query: string): Promise<SearchResult[]> {
  // Implementation
}
Parameters:
  • description: Explains what the tool does (shown to LLM)
  • Optional: permission: 'always' | 'confirm' | 'never'

@schedule()

Marks a method for periodic or scheduled execution.
@schedule('every 5 minutes')
async checkForUpdates() {
  // Runs every 5 minutes
}

@schedule('every day at 2am')
async dailyMaintenance() {
  // Runs once daily at 2 AM
}
Schedule formats:
  • 'every N minutes'
  • 'every N hours'
  • 'every day at HH:MM'
  • 'every monday at HH:MM'

Streaming Values

stream<T>()

Creates a streaming value that can be appended to incrementally.
@field output: stream<string>('');
@field items: stream<Item[]>([]);

// In a method:
this.output.append('chunk');       // Add to string
this.output.set('new value');      // Replace entirely
this.output.reset('');             // Clear to initial
const value = this.output.current(); // Get current value

this.items.set([...items, newItem]); // Update array
Use cases:
  • Token-by-token LLM output
  • Progress indicators
  • Real-time logs

Vector Store

VectorStore<T>

Semantic storage with similarity search.
@field knowledge: VectorStore<{
  content: string;
  source: string;
  timestamp: number;
}>;

// Insert
await this.knowledge.insert({
  content: 'fact about topic',
  source: 'documentation',
  timestamp: Date.now()
});

// Search
const results = await this.knowledge.search(query, 5);
// Returns: { item: T, similarity: number }[]

// Get by ID
const item = await this.knowledge.get(id);

// Update
await this.knowledge.update(id, newItem);

// Delete
await this.knowledge.delete(id);

System Lifecycle

Initialization

export default class MySystem extends AgenticSystem {
  async initialize() {
    // Called once when system first created
    // Set up initial state
  }
}

State Access

// Access system ID (stable across restarts)
const id = this.id;

// Access environment
const apiKey = this.env.API_KEY;

State Synchronization

@action()
async longRunningTask() {
  this.status = 'starting';
  sync(); // Broadcast current state to clients

  await this.doWork();

  this.status = 'complete';
  sync(); // Broadcast again
}

Client Integration

React Hook

import { useSystem } from '@idyllic/react';

function MyComponent() {
  const {
    count,           // Field values
    status,          // Streaming value
    increment,       // Action method
    isConnected,     // Connection state
    error            // Any errors
  } = useSystem<MySystemClient>();

  return (
    <div>
      <p>Count: {count}</p>
      <p>Status: {status}</p>
      <button onClick={() => increment()}>+1</button>
    </div>
  );
}

Streaming Values in React

function StreamingOutput() {
  const { output } = useSystem<MySystemClient>();

  // output updates in real-time as tokens arrive
  return <pre>{output}</pre>;
}

LLM Integration

Basic Completion

const response = await llm.complete([
  { role: 'system', content: 'You are a helpful assistant.' },
  { role: 'user', content: prompt }
]);

With Tools

const response = await llm.complete(messages, {
  tools: [this.searchWeb, this.readFile]
});

// Response may include tool calls
if (response.toolCalls) {
  for (const call of response.toolCalls) {
    const result = await this[call.name](...call.arguments);
    messages.push({
      role: 'tool',
      content: JSON.stringify(result),
      toolCallId: call.id
    });
  }
}

Streaming Completion

for await (const chunk of llm.stream(messages)) {
  this.output.append(chunk);
}
this.output.complete();

Complete Example

import { AgenticSystem, field, action, tool, schedule, stream, VectorStore } from '@idyllic/core';

export default class ResearchAssistant extends AgenticSystem {
  // Persisted state
  @field messages: Message[] = [];
  @field knowledge: VectorStore<Knowledge>;

  // Streaming output
  @field thinking: stream<string>('');
  @field response: stream<string>('');

  // Configuration
  @field systemPrompt = 'You are a research assistant...';

  async initialize() {
    this.knowledge = new VectorStore();
  }

  @action()
  async chat(input: string) {
    this.messages.push({ role: 'user', content: input });
    this.thinking.reset('');
    this.response.reset('');

    // Retrieve relevant knowledge
    const relevant = await this.knowledge.search(input, 5);
    this.thinking.append('Found ' + relevant.length + ' relevant items\n');

    // Generate response
    for await (const chunk of llm.stream([
      { role: 'system', content: this.systemPrompt },
      { role: 'system', content: 'Relevant knowledge:\n' + relevant.map(r => r.item.content).join('\n') },
      ...this.messages
    ])) {
      this.response.append(chunk);
    }

    this.messages.push({ role: 'assistant', content: this.response.current() });
  }

  @tool({ description: 'Search the web' })
  async search(query: string): Promise<SearchResult[]> {
    // Implementation
  }

  @schedule('every day at 3am')
  async maintenance() {
    // Clean up old messages
    if (this.messages.length > 100) {
      this.messages = this.messages.slice(-50);
    }
  }
}

interface Message {
  role: 'user' | 'assistant' | 'system';
  content: string;
}

interface Knowledge {
  content: string;
  source: string;
}

interface SearchResult {
  title: string;
  url: string;
  snippet: string;
}