LLM features that don't
re-invent your framework.
The default for "add AI to my app" is somewhere between "wire the Vercel SDK directly and pray" and "adopt a separate agent framework with its own database". Voltro takes a third path: agents, tools, RAG, streaming threads — all first-class primitives in the same runtime as your mutations and your reactive queries. The Vercel AI SDK does the heavy lifting; @voltro/ai gives you a stable surface that works with your tests.
// agents/support.agent.tsx — DESCRIPTOR (browser-safe wire contract)
import { defineAgent } from '@voltro/ai/agent'
import { Schema } from 'effect'
export const support = defineAgent({
name: 'support',
input: Schema.Struct({ prompt: Schema.String }),
})
// agents/support.agent.server.tsx — EXECUTOR (server-only behaviour)
import { defineAgentExecutor } from '@voltro/ai'
import { searchDocs } from '../tools/searchDocs.tool'
export default defineAgentExecutor(support, {
system: 'You are a friendly support agent. Be concise.',
tools: { searchDocs },
maxSteps: 8,
})
// The framework synthesises TWO procedures for free:
// support.send — kicks off a streaming turn
// support.messages — reactive query, streams deltas
// On the client:
const { data: messages } = useSubscription(
'app', ['support.messages', { threadId }], { threadId },
)
const send = useMutation('app', 'support.send')
// Live typewriter bubble is just a row with streaming: true.Six primitives for the AI feature you were going to build.
defineAgent — the chat primitive
One .agent.tsx file declares an LLM chat with typed input, a system prompt, optional tools, and a token budget. The framework auto-synthesises a streaming send action + a reactive query that streams persisted message deltas to the browser.
defineTool — typed function-calling
A *.tool.tsx file declares a Schema-typed input + output. The framework wires it as a model tool. Inputs are validated; the result is decoded against the output schema before the model sees it.
pgvector RAG out of the box
vector(1536) is a first-class column type. The vectorEmbedding() mixin adds the column, the HNSW index, AND a re-embed hook that calls @voltro/ai's embed on every insert/update. nearestNeighbours('text', k) embeds the query inline.
Streaming as a React row
runAssistant patches a persisted message row as deltas arrive. The reactive subscription streams every patch. The live typewriter effect is just a row with streaming: true — no manual WebSocket plumbing, no SSE handler.
Durable agent loops via workflows
For multi-step agent reasoning that must survive a crash (the model call, then a tool call, then waiting for human approval), wrap the loop in a *.workflow.tsx. Each step result is journaled; resumption replays from where it died.
Provider switching + mock for tests
@voltro/ai wraps the Vercel AI SDK behind a stable surface. Swap the provider (Anthropic, OpenAI, local Ollama) without touching call sites. mockAi(...) installs a deterministic provider for tests — no real model calls in CI.
The agent loop IS the runtime.
An agent that needs to (a) stream tokens to a browser, (b) call tools that touch your database, (c) survive a deploy mid-loop, and (d) handle 1000 concurrent users — that's four problems Voltro's existing primitives already solve. Reactive queries do the streaming. Mutations + actions do the tool calls. Workflows do the durability. Multi-tenancy does the isolation.
Choose the right shape:
- • One-shot prompt → text. generateText(prompt). Inside any handler.
- • Schema-constrained output. generateObject(schema, prompt). Inside a mutation or action.
- • Live chat thread. defineAgent + useSubscription on the synthesised messages query.
- • Long agent loop with approvals. Wrap the loop in a workflow; use awaitSignal for the approval.
- • RAG search. vectorEmbedding() mixin on the source table; nearestNeighbours on the query.
AI is just one more handler shape.
Durable workflows
Multi-step agent loops belong in a workflow — each LLM call, each tool call, each approval is a durable step.
Reactive queries
Agent message streams ride the SAME reactive subscription as a todo list. No special chat-streaming infrastructure.
Schema DSL
vector(1536) + vectorEmbedding() mixin add a typed vector column, HNSW index, and auto-embed hook in one declaration.
Open the framework. See it for yourself.
Every primitive on this page is in the framework today. Clone the starter, run `voltro dev`, and have it on screen in two minutes.