@roostjs/ai

Laravel-ergonomics AI agents for Cloudflare Workers. Class-based agents with tools, streaming, sessions, workflows, MCP, HITL, memory, and multi-provider failover.

@roostjs/ai blends Laravel 13's AI SDK ergonomics with Cloudflare Agents SDK primitive semantics. Every primitive composes — a StatefulAgent decorated with @Stateful({binding}) unlocks Sessions, Schedule, Workflows, sub-agents, MCP, and HITL without additional wiring.

Installation

bun add @roostjs/ai @roostjs/schema

Subpaths (opt-in per feature):

import { Agent } from '@roostjs/ai';
import { StatefulAgent } from '@roostjs/ai/stateful';
import { RAGPipeline, Files, Stores, Reranking } from '@roostjs/ai/rag';
import { Image, Audio, Transcription } from '@roostjs/ai/media';
import { McpClient, createMcpHandler, McpPortal } from '@roostjs/ai/mcp';
import { useAgent, useAgentStream } from '@roostjs/ai/client';
import { requireApproval, approve } from '@roostjs/ai/hitl';
import { Memory } from '@roostjs/ai/memory';
import { chargeForTool, payAgent } from '@roostjs/ai/payments';
import { Voice } from '@roostjs/ai/voice';
import { Email, createEmailHandler } from '@roostjs/ai/email';
import { Browser } from '@roostjs/ai/browser';
import { runCodeMode, CodeMode } from '@roostjs/ai/code-mode';

Configuration

Register AiServiceProvider in your Roost app config. binding is the Workers AI binding name; providers configures native adapters; default picks the failover order used by agents without an explicit @Provider.

import { AiServiceProvider, Lab } from '@roostjs/ai';

export default {
  providers: [AiServiceProvider],
  ai: {
    binding: 'AI',
    gateway: { accountId: env.CF_ACCOUNT_ID, gatewayId: 'roost-ai' },
    providers: {
      anthropic: { apiKey: env.ANTHROPIC_KEY },
      openai: { apiKey: env.OPENAI_KEY, organization: env.OPENAI_ORG },
      gemini: { apiKey: env.GEMINI_KEY },
    },
    default: [Lab.Anthropic, Lab.OpenAI],
  },
};

The AI binding must also be declared in wrangler.jsonc:

{
  "ai": { "binding": "AI" }
}

Custom Base URLs

Route external providers through Cloudflare AI Gateway for observability, caching, and rate limiting:

ai: {
  gateway: {
    accountId: 'your-cf-account-id',
    gatewayId: 'your-gateway-id',
  },
}

When gateway is configured, external providers (OpenAI, Anthropic, Gemini) route through Gateway by default. Workers AI stays direct via the binding to preserve zero-hop latency.

Provider Support

ProviderTransportStreamingToolsStructured
Workers AIdirect binding
AnthropicGateway or native
OpenAIGateway or native
GeminiGateway or native

Providers implement the AIProvider interface from ./providers/interface.js. Built-in implementations are WorkersAIProvider, GatewayAIProvider, AnthropicProvider, OpenAIProvider, GeminiProvider, and FailoverProvider (all exported from @roostjs/ai).

Agents

Agent is an abstract base class. Extend it, implement instructions(), and opt into capabilities by implementing contract interfaces (Conversational, HasTools, HasStructuredOutput, HasMiddleware, HasProviderOptions).

Prompting

import { Agent } from '@roostjs/ai';

class SupportAgent extends Agent {
  instructions() { return 'You are a support bot.'; }
}

const response = await new SupportAgent().prompt('How do I reset my password?');
console.log(response.text);       // Final model text
console.log(response.toolCalls);  // Any tool calls made during the loop
console.log(response.messages);   // Full message history

prompt() enters the agentic loop: the agent sends the current message history to the model, executes any tool calls returned, appends results, and repeats until the model emits a final response or maxSteps is reached.

Conversation Context (Sessions)

For durable conversation state across requests, extend StatefulAgent and apply RemembersConversations:

import { StatefulAgent, RemembersConversations } from '@roostjs/ai/stateful';
import { Stateful } from '@roostjs/ai';

