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.
[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:
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 --checkSome 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).
// 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 modeltrigger on (function) / run1 s / 2 sno match, racing the reply / the turn continuesprocess.completeWhen.custom / onField / onComplete2 s / 2 s / 5 snot complete / the field stays captured / the process stays completehandoff.briefing (function)2 sthe platform’s own briefingparticipants.attributes / endCall.onEnd1 s / 2 sno attributes / the call tears down, no vetoonUtterance, onFieldCaptured, onCallEnd, onDtmf, participants.onJoin / onLeavenotificationnothing: delivered at least once, in order, never waited onReal 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.
...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
allowPrivateNetworkandVL_ALLOW_PRIVATE_NETWORKare gone: the pool never reaches a private address. Call a private service from adefineToolhandler, which runs on your network. AnhttpToolis a declaration the pool runs; a header that looks like a credential is refused at upload (use Connections).toolBindingsis 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.
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 exitA 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
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 lintWhen 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
Upgrade to 0.8 and read what your worker prints
Nothing about how the agent runs changes.
- 2
Move provider keys to Connections
Remove the LiveKit and provider variables and the plugin packages.
- 3
Replace onCall
A greeting or the prompt. Delete models.vad and models.turnDetector.
- 4
Declare what handlers do
capabilities: ['system.say', …] and toolBindings.
- 5
Opt tools in to real values
sendRealValues: true on the ones that need a real caller value.
- 6
Move private-network calls into handlers
And URL-and-key brains to a brain connector or onQuery.
- 7
Ask the workspace owner for the transfer policy
Allow the numbers your code looks up.
- 8
Switch the import, check, then dev
Import from @voicelayer/sdk/v1, run voicelayer manifest --check, then voicelayer dev.
- 9
Deploy
Mint a vlw_ key per environment, voicelayer deploy, roll the workers, publish.