â—˘VoiceLayer
Dashboard

Build / Migrating to 1.0

Moving from 0.x to 1.0

In 0.x your process was the whole call. In 1.0 VoiceLayer runs every call and your code is the handlers it invokes. This is how a 0.x agent moves across, and what the 0.8 bridge release tells you on the way.

đź“…

Dates. The bridge release, 0.8, prints what to change while your agent still runs as before. 1.0.0 follows when the split runtime passes its acceptance. 0.x gets no releases after the bridge, security fixes included, and keeps working against the platform until 2027-03-15. Plan to move inside that window.

What changed, and why

Your process held a LiveKit connection, ran the speech pipeline and called your functions in process. Now the pool runs the call, behind its guard, and your worker opens one outbound WebSocket to VoiceLayer and answers handler invocations. You get the Playground, VoiceLayer numbers and every guard for code agents; you give up the raw media, and you accept that a handler holds no call state in memory between invocations.

The perimeter is every conversation VoiceLayer runs. A customer brain’s internals and a call handed off at SIP are outside it; VoiceLayer guards what goes to a brain, what comes back, and every action it asks for.

Start with the bridge

Upgrade to 0.8. Each time a 0.x worker starts it prints, once per finding, what that agent would lose or must change for 1.0 — error is something 1.0 refuses, change runs but behaves differently, note is worth knowing. It is the 1.0 manifest build run on your config.

worker log
[voicelayer] agent “venue-escalation” needs changes before @voicelayer/sdk 1.0
  error   on_call_unsupported (onCall): onCall can’t run on the VoiceLayer pool — move its opening line to `greeting` …
  error   models_vad_unsupported (models.vad): models.vad can’t be set: the pool always runs its own VAD
  change  tool_real_values (tools): code tool lookup_resident: in 0.x a code tool is handed the caller's real values …
  change  tool_bindings_implicit (toolBindings): in 0.x an agent attaches every enabled dashboard registry tool …
  note    start_runs_the_runner (start): `.start(import.meta.url)` boots a LiveKit worker in 0.x; in 1.0 …

Run the same check without starting anything:

check.ts
import { checkMigration, formatMigrationReport } from '@voicelayer/sdk';
console.log(formatMigrationReport(checkMigration(myAgentConfig)));

// or, offline, after switching the import to '@voicelayer/sdk/v1':
//   npx voicelayer manifest ./agent.ts --check

Some 1.0 forms already work in 0.8, so most agents need no handler for them: handoff.briefing as a string, a top-level onDtmf, and the model helpers.

Install and run

Drop the plugin packages and the LIVEKIT_*, OpenAI and Deepgram variables from the worker. Provider keys now live under Connections. Two keys replace them: VOICELAYER_API_KEY (a project key, for the CLI) and VOICELAYER_WORKER_KEY (a vlw_ worker credential for one environment, for the running worker).

agent.ts
// before (0.x)
import { defineAgent } from '@voicelayer/sdk';
export default await defineAgent({ name: 'support', prompt: '…' }).start(import.meta.url);

// after (1.0 — from 0.8 on the /v1 subpath)
import { defineAgent } from '@voicelayer/sdk/v1';
export default defineAgent({ name: 'support', prompt: '…' });

Develop with voicelayer dev ./agent.ts; in production a worker is agent.start() with the worker key set. See the SDK page.

Models

deepgram.stt({ model }), openai.llm({ model }), …Unchanged. In 0.8 the factory also carries its catalog reference, so the same call builds in both.
models.llm: 'gpt-4o' (every string ran on OpenAI)'gpt-4o' is OpenAI’s; 'anthropic/…' and 'google/…' now run on that provider.
wrap(...), a plugin instance, a custom providerNot supported: use a helper or a provider/model string.
models.vad: silero.vad()Delete it. The pool runs its own VAD.
models.turnDetector: livekitTurn.english()models.turnDetection: 'model' (or 'vad').
apiKey / baseURL in a helperRefused. Add the key under Connections.
connector.llm({ onQuery })onQuery on the agent: your worker serves it as the brain.
connector.llm({ url, apiKey })Register the endpoint as a brain connector (the key goes in the vault) and bind it to the agent.

ctx

say, respond, ask, handoff, endCall, sendDtmf, recordEvent, memory, watchFormSessionSame signatures. Each is a request to the pool and passes the Action Guard under the system.* capability you declare.
ctx.call, ctx.process, ctx.channelSnapshots, refreshed for each invocation.
ctx.metadataPool-held call state: what one handler writes, the next sees, on any process. At most 64 KiB.
ctx.connectorsYour own objects, in your worker; never on the wire.
ctx.onDtmf(handler)The top-level onDtmf(event, ctx) notification (works in 0.8).
ctx.roomNot in the 1.0 context yet.
ctx.livekit, ctx.adapterRemoved.
ctx.audio.*Removed. Per-participant audio is media work (see the last section).

