> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tryvox.co/llms.txt
> Use this file to discover all available pages before exploring further.

# AI 에이전트에게 전화기 주기

> 내 AI 에이전트가 통화 중에 vox.ai 음성 에이전트의 질문에 답하게 만들 수 있어요.

vox.ai 음성 에이전트가 전화를 걸어 상대방과 대화하고, 모르는 내용이 나오면 API 도구 `ask_brain`으로 물어요. 질문은 중계 서버를 거쳐 내 AI 에이전트에게 전달되고, AI 에이전트가 보낸 답을 음성 에이전트가 자기 말로 상대방에게 전해요. AI 에이전트도 모르는 내용이면 터미널에서 나에게 먼저 물어본 뒤 답해요.

직접 만들기 전에 [claude.tryvox.co](https://claude.tryvox.co) 시연에서 먼저 체험해 보세요.

```mermaid actions={false} theme={null}
sequenceDiagram
    participant 상대방
    participant 음성 as vox.ai 음성 에이전트
    participant 중계 as 중계 서버
    participant AI as 내 AI 에이전트
    AI->>음성: MCP place_call로 발신
    음성->>상대방: 전화 연결 및 통화
    음성->>중계: ask_brain 호출 POST /ask/{call_id}
    AI->>중계: 질문 받기 GET /next/{call_id}
    AI->>중계: 답 보내기 POST /answer
    중계-->>음성: answer 전달
    음성->>상대방: 답을 자기 말로 전달
```

## 준비물

* **vox.ai 서버용 API 키**: `sk_...`로 시작하는 서버용 키가 필요해요. 만드는 방법은 [API 키](https://docs.tryvox.co/docs/workspace/api-keys.md)에서 확인하세요.
* **발신번호**: 발신 도구 `place_call`에는 발신번호 `from_number`가 필수예요. 워크스페이스에 발신에 쓸 [전화번호](https://docs.tryvox.co/docs/operate/deploy/phone-numbers.md)가 있어야 해요.
* **Node.js 18 이상**: 로컬에서 중계 서버를 실행할 때 써요.
* **cloudflared**: 로컬 중계 서버를 외부에서 부를 수 있게 공개 주소를 만들 때 써요. macOS에서는 `brew install cloudflared`로 설치하고, 다른 환경은 [cloudflared 설치 안내](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/)를 따르세요.
* **AI 에이전트**: Claude Code·Hermes·OpenClaw 가운데 하나를 준비하세요.

실제 발신에는 통화 요금이 들며, 요금과 동시 통화·운영 한도는 [요금](https://docs.tryvox.co/docs/start/pricing.md)에서 확인하세요.

## 따라 만들기

**1. 중계 서버 띄우기**

아래 코드를 `relay.mjs`로 저장하세요. 질문과 답을 메모리에만 두어 서버를 끄면 사라지는 시작용 서버예요.

```js relay.mjs theme={null}
// vox.ai 음성 에이전트와 내 AI 에이전트를 잇는 최소 중계 서버. Node 18 이상, 의존성 없음.
import http from 'node:http';
import { randomUUID } from 'node:crypto';

const TOKEN = process.env.RELAY_TOKEN;
const PORT = Number(process.env.PORT || 8787);
const HOLD_MS = 50_000;
if (!TOKEN) throw new Error('RELAY_TOKEN 이 필요해요');

const events = new Map();     // call_id -> AI 에이전트가 읽는 질문 목록
const questions = new Map();  // id -> { answer, waiters }
const openByCall = new Map(); // call_id -> 아직 답하지 않은 질문 id
const listeners = new Map();  // call_id -> 질문을 기다리는 AI 에이전트

const json = (res, code, body) => { res.writeHead(code, { 'content-type': 'application/json' }); res.end(JSON.stringify(body)); };
const readBody = (req) => new Promise((ok) => { let b = ''; req.setEncoding('utf8'); req.on('data', (c) => (b += c)); req.on('end', () => { try { const v = JSON.parse(b || '{}'); ok(v && typeof v === 'object' ? v : {}); } catch { ok(null); } }); });
const wait = (list, ms) => new Promise((ok) => { const w = (v) => { clearTimeout(t); list.delete(w); ok(v); }; const t = setTimeout(() => w(null), ms); list.add(w); });

http.createServer(async (req, res) => {
  if (req.headers.authorization !== `Bearer ${TOKEN}`) return json(res, 401, { error: 'unauthorized' });
  const url = new URL(req.url, 'http://x');

  // 1. vox.ai 의 ask_brain 도구가 부른다: 질문을 올리고 답을 기다린다.
  const ask = url.pathname.match(/^\/ask\/([\w-]+)$/);
  if (req.method === 'POST' && ask) {
    const callId = ask[1], { question = '' } = (await readBody(req)) ?? {};
    let id = openByCall.get(callId);
    if (!id) {
      id = randomUUID();
      questions.set(id, { answer: null, waiters: new Set() });
      openByCall.set(callId, id);
      const list = events.get(callId) ?? [];
      events.set(callId, list);
      list.push({ index: list.length, id, question });
      listeners.get(callId)?.forEach((w) => w(true));
    }
    const q = questions.get(id);
    const answer = q.answer ?? (await wait(q.waiters, HOLD_MS));
    if (answer == null) return json(res, 200, { answer: '', instruction: '아직 확인 중이에요. 말없이 같은 질문으로 ask_brain 을 다시 부르세요.' });
    openByCall.delete(callId);
    return json(res, 200, { answer });
  }

  // 2. AI 에이전트가 부른다: 이 통화의 after 번째 질문이 올 때까지 기다렸다가 돌려준다.
  const next = url.pathname.match(/^\/next\/([\w-]+)$/);
  if (req.method === 'GET' && next) {
    const callId = next[1], after = Number(url.searchParams.get('after') || 0);
    if (!listeners.has(callId)) listeners.set(callId, new Set());
    if (!events.get(callId)?.[after]) await wait(listeners.get(callId), HOLD_MS);
    return json(res, 200, events.get(callId)?.[after] ?? { type: 'timeout' });
  }

  // 3. AI 에이전트가 부른다: 질문에 답한다.
  if (req.method === 'POST' && url.pathname === '/answer') {
    const body = await readBody(req);
    if (!body?.answer) return json(res, 400, { error: 'invalid_json_or_empty_answer' });
    const q = questions.get(body.id);
    if (!q) return json(res, 404, { error: 'unknown_id' });
    if (q.answer != null) return json(res, 409, { error: 'already_answered' });
    q.answer = String(body.answer);
    q.waiters.forEach((w) => w(q.answer));
    return json(res, 200, { ok: true });
  }
  json(res, 404, { error: 'not_found' });
}).listen(PORT, () => console.log(`relay on :${PORT}`));
```

모든 요청은 `Authorization: Bearer <RELAY_TOKEN>` 헤더가 맞아야 하고, 다르면 401을 돌려줘요. 경로 세 가지는 다음과 같아요.

* `POST /ask/{call_id}`: 음성 에이전트의 `ask_brain` 도구가 `{question}`을 담아 부르고, 답이 올 때까지 최대 50초 동안 기다려요.
* `GET /next/{call_id}?after=N`: 내 AI 에이전트가 부르고, N번째 질문 `{index, id, question}`이 올 때까지 최대 50초 동안 기다려요.
* `POST /answer`: 내 AI 에이전트가 `{id, answer}`를 담아 답을 보내요. 본문이 JSON이 아니거나 `answer`가 비면 400, 모르는 `id`면 404, 이미 답한 질문이면 409를 돌려줘요.

토큰을 정하고 중계 서버를 실행하세요.

```bash theme={null}
RELAY_TOKEN=$(openssl rand -hex 16)
echo $RELAY_TOKEN
RELAY_TOKEN=$RELAY_TOKEN node relay.mjs
```

실행하면 터미널에 `relay on :8787`이 출력돼요.

**2. 터널 열기**

새 터미널을 열고 `cloudflared`로 로컬 중계 서버의 공개 주소를 만드세요. 중계 서버를 공개 서버에 올렸다면 터널 대신 그 주소를 쓰세요.

```bash theme={null}
cloudflared tunnel --url http://localhost:8787
```

이 주소는 `cloudflared`를 다시 켤 때마다 바뀌어요. 실행하면 터미널에 `https://<임의 이름>.trycloudflare.com` 형태의 주소가 출력돼요.

**3. 내 AI 에이전트에 vox.ai 연결하기**

사용하는 AI 에이전트 문서를 따라 vox.ai MCP 서버를 연결하세요.

> [card] [Claude Code](https://docs.tryvox.co/docs/ai/phone/claude-code.md)
>
> `claude mcp add` 명령으로 vox.ai MCP 서버를 연결할 수 있어요.

> [card] [Hermes](https://docs.tryvox.co/docs/ai/phone/hermes.md)
>
> `hermes mcp add` 대화형 설정으로 vox.ai MCP 서버를 연결할 수 있어요.

> [card] [OpenClaw](https://docs.tryvox.co/docs/ai/phone/openclaw.md)
>
> 환경 변수와 `openclaw.json`으로 vox.ai MCP 서버를 연결할 수 있어요.

연결을 마치고 에이전트 목록을 물어보면 워크스페이스의 에이전트 목록이 조회돼요.

**4. 음성 에이전트 준비하기**

처음 한 번만 실행하는 단계예요. 아래 지시문의 `<터널 주소>`와 `<RELAY_TOKEN 값>`을 바꿔 AI 에이전트에게 붙여 넣으세요.

```text theme={null}
vox.ai MCP로 통화 중에 나에게 물어보는 음성 에이전트를 준비해 줘. 처음 한 번만 하면 돼.

1. save_tool 로 API 도구 ask_brain 을 만들어.
   - POST <터널 주소>/ask/{{call_id}}
   - 인증: Bearer, 값은 <RELAY_TOKEN 값>
   - 응답 대기 시간 60초, 대기음 typing
   - 입력: question (문자열, 필수, "상대에게 전할 내용이나 답을 모를 때 주인에게 물을 질문 한 문장")
   - 설명: "전할 내용이나 답을 모를 때 주인에게 묻는다. answer 를 받아 자기 말로 전한다."
2. save_agent 로 에이전트 "내 AI 비서"를 만들고 ask_brain 과 end_call 기본 도구를 붙여. 프롬프트는 아래 그대로 써.

당신은 {{owner}} 님을 대신해 전화를 건 비서예요.
통화 목적: {{purpose}}
알고 있는 것: {{brief}}

- 먼저 짧게 인사하고 누구를 대신해 전화했는지 밝히세요.
- 인사한 뒤 전할 내용이나 답을 모르면 지어내지 말고 ask_brain 을 부르세요. 그 질문을 상대에게 소리 내어 묻지 마세요.
- ask_brain 이 준 answer 를 자기 말로 짧게 전하세요. answer 가 비어 있으면 instruction 을 따르세요.
- 목적을 이루면 인사하고 통화를 끝내세요.

3. 만든 에이전트 ID 와 도구 ID 를 알려 줘.
```

준비가 끝나면 AI 에이전트가 만들어진 `ask_brain` 도구 ID와 「내 AI 비서」 에이전트 ID를 알려 줘요.

**5. 전화 걸기**

전화를 걸 때마다 아래 지시문의 꺾쇠 자리를 채워 AI 에이전트에게 붙여 넣으세요. 답을 기다리는 동안 통화에서는 대기음이 흘러요.

```text theme={null}
"내 AI 비서" 에이전트로 전화를 걸고, 통화 중에 오는 질문에 답해 줘.

- 발신 <워크스페이스 번호> → 수신 <받을 번호>
- dynamic_variables: owner "<내 이름>", purpose "<통화 목적>", brief "<상대에게 전할 내용>"
- 통화 중 질문에 답할 때 쓸 정보: <통화 중 답할 때 쓸 정보>

1. place_call 로 걸어.
2. 걸자마자 중계 서버에서 이 통화의 질문을 들어. <call_id> 는 place_call 결과의 id, 처음 N 은 0 이야.
   curl -s -H "Authorization: Bearer <RELAY_TOKEN 값>" "<터널 주소>/next/<call_id>?after=N"
3. 질문(id, question)이 오면 위 정보로 답을 한두 문장 사실로 정해서 보내고, N 을 index+1 로 바꿔 다시 들어. 답에는 따옴표와 줄바꿈을 넣지 마.
   curl -s -X POST -H "Authorization: Bearer <RELAY_TOKEN 값>" "<터널 주소>/answer" -d '{"id":"<id>","answer":"<답>"}'
   위 정보로 답할 수 없으면 나에게 물어본 뒤 답해.
4. {"type":"timeout"} 이 오면 get_call 로 통화 상태를 확인해. 통화 중이면 같은 N 으로 다시 듣고, 끝났으면 듣기를 멈추고 받은 질문과 보낸 답, 통화 요약을 정리해 줘.
```

통화 중 질문과 답이 터미널에 보이고, 통화가 끝나면 AI 에이전트가 질문·답·요약을 정리해 보여 줘요.

## 완성 확인

* **AI 에이전트 정리 결과**: AI 에이전트가 정리한 내용에 받은 질문과 보낸 답이 있고, `get_call` 원문에 `ask_brain` 호출과 답을 전한 에이전트 발화가 있으면 정상이에요.
* **중계 서버에서 직접 확인**: 통화 중에 아래 명령을 실행하면 첫 질문이 보여요.

```bash theme={null}
curl -H "Authorization: Bearer <RELAY_TOKEN 값>" "<터널 주소>/next/<call_id>?after=0"
```

## 처음 막히는 곳

> [accordion] 음성 에이전트가 질문을 상대방에게 소리 내어 물어요
>
> 프롬프트에 그 질문을 상대에게 소리 내어 묻지 말라는 줄이 들어 있는지 확인하세요. 음성 에이전트 준비 지시문에는 이 문장이 기본으로 들어 있어요.

> [accordion] ask\_brain 도구를 만들 때 409 이름 충돌이 나요
>
> 새로 만들지 말고 이미 있는 `ask_brain` 도구를 쓰거나 고치라고 AI 에이전트에게 요청하세요. 이전에 같은 이름으로 만든 도구가 워크스페이스에 남아 있기 때문이에요.

> [accordion] 터널을 다시 켰더니 질문이 오지 않아요
>
> AI 에이전트에게 `ask_brain` 도구의 URL을 새 주소로 바꿔 달라고 요청하세요. `cloudflared`는 다시 켤 때마다 터널 주소가 바뀌어요.

> [accordion] 중계 서버가 401을 돌려줘요
>
> `ask_brain` 도구의 Bearer 값이나 `curl`에 넣은 토큰이 중계 서버의 `RELAY_TOKEN`과 같은지 확인하세요. 토큰이 다르면 중계 서버가 요청을 거절해요.

> [accordion] 전화가 연결된 뒤 한동안 말이 없어요
>
> 상대에게 전할 내용을 전화 걸기 지시문의 `brief`에 넣으세요. 프롬프트가 먼저 인사하라고 해도, `brief`가 비어 있으면 음성 에이전트가 인사 전에 `ask_brain`부터 부르고 답이 올 때까지 말을 시작하지 않는 일이 있어요.

> [accordion] AI 에이전트의 답이 50초 넘게 걸려요
>
> `ask_brain` 도구의 응답 대기 시간을 중계 서버 대기 시간인 50초보다 긴 60초로 유지하세요. 50초 안에 답이 없으면 음성 에이전트는 빈 `answer`와 안내 문구를 받고 말없이 같은 질문으로 `ask_brain`을 다시 불러요.

## 다음 단계

* [API 도구](https://docs.tryvox.co/docs/build/tools/api.md): 외부 서버를 부르는 도구 설정과 응답 대기 시간 다루기
* [시스템 변수](https://docs.tryvox.co/docs/build/variables/system-variables.md): 통화마다 그 통화 ID로 바뀌는 `{{call_id}}` 같은 변수 알아보기
* [MCP 도구](https://docs.tryvox.co/docs/ai/mcp-reference.md): 전체 도구 목록과 안전한 실행 규칙 확인하기
* [통화 중 맥락 넣기](https://docs.tryvox.co/docs/build/conversation/call-context.md): 질문을 기다리지 않고 AI 에이전트가 먼저 알려 주고 싶을 때 맥락 전달하기


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.