> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.agentduet.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.agentduet.com/_mcp/server.

# VoiceAgent

`VoiceAgent` is an optional high-level runner in
[`agentduet` 1.0.0](https://pypi.org/project/agentduet/1.0.0/).
It answers the call, opens a model session, streams PCM both ways, clears the
buffer on barge-in, dispatches tool calls, and cleans up on hangup.

Integrations document the direct PCM bridge with `agentduet` and the provider
SDK. Use this page when you want the same loop behind a thin adapter
abstraction.

## Install

```bash
# try everything (no --pre; the extra already pins a pre-release adapters version)
pip install "agentduet[adapters]"

# production: one provider only (adapters package is still pre-release)
pip install --pre "agentduet-adapters[gemini]"
```

Prefer the second for anything you deploy. `[adapters]` pulls all four
providers' SDKs; naming the provider keeps the install to what you use.

## Credentials

`VoiceAgent.from_env()` reads **AgentDuet** connector credentials only:

```bash
export AGENTDUET_API_KEY=your-connector-api-key
export AGENTDUET_CONNECTOR_UUID=your-connector-uuid
```

Each model adapter reads its **provider** key separately (from the environment by
default, or pass `api_key=` to the adapter constructor):

| Adapter | Environment variable |
|---|---|
| `GeminiLive` | `GEMINI_API_KEY` |
| `GrokVoice` | `XAI_API_KEY` |
| `QwenVoice` | `DASHSCOPE_API_KEY` |
| `NovaSonic` | AWS credentials + `AWS_REGION` |

## Three-line Gemini agent

```python
from agentduet import VoiceAgent
from agentduet_adapters.gemini import GeminiLive

VoiceAgent.from_env().run(
    GeminiLive(instruction="You are May, a warm phone concierge. Keep replies short.")
)
```

Needs `AGENTDUET_API_KEY`, `AGENTDUET_CONNECTOR_UUID`, and `GEMINI_API_KEY`.

`from_env()` reads connector credentials and defaults `sample_rate` to **24000**
(matching most realtime speech models). The SDK-wide `CallAudioConfig` default
remains 16000 when you omit `call_audio=` on `SessionManagerConfig.create()`.

## Other adapters

```python
from agentduet_adapters.grok_voice import GrokVoice
from agentduet_adapters.qwen import QwenVoice
from agentduet_adapters.nova_sonic import NovaSonic

VoiceAgent.from_env().run(GrokVoice())
# VoiceAgent.from_env().run(QwenVoice())
# VoiceAgent.from_env().run(NovaSonic())
```

Install the matching extra: `agentduet-adapters[grok]`, `[qwen]`, or `[nova-sonic]`.

OpenAI Realtime is not packaged as an adapter yet. Use the
[OpenAI Realtime](/integrations/open-ai-realtime) bridge tutorial.

## Inbound and outbound

| Method | Role |
|---|---|
| `run(model)` | Blocking: connect and serve inbound calls until interrupted. |
| `await serve(model)` | Async form of `run()`. |
| `call_out(dest, model, *, subscriber, ...)` | Blocking one-shot outbound call on its own client. |
| `await dial(dest, model, *, subscriber, ...)` | Async form of `call_out()`. |
| `await place_call(dest, model, *, subscriber, ...)` | Outbound on the same client as a running `serve()`. Fire-and-forget. |

Property: `is_serving` is `True` while `serve()` is running (required for `place_call`).

### Trigger conditions on inbound

`VoiceAgent(..., inbound=InboundCallMode.ALL)` calls `setup_trigger_conditions`
on startup with `TriggerConditionsBuilder().inbound_call(...).build()` (absolute
replace). Message-flow toggles reset to their defaults. Pass `inbound=None` to
leave the server-side configuration untouched. See
[Trigger Conditions](/concepts/trigger-conditions) and
[WhatsApp Messaging](/concepts/whats-app-messaging).

## Tools, transcripts, usage

```python
from agentduet import VoiceAgent

async def tools(name: str, args: dict) -> dict:
    if name == "lookup_order":
        return {"status": "shipped"}
    return {"error": f"unknown tool {name}"}

async def on_transcript(delta) -> None:
    print(delta.role, delta.text)

VoiceAgent.from_env(
    tools=tools,
    on_transcript=on_transcript,
).run(GeminiLive(instruction="You help with orders. Keep replies short."))
```

## Custom model adapter

Adapters are protocols. Any object with matching methods works; no base class
required. Writing your own needs only `agentduet` (not `agentduet-adapters`):

```python
from agentduet import AudioOut, Interrupted, ToolCall, VoiceAgent

class MyModel:  # satisfies VoiceModel
    async def open(self):
        return MySession()

class MySession:  # satisfies ModelSession
    async def push_audio(self, pcm: bytes): ...

    def events(self):
        # async iterator of AudioOut / Interrupted / ToolCall /
        # TranscriptDelta / Usage
        ...

    async def send_tool_result(self, call_id, result): ...
    async def close(self): ...

VoiceAgent.from_env().run(MyModel())
```

### Events VoiceAgent understands

| Event | Fields | Reaction |
|---|---|---|
| `AudioOut` | `pcm` | Send to the call. Full send buffer drops the chunk. |
| `Interrupted` | (none) | `clear_send_audio_buffer()` (barge-in). |
| `ToolCall` | `id`, `name`, `args` | Dispatch `tools=` handler; return via `send_tool_result`. |
| `TranscriptDelta` | `text`, `role` | Forward to `on_transcript` if set. |
| `Usage` | `total`, `input`, `output`, `detail` | Forward to `on_usage` if set. |

## When to use a full bridge instead

Integrations use a direct PCM bridge with `agentduet` plus the provider SDK.
Prefer that path when you need custom session lifecycle, OpenAI Agents /
Google ADK shapes, resampling control, or multi-call topology. `VoiceAgent` is
an optional abstraction over the same loop.

## Related

- [Installation](/introduction/installation)
- [Gemini Live](/integrations/gemini-live)
- [API Reference](/reference/api-reference)