@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/schemaSubpaths (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
| Provider | Transport | Streaming | Tools | Structured |
|---|---|---|---|---|
| Workers AI | direct binding | ✓ | ✓ | ✓ |
| Anthropic | Gateway or native | ✓ | ✓ | ✓ |
| OpenAI | Gateway or native | ✓ | ✓ | ✓ |
| Gemini | Gateway 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 historyprompt() 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.
| Decorator | Purpose |
|---|---|
@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.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
@roostjs/aiguides — task-oriented how-tos for every primitive above.@roostjs/aiconcepts — opt-in contract pattern, multi-provider strategy, and the Laravel + CF-native philosophy.- Tutorial: build a stateful agent — end-to-end walkthrough from empty project to deployed agent.
- MIGRATION.md — v0.2 → v0.3 upgrade notes.