@Stateful({ binding: 'SUPPORT_AGENT' })
class Support extends RemembersConversations(StatefulAgent) {
  instructions() { return 'help'; }
}

Conversations persist in Durable Object storage with tree-structured history, compaction, and full-text search. See Stateful Agents.

Import from @roostjs/ai/stateful.

Structured Output

Implement HasStructuredOutput to constrain the model output to a schema. prompt() returns a StructuredAgentResponse with a data field validated against the schema.

import { Agent, type HasStructuredOutput } from '@roostjs/ai';
import type { schema } from '@roostjs/schema';

class ReportAgent extends Agent implements HasStructuredOutput {
  instructions() { return 'Extract structured data.'; }
  schema(s: typeof schema) {
    return { summary: s.string(), tags: s.array().items(s.string()) };
  }
}

const { data } = await new ReportAgent().prompt('Analyze: ...');
console.log(data.tags);

Attachments

Attach images or documents to a prompt via Files.Image or Files.Document constructors from @roostjs/ai/rag. Supported sources: URL, path, buffer, blob, base64, and uploaded file.

import { Files } from '@roostjs/ai/rag';

const image = await Files.Image.fromUrl('https://example.com/diagram.png');
await new VisualAgent().prompt('Describe this', { attachments: [image] });

Streaming

agent.stream(input) returns a StreamableAgentResponse that is both an async iterable of StreamEvent values and a Response-compatible body. Narrow on event.type before reading payload fields.

const response = new Support().stream('hi');

for await (const event of response) {
  if (event.type === 'text-delta') process.stdout.write(event.text);
  if (event.type === 'tool-call') console.log('tool:', event.name);
  if (event.type === 'done') break;
}

Use the Vercel AI SDK data protocol when streaming to a useChat-compatible client:

return new Support().stream('hi').usingVercelDataProtocol();

StreamEvent is a discriminated union:

type StreamEvent =
  | { type: 'text-delta'; text: string }
  | { type: 'tool-call'; id: string; name: string; arguments: Record<string, unknown> }
  | { type: 'tool-result'; toolCallId: string; content: string }
  | { type: 'usage'; promptTokens: number; completionTokens: number }
  | { type: 'error'; message: string; code?: string }
  | { type: 'done' };

Broadcasting

Fan a streamed agent response out to every connected WebSocket client by wrapping each StreamEvent in a BroadcastableEvent and dispatching it through BroadcastManager:

import { BroadcastManager, Channel, type BroadcastableEvent } from '@roostjs/broadcast';
import type { StreamEvent } from '@roostjs/ai';

class StreamEventBroadcast implements BroadcastableEvent {
  constructor(
    private readonly event:    StreamEvent,
    private readonly channels: Channel[],
  ) {}
  broadcastOn()   { return this.channels; }
  broadcastWith() { return this.event as unknown as Record<string, unknown>; }
  broadcastAs()   { return 'ai.stream'; }
}

const manager  = BroadcastManager.get();
const channels = [new Channel('conv:42')];

for await (const event of new Support().stream('hi')) {
  await manager.broadcast(new StreamEventBroadcast(event, channels));
}

@roostjs/broadcast must be configured at app boot for BroadcastManager.get() to resolve. See @roostjs/broadcast reference for channel configuration and DO setup.

Queueing

.queue(input) bridges to @roostjs/queue. Returns a QueuedPromptHandle that is thenable — await it to resolve the final AgentResponse.

const handle = new ReportAgent()
  .queue('Analyze Q3 data')
  .then((response) => store(response.text));

Add queue metadata via decorators:

import { Queue, MaxRetries, Backoff, JobTimeout } from '@roostjs/ai';

@Queue('ai')
@MaxRetries(3)
@Backoff('exponential')
@JobTimeout(300)
class ReportAgent extends Agent { /* ... */ }

Tools

Tools are classes implementing the Tool interface. Return tool instances from tools() on an agent that implements HasTools.

import { Agent, type HasTools, type Tool, type ToolRequest } from '@roostjs/ai';
import type { schema } from '@roostjs/schema';

