Concept

Two ways to build an agent in code

Let VoiceLayer run the agent loop with tools written like Vercel AI SDK tools, or run the loop yourself and bring it as the brain of a VoiceLayer agent.

An agent in code is a prompt, a model and tools. The one real choice is who runs the loop: the part that sends the conversation to the model, reads the tool calls it asks for, runs them and goes round again. VoiceLayer runs every call and text in both cases, so speech, telephony, turn-taking and the guard on what the agent says stay with VoiceLayer.

HostedBring your own brain
Who runs the loopVoiceLayerYou
What you writedefineAgent with tools, a model and actionsA framework agent (Claude Agent SDK, OpenAI Agents SDK, Vercel AI SDK, LangGraph) or a function
Where your code runsYour worker runs each tool when the loop calls itYour worker runs the whole agent
The model and its keyChosen in code, the key saved under ConnectionsYours, on your provider account
ToolsYour own, plus VoiceLayer's and your connected apps'Whatever your framework has, plus VoiceLayer's tools
Package@voicelayer/sdk@voicelayer/sdk and @voicelayer/agents
Pick it whenYou want the shortest path, and tools in your own code are enoughYou already have an agent, or you need a loop VoiceLayer does not run

You can use both in one workspace, one agent each.

Hosted: tools written like Vercel AI SDK tools

defineAgent reads tools in the shape the Vercel AI SDK uses: a description, an inputSchema and an execute. The execute runs in your worker, on your network, so it can reach a private service. VoiceLayer's loop decides when to call it.

import { defineAgent, tool } from '@voicelayer/sdk';
import { z } from 'zod';

export default defineAgent({
  name: 'order-desk',
  prompt: 'You answer for Acme. Look an order up before you talk about it.',
  capabilities: ['system.say'],

  model: { provider: 'anthropic', id: 'claude-haiku-4-5' },

  tools: {
    lookup_order: tool({
      description: 'Find an order by its number and return its status.',
      inputSchema: z.object({ orderId: z.string() }),
      execute: async ({ orderId }) => db.orders.find(orderId),
    }),
    refund_order: tool({
      description: 'Refund an order in full.',
      inputSchema: z.object({ orderId: z.string() }),
      needsApproval: true,
      execute: async ({ orderId }) => db.orders.refund(orderId),
    }),
  },

  actions: ['vl_check_availability', { name: 'vl_send_text', confirm: true }],
});

@voicelayer/sdk does not depend on the ai package. A tool from ai's own tool() works in the same place, and so does the tool exported here, which needs nothing but zod.

Tools

  • inputSchema is a zod schema (version 3 or 4) or the AI SDK's jsonSchema(...). The arguments are checked against it before execute runs.
  • execute receives the arguments and the AI SDK's options. Its abort signal is the invocation's, and experimental_context is the VoiceLayer context (ctx.say, ctx.metadata, and the rest). Return anything JSON can hold; throw an error with a message that is safe to say aloud, and the agent can apologise with it.
  • needsApproval (or approval) means ask the person first. The agent reads back what it is about to do and runs execute only after a yes. A function is read as always ask. On a realtime voice model (models.realtime) the tool does not ask first: it runs without asking, and the build warns you when an agent sets both.
  • Every run of your tools is recorded with the conversation, with its outcome. The arguments are kept as a digest, never as values.

Models

model is a plain description: { provider: 'anthropic', id: 'claude-haiku-4-5' }, with optional options for temperature, maxOutputTokens and topP. The providers are openai, anthropic and google. An AI SDK model such as anthropic('claude-haiku-4-5') is also accepted: only its provider and model id are read, and the object never leaves your machine. A key or base URL is refused in code. Save the provider key under Connections in the dashboard and VoiceLayer uses it for this workspace.

Actions

actions names VoiceLayer's own tools (vl_book_meeting, vl_check_availability, vl_send_text, vl_send_email) and your connected apps' tools (int_hubspot_contacts_create). Each entry is a name, or an object with confirm, sendRealValues and the connection to use.

What you declare is what the agent has. It does not run until an Admin of your workspace switches it on, so a deploy can never quietly widen what an agent may do. If you change a declaration (a different connection, confirm turned on), it waits for the Admin again. Until then the agent is told it cannot do that thing and does not promise it. An Admin can still set argument rules and limits, and can switch an action off.

Bring your own brain

Keep the loop you already have. Your agent becomes the brain of a VoiceLayer agent: VoiceLayer does the listening and speaking, sends each turn to your worker, and says what comes back.

import { Agent } from '@openai/agents';
import { defineAgent } from '@voicelayer/sdk';
import { openaiBrain } from '@voicelayer/agents/openai';

export default defineAgent({
  name: 'front-desk',
  prompt: 'You are the front desk of Harbor Dental.',
  capabilities: ['system.say', 'system.transfer_call', 'system.end_call'],
  brain: openaiBrain(new Agent({ name: 'front-desk', model: 'gpt-4o-mini', instructions: 'Short, natural sentences.' })),
});

The adapters are openaiBrain, claudeBrain, aiSdkBrain and langchainBrain, in @voicelayer/agents. With the Vercel AI SDK that is your own streamText or ToolLoopAgent loop. VoiceLayer's tools reach your framework as its own tools, and every call goes back through VoiceLayer's checks. Your model usage is on your provider account.

For texts, serveDirect answers the conversations your code controls with the same agent. See Bring your agent for each framework and Brains in code for the details of a voice brain.

Which one

  • You are starting fresh, and your tools are functions: hosted.
  • You have an agent that already works and you want it on the phone and on text channels: bring your own brain.
  • You want the agent to use a tool VoiceLayer runs (booking, texting, a connected app) without writing the integration: either. Hosted names it in actions; a brain reaches it as a tool.

Common errors

ProblemFix
The build says a tool needs executeA hosted tool is an inputSchema and an execute. A tool with no function to run has nothing for your worker to serve.
The build refuses modelUse { provider, id } with openai, anthropic or google. Keys belong under Connections, not in code.
An action never runsIt is waiting for an Admin to switch it on. Ask one, and check you did not change the declaration since.

Next steps

On this page