onCall is refused. Port an opening line to greeting (spoken as written, no model call) or to the prompt (the model generates it, the default when greeting is unset). Whatever drove the media is the last section.

Hooks have deadlines

In 0.x every hook ran in your process with no limit. In 1.0 a hook that blocks the call runs on your worker with a deadline and a fail-safe default, so a slow or crashed worker never hangs a call. onUtterance used to be awaited before field capture; it is now a notification.

tool run10 sa failed tool result to the model
trigger on (function) / run1 s / 2 sno match, racing the reply / the turn continues
process.completeWhen.custom / onField / onComplete2 s / 2 s / 5 snot complete / the field stays captured / the process stays complete
handoff.briefing (function)2 sthe platform’s own briefing
participants.attributes / endCall.onEnd1 s / 2 sno attributes / the call tears down, no veto
onUtterance, onFieldCaptured, onCallEnd, onDtmf, participants.onJoin / onLeavenotificationnothing: delivered at least once, in order, never waited on

Real values and placeholders

In 0.x a code tool was handed the caller’s real values by default. In 1.0 a handler is handed placeholders unless its definition says otherwise, and each release is recorded on the call.

sendRealValues
...defineTool({
  name: 'lookup_resident',
  description: 'Find the resident calling by their phone number',
  input: { phone: 'string' },
  sendRealValues: true, // this handler is handed the real number; every other one gets <PII_PHONE_1>
  run: async ({ phone }) => residents.byPhone(phone)
})

Private networks, tool bindings, transfers

  • allowPrivateNetwork and VL_ALLOW_PRIVATE_NETWORK are gone: the pool never reaches a private address. Call a private service from a defineTool handler, which runs on your network. An httpTool is a declaration the pool runs; a header that looks like a credential is refused at upload (use Connections).
  • toolBindings is explicit. In 0.x an agent attaches every enabled registry tool, and [] does not change that: name the tools you use. In 1.0 an explicit [] means none and leaving it out is refused.
  • ctx.handoff(to) no longer dials any number. The destination must be declared in the manifest or allowed by the workspace’s Transfer destinations policy, and the leg passes the outbound gate. See Transfers.

Brains

onQuery keeps its shape and is now served by your worker as the call’s brain. A framework brain that needs VoiceLayer’s tools uses an adapter. Brains in code.

Deploying

A deploy is a new immutable version, named by the hash of its manifest. The order is fixed: upload, start the new workers beside the old, publish, let the old drain. SIGTERM drains; it does not exit, so set the host’s kill timeout to your longest call plus two minutes. Roll back with voicelayer publish --manifest <older hash> while the old workers are still up; once they are gone it is refused (no_workers) until you redeploy the old build from its lock file.

terminal
voicelayer deploy ./agent.ts --env prd --no-publish   # 1. upload; writes voicelayer.lock.json
# 2. roll out the worker build: new workers register the new hash beside the old
voicelayer publish --env prd --min-workers 2          # 3. publish once 2 sessions of the new hash are connected
# 4. the old hash drains after its last pinned call, then its workers exit

A 1.0 code agent no longer registers <name>::text; the pool’s text engine serves text lines in the text phase, so a 1.0 code agent answers text lines only from then.

Testing

agent.test.ts
import { assertAgent, invoke, runBrain, testContext } from '@voicelayer/sdk/testing';

const ctx = testContext({ call: { callerId: '+15550100' } });
expect(await invoke(agent, 'tool.lookup_resident', { phone: '+15550100' }, ctx)).toMatchObject({ id: 'r-1' });
assertAgent(agent); // the manifest builds and passes the upload’s lint

When you need the whole call

A conversation that needs raw audio, a custom pipeline or your own conversation loop runs in your own runtime on your own LiveKit project. The VoiceLayer line hands the call over at SIP, and the call leaves VoiceLayer’s guard and evidence perimeter. That tier is built on demand and is not part of the SDK.

Checklist

  1. 1

    Upgrade to 0.8 and read what your worker prints

    Nothing about how the agent runs changes.

  2. 2

    Move provider keys to Connections

    Remove the LiveKit and provider variables and the plugin packages.

  3. 3

    Replace onCall

    A greeting or the prompt. Delete models.vad and models.turnDetector.

  4. 4

    Declare what handlers do

    capabilities: ['system.say', …] and toolBindings.

  5. 5

    Opt tools in to real values

    sendRealValues: true on the ones that need a real caller value.

  6. 6

    Move private-network calls into handlers

    And URL-and-key brains to a brain connector or onQuery.

  7. 7

    Ask the workspace owner for the transfer policy

    Allow the numbers your code looks up.

  8. 8

    Switch the import, check, then dev

    Import from @voicelayer/sdk/v1, run voicelayer manifest --check, then voicelayer dev.

  9. 9

    Deploy

    Mint a vlw_ key per environment, voicelayer deploy, roll the workers, publish.