Skip to content

Build an agent

An agent is a plain object built with defineAgent(). The same definition runs on any model and any runtime.

import { defineAgent, tool } from '@vrealtime/core';
import { openai } from '@vrealtime/openai';
export const agent = defineAgent({
model: openai({ voice: 'marin' }),
instructions: 'You help users plan trips. Keep answers short; they are spoken.',
tools: { get_weather: getWeather },
clientTools: 'accept',
});
OptionDefaultMeaning
modelrequiredA RealtimeModel from an adapter package
instructionsrequiredThe system instructions
toolsnoneServer tools, by name
clientTools'ignore''accept' offers the tools a connecting app declares to the model
hooksnoneSee Hooks
voicethe model’sThe voice to speak with
turnDetectionthe model’s'server', 'semantic', or null for manual turns
clientToolTimeoutMsnoneApp tool calls without a result in this time settle as TIMEOUT
vadnoneVoice activity detection for models without their own, for example energyVad() from @vrealtime/vad
import { tool } from '@vrealtime/core';
import { z } from 'zod';
const getWeather = tool({
description: 'Get the current weather for a city.',
parameters: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] },
validate: z.object({ city: z.string().min(1) }), // optional: any Standard Schema
run: async ({ city }, { signal }) => {
const response = await fetch(`https://weather.example/api?city=${encodeURIComponent(city)}`, { signal });
return response.json();
},
});

The run context provides:

FieldMeaning
signalAborts when the call’s turn is interrupted; pass it to fetch and check it before committing
callId, turnId, epochThe call’s identity and its turn
dataWhat the server attached to the session, for example the authenticated principal
emit(preview)Streams progress to the app as tool.preview while the tool runs

Two more fields shape how a tool behaves:

  • risk: 'low' | 'medium' | 'high' is passed to beforeToolCall, so you can require confirmation.
  • async: true keeps the tool running after an interruption and delivers its result later. Use it only for work that is safe to finish, such as sending a requested email that was already confirmed.
defineAgent({
// ...
hooks: {
beforeToolCall: async (call) => {
if (call.risk === 'high' && !(await confirmWithUser(call))) return { action: 'reject', message: 'The user declined.' };
},
afterToolCall: (call, result) => audit.log({ tool: call.name, where: call.where, ok: result.ok }),
onTurnEnd: (turn, metrics) => metricsSink.record(turn.id, metrics),
onError: (error, recoverable) => logger.error({ error, recoverable }),
},
});
HookCalledCan
beforeToolCall(call)Before any tool call, server or appReturn nothing or { action: 'allow' }, { action: 'reject', message }, or { action: 'rewrite', arguments }
afterToolCall(call, result)After each call settlesObserve
onTurnStart(turn)A turn startsObserve
onTurnEnd(turn, metrics)A turn endsObserve metrics: time to first audio, time to first tool call, interrupt latency, tool calls, and usage summed over the turn’s responses
onTranscript(text, final, turn)User speech is transcribedObserve
onError(error, recoverable)Something failedObserve; model errors are a RealtimeError with a provider-neutral code (see Errors)

call carries callId, name, arguments, where ('server' or 'client'), risk, turn and data.

Pass the agent to serve(), attach() or upgrade(); see Deploy. Per-user configuration, such as instructions that name the open document or a recognition vocabulary, comes from the server’s configure option; see Authentication.