class LookupTool implements Tool {
  name() { return 'lookup'; }
  description() { return 'Look up a user by id'; }
  schema(s: typeof schema) { return { id: s.string() }; }
  async handle(request: ToolRequest) { return queryDB(request.get<string>('id')); }
}

class Support extends Agent implements HasTools {
  tools() { return [new LookupTool(), new WebSearch()]; }
}

Provider tools (WebSearch, WebFetch, FileSearch) are native provider capabilities exposed as tool instances — the provider executes them, the handler is never invoked locally.

Middleware

Wrap every prompt/response in a middleware chain. Each middleware receives the current AgentPrompt and a next function; call next(prompt) to continue the chain.

import { Agent, type HasMiddleware, type AgentMiddleware } from '@roostjs/ai';

class Support extends Agent implements HasMiddleware {
  middleware(): AgentMiddleware[] {
    return [
      async (prompt, next) => {
        logPrompt(prompt);
        const response = await next(prompt);
        metrics.record(response.usage);
        return response;
      },
    ];
  }
}

Anonymous Agents

Create a one-off agent without a class via the agent() factory. Supports instructions, messages, tools, schema, middleware, providerOptions, and provider. Returns an object with a single prompt() method.

import { agent } from '@roostjs/ai';

const quick = agent({
  instructions: 'Be terse.',
  tools: [new LookupTool()],
});

await quick.prompt('hi');

Agent Configuration (decorators)

Class decorators configure model selection, limits, and runtime behaviour.

DecoratorPurpose
@Provider(lab)Choose a provider. Pass an array to enable failover.
@Model(name)Provider-scoped model name, e.g. 'anthropic/claude-4-opus'.
@MaxSteps(n)Maximum tool-call iterations per prompt().
@MaxTokens(n)Per-call token cap on the response.
@Temperature(t)Sampling temperature (0–1).
@Timeout(ms)Single-call timeout.
@UseCheapestModel(lab?)Auto-select cheapest capable model.
@UseSmartestModel(lab?)Auto-select smartest capable model.
@Stateful({binding})Mark as DO-backed (see Stateful Agents).
@Scheduled(cron)Cron-triggered method on a StatefulAgent.
@Queue(name)Queue binding for .queue().
@MaxRetries(n)Queued-job retry limit.
@RetryAfter(ms)Queued-job fixed backoff.
@Backoff(strategy)Queued-job backoff strategy ('exponential' / 'linear').
@JobTimeout(s)Queued-job visibility timeout.
@SubAgentCapable()Allow a StatefulAgent to spawn sub-agents.
@RequiresApproval(step)Require HITL approval before a method runs.
@CodeMode()Execute a method through the CodeMode sandbox.
import { Agent, Provider, Model, MaxTokens, Temperature, Timeout, Lab } from '@roostjs/ai';

@Provider([Lab.Anthropic, Lab.OpenAI])
@Model('anthropic/claude-4-opus')
@MaxTokens(4096)
@Temperature(0.3)
@Timeout(30_000)
class Precise extends Agent {
  instructions() { return 'Be precise.'; }
}

Provider Options

Implement HasProviderOptions to return a per-provider options object. Unknown keys for the active provider are dropped silently (the UnsupportedOptionDropped event fires for observability).

import { Agent, type HasProviderOptions, Lab } from '@roostjs/ai';

class Support extends Agent implements HasProviderOptions {
  providerOptions(provider: Lab | string) {
    if (provider === Lab.Anthropic) return { reasoning: { maxTokens: 8192 } };
    return {};
  }
}

Images

Generate images via the Image.of(prompt) builder. Chain aspect ratio and quality, then call .generate() to get an ImageResponse.bytes is a Uint8Array; .store({ disk }) writes to the configured media-storage resolver (R2 by default).

import { Image } from '@roostjs/ai/media/image';

const image = await Image.of('a happy dog').square().quality('high').generate();
const bytes: Uint8Array = image.bytes;
await image.store({ disk: 'R2_IMAGES' });

Import from @roostjs/ai/media/image.

