Overview

How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework.

AI in Voltro isn't a library you bolt on. It's a primitive: agents are files, tools are files, embeddings are columns, streaming is the same WebSocket that carries queries + mutations.

The implementation is the Vercel AI SDK wrapped behind the @voltro/ai surface so the provider choice (Anthropic, OpenAI, the Vercel AI Gateway, mock-for-tests) is one env var — or a per-agent / per-call override carrying its own key.

The model

               ┌───────────────────────────────────────┐
               │  Vercel AI SDK                        │
               │   anthropic / openai / gateway / mock │
               └───────────────────┬───────────────────┘
                                   │
                                   ▼
┌──────────────────────────────────────────────────────────────────────┐
│  @voltro/ai                                                          │
│    generateText(...) / generateObject(...)                           │
│    streamText(...)                                                   │
│    embed(...) / embedMany(...)                                       │
└────────────────┬────────────────────────────────────┬────────────────┘
                 │                                    │
                 ▼                                    ▼
┌────────────────────────────────┐   ┌─────────────────────────────────┐
│  *.agent.tsx (descriptor)      │   │  *.tool.tsx                     │
│   defineAgent({ name, input }) │   │   defineTool({ name, input,     │
│  *.agent.server.tsx (executor) │   │     output })                   │
│   defineAgentExecutor(desc, {  │   │   typed + wired on the executor │
│     system, tools, model })    │   │                                 │
└────────────────────────────────┘   └─────────────────────────────────┘

Conversations, sampling and sources

const result = yield* generateText({
  messages: [
    { role: 'user', content: 'What did we decide?' },
    { role: 'assistant', content: 'Ship the measured fix first.' },
    { role: 'user', content: 'Summarize that decision.' },
  ],
  temperature: 0,
  provider: { name: 'openai', model: 'gpt-4o' },
})
const citations = result.sources

generateText takes exactly one of prompt or messages; the full history must include the latest user turn. Supplying both or neither fails before any provider is resolved. The result includes the SDK's cited sources; these are not independent verification of the model's claims.

temperature reaches text generation, structured generation (including raw JSON Schema and tool loops), and streaming. Explicit zero is preserved; model support and valid ranges remain provider-specific.

semanticGenerateText bypasses its single-prompt cache for messages and returns cached: false. Different histories must not share an answer merely because the latest question is similar. Single-prompt caching is unchanged.

Validated structured output

generateObjectJson validates the returned value against the supplied JSON Schema. It does not coerce strings to numbers, insert defaults or remove extra properties. Invalid output is an AiError with reason: 'decode' and does not trigger a provider fallback. generateObject additionally decodes through its Effect Schema for transformations and refinements.

Draft-07 is the default; an explicit $schema can select draft-2019-09 or draft-2020-12. Local references and standard string formats are supported. Invalid schemas, unknown keywords, unresolved external references and async validation are refused before provider I/O. The framework never fetches remote schemas. Schemas are application configuration, not a sandbox for hostile code or unbounded validation work.

What's in this section

  • Providers — Anthropic, OpenAI, the Vercel AI Gateway, mock; per-config keys (BYOK), switching at runtime, model-wrapping middleware
  • Agents — *.agent.tsx shape, system prompts, tool wiring
  • Tools — *.tool.tsx shape, validation, side effects
  • MCP clients — mount an EXTERNAL MCP server's tools, through the same allow/deny policy, with the untrusted server bounded
  • Streaming — tokens-over-WebSocket, client hooks, backpressure
  • RAG — pgvector, embedding mixin, hybrid search
  • Cost tracking — the token usage returned on every call
  • Prompt versioning — definePrompt, a content digest as the version, and which prompt version produced (and cost) a given run

Why one surface

The Vercel AI SDK is excellent. It also rev'd its public API three times in 2024. Wrapping it gives us:

  1. One break when the SDK changes shape — we update @voltro/ai, your code keeps working.
  2. One mock implementation for tests — AI_PROVIDER=mock makes every call deterministic.
  3. Provider portability — switch the provider with one env var, no call-site changes.

Conceptual differences vs. raw SDK

Vercel AI SDK @voltro/ai
generateText(...) generateText({ prompt, system }) returns an Effect
streamText(...) streamText({ prompt, system }) returns an Effect Stream
Embeddings via embedMany([texts]) embed(text) / embedMany(texts)
Tool definitions are loose objects defineTool({ name, input, output, … }) with Schema
Provider is hardcoded in code AI_PROVIDER env var

When NOT to use Voltro's AI surface

  • You need bleeding-edge SDK features before they land in @voltro/ai. Drop down to the raw SDK via import { anthropic } from '@ai-sdk/anthropic'.
  • You're calling AI from outside an executor. Workers, CLI scripts, etc. can still use @voltro/ai directly when they provide the right env/config.

For 95% of app code inside actions, streams, workflows, and agent helpers, use @voltro/ai instead of importing provider SDKs directly. The portability and test mock surface stay in one place.