claude_agent_sdk's _AsyncioTaskHandle.wait() uses `with suppress(asyncio.CancelledError)` to silence the inner read task's expected cancellation, but it also swallows the outer task's cancellation if it lands on the same await — causing cancel_task to time out. Bypass `async with ClaudeSDKClient` and drive connect/disconnect ourselves so disconnect() runs in a finally where the outer CancelledError has already been raised and suspended by Python's exception machinery, out of reach of the SDK's suppress.
Pipecat Multi-Task Examples
This directory contains example bots that use the multi-task framework in pipecat.tasks, pipecat.pipeline.runner (with spawn()), and the TaskBus. Each example shows a different cooperation pattern between tasks: hand-off, parallel fan-out, remote workers, etc.
Setup
From the repo root:
uv sync --all-extras
source .venv/bin/activate
cd examples/multi-task
Copy the env template and fill in your API keys:
cp env.example .env
Environment variables
| Variable | Required by |
|---|---|
OPENAI_API_KEY |
LLM tasks |
DEEPGRAM_API_KEY |
STT |
CARTESIA_API_KEY |
TTS |
DAILY_API_KEY |
Optional: only with --transport daily |
Additional, example-specific variables are listed below.
Table of contents
Local (single process)
Distributed (multi-process)
Local
Examples where all tasks run in the same process on an AsyncQueueBus.
Handoff between LLM tasks
Two LLM tasks (greeter + support) that transfer control to each other during a voice conversation. A main task owns the transport pipeline and bridges frames to the bus.
Running
uv run local-handoff/local-handoff-two-agents.py
Open http://localhost:7860/client in your browser to talk to your bot.
To use Daily transport:
uv run local-handoff/local-handoff-two-agents.py --transport daily
Overview
local-handoff-two-agents.py— Two LLM tasks (greeter + support) that hand off viaBaseTask.handoff_to(...). The main task owns STT, TTS, transport, and aBusBridgeProcessor.local-handoff-two-agents-tts.py— Same shape, but each child task ships with its ownCartesiaTTSServicein a custom pipeline. The main task has no TTS — audio comes from whichever child is active over the bus.
Parallel debate
Parallel fan-out using task.job_group(...). A voice bot takes a topic from the user, kicks off three worker tasks in parallel (advocate, critic, analyst), waits for all three to respond, and synthesizes a balanced answer. Each worker keeps its own LLM context across rounds.
Running
uv run parallel-debate/parallel-debate.py
Open http://localhost:7860/client in your browser to talk to your bot.
To use Daily transport:
uv run parallel-debate/parallel-debate.py --transport daily
Architecture
Main task (transport + LLM + `debate` tool)
└── job_group(advocate, critic, analyst)
└── DebateWorker (LLMContextTask, one per role)
- Main task: transport (STT, TTS) + LLM moderator with a
debatedirect function that fans out viatask.job_group(...). - Debate workers:
LLMContextTasks spawned on the runner. Each keeps its ownLLMContextacross rounds and ships its completed turn back as a job response via the assistant-aggregator'son_assistant_turn_stoppedevent.
Voice code assistant
Talk to your codebase hands-free. Ask questions about code, project structure, or file contents and get spoken answers based on actual files. The Claude Agent SDK worker navigates the filesystem using Read, Bash, Glob, and Grep tools.
Additional environment variables
| Variable | Required by |
|---|---|
ANTHROPIC_API_KEY |
Code worker (Claude Agent SDK) |
PROJECT_PATH |
Optional, defaults to cwd |
Running
# Default: explores the current directory
uv run code-assistant/code-assistant.py
# Specify a project path
PROJECT_PATH=/path/to/your/project uv run code-assistant/code-assistant.py
Open http://localhost:7860/client in your browser to talk to your bot.
To use Daily transport:
uv run code-assistant/code-assistant.py --transport daily
Example questions
- "What does the main module do?"
- "Find all TODO comments in the project"
- "How is error handling implemented?"
- "What dependencies does this project use?"
- "Explain the test structure"
Architecture
Main task (transport + LLM + `ask_code` tool)
└── job → CodeWorker (Claude Agent SDK)
code-assistant.py— Main task: STT, LLM (with system prompt +ask_codedirect function), TTS, and transport. Theask_codetool dispatches a job to the worker viatask.job("code_worker", payload=...).code_worker.py—CodeWorker: a bus-onlyBaseTaskspawned on the runner. It accepts@job-style requests through the bus and runs them sequentially through a persistent Claude SDK session so follow-up questions share context.
Distributed
Examples where tasks run across separate processes or machines.
Handoff via Redis
Same two-task handoff as the local example, but each task runs as a separate process connected via Redis pub/sub. Requires pip install pipecat-ai[redis].
Quick start (single machine, local Redis)
Terminal 1: start Redis
docker run --rm -p 6379:6379 redis:7
Terminal 2: start the greeter worker
uv run distributed-handoff/redis-handoff/llm.py greeter
Terminal 3: start the support worker
uv run distributed-handoff/redis-handoff/llm.py support
Terminal 4: start the main transport task
uv run distributed-handoff/redis-handoff/main.py
All processes connect to redis://localhost:6379 by default.
Running across machines
Point each process at the same Redis instance:
Machine A
uv run distributed-handoff/redis-handoff/main.py --redis-url redis://your-redis-host:6379
Machine B
uv run distributed-handoff/redis-handoff/llm.py greeter --redis-url redis://your-redis-host:6379
Machine C
uv run distributed-handoff/redis-handoff/llm.py support --redis-url redis://your-redis-host:6379
Architecture
Machine A Redis Machine B
+------------+ +-------------+ +-------------+
| main.py | <----> | pub/sub | <----> | llm.py |
| (transport,| | channel: | | (greeter) |
| STT, TTS) | | pipecat:acme| +-------------+
+------------+ +-------------+ +-------------+
^ | llm.py |
+--------------> | (support) |
+-------------+
- main.py — Transport task: Daily/WebRTC, Deepgram STT, Cartesia TTS, and a
BusBridgeProcessorover aRedisBus. - llm.py — LLM worker: runs either
greeterorsupportwith OpenAI behind a bridgedLLMTask.
Handoff via PGMQ (Postgres)
Same shape as the Redis handoff, but the bus is backed by PGMQ on a shared Postgres database (e.g. Supabase). Requires pip install pipecat-ai[pgmq].
Additional environment variables
| Variable | Required by |
|---|---|
DATABASE_URL |
PostgreSQL DSN (e.g. Supabase pooled connection string) |
PGMQ_CHANNEL |
Optional, channel prefix for queue names. Defaults to pipecat_acme |
Quick start
Terminal 1: start the greeter worker
uv run distributed-handoff/pgmq-handoff/llm.py greeter --database-url $DATABASE_URL
Terminal 2: start the support worker
uv run distributed-handoff/pgmq-handoff/llm.py support --database-url $DATABASE_URL
Terminal 3: start the main transport task
uv run distributed-handoff/pgmq-handoff/main.py --database-url $DATABASE_URL
You can also set DATABASE_URL in .env and omit the --database-url flag.
Architecture
Same as the Redis handoff above; the RedisBus is replaced by a PgmqBus, and the "pub/sub channel" is a set of PGMQ queues on the shared Postgres instance.
LLM task via WebSocket proxy
Runs an LLM task on a remote server, connected to the main transport task via a WebSocket proxy. No shared bus required — the proxy tasks forward bus messages point-to-point over the WebSocket.
Quick start (single machine)
Terminal 1: start the remote assistant server
uv run remote-proxy-assistant/assistant.py
Terminal 2: start the main transport task
uv run remote-proxy-assistant/main.py --remote-url ws://localhost:8765/ws
Open http://localhost:7860/client in your browser to talk to the bot.
Running across machines
Server machine: start the assistant
uv run remote-proxy-assistant/assistant.py --host 0.0.0.0 --port 8765
Client machine: point at the server
uv run remote-proxy-assistant/main.py --remote-url ws://server-host:8765/ws
Architecture
+-------------+ +-------------+ +-------------+ +-----------------+
| | | | | | | |
| Main task | | Proxy task | <~~~~~> | Proxy task | | Assistant task |
| | | (client) | | (server) | | |
+-------------+ +-------------+ +-------------+ +-----------------+
messages messages messages messages
│ │ │ │
══════════╧═════════════════╧════════ ════════╧════════════════════╧═══════════
Task Bus Task Bus
═════════════════════════════════════ ═════════════════════════════════════════
- main.py — Transport task with STT, TTS, and a
BusBridge. Spawns aWebSocketProxyClientTaskthat connects to the remote server and forwardsBusFrameMessages. - assistant.py — FastAPI server. Each WebSocket connection spawns a
WebSocketProxyServerTaskplus a bridgedAcmeAssistantLLM task on a per-sessionPipelineRunner.
Security
The proxy tasks filter messages by task name:
- Only messages targeted at the remote task cross the WebSocket
- Only messages targeted at the local task are accepted from the WebSocket
- Broadcast messages never cross the WebSocket
Pass HTTP headers for authentication:
proxy = WebSocketProxyClientTask(
"proxy",
url="wss://server-host:8765/ws",
remote_task_name="assistant",
local_task_name="acme",
headers={"Authorization": "Bearer <token>"},
)