Audio (TTS)

Synthesise speech via the Audio.of(text) builder.

import { Audio } from '@roostjs/ai/media/audio';

const mp3 = await Audio.of('Hello world').female().voice('warm').generate();

Import from @roostjs/ai/media/audio.

Transcription (STT)

Transcribe an audio source via Transcription.fromStorage, fromPath, or fromUpload.

import { Transcription } from '@roostjs/ai/media/transcription';

const { text, segments } = await Transcription
  .fromStorage('call.wav', { disk: 'R2_AUDIO' })
  .diarize()
  .generate();

Import from @roostjs/ai/media/transcription. Other sources: Transcription.fromPath(path), Transcription.fromUpload(blob), Transcription.fromString(bytes, mimeType).

Embeddings

Embed text via the EmbeddingPipeline. The constructor takes an AIClient, an optional model, and options for KV-backed caching. Use Str.toEmbeddings for one-off embeddings against the registered default pipeline.

import { EmbeddingPipeline, EmbeddingCache, Str } from '@roostjs/ai/rag';
import { AIClient, KVStore } from '@roostjs/cloudflare';

const pipeline = new EmbeddingPipeline(
  new AIClient(env.AI),
  '@cf/baai/bge-base-en-v1.5',
  {
    cache:           new EmbeddingCache(new KVStore(env.KV)),
    cacheTtlSeconds: 30 * 24 * 3600,
  },
);

const vectors = await pipeline.embed(['hi', 'bye']);
const vec = await Str.toEmbeddings('single doc');

Import from @roostjs/ai/rag.

Reranking

Rerank retrieval hits via Reranking.of(hits).rerank(). Ships Cohere and Jina adapters; register others via registerReranker.

import { Reranking } from '@roostjs/ai/rag';

const reranked = await Reranking.of(hits).usingCohere().rerank();

Files

Files.store persists a StorableFile through the registered adapter and returns a FileRecord. Delete by id via Files.delete(id). Attach the record to a prompt via attachments, or pass record.id to a Store to index it.

import { Files } from '@roostjs/ai/rag';
import { Document } from '@roostjs/ai';

const file = Document.fromUpload(blob, { name: 'doc.pdf' });
const stored = await Files.store(file);
await Files.delete(stored.id);

FilesStoreOptions accepts { provider?, purpose? } for routing to a non-default adapter. Constructors on Files.Image and Files.Document cover URL, path, buffer, blob, base64, and upload sources.

Vector Stores

Stores.create(name) returns a VectorStoreHandle — an accessor over a named region of the configured Vectorize index. Call configureStores({ index, embeddings, namespacePrefix }) once at app boot. handle.add accepts a StorableFile or a known file id; handle.remove takes a file id.

import { Stores, configureStores, EmbeddingPipeline } from '@roostjs/ai/rag';
import { AIClient, VectorStore } from '@roostjs/cloudflare';

// Once at app boot:
configureStores({
  index:           new VectorStore(env.VECTORIZE),
  embeddings:      new EmbeddingPipeline(new AIClient(env.AI)),
  namespacePrefix: 'stores',
});

const store = await Stores.create('legal-docs');
await store.add(stored.id, { metadata: { dept: 'legal' } });
await store.remove(stored.id);

Query via RAGPipeline (constructed with a store, embeddings, and a chunker):

import { RAGPipeline, SemanticChunker } from '@roostjs/ai/rag';

const pipeline = new RAGPipeline(
  new VectorStore(env.VECTORIZE),
  new EmbeddingPipeline(new AIClient(env.AI)),
  new SemanticChunker(),
  { namespace: 'legal-docs' },
);
const hits = await pipeline.query('refund policy', { topK: 5 });

Failover

Wrap an ordered list of providers in FailoverProvider. Each provider is tried in sequence on transient errors; all-failed raises AllProvidersFailedError and dispatches AllProvidersFailed.

import {
  FailoverProvider,
  AnthropicProvider,
  WorkersAIProvider,
} from '@roostjs/ai';
import { AIClient } from '@roostjs/cloudflare';

