Voqalize Agent SDK (Python)
Part of the Voqalize voice AI platform: you bring the brain, we bring the voice.
Pipecat-free. Installing this SDK pulls no pipecat dependency — the
promise is "bring the brain, not the voice infra." The customer writes a
Brain of callbacks; the wire is plain protobuf and the Brain surface is
plain dataclasses. (Pipecat lives only inside the Voqalize voice runtime, on
the far side of the socket.) The Vql* wire is language-neutral — see
proto/ for the contract.
The Brain is the sole customer surface — there is no raw FrameProcessor
path. One Brain runs on either transport; a config flip picks which, with no
brain-code change (serve_auto). The SDK does not own a WebSocket server — its
production entrypoint is a connected socket:
run_session()(src/voqalize/sdk/session.py) — the primary inbound surface. Your web framework (FastAPI/Starlette, Django Channels, Flask, aiohttp) accepts the upgrade and hands the connected socket (anything withsend(bytes)/recv()->bytes— theChannelprotocol) to the SDK, along with the URLsession_idand theAuthorizationheader. The voice runtime dials{brain_url}/s/{session_id}per session; one connection = one session. No Cortex relay, no server owned by the SDK.DirectAgent/serve_direct()(src/voqalize/sdk/inbound.py) — a localhost/dev convenience. Owns awebsocketsserver and runs each connection through the samerun_sessionloop. For quick scripts and local dev only.CortexAgent/serve()(src/voqalize/sdk/outbound.py) — the optional fallback. One outbound multiplexed WebSocket to a Cortex relay; many sessions demuxed by a 16-byte prefix. For brains that can't accept inbound (serverless/FaaS, laptops, egress-only).
Install
pip install voqalize-agent-sdk # core, pipecat-free
pip install "voqalize-agent-sdk[adk]" # + the Google ADK integration
pip install "voqalize-agent-sdk[examples]" # + deps used only by examples/
Already have an ADK agent? Wrap it
If your brain is already a Google ADK agent, you don't port it to the Brain
API. You hand the SDK a factory for the agent you already have, and it drives your
agent's own run loop — adding only the voice concerns: one speech bracket per model
call, barge-in, heard-truth history (what the user actually heard, truncated on
interruption), and the wire. Your agent, tools, model, and prompt stay exactly as
they are.
The integration is an optional extra — import voqalize.sdk pulls none of it;
installing [adk] is what pulls in google-adk.
from google.adk.agents import LlmAgent
from voqalize.google_adk import adk_brain
from voqalize.sdk import serve_direct
def build_agent() -> LlmAgent: # your existing agent, unchanged
return LlmAgent(name="desk", model="gemini-2.5-flash",
instruction="You are a travel desk.", tools=[book_flight])
make = adk_brain(build_agent, greeting="Travel desk — where to?")
await serve_direct(make) # or mount make() in your own route
Every default is overridable and your existing framework customizations survive:
a dynamic greeting= callback, your own ADK Runner / SessionService via
runner_factory=, multi-agent trees, on_resume= to rehydrate a conversation that
spanned an earlier call, turn_timeout / error_fallback, and voice().action(...)
from inside a tool to drive the browser. For the full knob list, read the
adk_brain docstring. ADK is the one shipped framework integration today.
Subclass AdkBrain when the agent needs the screen
A screen-driving agent extends AdkBrain instead of calling adk_brain(...), and
gets four things the raw framework doesn't give you:
class TravelBrain(AdkBrain):
def __init__(self) -> None:
super().__init__(lambda: build_agent(self.desk), greeting="Where to?")
self.desk = TravelDesk() # the agent is built lazily — this is in time
def grounding(self) -> str: # appended to the system instruction, every call
return "ON SCREEN NOW: " + json.dumps(self.browser_state or {})
grounding()is appended to the fully assembled system instruction on every model call — the root agent's and each sub-agent's. It composes with your owninstructionrather than replacing it, is re-read per call (soreturn Noneomits the block turn by turn), and costs no round-trip. Use it for anything the model must not answer from a stale turn.self.browser_stateis the laststate_syncclient message your UI pushed, parsed and kept for you. It takes no floor — a screen change never makes the agent talk — and replaces rather than merges. Overrideon_client_messagefor your own message types and callsuper()to keep it.- Tool arguments arrive as the models you annotated. A parameter typed
Legorlist[Leg]is constructed before your tool runs,Field(alias=...)honored both ways; an argument the model shaped wrong comes back to it as a retryable tool error, not an exception. No defensiveisinstance(raw, dict)in the body. - Tools must be
async. A sync tool is rejected when the agent is built, naming it — ADK would dispatch it on a thread pool wherevoice()is unset, and you'd find out mid-call.allow_sync_tools=Trueopts out.
The travel demo
is the worked example: a prompt, ten async tools, and one grounding() override.
Layout
src/voqalize/sdk/brain.py— the ergonomic surface:Brain(implementon_interaction; the rest are optional —on_session_start/on_session_end/on_user_idle/on_inference_finalized/on_client_message/on_error) +Session/Interaction/Inference/Conversation/Outcome/ClientMessage/IdleInfo, the_BrainAdapterthat mapsVql*frames ↔ callbacks, and the entry points (serve/serve_direct/make_agent/make_direct_agent/brain_factory).src/voqalize/sdk/engine.py— the pipecat-free per-session runtime:SessionRunner(two-lane in/out, system-first feeder, ack-after-dispatch, drop-newest +ErrorFrame, teardown), theEmitter/SessionAdapter/SessionFactory/RunnerHostseams. One runner drives both transports.src/voqalize/sdk/session.py— the connection-handoff surface: theChannelprotocol (send/recvbytes),run_session()(verify token → run one session over a caller-supplied channel),serve_channel()(the transport-neutral loop, no auth — reused byDirectAgent), andverify_token. Owns no server.src/voqalize/sdk/inbound.py—DirectAgent(localhost WS server) +_ServerChannel(adapts awebsocketsServerConnectiontoChannel); verifies and delegates toserve_channel.src/voqalize/sdk/outbound.py—CortexAgent(multiplexed demux + shared fair writer over one wire), implementingRunnerHost.src/voqalize/sdk/_platform_keys.py— the embedded Voqalize public key(s) the direct server verifies against by default.src/voqalize/sdk/wire/— plain-dataclassVql*+ lifecycle/RTVI frames,FrameDirection,is_system(),CortexFrameSerializer(protobuf transcoder, no base class),Wire/MultiplexedWiretransport, protobuf stubs.src/voqalize/_framework/— the shared, framework-agnostic core every framework integration is built on:_FrameworkBrain(ownsrun_inference, the one primitive that spends a floor on a model turn),voice()(theContextVaraccessor a native tool uses for UI side-effects), heard-truth readers, the greeting/resume resolver, and the no-dead-air turn runner. Internal.src/voqalize/google_adk/— the Google ADK integration ([adk]extra):AdkBrain/adk_brain(...)plusScriptedLlmfor tests. See Already have an ADK agent? Wrap it.src/voqalize/conformance/— the wire-level conformance harness:VoiceDriver(drives a brain over a real socket from the voice-runtime side, no runtime needed), the scenario catalog, the MUST checks, and apython -m voqalize.conformanceCLI. Point it at your brain to prove it speaks the protocol correctly.
Core invariants
- Pipecat-free customer surface.
import voqalize.sdkloads zero pipecat modules.pyjwtis a runtime dependency (the direct server verifies the runtime's token). - Connection-handoff, not a server. The production inbound surface is
run_session(channel, *, brain, session_id, token=...): the customer's framework owns the listener + upgrade and hands the SDK a connectedChannel. The SDK verifies by default against the embedded Voqalize public keys (_platform_keys.py) — the token shape is uniform for every brain (iss=pygato, aud=brain, sub=session_id), andsubmust equal the passedsession_id. The audience is a protocol constant (BRAIN_AUDIENCE = "brain"), verified unconditionally alongsideiss="pygato"andexp— there is no per-agent audience and noaudience=parameter; overridepublic_keys=, orallow_unverified=True(local dev). A bad token raisesSessionRejected(caller closes 4000).serve_direct()is the localhost wrapper that owns awebsocketsserver and calls the same loop. One socket = one session; framing is bare[1-byte direction][protobuf], session implicit in the URL. - Config picks the transport, brain code doesn't change.
serve_auto(MyBrain, mode=…)(default$VOQAL_AGENT_MODE) dispatches toserve(outbound Cortex) orserve_direct(localhost inbound); production inbound mountsrun_sessionin the customer's framework. SameBraineither way. - Cortex (fallback): one
CortexAgentprocess → one outbound WebSocket to awss://.../agentURL. Auth isAuthorization: Bearer <api_key>(or a per-connect JWT viaauthorization_provider) +X-Agent-Version. Many sessions multiplex over the connection, demuxed by a 16-byte rawsession_idprefix. - One
SessionRunnerpersession_id.factory(emitter)(aSessionFactory) runs once per session, building a fresh_BrainAdapter(Brain(), emitter). Cross-session writes are structurally unreachable. Holds identically for both transports —directjust has one session per connection. - Two lanes each way. System frames (
VqlStart/Interruption/Cancel, peris_system()) ride a priority lane that bypasses queued data; everything else rides a bounded normal lane (default 256) with drop-newest.Endis not system — it rides the normal lane so a session tears down only after its queued data drains. - Ack-gated ordering. Every wire-vocab data frame carries
request_id > 0. The runner emits anAck(request_id)envelope afteradapter.handle_framereturns — so the ack FIFOs behind any frames the handler emitted synchronously. The adapter spawnson_interaction(rather than awaiting it), so theVqlUserTextack is prompt and the runtime's per-frame flow control keeps moving. - Interruption is a drain barrier. Barge-in rides the wire as a field-less
InterruptionFrame(system lane); the adapter cancels the in-flight interaction task(s) and echoes anInterruptionFrameback on the outbound system lane — the runtime's drain barrier. Correlation lives oninference_id, not on the interrupt. - Backpressure never kills a session. On normal-lane overflow the runner drops
the newest frame and delivers a non-fatal
ErrorFrameto the adapter (edge-triggered: one per congestion episode per direction), surfaced to the Brain via optionalon_error. - Framework-owned
Conversation(heard-text contract). The SDK commits the user utterance at interaction start and one assistant message per inference from its HEARD text at finalize; the Brain keeps no parallel history and cannot commit generated text.
Read next
- docs/architecture.md — connection model, per-session engine, ack-gated ordering, backpressure, reconnect.
- docs/decisions.md — why the SDK is pipecat-free, why the Brain is the sole surface, why routing stays out of the SDK, drop-newest, etc.
- docs/wire-protocol.md — envelope shapes, frame vocabulary, close codes.
examples/— runnable brains:echo(smallest complete brain),travel(a hand-writtenBrainover Gemini with screen-driving tools),travel_adk(the same agent as a native ADKLlmAgent, wrapped withadk_brain),fastapi_inbound(mount a brain in your own FastAPI app).
Development
uv run pytest
Integration tests run a FakeCortex over real TCP; the runtime leg is simulated
by the SDK's own Wire client. No MagicMock / AsyncMock anywhere.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file voqalize_agent_sdk-0.0.1.tar.gz.
File metadata
- Download URL: voqalize_agent_sdk-0.0.1.tar.gz
- Upload date:
- Size: 219.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9e087375bd2a5f06b15c7cf84c39f1d8a17f36c34761698b243d3cd99ae99e62
|
|
| MD5 |
373ffb5df8ca61c3a471209e46b979a8
|
|
| BLAKE2b-256 |
c437f4e711da07b3fdf19cebe6b40ca239c60e3e7d9c26e97f1c199297ba421d
|
Provenance
The following attestation bundles were made for voqalize_agent_sdk-0.0.1.tar.gz:
Publisher:
release-python-sdk.yml on voqalize/voqalize
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
voqalize_agent_sdk-0.0.1.tar.gz -
Subject digest:
9e087375bd2a5f06b15c7cf84c39f1d8a17f36c34761698b243d3cd99ae99e62 - Sigstore transparency entry: 2419451177
- Sigstore integration time:
-
Permalink:
voqalize/voqalize@c007eb9080fbf4762d87a3b0944048a3c580cfb0 -
Branch / Tag:
refs/tags/python-sdk-v0.0.1 - Owner: https://github.com/voqalize
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-python-sdk.yml@c007eb9080fbf4762d87a3b0944048a3c580cfb0 -
Trigger Event:
push
-
Statement type:
File details
Details for the file voqalize_agent_sdk-0.0.1-py3-none-any.whl.
File metadata
- Download URL: voqalize_agent_sdk-0.0.1-py3-none-any.whl
- Upload date:
- Size: 140.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fe336eb722f395f77e308765c15d2759764943716760476d253b876ed734c49f
|
|
| MD5 |
908a5b6344429027df24c276f4241ce9
|
|
| BLAKE2b-256 |
27daaae2f9b802dc445274137cb0a2e3b7ef4dd5ea037369e1daec0ddfb212a1
|
Provenance
The following attestation bundles were made for voqalize_agent_sdk-0.0.1-py3-none-any.whl:
Publisher:
release-python-sdk.yml on voqalize/voqalize
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
voqalize_agent_sdk-0.0.1-py3-none-any.whl -
Subject digest:
fe336eb722f395f77e308765c15d2759764943716760476d253b876ed734c49f - Sigstore transparency entry: 2419452181
- Sigstore integration time:
-
Permalink:
voqalize/voqalize@c007eb9080fbf4762d87a3b0944048a3c580cfb0 -
Branch / Tag:
refs/tags/python-sdk-v0.0.1 - Owner: https://github.com/voqalize
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-python-sdk.yml@c007eb9080fbf4762d87a3b0944048a3c580cfb0 -
Trigger Event:
push
-
Statement type: