# Kinds of agent

One field — `mode` — decides how much of the platform a call actually uses. It is worth setting deliberately, because the difference between the modes is not a setting or two: it is whether a model is in the loop at all.

## The four modes

| Mode | What happens on a turn | Reach for it when |
| --- | --- | --- |
| `conversation` the default | Speech is transcribed, a model answers, a voice speaks it. Tools, knowledge and flows are all available. | The caller is going to say something you cannot enumerate in advance. |
| `keypad` | A menu, and nothing else. No speech recognition, no model, no network call inside a turn — the reply is already rendered, so it lands in milliseconds. | "Press 1 to confirm." The whole job is a fixed menu, and you want it to keep working on the day a speech vendor is down. |
| `normal` | Answers, reads the greeting out, and puts the caller through to `transfer_number`. No AI anywhere in it. | An overflow line, a holiday notice, the number on the back of a card — or the thing you point a number at on the day the clever line has to be switched off. |
| `webrtc` superseded | Two people, introduced. Still accepted on an agent, and still works — but there is now a better way to ask for it that involves no agent at all. See below. | Nothing new. Use `POST /v1/calls/rendezvous`. |

## Person to person needs no agent

Introducing two browsers to each other used to mean creating an agent in `webrtc` mode, and that was a mistake this platform made rather than a requirement of the problem. An agent is a model's configuration — a prompt, a voice, a language, a set of tools, a post-call pass — and a conversation between two people reads *none* of it. What the record actually supplied was two capture switches and a ring window.

The cost was not tidiness. An agent has exactly one mode, so an account that wanted a talking line *and* a person-to-person one needed two agents and a branch in its own code to pick between them — and the first time somebody edited the wrong one in the portal, a customer who had been promised a colleague reached a model instead.

So a call between two people is now its own thing, with no agent to configure and no mode to get wrong:

**Shell**

```bash
# Place it. No agent_id, and no field for one.
curl -X POST https://voice.example.com/v1/calls/rendezvous \
  -H "Authorization: Bearer $VOICE_API_KEY" -H 'Content-Type: application/json' \
  -d '{"ring_seconds":30,"record":false,"transcribe":false,
       "metadata":{"ticket":"ABC-123"}}'

# Hand the person who placed it their end. The caller seat claims nothing:
# the call stays ringing, which is the point of being on that end of it.
curl -X POST https://voice.example.com/v1/calls/$CALL_ID/seat \
  -H "Authorization: Bearer $VOICE_API_KEY" -H 'Content-Type: application/json' \
  -d '{"role":"caller"}'

# When somebody answers. Taking the responder seat IS answering: it claims the
# call under the same lock /calls/{id}/claim uses, so two people pressing
# Answer in the same second produce one connection and one 409.
curl -X POST https://voice.example.com/v1/calls/$CALL_ID/seat \
  -H "Authorization: Bearer $VOICE_API_KEY" -H 'Content-Type: application/json' \
  -d '{"role":"responder","participant":"member_7"}'
```

Each seat comes back with a `url` that already carries the credential and the role — hand it to the browser and nothing else. The role is not a label: the responder makes the WebRTC offer, so a call where both ends think they are the same seat never connects.

Everything else is an ordinary call. It rings, it is claimed, it ends, it is metered, it appears in `GET /v1/calls`, and it emits the same `call.*` webhooks in the same order — with no `agent_id` in the payload, which is the one visible difference on the wire.

## Picking one

```
Does a human need to hear another human?
  └─ yes ─▶ Is one of them an operator on your team?
              ├─ yes ─▶ normal        (answer, then transfer)
              └─ no  ─▶ no agent at all
                        POST /v1/calls/rendezvous
  └─ no  ─▶ Can every reply be enumerated in advance?
              ├─ yes ─▶ keypad        (a menu, instant, no model)
              └─ no  ─▶ conversation
```

## Three shapes of conversation agent

Every conversation agent is one of these, and the difference is how much structure you have imposed on it. They are not modes — you move between them by adding fields.

| Shape | How it is configured | Good for |
| --- | --- | --- |
| **Prompted** | A prompt on the first language entry, and nothing else. The model decides everything. | Support, FAQ, anything open-ended. Start here. |
| **Grounded** | The above, plus `knowledge_base_ids` so answers come from your documents rather than the model's memory. | Anything where a confidently wrong answer costs you something. See [knowledge base](https://voice.sphoro.com/docs/knowledge-base). |
| **Flowed** | A `flow`: named nodes, each with its own instructions, and edges the call moves along. | Conversations with an order that must hold — verify, then collect, then confirm. See [flows](https://voice.sphoro.com/docs/flows). |

> **Add structure when a prompt starts failing, not before.** A flow is more work to write and more work to change. The signal that you need one is specific: the model is doing the steps out of order, or skipping one when the caller sounds impatient. If it is answering wrongly rather than out of order, that is a knowledge problem, not a flow one.

## Autonomy: who holds this end of the call

`autonomy` is read only by a `conversation` agent — every other mode never reaches a model, so there is nothing to decide. It answers a different question from the tool list: not "which tools does this agent have" but "is there a model on this call at all".

| Value | Who answers |
| --- | --- |
| `agentic` the default | A model, answering by doing. Before it speaks it searches every base in `knowledge_base_ids`, and it may call anything in `tools`. |
| `simple` | **A person**, from their browser. No recognizer, model or voice is opened for the call — the audio passes between the phone and the operator and nothing here listens to it. There is no transcript, because nothing on this side heard the call. |
| `custom` | A model, with exactly the switches `capabilities` names and nothing else. This is the only value under which that block is read. |

Under `custom`, `capabilities` takes `search_knowledge`, `call_functions`, `transfer`, `end_call`, `max_tool_rounds` and `retrieval_timeout_ms`. The last two are the ones that matter under load: a model that can call tools in a loop will, and a retrieval allowed to take as long as it likes is a silence the caller hears.

> **A number pointed at a `simple` agent rings rather than answers.** There is nobody on this end until a person arrives, so the call is created `ringing`, an operator claims it, and a call nobody claims inside `ring_seconds` is recorded as `missed`. An outbound simple call does not ring — the operator who placed it is already there.

## Changing your mind later

`mode` is patchable, and the change takes effect on the next call — calls already running finish under the mode they started in. What does not survive the change is the configuration that only one mode reads: a `keypad` map means nothing to a `normal` agent, and a `transfer_number` means nothing to a `keypad` one. Nothing is deleted, so switching back restores the behaviour.

`PATCH /v1/agents/{id}`

**Shell**

```bash
curl -s -X PATCH https://voice.sphoro.com/v1/agents/$AGENT_ID \
  -H "Authorization: Bearer $SPHORO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"mode": "normal", "transfer_number": "+912261234567"}'
```