const provider = new FailoverProvider([
  new AnthropicProvider({ apiKey }),
  new WorkersAIProvider(new AIClient(env.AI)),
]);

The decorator form registers the failover list at the class level:

import { Provider, Lab } from '@roostjs/ai';

@Provider([Lab.Anthropic, Lab.WorkersAI])
class Support extends Agent { /* ... */ }

Testing

Fakes replace provider mocking. fake() attaches canned responses; restore() removes the fake. Auto-generated structured-output fakes use the agent's schema when no explicit response is supplied.

import { SupportAgent } from './support.js';
import { Image } from '@roostjs/ai/media/image';
import { Files } from '@roostjs/ai/rag';

SupportAgent.fake(['canned reply', (p) => `echo: ${p.input}`]);
Image.fake();
Files.fake();

await new SupportAgent().prompt('hi');
SupportAgent.assertPrompted('hi');
Image.assertNothingGenerated();

SupportAgent.restore();
Image.restore();

preventStrayPrompts() throws on any prompt() call not matched by a canned response — useful as a final guard in CI.

SupportAgent.fake(['canned']).preventStrayPrompts();

For Durable Object-backed agents, use TestStatefulAgentHarness:

import { TestStatefulAgentHarness } from '@roostjs/ai/testing';

const harness = TestStatefulAgentHarness
  .for(Support)
  .withSessions()
  .withMockClock()
  .withEnv({ SOMETHING: 'x' })
  .build();

Import testing helpers from @roostjs/ai/testing.

Events

Every primitive dispatches an Event class via @roostjs/events. Register listener classes on an EventServiceProvider; each listener exposes a handle(event) method that receives a typed instance.

import { EventServiceProvider } from '@roostjs/events';
import type { EventClass, ListenerClass } from '@roostjs/events';
import {
  PromptingAgent,
  AgentPrompted,
  ProviderFailoverTriggered,
} from '@roostjs/ai';

class LogPrompting {
  handle(e: PromptingAgent) { console.log('about to prompt', e.prompt); }
}

class RecordUsage {
  handle(e: AgentPrompted) { metrics.record(e.response.usage); }
}

class AlertFailover {
  handle(e: ProviderFailoverTriggered) { alerts.send(e.cause); }
}

export class AiEventsProvider extends EventServiceProvider {
  protected listen(): Map<EventClass<any>, ListenerClass[]> {
    return new Map<EventClass<any>, ListenerClass[]>([
      [PromptingAgent,            [LogPrompting]],
      [AgentPrompted,             [RecordUsage]],
      [ProviderFailoverTriggered, [AlertFailover]],
    ]);
  }
}

For one-off registration (tests, scripts) use the dispatcher directly:

import { EventDispatcher } from '@roostjs/events';

EventDispatcher.get().registerListener(PromptingAgent, LogPrompting);

30+ event classes cover prompting, streaming, tools, providers, RAG, media, HITL, payments, voice, email, browser, and CodeMode. See src/events.ts and src/advanced-events.ts in the package for the full inventory.

Stateful Agents

StatefulAgent is a Durable Object-backed agent base class. It implements the DurableObject interface directly — no SDK inheritance — and unlocks Sessions, Schedule, Workflows, sub-agents, MCP, HITL, and Memory.

import { StatefulAgent } from '@roostjs/ai/stateful';
import { Stateful } from '@roostjs/ai';

@Stateful({ binding: 'SUPPORT_AGENT' })
class Support extends StatefulAgent {
  instructions() { return 'help'; }
}

Declare the binding in wrangler.jsonc:

{
  "durable_objects": {
    "bindings": [
      { "name": "SUPPORT_AGENT", "class_name": "Support" }
    ]
  },
  "migrations": [
    { "tag": "v1", "new_sqlite_classes": ["Support"] }
  ]
}

Sessions

Apply the RemembersConversations mixin to auto-persist tree-structured message history. Conversations support compaction and FTS:

import { StatefulAgent, RemembersConversations } from '@roostjs/ai/stateful';
import { Stateful } from '@roostjs/ai';

@Stateful({ binding: 'SUPPORT_AGENT' })
class Support extends RemembersConversations(StatefulAgent) { /* ... */ }

Schedule

Schedule one-off methods with agent.schedule(when, method, payload). Declare cron-triggered methods with @Scheduled(cron):

import { Scheduled } from '@roostjs/ai';

@Stateful({ binding: 'DIGEST_AGENT' })
class Digest extends StatefulAgent {
  @Scheduled('0 9 * * *')
  async sendDigest() { /* runs daily at 09:00 UTC */ }
}

// Or ad-hoc:
await agent.schedule(60, 'check', { orderId: 42 });

Import from @roostjs/ai/stateful.

Workflows

The @Workflow({binding}) method decorator runs the method as a durable Cloudflare Workflow. The method receives a step parameter bound to @roostjs/workflow's step.do(), step.sleep(), and friends.

import { Workflow } from '@roostjs/ai';

@Stateful({ binding: 'REPORT_AGENT' })
class Report extends StatefulAgent {
  @Workflow({ binding: 'REPORT_FLOW' })
  async processReport(step, reportId: string) {
    const data = await step.do('fetch', () => this.fetchData(reportId));
    await step.sleep('cooldown', '1 minute');
    return data;
  }
}

See @roostjs/workflow for step.do, step.sleep, pause/resume, and compensation patterns.

Sub-agents

From inside a StatefulAgent, spawn a typed sub-agent over DO RPC:

const summarizer = this.subAgent(SummarizerAgent);
const summary = await summarizer.summarize(doc);
await summarizer.abort();

Sub-agent depth is tracked via the SUB_AGENT_DEPTH_HEADER; calls beyond SUB_AGENT_MAX_DEPTH throw SubAgentDepthExceededError. Spawn capability requires @SubAgentCapable() on the parent agent class.

MCP

Implements both halves of the Model Context Protocol: consume remote MCP servers as tools, and expose agents as MCP servers.

import { McpClient, createMcpHandler, McpPortal } from '@roostjs/ai/mcp';

// Consume a remote MCP server.
const github = await McpClient.connect({
  url: 'https://mcp.github.com',
  transport: 'streamable-http',
});

class Bug extends Agent implements HasTools {
  async tools() { return [...await github.tools(), new CustomTool()]; }
}

// Expose this agent class as an MCP server.
export default createMcpHandler(Bug, { transport: 'streamable-http' });

// Or combine multiple remote servers behind a single prefix-namespaced portal.
const portal = new McpPortal([
  { prefix: 'gh', client: github },
  { prefix: 'jira', client: await McpClient.connect({ url: 'https://mcp.jira.com' }) },
]);
await portal.callTool('gh.search_code', { q: 'roost' });

Transports: streamable-http (HTTP/SSE, production default), sse (legacy, degrades to streamable when available), stdio (local dev only).

Import from @roostjs/ai/mcp. This is distinct from @roostjs/mcp, a standalone MCP server package (tools, resources, prompts) without the agent runtime.

HITL

requireApproval pauses execution until a human decides via approve. The approval state is persisted on the agent and can be routed through MCP elicitation or any custom ApprovalRoute.

import { requireApproval, approve } from '@roostjs/ai/hitl';

const decision = await requireApproval(this, 'charge', { amount: 500 });
if (decision.status === 'approved') chargeCustomer();

// Elsewhere — from a reviewer UI or MCP tool:
await approve(approvalId, { approverId: 'user_42' });

@RequiresApproval(step) on a method enforces approval before the method body runs. Import from @roostjs/ai/hitl.

Memory

agent.memory exposes four tiers on a StatefulAgent: read-only context (request-scoped), short-form (scratchpad), knowledge (vector-backed long-term), and skills (registered callable capabilities).

// Read the per-request context.
const tenant = agent.memory.context.get('tenant');

// Scratch a draft into short-form memory.
await agent.memory.shortForm.set('draft', text);

// Query long-term vector knowledge.
const hits = await agent.memory.knowledge.query({ query: 'refund policy' });

// Register a lazily-loaded skill (the `load` thunk returns a `Tool`).
agent.memory.skills.register({
  name:        'translate',
  description: 'Translate input text to English.',
  load:        () => new TranslateTool(),
});

// Materialise registered skills as tool instances. Pass a filter to scope.
const tools = await agent.memory.skills.tools(['translate']);

Import from @roostjs/ai/memory.

Payments

x402 primitives for per-call pricing and MPP for agent-to-agent transfers. chargeForTool wraps a Tool so it issues a PaymentRequiredError on un-paid calls and executes only after a verified PaymentProof.

import {
  chargeForTool,
  payAgent,
  InMemoryWallet,
  type MppRecipient,
  type Price,
} from '@roostjs/ai/payments';

const premium = chargeForTool(new ReportTool(), { amount: 100, currency: 'usd' });

// Agent-to-agent payment: sender signs a challenge, recipient accepts the proof.
const sender: { wallet: InMemoryWallet } = { wallet: new InMemoryWallet(env) };
const recipient: MppRecipient = await resolveRemoteAgent('agent-42');
const price: Price = { amount: 250, currency: 'usd' };

const proof = await payAgent(sender, recipient, price);

Import from @roostjs/ai/payments.

Voice

Voice.stream() opens a VoiceSession with pluggable transcribe and synthesise backends. InMemoryRealtimeBridge is included for local testing.

import { Voice } from '@roostjs/ai/voice';

const session = await Voice.stream({
  agent,
  transcribe: async (chunk) => transcribeChunk(chunk),
  synthesize: async (text)  => synthesizeText(text),
});

session.onUtterance(async (text) => {
  const response = await agent.prompt(text);
  await session.say(response.text);
});

// Later: send audio chunks in, close the session when done.
await session.send(audioChunk);
await session.close();

Import from @roostjs/ai/voice. The default InMemoryRealtimeBridge is suitable for unit tests; pass bridge in VoiceStreamOptions to swap in a WebRTC or WebSocket implementation in production.

Email

Email.send dispatches outbound mail through any EmailTransport. createEmailHandler wires inbound mail routing from Cloudflare Email Workers into an agent.

import { Email, createEmailHandler } from '@roostjs/ai/email';

// Outbound — `from` is required by EmailMessage.
await Email.send({
  from:    'support@acme.com',
  to:      'x@y.com',
  subject: 'hi',
  text:    'hello',
});

// Inbound — route ForwardedEmail into the agent's onEmail() method.
// The handler is a factory so each inbound email gets a fresh instance.
export const email = createEmailHandler(() => agentStubFor(env));

The agent returned by the factory must implement onEmail(message). Import from @roostjs/ai/email.

Browser

Browser.navigate() drives a pluggable headless browser ( Cloudflare Browser Rendering, Puppeteer, or any BrowserDriver). Browser.asTool() wraps it as a Tool for an agent.

import { Browser } from '@roostjs/ai/browser';

// Once at app boot — register the driver.
Browser.configure(driver);

// Navigate and read content off the page.
const page = await Browser.navigate('https://example.com');
const html = await page.html();
const text = await page.text();
await page.close();

class Research extends Agent implements HasTools {
  tools() { return [Browser.asTool({ maxPages: 25 })]; }
}

Import from @roostjs/ai/browser. BrowserPage exposes html, text, screenshot, pdf, click, fill, and close.

CodeMode

runCodeMode generates and executes TypeScript in a sandbox, returning the typed result. @CodeMode() decorates a method to replace its body with a runCodeMode call driven by the method's intent and signature. InMemoryCodeModeCache keys generated code by intent hash so repeat runs skip generation.

import { runCodeMode, CodeMode, InProcessSandbox, InMemoryCodeModeCache } from '@roostjs/ai/code-mode';

const result = await runCodeMode(agent, 'compute Levenshtein of A and B', {
  sandbox: new InProcessSandbox(),
  cache: new InMemoryCodeModeCache(),
});

class Compute extends Agent {
  @CodeMode()
  async distance(a: string, b: string): Promise<number> { /* body replaced */ return 0; }
}

Import from @roostjs/ai/code-mode.

See Also