# Agents

> Authoring agents: the characters `/v1/agent/*` talks to, their organisation-wide defaults, and what each may look up.

Authoring agents: the characters `/v1/agent/*` talks to, their organisation-wide defaults, and what each may look up. Reads on `agent`, writes on `agent:write`.

When and why to call these: the [agents guide](https://docs.lumavox.ai/agents.md).

## List agents

`GET /v1/agents`

Every agent in your organisation, with how much each has been talked to. `id` is what `POST /v1/agent/sessions` takes.

Needs only the `agent` scope: an integration that may open a conversation must be able to find the agents it may open one with.

```bash title="curl"
curl "https://api.lumavox.ai/v1/agents" \
  -H "Authorization: Bearer $API_KEY"
```

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — Every agent, newest first |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `data` | array of AgentSummary |  |
| `data[].code_execution` | boolean | Whether the model may write and run Python while it replies — sums, unit conversions, dates — and answer from what it printed. Gemini's own tool: it runs at Google, with no network, and nothing of yours. **No fee, but not free**: the code and its output are model tokens, billed like any reply. |
| `data[].compact_instruction` | string or null | What to tell the model when summarising. Unset uses the organisation's default, then a built-in instruction written for in-character conversations. At most 4000 characters. |
| `data[].compact_max_output_tokens` | integer or null | Length cap on the summary, in tokens — the model's thinking included, which shares the budget. Unset uses the platform's `agents.memory.compaction.max_output_tokens` on the Limits page, twice it when the compaction extracts facts too. 1–65536. |
| `data[].compact_model` | string or null | Model for the summarising call. Unset uses the agent's own. At most 64 characters. |
| `data[].compact_thinking_level` | `"minimal"`, `"low"`, `"medium"`, `"high"` or null | Thinking level for the summarising call. |
| `data[].compact_threshold_tokens` | integer or null | Compact automatically once a reply's first model call reads this many tokens — the conversation, its summary and the instruction, without the inline knowledge or the turn's looked-up passages, which no summary shrinks. Tool results do not count. Unset uses the platform's default, `agents.memory.compaction.default_threshold_tokens` on the Limits page. At most 200,000, and at least twice what the instruction costs on its own. `POST /v1/agent/sessions/{id}/compact` works whatever it is. 1–200000. |
| `data[].emotion_enabled` | boolean | Whether the model reports its own mood — one line in front of each reply, which is taken off the reply and sent as the socket's `mood` event, and kept on each stored message. Costs about six output tokens per turn and no extra model call. The reply text you are handed does not contain the line: a marker with a known label, or with an intensity, or alone on its own line is always removed, and only a marker with an unknown label, no intensity and prose on the same line is left — at that point it cannot be told from a sentence that happens to start with the word. |
| `data[].emotion_instruction` | string or null | What the model is told about reporting a mood. Unset uses the organisation's default, then a built-in one naming every supported label. **Override this and you own the format.** The gateway strips a first line shaped `mood: <label> <intensity>` — with a label it knows, or with an intensity, or alone on its line — and records a mood only for a label it knows. An instruction that asks for any other shape leaves the model's line in the reply as text and stores no mood. At most 4000 characters. |
| `data[].environment` | string or null | The setting the conversation takes place in. At most 8000 characters. |
| `data[].extract_facts_on_compact` | boolean | Whether a compaction also extracts durable facts about the user, in the same model call. A caller can always override this per request; this is what the automatic path uses. |
| `data[].fact_instruction` | string or null | What counts as worth remembering. Unset falls back to the organisation's default, then to a built-in instruction. At most 4000 characters. |
| `data[].facts_enabled` | boolean | Whether this agent remembers facts about the people it talks to. **On by default**, unlike most switches here. Off, its conversations are never read for facts and its turns recall none — nothing is charged for either — and a compaction extracts nothing whatever it is asked. Facts already stored stay, and other agents still recall the ones they share. Turning it back on reads only what is said from then on. |
| `data[].facts_scope` | `"person"`, `"agent"` or null | Whose facts this agent sees and adds to. `person` (the default, and what unset means): what any of your agents learnt about the person that shares. `agent`: its own only — what it learns is not recalled by your other agents, and it does not recall theirs. A change applies to what it learns from then on: a fact keeps the scope it was learnt under. |
| `data[].first_message` | string or null | The agent's opening line — or, with `first_message_generated` on, the direction for one. As a line it costs no tokens and is identical every time. As a direction ("say hello and catch up with them") the model writes it fresh, so it can use whatever long-term memory recalled about that person — at the cost of one billed model call. **Either way it is delivered as the agent's first turn on `/v1/agent/stream`**, so it is streamed and spoken a sentence at a time like every other reply. Opening a session with `POST /v1/agent/sessions` does not produce it, and an integration that never opens a socket never hears it. The agent is not greeted twice: a conversation continued within a few minutes gets no fresh opening line, and `ready.greeting` says which before a frame arrives. At most 4000 characters. |
| `data[].first_message_generated` | boolean | Whether `first_message` is a direction for the model rather than the words themselves. **The same text means different things under this flag**, and nothing on the server can tell them apart. Turning it off while the text is still written as an instruction shows that instruction to your users verbatim. It needs a `first_message` to act on: switched on with the text empty is refused (422), because an agent with a direction to follow and no direction never greets. |
| `data[].goal` | string or null | What it is trying to achieve in the conversation. At most 8000 characters. |
| `data[].guardrails` | string or null | What it must not do or discuss. At most 8000 characters. |
| `data[].knowledge_enabled` | boolean | Whether the agent may use the knowledge entries assigned to it. **Defaults to on**, unlike the other switches here: knowledge works as soon as an entry is assigned, and this only takes it away again. Off, the entries are not listed in the system instruction and the `search_knowledge` tool is not declared, so the agent answers as though it had none. **Nothing is deleted or unassigned** — turning it back on restores exactly what was there, which is what makes this the way to try an agent without its reference material. Assign entries with `PUT /v1/agents/{agent_id}/knowledge`. |
| `data[].knowledge_lookup_first` | boolean or null | When knowledge is looked up: search before every reply and hand the model what matches, so it never answers from memory what its material says. **On by default.** Off, the model searches only when it decides to, which is slower when it does and wrong more often when it does not. |
| `data[].knowledge_max_results` | integer or null | Pieces of material a lookup hands the model, 1-8. Unset is the platform's `agents.knowledge.default_results` (`GET /v1/platform/limits`). 1–8. |
| `data[].knowledge_mode` | `"auto"`, `"prompt"`, `"lookup"` or null | How the agent reads its knowledge. `auto` (the default, and what unset means) puts the whole text in the prompt while it fits the platform's `agents.knowledge.inline_budget_tokens` — no lookup, nothing to miss — and looks it up beyond that. `prompt` puts it in the prompt up to the platform's `agents.knowledge.prompt_cap_tokens` (each turn's first word is slower the more there is). `lookup` always looks it up. Both are on `GET /v1/platform/limits`. |
| `data[].max_facts` | integer or null | The most of the person's facts put in front of the model, 1-100. Unset is as many as fit in the platform's `agents.memory.recall.budget_chars` (`GET /v1/platform/limits`; 6,000 characters is some 1,500 tokens). When they do not all fit, the ones a conversation confirmed or learnt most recently go in. They are prompt tokens on every turn. 1–100. |
| `data[].max_output_tokens` | integer or null | Cap on reply length. Unset means the model's own limit. 1–65536. |
| `data[].model` | string | Gemini model id. |
| `data[].name` | string | Human-readable name. Shown in the console; never sent to the model. |
| `data[].personality` | string or null | Who the agent is — character, background, quirks. At most 8000 characters. |
| `data[].relation_enabled` | boolean | Whether the agent keeps a standing relationship with each person it talks to — warmth, trust and how well they know each other, carried across every conversation with that end user and never forgotten. Turning this on declares a tool the model calls when something changes its view of them. **It adds no latency to an ordinary turn**: the provider returns the reply and the tool call together, and the call is answered on the next turn. The one exception is a reply that is only the call with no words in it, which costs a second model call to write them. Read it with `GET /v1/agent/relations`. |
| `data[].relation_instruction` | string or null | What the model is told about the relationship it keeps. Unset uses the organisation's default, then a built-in one. This is the *disposition* — that the history is real and that most replies should record nothing. The tool's own description covers the mechanism, and is not configurable. At most 4000 characters. |
| `data[].stream_chunk_schedule` | array of integer or null | How a streamed reply is cut into pieces for speech, smallest first - for example `[60, 160, 250]`. Ascending; a schedule out of order is refused. **Only `/v1/agent/stream` uses this**; a buffered turn has the whole reply before it speaks and nothing to schedule. The **first** number is the one that matters: it decides how much text must arrive before the agent starts talking, so lowering it starts speech sooner. There is a floor worth knowing - a chunk has to be generated faster than the previous one plays, so a first chunk under roughly 50 characters starts sooner and then runs out of audio before the next piece exists, which is heard as a stutter rather than as a fast reply. Later numbers trade first-audio latency for prosody: a longer piece is spoken with better intonation because the model can see more of the sentence at once. Omit it to use your organisation's default, and then the platform's. 1–8 items. |
| `data[].stt_config` | AgentSTTInfo or null | How a spoken turn is transcribed — the model, the language hint, and the sample rate a client sends PCM at on `/v1/agent/stream`. Only what you set is stored: a model left unset follows the platform's default. **Defaults, not policy.** `user_audio.start` may name any of these per turn and what it names wins. There is no switch for hearing, and no scope of its own: a credential that may talk to an agent may speak to it, exactly as it has always been able to hear it speak back. |
| `data[].stt_config.language` | string or null | ISO-639-1 or -639-3 code the agent's spoken turns are transcribed in. Omit it and the language is detected per turn. A `language` on `user_audio.start` overrides it. |
| `data[].stt_config.mark_spoken` | boolean or null | Whether a spoken turn reaches the model with a note saying it was transcribed from speech and may contain recognition errors, so the agent reads it for meaning rather than answering a misheard word or pointing out a typo nobody made. Typed turns are never marked, and the note is not stored in the transcript. Unset or null means on. |
| `data[].stt_config.model` | `"scribe_v2"` or null | Transcription model for this agent's spoken turns. Unset or null means the platform's default model, and follows it if that default ever changes. |
| `data[].stt_config.sample_rate` | `8000`, `16000`, `22050`, `24000`, `44100` or null | Hz of the PCM a client sends on `/v1/agent/stream`. One of 8000, 16000, 22050, 24000, 44100. **Only the socket uses this.** Every other way of sending audio sends a file, and a file states its own rate. Unset means 24000, which is what the socket *sends* — so a client that records what it plays needs nothing here. 16000 is a third less upstream for audio the transcriber resamples to 16000 anyway, which is the reason to change it. Announced back on `ready` either way. |
| `data[].thinking_level` | `"minimal"`, `"low"`, `"medium"`, `"high"` or null | How hard the model thinks before answering. `minimal` is fastest to first token; `medium` is the model's own default. Unset means the model decides. |
| `data[].thinking_summaries` | `"auto"`, `"none"` or null | Whether a readable summary of the reasoning is returned. |
| `data[].tone` | string or null | How it speaks: register, pacing, verbosity. At most 8000 characters. |
| `data[].url_context` | boolean | Whether the model may read a web page by its address while it replies, when the person gives one or one is in its instructions. Gemini's own tool: the page is fetched by Google. The pages it read come back as the reply's `sources`. **No fee, but not free**: a page's text is model tokens, billed like any reply — a long page can be thousands. A page is untrusted input: what it says reaches the model, so the most it can steer the agent towards is the agent's own tools, and a tool marked `ask_first` still needs the person. |
| `data[].voice_config` | AgentVoiceInfo or null | Voice and model settings. **A speaking agent speaks on `eleven_flash_v2_5`** (used when `model` is left unset) **or `eleven_multilingual_v2`**, with any voice from `GET /v1/voices` that offers it. `eleven_v3` is refused with 422 when `voice_enabled` is true: it cannot stream over the socket agents use. An agent that does not speak may leave this unset. |
| `data[].voice_config.format` | `"wav"`, `"pcm"` or null | The audio format this voice is configured with. On the WebSocket a reply is always raw 16-bit PCM, at the rate its `audio.start` names. |
| `data[].voice_config.model` | string or null | Which model speaks: `eleven_flash_v2_5` (the default) or `eleven_multilingual_v2`, and one the voice lists as `models` in `GET /v1/voices`. |
| `data[].voice_config.provider_options` | ProviderVoiceSettingsInfo or null | The provider's voice settings, on top of the ones the voice was registered with. Unset takes theirs. |
| `data[].voice_config.sample_rate` | `8000`, `16000`, `22050`, `24000`, `44100` or null | Hz of the returned audio — one of 8000, 16000, 22050, 24000, 44100, each served natively by the provider. Unset means 24000. The response's `sample_rate` and the `X-Sample-Rate` header always say what was actually produced. |
| `data[].voice_config.voice_id` | string or null | An id from `GET /v1/voices` — the built-in voice, a platform voice, or one your organisation uploaded. |
| `data[].voice_enabled` | boolean | Whether replies come back as audio as well as text. Speech is generated after the reply and adds its own latency — roughly a fifth of the spoken duration. A caller can override this per turn with `speech` on the socket's `user_message` or `user_audio.start`. |
| `data[].id` | string |  |
| `data[].created_at` | string (date-time) |  |
| `data[].updated_at` | string (date-time) |  |
| `data[].version` | integer | Moves by one on every save. Send it back as `expected_version` on an update to have the save refused (409) if somebody else changed the agent since you read it. |
| `data[].sessions` | integer | Conversations opened with this agent. |
| `data[].turns` | integer | Completed turns across all of them. |
| `data[].last_used_at` | string (date-time) or null | When a turn last completed. Null means no conversation with this agent has ever got past being opened — which is not the same as having none. |
| `next_cursor` | string or null | Pass as `cursor` for the next page; `null` when this is the last. |

## Create an agent

`POST /v1/agents`

Needs `agent:write`.

The five configuration fields are prose and are composed into one system instruction, in a fixed order. Leave any of them empty and that section is omitted entirely rather than sent blank.

`first_message` is what the agent says first - unless `first_message_generated` is on, in which case it is the *direction* for what the agent says first and the model writes the line fresh.

**Either form is delivered as the agent's first turn on the WebSocket**, not when a session opens - so it streams and is spoken a sentence at a time.

**The row is checked as a whole**: a `thinking_level` the model refuses, a generated opening with no direction, or a speech schedule out of order is a 422 naming the problem.

```bash title="curl"
curl -X POST "https://api.lumavox.ai/v1/agents" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"..."}'
```

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `code_execution` | boolean or null |  | Whether the model may write and run Python while it replies — sums, unit conversions, dates — and answer from what it printed. Gemini's own tool: it runs at Google, with no network, and nothing of yours. **No fee, but not free**: the code and its output are model tokens, billed like any reply. |
| `compact_instruction` | string or null |  | What to tell the model when summarising. Unset uses the organisation's default, then a built-in instruction written for in-character conversations. At most 4000 characters. |
| `compact_max_output_tokens` | integer or null |  | Length cap on the summary, in tokens — the model's thinking included, which shares the budget. Unset uses the platform's `agents.memory.compaction.max_output_tokens` on the Limits page, twice it when the compaction extracts facts too. 1–65536. |
| `compact_model` | string or null |  | Model for the summarising call. Unset uses the agent's own. At most 64 characters. |
| `compact_thinking_level` | `"minimal"`, `"low"`, `"medium"`, `"high"` or null |  | Thinking level for the summarising call. |
| `compact_threshold_tokens` | integer or null |  | Compact automatically once a reply's first model call reads this many tokens — the conversation, its summary and the instruction, without the inline knowledge or the turn's looked-up passages, which no summary shrinks. Tool results do not count. Unset uses the platform's default, `agents.memory.compaction.default_threshold_tokens` on the Limits page. At most 200,000, and at least twice what the instruction costs on its own. `POST /v1/agent/sessions/{id}/compact` works whatever it is. 1–200000. |
| `emotion_enabled` | boolean or null |  | Whether the model reports its own mood — one line in front of each reply, which is taken off the reply and sent as the socket's `mood` event, and kept on each stored message. Costs about six output tokens per turn and no extra model call. The reply text you are handed does not contain the line: a marker with a known label, or with an intensity, or alone on its own line is always removed, and only a marker with an unknown label, no intensity and prose on the same line is left — at that point it cannot be told from a sentence that happens to start with the word. |
| `emotion_instruction` | string or null |  | What the model is told about reporting a mood. Unset uses the organisation's default, then a built-in one naming every supported label. **Override this and you own the format.** The gateway strips a first line shaped `mood: <label> <intensity>` — with a label it knows, or with an intensity, or alone on its line — and records a mood only for a label it knows. An instruction that asks for any other shape leaves the model's line in the reply as text and stores no mood. At most 4000 characters. |
| `environment` | string or null |  | The setting the conversation takes place in. At most 8000 characters. |
| `extract_facts_on_compact` | boolean or null |  | Whether a compaction also extracts durable facts about the user, in the same model call. A caller can always override this per request; this is what the automatic path uses. |
| `fact_instruction` | string or null |  | What counts as worth remembering. Unset falls back to the organisation's default, then to a built-in instruction. At most 4000 characters. |
| `facts_enabled` | boolean or null |  | Whether this agent remembers facts about the people it talks to. **On by default**, unlike most switches here. Off, its conversations are never read for facts and its turns recall none — nothing is charged for either — and a compaction extracts nothing whatever it is asked. Facts already stored stay, and other agents still recall the ones they share. Turning it back on reads only what is said from then on. |
| `facts_scope` | `"person"`, `"agent"` or null |  | Whose facts this agent sees and adds to. `person` (the default, and what unset means): what any of your agents learnt about the person that shares. `agent`: its own only — what it learns is not recalled by your other agents, and it does not recall theirs. A change applies to what it learns from then on: a fact keeps the scope it was learnt under. |
| `first_message` | string or null |  | The agent's opening line — or, with `first_message_generated` on, the direction for one. As a line it costs no tokens and is identical every time. As a direction ("say hello and catch up with them") the model writes it fresh, so it can use whatever long-term memory recalled about that person — at the cost of one billed model call. **Either way it is delivered as the agent's first turn on `/v1/agent/stream`**, so it is streamed and spoken a sentence at a time like every other reply. Opening a session with `POST /v1/agent/sessions` does not produce it, and an integration that never opens a socket never hears it. The agent is not greeted twice: a conversation continued within a few minutes gets no fresh opening line, and `ready.greeting` says which before a frame arrives. At most 4000 characters. |
| `first_message_generated` | boolean or null |  | Whether `first_message` is a direction for the model rather than the words themselves. **The same text means different things under this flag**, and nothing on the server can tell them apart. Turning it off while the text is still written as an instruction shows that instruction to your users verbatim. It needs a `first_message` to act on: switched on with the text empty is refused (422), because an agent with a direction to follow and no direction never greets. |
| `goal` | string or null |  | What it is trying to achieve in the conversation. At most 8000 characters. |
| `guardrails` | string or null |  | What it must not do or discuss. At most 8000 characters. |
| `knowledge_enabled` | boolean or null |  | Whether the agent may use the knowledge entries assigned to it. **Defaults to on**, unlike the other switches here: knowledge works as soon as an entry is assigned, and this only takes it away again. Off, the entries are not listed in the system instruction and the `search_knowledge` tool is not declared, so the agent answers as though it had none. **Nothing is deleted or unassigned** — turning it back on restores exactly what was there, which is what makes this the way to try an agent without its reference material. Assign entries with `PUT /v1/agents/{agent_id}/knowledge`. |
| `knowledge_lookup_first` | boolean or null |  | When knowledge is looked up: search before every reply and hand the model what matches, so it never answers from memory what its material says. **On by default.** Off, the model searches only when it decides to, which is slower when it does and wrong more often when it does not. |
| `knowledge_max_results` | integer or null |  | Pieces of material a lookup hands the model, 1-8. Unset is the platform's `agents.knowledge.default_results` (`GET /v1/platform/limits`). 1–8. |
| `knowledge_mode` | `"auto"`, `"prompt"`, `"lookup"` or null |  | How the agent reads its knowledge. `auto` (the default, and what unset means) puts the whole text in the prompt while it fits the platform's `agents.knowledge.inline_budget_tokens` — no lookup, nothing to miss — and looks it up beyond that. `prompt` puts it in the prompt up to the platform's `agents.knowledge.prompt_cap_tokens` (each turn's first word is slower the more there is). `lookup` always looks it up. Both are on `GET /v1/platform/limits`. |
| `max_facts` | integer or null |  | The most of the person's facts put in front of the model, 1-100. Unset is as many as fit in the platform's `agents.memory.recall.budget_chars` (`GET /v1/platform/limits`; 6,000 characters is some 1,500 tokens). When they do not all fit, the ones a conversation confirmed or learnt most recently go in. They are prompt tokens on every turn. 1–100. |
| `max_output_tokens` | integer or null |  | Cap on reply length. Unset means the model's own limit. 1–65536. |
| `model` | string or null |  | Gemini model id. Defaults to the platform's `agents.model` (`GET /v1/platform/limits`). At most 64 characters. |
| `name` | string | yes | Human-readable name. Shown in the console; never sent to the model. 1–255 characters. |
| `personality` | string or null |  | Who the agent is — character, background, quirks. At most 8000 characters. |
| `relation_enabled` | boolean or null |  | Whether the agent keeps a standing relationship with each person it talks to — warmth, trust and how well they know each other, carried across every conversation with that end user and never forgotten. Turning this on declares a tool the model calls when something changes its view of them. **It adds no latency to an ordinary turn**: the provider returns the reply and the tool call together, and the call is answered on the next turn. The one exception is a reply that is only the call with no words in it, which costs a second model call to write them. Read it with `GET /v1/agent/relations`. |
| `relation_instruction` | string or null |  | What the model is told about the relationship it keeps. Unset uses the organisation's default, then a built-in one. This is the *disposition* — that the history is real and that most replies should record nothing. The tool's own description covers the mechanism, and is not configurable. At most 4000 characters. |
| `stream_chunk_schedule` | array of integer or null |  | How a streamed reply is cut into pieces for speech, smallest first - for example `[60, 160, 250]`. Ascending; a schedule out of order is refused. **Only `/v1/agent/stream` uses this**; a buffered turn has the whole reply before it speaks and nothing to schedule. The **first** number is the one that matters: it decides how much text must arrive before the agent starts talking, so lowering it starts speech sooner. There is a floor worth knowing - a chunk has to be generated faster than the previous one plays, so a first chunk under roughly 50 characters starts sooner and then runs out of audio before the next piece exists, which is heard as a stutter rather than as a fast reply. Later numbers trade first-audio latency for prosody: a longer piece is spoken with better intonation because the model can see more of the sentence at once. Omit it to use your organisation's default, and then the platform's. 1–8 items. |
| `stt_config` | AgentSTT or null |  | How a spoken turn is transcribed — the model, the language hint, and the sample rate a client sends PCM at on `/v1/agent/stream`. Only what you set is stored: a model left unset follows the platform's default. **Defaults, not policy.** `user_audio.start` may name any of these per turn and what it names wins. There is no switch for hearing, and no scope of its own: a credential that may talk to an agent may speak to it, exactly as it has always been able to hear it speak back. |
| `stt_config.language` | string or null |  | ISO-639-1 or -639-3 code the agent's spoken turns are transcribed in. Omit it and the language is detected per turn. A `language` on `user_audio.start` overrides it. 2–8 characters. |
| `stt_config.mark_spoken` | boolean or null |  | Whether a spoken turn reaches the model with a note saying it was transcribed from speech and may contain recognition errors, so the agent reads it for meaning rather than answering a misheard word or pointing out a typo nobody made. Typed turns are never marked, and the note is not stored in the transcript. Unset or null means on. |
| `stt_config.model` | `"scribe_v2"` or null |  | Transcription model for this agent's spoken turns. Unset or null means the platform's default model, and follows it if that default ever changes. |
| `stt_config.sample_rate` | `8000`, `16000`, `22050`, `24000`, `44100` or null |  | Hz of the PCM a client sends on `/v1/agent/stream`. One of 8000, 16000, 22050, 24000, 44100. **Only the socket uses this.** Every other way of sending audio sends a file, and a file states its own rate. Unset means 24000, which is what the socket *sends* — so a client that records what it plays needs nothing here. 16000 is a third less upstream for audio the transcriber resamples to 16000 anyway, which is the reason to change it. Announced back on `ready` either way. |
| `thinking_level` | `"minimal"`, `"low"`, `"medium"`, `"high"` or null |  | How hard the model thinks before answering. `minimal` is fastest to first token; `medium` is the model's own default. Unset means the model decides. |
| `thinking_summaries` | `"auto"`, `"none"` or null |  | Whether a readable summary of the reasoning is returned. |
| `tone` | string or null |  | How it speaks: register, pacing, verbosity. At most 8000 characters. |
| `url_context` | boolean or null |  | Whether the model may read a web page by its address while it replies, when the person gives one or one is in its instructions. Gemini's own tool: the page is fetched by Google. The pages it read come back as the reply's `sources`. **No fee, but not free**: a page's text is model tokens, billed like any reply — a long page can be thousands. A page is untrusted input: what it says reaches the model, so the most it can steer the agent towards is the agent's own tools, and a tool marked `ask_first` still needs the person. |
| `voice_config` | AgentVoice or null |  | Voice and model settings. **A speaking agent speaks on `eleven_flash_v2_5`** (used when `model` is left unset) **or `eleven_multilingual_v2`**, with any voice from `GET /v1/voices` that offers it. `eleven_v3` is refused with 422 when `voice_enabled` is true: it cannot stream over the socket agents use. An agent that does not speak may leave this unset. |
| `voice_config.format` | `"wav"`, `"pcm"` |  | The audio format this voice is configured with. On the WebSocket a reply is always raw 16-bit PCM, at the rate its `audio.start` names. Default `"wav"`. |
| `voice_config.model` | string |  | Which model speaks: `eleven_flash_v2_5` (the default) or `eleven_multilingual_v2`, and one the voice lists as `models` in `GET /v1/voices`. Default `"eleven_flash_v2_5"`. |
| `voice_config.provider_options` | ProviderVoiceSettings or null |  | The provider's voice settings, on top of the ones the voice was registered with. Unset takes theirs. |
| `voice_config.provider_options.stability` | number or null |  | Low is more expressive and less consistent between generations; high is flatter and more repeatable. 0–1. |
| `voice_config.provider_options.similarity_boost` | number or null |  | How closely to cling to the original voice. High can reproduce artefacts in the source. 0–1. |
| `voice_config.provider_options.style` | number or null |  | Style exaggeration. Costs latency; leave unset on anything conversational. 0–1. |
| `voice_config.provider_options.use_speaker_boost` | boolean or null |  | Similarity at the cost of latency. |
| `voice_config.provider_options.speed` | number or null |  | 1.0 is the voice's own pace. 0.7–1.2. |
| `voice_config.sample_rate` | `8000`, `16000`, `22050`, `24000`, `44100` or null |  | Hz of the returned audio — one of 8000, 16000, 22050, 24000, 44100, each served natively by the provider. Unset means 24000. The response's `sample_rate` and the `X-Sample-Rate` header always say what was actually produced. |
| `voice_config.voice_id` | string |  | An id from `GET /v1/voices` — the built-in voice, a platform voice, or one your organisation uploaded. Default `"default"`. |
| `voice_enabled` | boolean or null |  | Whether replies come back as audio as well as text. Speech is generated after the reply and adds its own latency — roughly a fifth of the spoken duration. A caller can override this per turn with `speech` on the socket's `user_message` or `user_audio.start`. |

**Responses**

| Status | Description |
|---|---|
| `201` | `application/json` — The agent, ready to open sessions with |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `409` | The organisation already has as many agents as its plan allows. Delete one, or ask for a higher limit. |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`201` response body**

| Field | Type | Description |
|---|---|---|
| `code_execution` | boolean | Whether the model may write and run Python while it replies — sums, unit conversions, dates — and answer from what it printed. Gemini's own tool: it runs at Google, with no network, and nothing of yours. **No fee, but not free**: the code and its output are model tokens, billed like any reply. |
| `compact_instruction` | string or null | What to tell the model when summarising. Unset uses the organisation's default, then a built-in instruction written for in-character conversations. At most 4000 characters. |
| `compact_max_output_tokens` | integer or null | Length cap on the summary, in tokens — the model's thinking included, which shares the budget. Unset uses the platform's `agents.memory.compaction.max_output_tokens` on the Limits page, twice it when the compaction extracts facts too. 1–65536. |
| `compact_model` | string or null | Model for the summarising call. Unset uses the agent's own. At most 64 characters. |
| `compact_thinking_level` | `"minimal"`, `"low"`, `"medium"`, `"high"` or null | Thinking level for the summarising call. |
| `compact_threshold_tokens` | integer or null | Compact automatically once a reply's first model call reads this many tokens — the conversation, its summary and the instruction, without the inline knowledge or the turn's looked-up passages, which no summary shrinks. Tool results do not count. Unset uses the platform's default, `agents.memory.compaction.default_threshold_tokens` on the Limits page. At most 200,000, and at least twice what the instruction costs on its own. `POST /v1/agent/sessions/{id}/compact` works whatever it is. 1–200000. |
| `emotion_enabled` | boolean | Whether the model reports its own mood — one line in front of each reply, which is taken off the reply and sent as the socket's `mood` event, and kept on each stored message. Costs about six output tokens per turn and no extra model call. The reply text you are handed does not contain the line: a marker with a known label, or with an intensity, or alone on its own line is always removed, and only a marker with an unknown label, no intensity and prose on the same line is left — at that point it cannot be told from a sentence that happens to start with the word. |
| `emotion_instruction` | string or null | What the model is told about reporting a mood. Unset uses the organisation's default, then a built-in one naming every supported label. **Override this and you own the format.** The gateway strips a first line shaped `mood: <label> <intensity>` — with a label it knows, or with an intensity, or alone on its line — and records a mood only for a label it knows. An instruction that asks for any other shape leaves the model's line in the reply as text and stores no mood. At most 4000 characters. |
| `environment` | string or null | The setting the conversation takes place in. At most 8000 characters. |
| `extract_facts_on_compact` | boolean | Whether a compaction also extracts durable facts about the user, in the same model call. A caller can always override this per request; this is what the automatic path uses. |
| `fact_instruction` | string or null | What counts as worth remembering. Unset falls back to the organisation's default, then to a built-in instruction. At most 4000 characters. |
| `facts_enabled` | boolean | Whether this agent remembers facts about the people it talks to. **On by default**, unlike most switches here. Off, its conversations are never read for facts and its turns recall none — nothing is charged for either — and a compaction extracts nothing whatever it is asked. Facts already stored stay, and other agents still recall the ones they share. Turning it back on reads only what is said from then on. |
| `facts_scope` | `"person"`, `"agent"` or null | Whose facts this agent sees and adds to. `person` (the default, and what unset means): what any of your agents learnt about the person that shares. `agent`: its own only — what it learns is not recalled by your other agents, and it does not recall theirs. A change applies to what it learns from then on: a fact keeps the scope it was learnt under. |
| `first_message` | string or null | The agent's opening line — or, with `first_message_generated` on, the direction for one. As a line it costs no tokens and is identical every time. As a direction ("say hello and catch up with them") the model writes it fresh, so it can use whatever long-term memory recalled about that person — at the cost of one billed model call. **Either way it is delivered as the agent's first turn on `/v1/agent/stream`**, so it is streamed and spoken a sentence at a time like every other reply. Opening a session with `POST /v1/agent/sessions` does not produce it, and an integration that never opens a socket never hears it. The agent is not greeted twice: a conversation continued within a few minutes gets no fresh opening line, and `ready.greeting` says which before a frame arrives. At most 4000 characters. |
| `first_message_generated` | boolean | Whether `first_message` is a direction for the model rather than the words themselves. **The same text means different things under this flag**, and nothing on the server can tell them apart. Turning it off while the text is still written as an instruction shows that instruction to your users verbatim. It needs a `first_message` to act on: switched on with the text empty is refused (422), because an agent with a direction to follow and no direction never greets. |
| `goal` | string or null | What it is trying to achieve in the conversation. At most 8000 characters. |
| `guardrails` | string or null | What it must not do or discuss. At most 8000 characters. |
| `knowledge_enabled` | boolean | Whether the agent may use the knowledge entries assigned to it. **Defaults to on**, unlike the other switches here: knowledge works as soon as an entry is assigned, and this only takes it away again. Off, the entries are not listed in the system instruction and the `search_knowledge` tool is not declared, so the agent answers as though it had none. **Nothing is deleted or unassigned** — turning it back on restores exactly what was there, which is what makes this the way to try an agent without its reference material. Assign entries with `PUT /v1/agents/{agent_id}/knowledge`. |
| `knowledge_lookup_first` | boolean or null | When knowledge is looked up: search before every reply and hand the model what matches, so it never answers from memory what its material says. **On by default.** Off, the model searches only when it decides to, which is slower when it does and wrong more often when it does not. |
| `knowledge_max_results` | integer or null | Pieces of material a lookup hands the model, 1-8. Unset is the platform's `agents.knowledge.default_results` (`GET /v1/platform/limits`). 1–8. |
| `knowledge_mode` | `"auto"`, `"prompt"`, `"lookup"` or null | How the agent reads its knowledge. `auto` (the default, and what unset means) puts the whole text in the prompt while it fits the platform's `agents.knowledge.inline_budget_tokens` — no lookup, nothing to miss — and looks it up beyond that. `prompt` puts it in the prompt up to the platform's `agents.knowledge.prompt_cap_tokens` (each turn's first word is slower the more there is). `lookup` always looks it up. Both are on `GET /v1/platform/limits`. |
| `max_facts` | integer or null | The most of the person's facts put in front of the model, 1-100. Unset is as many as fit in the platform's `agents.memory.recall.budget_chars` (`GET /v1/platform/limits`; 6,000 characters is some 1,500 tokens). When they do not all fit, the ones a conversation confirmed or learnt most recently go in. They are prompt tokens on every turn. 1–100. |
| `max_output_tokens` | integer or null | Cap on reply length. Unset means the model's own limit. 1–65536. |
| `model` | string | Gemini model id. |
| `name` | string | Human-readable name. Shown in the console; never sent to the model. |
| `personality` | string or null | Who the agent is — character, background, quirks. At most 8000 characters. |
| `relation_enabled` | boolean | Whether the agent keeps a standing relationship with each person it talks to — warmth, trust and how well they know each other, carried across every conversation with that end user and never forgotten. Turning this on declares a tool the model calls when something changes its view of them. **It adds no latency to an ordinary turn**: the provider returns the reply and the tool call together, and the call is answered on the next turn. The one exception is a reply that is only the call with no words in it, which costs a second model call to write them. Read it with `GET /v1/agent/relations`. |
| `relation_instruction` | string or null | What the model is told about the relationship it keeps. Unset uses the organisation's default, then a built-in one. This is the *disposition* — that the history is real and that most replies should record nothing. The tool's own description covers the mechanism, and is not configurable. At most 4000 characters. |
| `stream_chunk_schedule` | array of integer or null | How a streamed reply is cut into pieces for speech, smallest first - for example `[60, 160, 250]`. Ascending; a schedule out of order is refused. **Only `/v1/agent/stream` uses this**; a buffered turn has the whole reply before it speaks and nothing to schedule. The **first** number is the one that matters: it decides how much text must arrive before the agent starts talking, so lowering it starts speech sooner. There is a floor worth knowing - a chunk has to be generated faster than the previous one plays, so a first chunk under roughly 50 characters starts sooner and then runs out of audio before the next piece exists, which is heard as a stutter rather than as a fast reply. Later numbers trade first-audio latency for prosody: a longer piece is spoken with better intonation because the model can see more of the sentence at once. Omit it to use your organisation's default, and then the platform's. 1–8 items. |
| `stt_config` | AgentSTTInfo or null | How a spoken turn is transcribed — the model, the language hint, and the sample rate a client sends PCM at on `/v1/agent/stream`. Only what you set is stored: a model left unset follows the platform's default. **Defaults, not policy.** `user_audio.start` may name any of these per turn and what it names wins. There is no switch for hearing, and no scope of its own: a credential that may talk to an agent may speak to it, exactly as it has always been able to hear it speak back. |
| `stt_config.language` | string or null | ISO-639-1 or -639-3 code the agent's spoken turns are transcribed in. Omit it and the language is detected per turn. A `language` on `user_audio.start` overrides it. |
| `stt_config.mark_spoken` | boolean or null | Whether a spoken turn reaches the model with a note saying it was transcribed from speech and may contain recognition errors, so the agent reads it for meaning rather than answering a misheard word or pointing out a typo nobody made. Typed turns are never marked, and the note is not stored in the transcript. Unset or null means on. |
| `stt_config.model` | `"scribe_v2"` or null | Transcription model for this agent's spoken turns. Unset or null means the platform's default model, and follows it if that default ever changes. |
| `stt_config.sample_rate` | `8000`, `16000`, `22050`, `24000`, `44100` or null | Hz of the PCM a client sends on `/v1/agent/stream`. One of 8000, 16000, 22050, 24000, 44100. **Only the socket uses this.** Every other way of sending audio sends a file, and a file states its own rate. Unset means 24000, which is what the socket *sends* — so a client that records what it plays needs nothing here. 16000 is a third less upstream for audio the transcriber resamples to 16000 anyway, which is the reason to change it. Announced back on `ready` either way. |
| `thinking_level` | `"minimal"`, `"low"`, `"medium"`, `"high"` or null | How hard the model thinks before answering. `minimal` is fastest to first token; `medium` is the model's own default. Unset means the model decides. |
| `thinking_summaries` | `"auto"`, `"none"` or null | Whether a readable summary of the reasoning is returned. |
| `tone` | string or null | How it speaks: register, pacing, verbosity. At most 8000 characters. |
| `url_context` | boolean | Whether the model may read a web page by its address while it replies, when the person gives one or one is in its instructions. Gemini's own tool: the page is fetched by Google. The pages it read come back as the reply's `sources`. **No fee, but not free**: a page's text is model tokens, billed like any reply — a long page can be thousands. A page is untrusted input: what it says reaches the model, so the most it can steer the agent towards is the agent's own tools, and a tool marked `ask_first` still needs the person. |
| `voice_config` | AgentVoiceInfo or null | Voice and model settings. **A speaking agent speaks on `eleven_flash_v2_5`** (used when `model` is left unset) **or `eleven_multilingual_v2`**, with any voice from `GET /v1/voices` that offers it. `eleven_v3` is refused with 422 when `voice_enabled` is true: it cannot stream over the socket agents use. An agent that does not speak may leave this unset. |
| `voice_config.format` | `"wav"`, `"pcm"` or null | The audio format this voice is configured with. On the WebSocket a reply is always raw 16-bit PCM, at the rate its `audio.start` names. |
| `voice_config.model` | string or null | Which model speaks: `eleven_flash_v2_5` (the default) or `eleven_multilingual_v2`, and one the voice lists as `models` in `GET /v1/voices`. |
| `voice_config.provider_options` | ProviderVoiceSettingsInfo or null | The provider's voice settings, on top of the ones the voice was registered with. Unset takes theirs. |
| `voice_config.provider_options.stability` | number or null | Low is more expressive and less consistent between generations; high is flatter and more repeatable. |
| `voice_config.provider_options.similarity_boost` | number or null | How closely to cling to the original voice. High can reproduce artefacts in the source. |
| `voice_config.provider_options.style` | number or null | Style exaggeration. Costs latency; leave unset on anything conversational. |
| `voice_config.provider_options.use_speaker_boost` | boolean or null | Similarity at the cost of latency. |
| `voice_config.provider_options.speed` | number or null | 1.0 is the voice's own pace. |
| `voice_config.sample_rate` | `8000`, `16000`, `22050`, `24000`, `44100` or null | Hz of the returned audio — one of 8000, 16000, 22050, 24000, 44100, each served natively by the provider. Unset means 24000. The response's `sample_rate` and the `X-Sample-Rate` header always say what was actually produced. |
| `voice_config.voice_id` | string or null | An id from `GET /v1/voices` — the built-in voice, a platform voice, or one your organisation uploaded. |
| `voice_enabled` | boolean | Whether replies come back as audio as well as text. Speech is generated after the reply and adds its own latency — roughly a fifth of the spoken duration. A caller can override this per turn with `speech` on the socket's `user_message` or `user_audio.start`. |
| `id` | string |  |
| `created_at` | string (date-time) |  |
| `updated_at` | string (date-time) |  |
| `version` | integer | Moves by one on every save. Send it back as `expected_version` on an update to have the save refused (409) if somebody else changed the agent since you read it. |

## Your organisation's agent defaults

`GET /v1/agents/settings`

What every agent falls back to when it sets nothing of its own - the chain is agent, then organisation, then the built-in default. The built-in text comes back alongside, so a client can show what leaving a field empty actually means.

```bash title="curl"
curl "https://api.lumavox.ai/v1/agents/settings" \
  -H "Authorization: Bearer $API_KEY"
```

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — The defaults, and the built-in text behind each |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `default_compact_instruction` | string or null | What agents are told when summarising, unless they set their own. Clear it and the built-in instruction applies. At most 4000 characters. |
| `default_emotion_instruction` | string or null | Default mood-reporting instruction for agents in this organisation that do not set their own. **Overriding it means owning the format**: the gateway strips a first line shaped `mood: <label> <intensity>` — with a label it knows, or with an intensity, or alone on its line — so an instruction asking for any other shape leaves the marker in the reply. At most 4000 characters. |
| `default_fact_instruction` | string or null | What agents are told to remember about a user, unless they set their own. Clear it and the built-in instruction applies. At most 4000 characters. |
| `default_relation_instruction` | string or null | Default relationship instruction for agents in this organisation that do not set their own. At most 4000 characters. |
| `default_stream_chunk_schedule` | array of integer or null | Default speech chunk schedule for agents in this organisation that do not set their own — see `stream_chunk_schedule` on the agent for what the numbers mean. Ascending. Clear it and the platform's applies. 1–8 items. |
| `resume_greeting_after_minutes` | integer or null | How quiet a conversation must be before the agent greets the person again, in minutes. A conversation continued inside this window gets no fresh opening line, because re-greeting somebody who refreshed the page thirty seconds ago is worse than silence — and costs a model call and a speech call to be worse. Continued outside it, the agent says hello again, which is what a `first_message` direction reading "say hello and catch up with them" is asking for. **Zero means always greet**, on every connection. Clear it (`null`) and the platform's `agents.greeting.resume_after_minutes` applies (`GET /v1/platform/limits`). Organisation-wide: every agent here uses it. It is read when a socket connects, so a change reaches the next connection rather than the ones already open. 0–43200. |
| `resume_max_age_days` | integer or null | How old this person's last conversation may be and still be **continued** rather than replaced, in days. Applies to `POST /v1/agent/sessions` when it resumes — see `resume` on that request. Past this, opening a session starts a fresh conversation instead. Nothing is lost at the boundary: durable facts and the standing relationship belong to the person rather than the conversation, and carry into every new one. **Zero means never resume** — every session is a new conversation, which is what an integration that treats a visit as a conversation wants. Clear it (`null`) and the platform's `agents.sessions.resume_max_age_days` applies (`GET /v1/platform/limits`). 0–365. |
| `built_in_compact_instruction` | string | The summarising instruction used when neither the agent nor the organisation sets one. |
| `built_in_emotion_instruction` | string | The platform's mood-reporting instruction, served as it stands so the console never shows a stale copy of it. |
| `built_in_fact_instruction` | string | The extraction instruction used when neither the agent nor the organisation sets one. |
| `built_in_relation_instruction` | string | The platform's relationship instruction, served as it stands so the console never shows a stale copy of it. |
| `moods` | array of string | Every mood label the gateway accepts. A reply reporting anything else is stored with no mood rather than with a made-up one, so an overridden instruction should name these. |

## Set your organisation's agent defaults

`PUT /v1/agents/settings`

Needs `agent:write`.

A partial update: omitted fields are left alone, and an explicit `null` clears one back to the built-in default.

**Conversations already open pick this up**: a change here reaches every conversation in the organisation within about a minute, from its next message after that, exactly as editing an agent does. Agents that set their own value are unaffected.

```bash title="curl"
curl -X PUT "https://api.lumavox.ai/v1/agents/settings" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `default_compact_instruction` | string or null |  | What agents are told when summarising, unless they set their own. Clear it and the built-in instruction applies. At most 4000 characters. |
| `default_emotion_instruction` | string or null |  | Default mood-reporting instruction for agents in this organisation that do not set their own. **Overriding it means owning the format**: the gateway strips a first line shaped `mood: <label> <intensity>` — with a label it knows, or with an intensity, or alone on its line — so an instruction asking for any other shape leaves the marker in the reply. At most 4000 characters. |
| `default_fact_instruction` | string or null |  | What agents are told to remember about a user, unless they set their own. Clear it and the built-in instruction applies. At most 4000 characters. |
| `default_relation_instruction` | string or null |  | Default relationship instruction for agents in this organisation that do not set their own. At most 4000 characters. |
| `default_stream_chunk_schedule` | array of integer or null |  | Default speech chunk schedule for agents in this organisation that do not set their own — see `stream_chunk_schedule` on the agent for what the numbers mean. Ascending. Clear it and the platform's applies. 1–8 items. |
| `resume_greeting_after_minutes` | integer or null |  | How quiet a conversation must be before the agent greets the person again, in minutes. A conversation continued inside this window gets no fresh opening line, because re-greeting somebody who refreshed the page thirty seconds ago is worse than silence — and costs a model call and a speech call to be worse. Continued outside it, the agent says hello again, which is what a `first_message` direction reading "say hello and catch up with them" is asking for. **Zero means always greet**, on every connection. Clear it (`null`) and the platform's `agents.greeting.resume_after_minutes` applies (`GET /v1/platform/limits`). Organisation-wide: every agent here uses it. It is read when a socket connects, so a change reaches the next connection rather than the ones already open. 0–43200. |
| `resume_max_age_days` | integer or null |  | How old this person's last conversation may be and still be **continued** rather than replaced, in days. Applies to `POST /v1/agent/sessions` when it resumes — see `resume` on that request. Past this, opening a session starts a fresh conversation instead. Nothing is lost at the boundary: durable facts and the standing relationship belong to the person rather than the conversation, and carry into every new one. **Zero means never resume** — every session is a new conversation, which is what an integration that treats a visit as a conversation wants. Clear it (`null`) and the platform's `agents.sessions.resume_max_age_days` applies (`GET /v1/platform/limits`). 0–365. |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — The defaults as they now stand |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `default_compact_instruction` | string or null | What agents are told when summarising, unless they set their own. Clear it and the built-in instruction applies. At most 4000 characters. |
| `default_emotion_instruction` | string or null | Default mood-reporting instruction for agents in this organisation that do not set their own. **Overriding it means owning the format**: the gateway strips a first line shaped `mood: <label> <intensity>` — with a label it knows, or with an intensity, or alone on its line — so an instruction asking for any other shape leaves the marker in the reply. At most 4000 characters. |
| `default_fact_instruction` | string or null | What agents are told to remember about a user, unless they set their own. Clear it and the built-in instruction applies. At most 4000 characters. |
| `default_relation_instruction` | string or null | Default relationship instruction for agents in this organisation that do not set their own. At most 4000 characters. |
| `default_stream_chunk_schedule` | array of integer or null | Default speech chunk schedule for agents in this organisation that do not set their own — see `stream_chunk_schedule` on the agent for what the numbers mean. Ascending. Clear it and the platform's applies. 1–8 items. |
| `resume_greeting_after_minutes` | integer or null | How quiet a conversation must be before the agent greets the person again, in minutes. A conversation continued inside this window gets no fresh opening line, because re-greeting somebody who refreshed the page thirty seconds ago is worse than silence — and costs a model call and a speech call to be worse. Continued outside it, the agent says hello again, which is what a `first_message` direction reading "say hello and catch up with them" is asking for. **Zero means always greet**, on every connection. Clear it (`null`) and the platform's `agents.greeting.resume_after_minutes` applies (`GET /v1/platform/limits`). Organisation-wide: every agent here uses it. It is read when a socket connects, so a change reaches the next connection rather than the ones already open. 0–43200. |
| `resume_max_age_days` | integer or null | How old this person's last conversation may be and still be **continued** rather than replaced, in days. Applies to `POST /v1/agent/sessions` when it resumes — see `resume` on that request. Past this, opening a session starts a fresh conversation instead. Nothing is lost at the boundary: durable facts and the standing relationship belong to the person rather than the conversation, and carry into every new one. **Zero means never resume** — every session is a new conversation, which is what an integration that treats a visit as a conversation wants. Clear it (`null`) and the platform's `agents.sessions.resume_max_age_days` applies (`GET /v1/platform/limits`). 0–365. |
| `built_in_compact_instruction` | string | The summarising instruction used when neither the agent nor the organisation sets one. |
| `built_in_emotion_instruction` | string | The platform's mood-reporting instruction, served as it stands so the console never shows a stale copy of it. |
| `built_in_fact_instruction` | string | The extraction instruction used when neither the agent nor the organisation sets one. |
| `built_in_relation_instruction` | string | The platform's relationship instruction, served as it stands so the console never shows a stale copy of it. |
| `moods` | array of string | Every mood label the gateway accepts. A reply reporting anything else is stored with no mood rather than with a made-up one, so an overridden instruction should name these. |

## Read an agent

`GET /v1/agents/{agent_id}`

```bash title="curl"
curl "https://api.lumavox.ai/v1/agents/$AGENT_ID" \
  -H "Authorization: Bearer $API_KEY"
```

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `agent_id` | string | yes |  |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — The agent's full configuration |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | Unknown agent id |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `code_execution` | boolean | Whether the model may write and run Python while it replies — sums, unit conversions, dates — and answer from what it printed. Gemini's own tool: it runs at Google, with no network, and nothing of yours. **No fee, but not free**: the code and its output are model tokens, billed like any reply. |
| `compact_instruction` | string or null | What to tell the model when summarising. Unset uses the organisation's default, then a built-in instruction written for in-character conversations. At most 4000 characters. |
| `compact_max_output_tokens` | integer or null | Length cap on the summary, in tokens — the model's thinking included, which shares the budget. Unset uses the platform's `agents.memory.compaction.max_output_tokens` on the Limits page, twice it when the compaction extracts facts too. 1–65536. |
| `compact_model` | string or null | Model for the summarising call. Unset uses the agent's own. At most 64 characters. |
| `compact_thinking_level` | `"minimal"`, `"low"`, `"medium"`, `"high"` or null | Thinking level for the summarising call. |
| `compact_threshold_tokens` | integer or null | Compact automatically once a reply's first model call reads this many tokens — the conversation, its summary and the instruction, without the inline knowledge or the turn's looked-up passages, which no summary shrinks. Tool results do not count. Unset uses the platform's default, `agents.memory.compaction.default_threshold_tokens` on the Limits page. At most 200,000, and at least twice what the instruction costs on its own. `POST /v1/agent/sessions/{id}/compact` works whatever it is. 1–200000. |
| `emotion_enabled` | boolean | Whether the model reports its own mood — one line in front of each reply, which is taken off the reply and sent as the socket's `mood` event, and kept on each stored message. Costs about six output tokens per turn and no extra model call. The reply text you are handed does not contain the line: a marker with a known label, or with an intensity, or alone on its own line is always removed, and only a marker with an unknown label, no intensity and prose on the same line is left — at that point it cannot be told from a sentence that happens to start with the word. |
| `emotion_instruction` | string or null | What the model is told about reporting a mood. Unset uses the organisation's default, then a built-in one naming every supported label. **Override this and you own the format.** The gateway strips a first line shaped `mood: <label> <intensity>` — with a label it knows, or with an intensity, or alone on its line — and records a mood only for a label it knows. An instruction that asks for any other shape leaves the model's line in the reply as text and stores no mood. At most 4000 characters. |
| `environment` | string or null | The setting the conversation takes place in. At most 8000 characters. |
| `extract_facts_on_compact` | boolean | Whether a compaction also extracts durable facts about the user, in the same model call. A caller can always override this per request; this is what the automatic path uses. |
| `fact_instruction` | string or null | What counts as worth remembering. Unset falls back to the organisation's default, then to a built-in instruction. At most 4000 characters. |
| `facts_enabled` | boolean | Whether this agent remembers facts about the people it talks to. **On by default**, unlike most switches here. Off, its conversations are never read for facts and its turns recall none — nothing is charged for either — and a compaction extracts nothing whatever it is asked. Facts already stored stay, and other agents still recall the ones they share. Turning it back on reads only what is said from then on. |
| `facts_scope` | `"person"`, `"agent"` or null | Whose facts this agent sees and adds to. `person` (the default, and what unset means): what any of your agents learnt about the person that shares. `agent`: its own only — what it learns is not recalled by your other agents, and it does not recall theirs. A change applies to what it learns from then on: a fact keeps the scope it was learnt under. |
| `first_message` | string or null | The agent's opening line — or, with `first_message_generated` on, the direction for one. As a line it costs no tokens and is identical every time. As a direction ("say hello and catch up with them") the model writes it fresh, so it can use whatever long-term memory recalled about that person — at the cost of one billed model call. **Either way it is delivered as the agent's first turn on `/v1/agent/stream`**, so it is streamed and spoken a sentence at a time like every other reply. Opening a session with `POST /v1/agent/sessions` does not produce it, and an integration that never opens a socket never hears it. The agent is not greeted twice: a conversation continued within a few minutes gets no fresh opening line, and `ready.greeting` says which before a frame arrives. At most 4000 characters. |
| `first_message_generated` | boolean | Whether `first_message` is a direction for the model rather than the words themselves. **The same text means different things under this flag**, and nothing on the server can tell them apart. Turning it off while the text is still written as an instruction shows that instruction to your users verbatim. It needs a `first_message` to act on: switched on with the text empty is refused (422), because an agent with a direction to follow and no direction never greets. |
| `goal` | string or null | What it is trying to achieve in the conversation. At most 8000 characters. |
| `guardrails` | string or null | What it must not do or discuss. At most 8000 characters. |
| `knowledge_enabled` | boolean | Whether the agent may use the knowledge entries assigned to it. **Defaults to on**, unlike the other switches here: knowledge works as soon as an entry is assigned, and this only takes it away again. Off, the entries are not listed in the system instruction and the `search_knowledge` tool is not declared, so the agent answers as though it had none. **Nothing is deleted or unassigned** — turning it back on restores exactly what was there, which is what makes this the way to try an agent without its reference material. Assign entries with `PUT /v1/agents/{agent_id}/knowledge`. |
| `knowledge_lookup_first` | boolean or null | When knowledge is looked up: search before every reply and hand the model what matches, so it never answers from memory what its material says. **On by default.** Off, the model searches only when it decides to, which is slower when it does and wrong more often when it does not. |
| `knowledge_max_results` | integer or null | Pieces of material a lookup hands the model, 1-8. Unset is the platform's `agents.knowledge.default_results` (`GET /v1/platform/limits`). 1–8. |
| `knowledge_mode` | `"auto"`, `"prompt"`, `"lookup"` or null | How the agent reads its knowledge. `auto` (the default, and what unset means) puts the whole text in the prompt while it fits the platform's `agents.knowledge.inline_budget_tokens` — no lookup, nothing to miss — and looks it up beyond that. `prompt` puts it in the prompt up to the platform's `agents.knowledge.prompt_cap_tokens` (each turn's first word is slower the more there is). `lookup` always looks it up. Both are on `GET /v1/platform/limits`. |
| `max_facts` | integer or null | The most of the person's facts put in front of the model, 1-100. Unset is as many as fit in the platform's `agents.memory.recall.budget_chars` (`GET /v1/platform/limits`; 6,000 characters is some 1,500 tokens). When they do not all fit, the ones a conversation confirmed or learnt most recently go in. They are prompt tokens on every turn. 1–100. |
| `max_output_tokens` | integer or null | Cap on reply length. Unset means the model's own limit. 1–65536. |
| `model` | string | Gemini model id. |
| `name` | string | Human-readable name. Shown in the console; never sent to the model. |
| `personality` | string or null | Who the agent is — character, background, quirks. At most 8000 characters. |
| `relation_enabled` | boolean | Whether the agent keeps a standing relationship with each person it talks to — warmth, trust and how well they know each other, carried across every conversation with that end user and never forgotten. Turning this on declares a tool the model calls when something changes its view of them. **It adds no latency to an ordinary turn**: the provider returns the reply and the tool call together, and the call is answered on the next turn. The one exception is a reply that is only the call with no words in it, which costs a second model call to write them. Read it with `GET /v1/agent/relations`. |
| `relation_instruction` | string or null | What the model is told about the relationship it keeps. Unset uses the organisation's default, then a built-in one. This is the *disposition* — that the history is real and that most replies should record nothing. The tool's own description covers the mechanism, and is not configurable. At most 4000 characters. |
| `stream_chunk_schedule` | array of integer or null | How a streamed reply is cut into pieces for speech, smallest first - for example `[60, 160, 250]`. Ascending; a schedule out of order is refused. **Only `/v1/agent/stream` uses this**; a buffered turn has the whole reply before it speaks and nothing to schedule. The **first** number is the one that matters: it decides how much text must arrive before the agent starts talking, so lowering it starts speech sooner. There is a floor worth knowing - a chunk has to be generated faster than the previous one plays, so a first chunk under roughly 50 characters starts sooner and then runs out of audio before the next piece exists, which is heard as a stutter rather than as a fast reply. Later numbers trade first-audio latency for prosody: a longer piece is spoken with better intonation because the model can see more of the sentence at once. Omit it to use your organisation's default, and then the platform's. 1–8 items. |
| `stt_config` | AgentSTTInfo or null | How a spoken turn is transcribed — the model, the language hint, and the sample rate a client sends PCM at on `/v1/agent/stream`. Only what you set is stored: a model left unset follows the platform's default. **Defaults, not policy.** `user_audio.start` may name any of these per turn and what it names wins. There is no switch for hearing, and no scope of its own: a credential that may talk to an agent may speak to it, exactly as it has always been able to hear it speak back. |
| `stt_config.language` | string or null | ISO-639-1 or -639-3 code the agent's spoken turns are transcribed in. Omit it and the language is detected per turn. A `language` on `user_audio.start` overrides it. |
| `stt_config.mark_spoken` | boolean or null | Whether a spoken turn reaches the model with a note saying it was transcribed from speech and may contain recognition errors, so the agent reads it for meaning rather than answering a misheard word or pointing out a typo nobody made. Typed turns are never marked, and the note is not stored in the transcript. Unset or null means on. |
| `stt_config.model` | `"scribe_v2"` or null | Transcription model for this agent's spoken turns. Unset or null means the platform's default model, and follows it if that default ever changes. |
| `stt_config.sample_rate` | `8000`, `16000`, `22050`, `24000`, `44100` or null | Hz of the PCM a client sends on `/v1/agent/stream`. One of 8000, 16000, 22050, 24000, 44100. **Only the socket uses this.** Every other way of sending audio sends a file, and a file states its own rate. Unset means 24000, which is what the socket *sends* — so a client that records what it plays needs nothing here. 16000 is a third less upstream for audio the transcriber resamples to 16000 anyway, which is the reason to change it. Announced back on `ready` either way. |
| `thinking_level` | `"minimal"`, `"low"`, `"medium"`, `"high"` or null | How hard the model thinks before answering. `minimal` is fastest to first token; `medium` is the model's own default. Unset means the model decides. |
| `thinking_summaries` | `"auto"`, `"none"` or null | Whether a readable summary of the reasoning is returned. |
| `tone` | string or null | How it speaks: register, pacing, verbosity. At most 8000 characters. |
| `url_context` | boolean | Whether the model may read a web page by its address while it replies, when the person gives one or one is in its instructions. Gemini's own tool: the page is fetched by Google. The pages it read come back as the reply's `sources`. **No fee, but not free**: a page's text is model tokens, billed like any reply — a long page can be thousands. A page is untrusted input: what it says reaches the model, so the most it can steer the agent towards is the agent's own tools, and a tool marked `ask_first` still needs the person. |
| `voice_config` | AgentVoiceInfo or null | Voice and model settings. **A speaking agent speaks on `eleven_flash_v2_5`** (used when `model` is left unset) **or `eleven_multilingual_v2`**, with any voice from `GET /v1/voices` that offers it. `eleven_v3` is refused with 422 when `voice_enabled` is true: it cannot stream over the socket agents use. An agent that does not speak may leave this unset. |
| `voice_config.format` | `"wav"`, `"pcm"` or null | The audio format this voice is configured with. On the WebSocket a reply is always raw 16-bit PCM, at the rate its `audio.start` names. |
| `voice_config.model` | string or null | Which model speaks: `eleven_flash_v2_5` (the default) or `eleven_multilingual_v2`, and one the voice lists as `models` in `GET /v1/voices`. |
| `voice_config.provider_options` | ProviderVoiceSettingsInfo or null | The provider's voice settings, on top of the ones the voice was registered with. Unset takes theirs. |
| `voice_config.provider_options.stability` | number or null | Low is more expressive and less consistent between generations; high is flatter and more repeatable. |
| `voice_config.provider_options.similarity_boost` | number or null | How closely to cling to the original voice. High can reproduce artefacts in the source. |
| `voice_config.provider_options.style` | number or null | Style exaggeration. Costs latency; leave unset on anything conversational. |
| `voice_config.provider_options.use_speaker_boost` | boolean or null | Similarity at the cost of latency. |
| `voice_config.provider_options.speed` | number or null | 1.0 is the voice's own pace. |
| `voice_config.sample_rate` | `8000`, `16000`, `22050`, `24000`, `44100` or null | Hz of the returned audio — one of 8000, 16000, 22050, 24000, 44100, each served natively by the provider. Unset means 24000. The response's `sample_rate` and the `X-Sample-Rate` header always say what was actually produced. |
| `voice_config.voice_id` | string or null | An id from `GET /v1/voices` — the built-in voice, a platform voice, or one your organisation uploaded. |
| `voice_enabled` | boolean | Whether replies come back as audio as well as text. Speech is generated after the reply and adds its own latency — roughly a fifth of the spoken duration. A caller can override this per turn with `speech` on the socket's `user_message` or `user_audio.start`. |
| `id` | string |  |
| `created_at` | string (date-time) |  |
| `updated_at` | string (date-time) |  |
| `version` | integer | Moves by one on every save. Send it back as `expected_version` on an update to have the save refused (409) if somebody else changed the agent since you read it. |

## Update an agent

`PATCH /v1/agents/{agent_id}`

Needs `agent:write`.

A partial update: fields you omit are left alone, and an explicit `null` clears one.

**Conversations already open pick this up within about a minute**, at their next turn after the change reaches them, so there is nothing to reopen - and equally, an edit made while somebody is talking to the agent changes what they are talking to.

**The row is checked as a whole**: a `thinking_level` the agent's model refuses, a generated opening with no direction, or a speech schedule out of order is a 422 naming the problem, whichever field the request changed. Send `expected_version` to be refused with 409 if somebody else changed the agent since you read it.

```bash title="curl"
curl -X PATCH "https://api.lumavox.ai/v1/agents/$AGENT_ID" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `agent_id` | string | yes |  |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `code_execution` | boolean or null |  | Whether the model may write and run Python while it replies — sums, unit conversions, dates — and answer from what it printed. Gemini's own tool: it runs at Google, with no network, and nothing of yours. **No fee, but not free**: the code and its output are model tokens, billed like any reply. |
| `compact_instruction` | string or null |  | What to tell the model when summarising. Unset uses the organisation's default, then a built-in instruction written for in-character conversations. At most 4000 characters. |
| `compact_max_output_tokens` | integer or null |  | Length cap on the summary, in tokens — the model's thinking included, which shares the budget. Unset uses the platform's `agents.memory.compaction.max_output_tokens` on the Limits page, twice it when the compaction extracts facts too. 1–65536. |
| `compact_model` | string or null |  | Model for the summarising call. Unset uses the agent's own. At most 64 characters. |
| `compact_thinking_level` | `"minimal"`, `"low"`, `"medium"`, `"high"` or null |  | Thinking level for the summarising call. |
| `compact_threshold_tokens` | integer or null |  | Compact automatically once a reply's first model call reads this many tokens — the conversation, its summary and the instruction, without the inline knowledge or the turn's looked-up passages, which no summary shrinks. Tool results do not count. Unset uses the platform's default, `agents.memory.compaction.default_threshold_tokens` on the Limits page. At most 200,000, and at least twice what the instruction costs on its own. `POST /v1/agent/sessions/{id}/compact` works whatever it is. 1–200000. |
| `emotion_enabled` | boolean or null |  | Whether the model reports its own mood — one line in front of each reply, which is taken off the reply and sent as the socket's `mood` event, and kept on each stored message. Costs about six output tokens per turn and no extra model call. The reply text you are handed does not contain the line: a marker with a known label, or with an intensity, or alone on its own line is always removed, and only a marker with an unknown label, no intensity and prose on the same line is left — at that point it cannot be told from a sentence that happens to start with the word. |
| `emotion_instruction` | string or null |  | What the model is told about reporting a mood. Unset uses the organisation's default, then a built-in one naming every supported label. **Override this and you own the format.** The gateway strips a first line shaped `mood: <label> <intensity>` — with a label it knows, or with an intensity, or alone on its line — and records a mood only for a label it knows. An instruction that asks for any other shape leaves the model's line in the reply as text and stores no mood. At most 4000 characters. |
| `environment` | string or null |  | The setting the conversation takes place in. At most 8000 characters. |
| `extract_facts_on_compact` | boolean or null |  | Whether a compaction also extracts durable facts about the user, in the same model call. A caller can always override this per request; this is what the automatic path uses. |
| `fact_instruction` | string or null |  | What counts as worth remembering. Unset falls back to the organisation's default, then to a built-in instruction. At most 4000 characters. |
| `facts_enabled` | boolean or null |  | Whether this agent remembers facts about the people it talks to. **On by default**, unlike most switches here. Off, its conversations are never read for facts and its turns recall none — nothing is charged for either — and a compaction extracts nothing whatever it is asked. Facts already stored stay, and other agents still recall the ones they share. Turning it back on reads only what is said from then on. |
| `facts_scope` | `"person"`, `"agent"` or null |  | Whose facts this agent sees and adds to. `person` (the default, and what unset means): what any of your agents learnt about the person that shares. `agent`: its own only — what it learns is not recalled by your other agents, and it does not recall theirs. A change applies to what it learns from then on: a fact keeps the scope it was learnt under. |
| `first_message` | string or null |  | The agent's opening line — or, with `first_message_generated` on, the direction for one. As a line it costs no tokens and is identical every time. As a direction ("say hello and catch up with them") the model writes it fresh, so it can use whatever long-term memory recalled about that person — at the cost of one billed model call. **Either way it is delivered as the agent's first turn on `/v1/agent/stream`**, so it is streamed and spoken a sentence at a time like every other reply. Opening a session with `POST /v1/agent/sessions` does not produce it, and an integration that never opens a socket never hears it. The agent is not greeted twice: a conversation continued within a few minutes gets no fresh opening line, and `ready.greeting` says which before a frame arrives. At most 4000 characters. |
| `first_message_generated` | boolean or null |  | Whether `first_message` is a direction for the model rather than the words themselves. **The same text means different things under this flag**, and nothing on the server can tell them apart. Turning it off while the text is still written as an instruction shows that instruction to your users verbatim. It needs a `first_message` to act on: switched on with the text empty is refused (422), because an agent with a direction to follow and no direction never greets. |
| `goal` | string or null |  | What it is trying to achieve in the conversation. At most 8000 characters. |
| `guardrails` | string or null |  | What it must not do or discuss. At most 8000 characters. |
| `knowledge_enabled` | boolean or null |  | Whether the agent may use the knowledge entries assigned to it. **Defaults to on**, unlike the other switches here: knowledge works as soon as an entry is assigned, and this only takes it away again. Off, the entries are not listed in the system instruction and the `search_knowledge` tool is not declared, so the agent answers as though it had none. **Nothing is deleted or unassigned** — turning it back on restores exactly what was there, which is what makes this the way to try an agent without its reference material. Assign entries with `PUT /v1/agents/{agent_id}/knowledge`. |
| `knowledge_lookup_first` | boolean or null |  | When knowledge is looked up: search before every reply and hand the model what matches, so it never answers from memory what its material says. **On by default.** Off, the model searches only when it decides to, which is slower when it does and wrong more often when it does not. |
| `knowledge_max_results` | integer or null |  | Pieces of material a lookup hands the model, 1-8. Unset is the platform's `agents.knowledge.default_results` (`GET /v1/platform/limits`). 1–8. |
| `knowledge_mode` | `"auto"`, `"prompt"`, `"lookup"` or null |  | How the agent reads its knowledge. `auto` (the default, and what unset means) puts the whole text in the prompt while it fits the platform's `agents.knowledge.inline_budget_tokens` — no lookup, nothing to miss — and looks it up beyond that. `prompt` puts it in the prompt up to the platform's `agents.knowledge.prompt_cap_tokens` (each turn's first word is slower the more there is). `lookup` always looks it up. Both are on `GET /v1/platform/limits`. |
| `max_facts` | integer or null |  | The most of the person's facts put in front of the model, 1-100. Unset is as many as fit in the platform's `agents.memory.recall.budget_chars` (`GET /v1/platform/limits`; 6,000 characters is some 1,500 tokens). When they do not all fit, the ones a conversation confirmed or learnt most recently go in. They are prompt tokens on every turn. 1–100. |
| `max_output_tokens` | integer or null |  | Cap on reply length. Unset means the model's own limit. 1–65536. |
| `model` | string or null |  | Gemini model id. `null` or empty returns it to the default, the platform's `agents.model` (`GET /v1/platform/limits`). At most 64 characters. |
| `name` | string |  | Human-readable name. Shown in the console; never sent to the model. 1–255 characters. |
| `personality` | string or null |  | Who the agent is — character, background, quirks. At most 8000 characters. |
| `relation_enabled` | boolean or null |  | Whether the agent keeps a standing relationship with each person it talks to — warmth, trust and how well they know each other, carried across every conversation with that end user and never forgotten. Turning this on declares a tool the model calls when something changes its view of them. **It adds no latency to an ordinary turn**: the provider returns the reply and the tool call together, and the call is answered on the next turn. The one exception is a reply that is only the call with no words in it, which costs a second model call to write them. Read it with `GET /v1/agent/relations`. |
| `relation_instruction` | string or null |  | What the model is told about the relationship it keeps. Unset uses the organisation's default, then a built-in one. This is the *disposition* — that the history is real and that most replies should record nothing. The tool's own description covers the mechanism, and is not configurable. At most 4000 characters. |
| `stream_chunk_schedule` | array of integer or null |  | How a streamed reply is cut into pieces for speech, smallest first - for example `[60, 160, 250]`. Ascending; a schedule out of order is refused. **Only `/v1/agent/stream` uses this**; a buffered turn has the whole reply before it speaks and nothing to schedule. The **first** number is the one that matters: it decides how much text must arrive before the agent starts talking, so lowering it starts speech sooner. There is a floor worth knowing - a chunk has to be generated faster than the previous one plays, so a first chunk under roughly 50 characters starts sooner and then runs out of audio before the next piece exists, which is heard as a stutter rather than as a fast reply. Later numbers trade first-audio latency for prosody: a longer piece is spoken with better intonation because the model can see more of the sentence at once. Omit it to use your organisation's default, and then the platform's. 1–8 items. |
| `stt_config` | AgentSTT or null |  | How a spoken turn is transcribed — the model, the language hint, and the sample rate a client sends PCM at on `/v1/agent/stream`. Only what you set is stored: a model left unset follows the platform's default. **Defaults, not policy.** `user_audio.start` may name any of these per turn and what it names wins. There is no switch for hearing, and no scope of its own: a credential that may talk to an agent may speak to it, exactly as it has always been able to hear it speak back. |
| `stt_config.language` | string or null |  | ISO-639-1 or -639-3 code the agent's spoken turns are transcribed in. Omit it and the language is detected per turn. A `language` on `user_audio.start` overrides it. 2–8 characters. |
| `stt_config.mark_spoken` | boolean or null |  | Whether a spoken turn reaches the model with a note saying it was transcribed from speech and may contain recognition errors, so the agent reads it for meaning rather than answering a misheard word or pointing out a typo nobody made. Typed turns are never marked, and the note is not stored in the transcript. Unset or null means on. |
| `stt_config.model` | `"scribe_v2"` or null |  | Transcription model for this agent's spoken turns. Unset or null means the platform's default model, and follows it if that default ever changes. |
| `stt_config.sample_rate` | `8000`, `16000`, `22050`, `24000`, `44100` or null |  | Hz of the PCM a client sends on `/v1/agent/stream`. One of 8000, 16000, 22050, 24000, 44100. **Only the socket uses this.** Every other way of sending audio sends a file, and a file states its own rate. Unset means 24000, which is what the socket *sends* — so a client that records what it plays needs nothing here. 16000 is a third less upstream for audio the transcriber resamples to 16000 anyway, which is the reason to change it. Announced back on `ready` either way. |
| `thinking_level` | `"minimal"`, `"low"`, `"medium"`, `"high"` or null |  | How hard the model thinks before answering. `minimal` is fastest to first token; `medium` is the model's own default. Unset means the model decides. |
| `thinking_summaries` | `"auto"`, `"none"` or null |  | Whether a readable summary of the reasoning is returned. |
| `tone` | string or null |  | How it speaks: register, pacing, verbosity. At most 8000 characters. |
| `url_context` | boolean or null |  | Whether the model may read a web page by its address while it replies, when the person gives one or one is in its instructions. Gemini's own tool: the page is fetched by Google. The pages it read come back as the reply's `sources`. **No fee, but not free**: a page's text is model tokens, billed like any reply — a long page can be thousands. A page is untrusted input: what it says reaches the model, so the most it can steer the agent towards is the agent's own tools, and a tool marked `ask_first` still needs the person. |
| `voice_config` | AgentVoice or null |  | Voice and model settings. **A speaking agent speaks on `eleven_flash_v2_5`** (used when `model` is left unset) **or `eleven_multilingual_v2`**, with any voice from `GET /v1/voices` that offers it. `eleven_v3` is refused with 422 when `voice_enabled` is true: it cannot stream over the socket agents use. An agent that does not speak may leave this unset. |
| `voice_config.format` | `"wav"`, `"pcm"` |  | The audio format this voice is configured with. On the WebSocket a reply is always raw 16-bit PCM, at the rate its `audio.start` names. Default `"wav"`. |
| `voice_config.model` | string |  | Which model speaks: `eleven_flash_v2_5` (the default) or `eleven_multilingual_v2`, and one the voice lists as `models` in `GET /v1/voices`. Default `"eleven_flash_v2_5"`. |
| `voice_config.provider_options` | ProviderVoiceSettings or null |  | The provider's voice settings, on top of the ones the voice was registered with. Unset takes theirs. |
| `voice_config.provider_options.stability` | number or null |  | Low is more expressive and less consistent between generations; high is flatter and more repeatable. 0–1. |
| `voice_config.provider_options.similarity_boost` | number or null |  | How closely to cling to the original voice. High can reproduce artefacts in the source. 0–1. |
| `voice_config.provider_options.style` | number or null |  | Style exaggeration. Costs latency; leave unset on anything conversational. 0–1. |
| `voice_config.provider_options.use_speaker_boost` | boolean or null |  | Similarity at the cost of latency. |
| `voice_config.provider_options.speed` | number or null |  | 1.0 is the voice's own pace. 0.7–1.2. |
| `voice_config.sample_rate` | `8000`, `16000`, `22050`, `24000`, `44100` or null |  | Hz of the returned audio — one of 8000, 16000, 22050, 24000, 44100, each served natively by the provider. Unset means 24000. The response's `sample_rate` and the `X-Sample-Rate` header always say what was actually produced. |
| `voice_config.voice_id` | string |  | An id from `GET /v1/voices` — the built-in voice, a platform voice, or one your organisation uploaded. Default `"default"`. |
| `voice_enabled` | boolean or null |  | Whether replies come back as audio as well as text. Speech is generated after the reply and adds its own latency — roughly a fifth of the spoken duration. A caller can override this per turn with `speech` on the socket's `user_message` or `user_audio.start`. |
| `expected_version` | integer or null |  | The `version` you last read for this agent. When set and the agent has been changed since, the update is refused with 409 and nothing is written - re-read it and decide again. Omit it to write unconditionally. |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — The agent as it now stands |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | Unknown agent id |
| `409` | `expected_version` was sent and the agent has been changed since. Nothing was written. |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `code_execution` | boolean | Whether the model may write and run Python while it replies — sums, unit conversions, dates — and answer from what it printed. Gemini's own tool: it runs at Google, with no network, and nothing of yours. **No fee, but not free**: the code and its output are model tokens, billed like any reply. |
| `compact_instruction` | string or null | What to tell the model when summarising. Unset uses the organisation's default, then a built-in instruction written for in-character conversations. At most 4000 characters. |
| `compact_max_output_tokens` | integer or null | Length cap on the summary, in tokens — the model's thinking included, which shares the budget. Unset uses the platform's `agents.memory.compaction.max_output_tokens` on the Limits page, twice it when the compaction extracts facts too. 1–65536. |
| `compact_model` | string or null | Model for the summarising call. Unset uses the agent's own. At most 64 characters. |
| `compact_thinking_level` | `"minimal"`, `"low"`, `"medium"`, `"high"` or null | Thinking level for the summarising call. |
| `compact_threshold_tokens` | integer or null | Compact automatically once a reply's first model call reads this many tokens — the conversation, its summary and the instruction, without the inline knowledge or the turn's looked-up passages, which no summary shrinks. Tool results do not count. Unset uses the platform's default, `agents.memory.compaction.default_threshold_tokens` on the Limits page. At most 200,000, and at least twice what the instruction costs on its own. `POST /v1/agent/sessions/{id}/compact` works whatever it is. 1–200000. |
| `emotion_enabled` | boolean | Whether the model reports its own mood — one line in front of each reply, which is taken off the reply and sent as the socket's `mood` event, and kept on each stored message. Costs about six output tokens per turn and no extra model call. The reply text you are handed does not contain the line: a marker with a known label, or with an intensity, or alone on its own line is always removed, and only a marker with an unknown label, no intensity and prose on the same line is left — at that point it cannot be told from a sentence that happens to start with the word. |
| `emotion_instruction` | string or null | What the model is told about reporting a mood. Unset uses the organisation's default, then a built-in one naming every supported label. **Override this and you own the format.** The gateway strips a first line shaped `mood: <label> <intensity>` — with a label it knows, or with an intensity, or alone on its line — and records a mood only for a label it knows. An instruction that asks for any other shape leaves the model's line in the reply as text and stores no mood. At most 4000 characters. |
| `environment` | string or null | The setting the conversation takes place in. At most 8000 characters. |
| `extract_facts_on_compact` | boolean | Whether a compaction also extracts durable facts about the user, in the same model call. A caller can always override this per request; this is what the automatic path uses. |
| `fact_instruction` | string or null | What counts as worth remembering. Unset falls back to the organisation's default, then to a built-in instruction. At most 4000 characters. |
| `facts_enabled` | boolean | Whether this agent remembers facts about the people it talks to. **On by default**, unlike most switches here. Off, its conversations are never read for facts and its turns recall none — nothing is charged for either — and a compaction extracts nothing whatever it is asked. Facts already stored stay, and other agents still recall the ones they share. Turning it back on reads only what is said from then on. |
| `facts_scope` | `"person"`, `"agent"` or null | Whose facts this agent sees and adds to. `person` (the default, and what unset means): what any of your agents learnt about the person that shares. `agent`: its own only — what it learns is not recalled by your other agents, and it does not recall theirs. A change applies to what it learns from then on: a fact keeps the scope it was learnt under. |
| `first_message` | string or null | The agent's opening line — or, with `first_message_generated` on, the direction for one. As a line it costs no tokens and is identical every time. As a direction ("say hello and catch up with them") the model writes it fresh, so it can use whatever long-term memory recalled about that person — at the cost of one billed model call. **Either way it is delivered as the agent's first turn on `/v1/agent/stream`**, so it is streamed and spoken a sentence at a time like every other reply. Opening a session with `POST /v1/agent/sessions` does not produce it, and an integration that never opens a socket never hears it. The agent is not greeted twice: a conversation continued within a few minutes gets no fresh opening line, and `ready.greeting` says which before a frame arrives. At most 4000 characters. |
| `first_message_generated` | boolean | Whether `first_message` is a direction for the model rather than the words themselves. **The same text means different things under this flag**, and nothing on the server can tell them apart. Turning it off while the text is still written as an instruction shows that instruction to your users verbatim. It needs a `first_message` to act on: switched on with the text empty is refused (422), because an agent with a direction to follow and no direction never greets. |
| `goal` | string or null | What it is trying to achieve in the conversation. At most 8000 characters. |
| `guardrails` | string or null | What it must not do or discuss. At most 8000 characters. |
| `knowledge_enabled` | boolean | Whether the agent may use the knowledge entries assigned to it. **Defaults to on**, unlike the other switches here: knowledge works as soon as an entry is assigned, and this only takes it away again. Off, the entries are not listed in the system instruction and the `search_knowledge` tool is not declared, so the agent answers as though it had none. **Nothing is deleted or unassigned** — turning it back on restores exactly what was there, which is what makes this the way to try an agent without its reference material. Assign entries with `PUT /v1/agents/{agent_id}/knowledge`. |
| `knowledge_lookup_first` | boolean or null | When knowledge is looked up: search before every reply and hand the model what matches, so it never answers from memory what its material says. **On by default.** Off, the model searches only when it decides to, which is slower when it does and wrong more often when it does not. |
| `knowledge_max_results` | integer or null | Pieces of material a lookup hands the model, 1-8. Unset is the platform's `agents.knowledge.default_results` (`GET /v1/platform/limits`). 1–8. |
| `knowledge_mode` | `"auto"`, `"prompt"`, `"lookup"` or null | How the agent reads its knowledge. `auto` (the default, and what unset means) puts the whole text in the prompt while it fits the platform's `agents.knowledge.inline_budget_tokens` — no lookup, nothing to miss — and looks it up beyond that. `prompt` puts it in the prompt up to the platform's `agents.knowledge.prompt_cap_tokens` (each turn's first word is slower the more there is). `lookup` always looks it up. Both are on `GET /v1/platform/limits`. |
| `max_facts` | integer or null | The most of the person's facts put in front of the model, 1-100. Unset is as many as fit in the platform's `agents.memory.recall.budget_chars` (`GET /v1/platform/limits`; 6,000 characters is some 1,500 tokens). When they do not all fit, the ones a conversation confirmed or learnt most recently go in. They are prompt tokens on every turn. 1–100. |
| `max_output_tokens` | integer or null | Cap on reply length. Unset means the model's own limit. 1–65536. |
| `model` | string | Gemini model id. |
| `name` | string | Human-readable name. Shown in the console; never sent to the model. |
| `personality` | string or null | Who the agent is — character, background, quirks. At most 8000 characters. |
| `relation_enabled` | boolean | Whether the agent keeps a standing relationship with each person it talks to — warmth, trust and how well they know each other, carried across every conversation with that end user and never forgotten. Turning this on declares a tool the model calls when something changes its view of them. **It adds no latency to an ordinary turn**: the provider returns the reply and the tool call together, and the call is answered on the next turn. The one exception is a reply that is only the call with no words in it, which costs a second model call to write them. Read it with `GET /v1/agent/relations`. |
| `relation_instruction` | string or null | What the model is told about the relationship it keeps. Unset uses the organisation's default, then a built-in one. This is the *disposition* — that the history is real and that most replies should record nothing. The tool's own description covers the mechanism, and is not configurable. At most 4000 characters. |
| `stream_chunk_schedule` | array of integer or null | How a streamed reply is cut into pieces for speech, smallest first - for example `[60, 160, 250]`. Ascending; a schedule out of order is refused. **Only `/v1/agent/stream` uses this**; a buffered turn has the whole reply before it speaks and nothing to schedule. The **first** number is the one that matters: it decides how much text must arrive before the agent starts talking, so lowering it starts speech sooner. There is a floor worth knowing - a chunk has to be generated faster than the previous one plays, so a first chunk under roughly 50 characters starts sooner and then runs out of audio before the next piece exists, which is heard as a stutter rather than as a fast reply. Later numbers trade first-audio latency for prosody: a longer piece is spoken with better intonation because the model can see more of the sentence at once. Omit it to use your organisation's default, and then the platform's. 1–8 items. |
| `stt_config` | AgentSTTInfo or null | How a spoken turn is transcribed — the model, the language hint, and the sample rate a client sends PCM at on `/v1/agent/stream`. Only what you set is stored: a model left unset follows the platform's default. **Defaults, not policy.** `user_audio.start` may name any of these per turn and what it names wins. There is no switch for hearing, and no scope of its own: a credential that may talk to an agent may speak to it, exactly as it has always been able to hear it speak back. |
| `stt_config.language` | string or null | ISO-639-1 or -639-3 code the agent's spoken turns are transcribed in. Omit it and the language is detected per turn. A `language` on `user_audio.start` overrides it. |
| `stt_config.mark_spoken` | boolean or null | Whether a spoken turn reaches the model with a note saying it was transcribed from speech and may contain recognition errors, so the agent reads it for meaning rather than answering a misheard word or pointing out a typo nobody made. Typed turns are never marked, and the note is not stored in the transcript. Unset or null means on. |
| `stt_config.model` | `"scribe_v2"` or null | Transcription model for this agent's spoken turns. Unset or null means the platform's default model, and follows it if that default ever changes. |
| `stt_config.sample_rate` | `8000`, `16000`, `22050`, `24000`, `44100` or null | Hz of the PCM a client sends on `/v1/agent/stream`. One of 8000, 16000, 22050, 24000, 44100. **Only the socket uses this.** Every other way of sending audio sends a file, and a file states its own rate. Unset means 24000, which is what the socket *sends* — so a client that records what it plays needs nothing here. 16000 is a third less upstream for audio the transcriber resamples to 16000 anyway, which is the reason to change it. Announced back on `ready` either way. |
| `thinking_level` | `"minimal"`, `"low"`, `"medium"`, `"high"` or null | How hard the model thinks before answering. `minimal` is fastest to first token; `medium` is the model's own default. Unset means the model decides. |
| `thinking_summaries` | `"auto"`, `"none"` or null | Whether a readable summary of the reasoning is returned. |
| `tone` | string or null | How it speaks: register, pacing, verbosity. At most 8000 characters. |
| `url_context` | boolean | Whether the model may read a web page by its address while it replies, when the person gives one or one is in its instructions. Gemini's own tool: the page is fetched by Google. The pages it read come back as the reply's `sources`. **No fee, but not free**: a page's text is model tokens, billed like any reply — a long page can be thousands. A page is untrusted input: what it says reaches the model, so the most it can steer the agent towards is the agent's own tools, and a tool marked `ask_first` still needs the person. |
| `voice_config` | AgentVoiceInfo or null | Voice and model settings. **A speaking agent speaks on `eleven_flash_v2_5`** (used when `model` is left unset) **or `eleven_multilingual_v2`**, with any voice from `GET /v1/voices` that offers it. `eleven_v3` is refused with 422 when `voice_enabled` is true: it cannot stream over the socket agents use. An agent that does not speak may leave this unset. |
| `voice_config.format` | `"wav"`, `"pcm"` or null | The audio format this voice is configured with. On the WebSocket a reply is always raw 16-bit PCM, at the rate its `audio.start` names. |
| `voice_config.model` | string or null | Which model speaks: `eleven_flash_v2_5` (the default) or `eleven_multilingual_v2`, and one the voice lists as `models` in `GET /v1/voices`. |
| `voice_config.provider_options` | ProviderVoiceSettingsInfo or null | The provider's voice settings, on top of the ones the voice was registered with. Unset takes theirs. |
| `voice_config.provider_options.stability` | number or null | Low is more expressive and less consistent between generations; high is flatter and more repeatable. |
| `voice_config.provider_options.similarity_boost` | number or null | How closely to cling to the original voice. High can reproduce artefacts in the source. |
| `voice_config.provider_options.style` | number or null | Style exaggeration. Costs latency; leave unset on anything conversational. |
| `voice_config.provider_options.use_speaker_boost` | boolean or null | Similarity at the cost of latency. |
| `voice_config.provider_options.speed` | number or null | 1.0 is the voice's own pace. |
| `voice_config.sample_rate` | `8000`, `16000`, `22050`, `24000`, `44100` or null | Hz of the returned audio — one of 8000, 16000, 22050, 24000, 44100, each served natively by the provider. Unset means 24000. The response's `sample_rate` and the `X-Sample-Rate` header always say what was actually produced. |
| `voice_config.voice_id` | string or null | An id from `GET /v1/voices` — the built-in voice, a platform voice, or one your organisation uploaded. |
| `voice_enabled` | boolean | Whether replies come back as audio as well as text. Speech is generated after the reply and adds its own latency — roughly a fifth of the spoken duration. A caller can override this per turn with `speech` on the socket's `user_message` or `user_audio.start`. |
| `id` | string |  |
| `created_at` | string (date-time) |  |
| `updated_at` | string (date-time) |  |
| `version` | integer | Moves by one on every save. Send it back as `expected_version` on an update to have the save refused (409) if somebody else changed the agent since you read it. |

## Delete an agent

`DELETE /v1/agents/{agent_id}`

Needs `agent:write`.

Also deletes every session and message belonging to it. This cannot be undone.

```bash title="curl"
curl -X DELETE "https://api.lumavox.ai/v1/agents/$AGENT_ID" \
  -H "Authorization: Bearer $API_KEY"
```

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `agent_id` | string | yes |  |

**Responses**

| Status | Description |
|---|---|
| `204` | The agent is gone |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | Unknown agent id |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

## What an agent can look up

`GET /v1/agents/{agent_id}/knowledge`

The knowledge entries assigned to this agent, newest first, in one page: they are bounded by your organisation's entry limit.

**Entries belong to the organisation, not to this agent.** The same one can be assigned to any number of agents, and it is written and edited on `/v1/knowledge`. This endpoint and its sibling below only say *which* an agent may read.

Only `ready` entries are visible to the agent itself. A `pending` one is still being indexed; a `failed` one carries the reason in `error`.

```bash title="curl"
curl "https://api.lumavox.ai/v1/agents/$AGENT_ID/knowledge" \
  -H "Authorization: Bearer $API_KEY"
```

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `agent_id` | string | yes |  |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — The entries this agent may look up |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | Unknown agent id |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `data` | array of KnowledgeInfo |  |
| `data[].agents` | array of AssignedAgent | Which agents have been given this entry. Empty means nobody has yet — the entry exists and no agent can look it up, which is a draft rather than a fault. |
| `data[].agents[].id` | string |  |
| `data[].agents[].name` | string |  |
| `data[].body` | string |  |
| `data[].body_chars` | integer | How long `body` is in full, in characters. A listing on the console sends the first few hundred characters only; read the entry for all of it. |
| `data[].category` | string or null |  |
| `data[].chunks` | integer | How many pieces the body was split into for searching. Most hand-written entries are one. |
| `data[].created_at` | string (date-time) |  |
| `data[].error` | string or null | Why indexing failed, when it did. |
| `data[].id` | string |  |
| `data[].refresh` | `"off"`, `"daily"`, `"weekly"` or null | For an entry made from a web page: how often it is read again. Null for the rest. |
| `data[].refreshed_at` | string (date-time) or null | When the page or file was last read. |
| `data[].searchable` | boolean | Whether an agent given this entry can find it now. False only until its first indexing succeeds; an edit or a failed re-index leaves the previous version searchable. |
| `data[].source_kind` | `"text"`, `"file"`, `"url"` | Where the text came from: `text` (written or pasted), `file` or `url`. |
| `data[].source_name` | string or null | The file's name, or the page's address. |
| `data[].status` | `"pending"`, `"ready"`, `"failed"`, `"importing"` | How the **latest** body went: `importing` while a file or a page is read into text, `pending` while it is indexed, `ready` once it is done, `failed` with the reason in `error`. Indexing takes seconds to a few minutes: an entry is `ready` once the search index returns it. Whether an agent can find the entry is `searchable`: an edited entry stays searchable at its previous version while the new one indexes. |
| `data[].summary` | string or null | One line saying what this entry answers, written by the model when the entry is saved and editable afterwards. **This is what the agent sees on every turn** — the body is fetched only when it chooses to look something up — so a vague line here makes the agent less likely to consult a perfectly good entry. |
| `data[].summary_source` | `"model"`, `"customer"` or null | Who wrote `summary`: `model` (rewritten with every new body) or `customer` (kept through every edit). Null while there is none. |
| `data[].title` | string |  |
| `data[].updated_at` | string (date-time) |  |
| `data[].version` | integer | Moves with every edit. Send it back as `expected_version` to refuse an edit made over somebody else's. |
| `next_cursor` | string or null | Pass as `cursor` for the next page; `null` when this is the last. |

## Set which entries an agent can look up

`PUT /v1/agents/{agent_id}/knowledge`

Needs `agent:write`.

Replaces this agent's assignments with exactly the entries named. **Declarative, not add and remove**: the list is the complete set, so there is no sequence of calls that can end up disagreeing with itself, and an empty list unassigns everything.

**Nothing is created or destroyed here.** Unassigning leaves the entry alone for every other agent that has it; deleting one is `DELETE /v1/knowledge/{entry_id}`. Ids that are not yours come back in `skipped` rather than failing the call.

```bash title="curl"
curl -X PUT "https://api.lumavox.ai/v1/agents/$AGENT_ID/knowledge" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"entry_ids":[]}'
```

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `agent_id` | string | yes |  |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `entry_ids` | array of string | yes | The complete set. An empty list clears every assignment, which is a real thing to want and not an error. At most 1000 items. |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — How many links were added and removed |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | Unknown agent id |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `added` | integer |  |
| `removed` | integer |  |
| `skipped` | array of string | Ids sent that are not this organisation's, ignored. |

## The tools an agent can call

`GET /v1/agents/{agent_id}/tools`

The tools turned on for this agent, by name, in one page: at most the platform's `limits.agent.tools` (`GET /v1/platform/limits`).

**Tools belong to the organisation, not to this agent**: the same one can be on any number of agents, and it is written on `/v1/tools`.

```bash title="curl"
curl "https://api.lumavox.ai/v1/agents/$AGENT_ID/tools" \
  -H "Authorization: Bearer $API_KEY"
```

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `agent_id` | string | yes |  |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — The tools this agent can call |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | Unknown agent id |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `data` | array of ToolInfo |  |
| `data[].id` | string | The tool's id, `tl_…`. |
| `data[].name` | string | What the model calls the tool by: 1 to 64 lower-case letters, digits and underscores, starting with a letter. Unique in your organisation; `search_knowledge` and `remember_relation` are the platform's own. |
| `data[].description` | string |  |
| `data[].kind` | `"webhook"`, `"app"` | `webhook`: your server, sent a signed `POST` with the call. `app`: the person's app, called over the agent's WebSocket. Fixed at creation. |
| `data[].parameters` | map of any | Its inputs, as a JSON Schema object: `{"type": "object", "properties": {…}, "required": […]}`, each property `string` (with an optional `enum`), `number`, `integer`, `boolean`, `array` (with `items`) or `object`, each with an optional `description`. Nothing else is taken: a key outside this set is refused rather than passed on unenforced. |
| `data[].url` | string or null | A webhook's address; `null` for an app's. |
| `data[].timeout_ms` | integer | How long a call may take, in milliseconds: 1000 to 30000, 10000 by default. Past it the call failed, and the model is told so. 1000–30000. |
| `data[].ask_first` | boolean | Never run on the model's word alone: the agent asks the person first, and the tool runs only after they say yes. |
| `data[].action_only` | boolean | An app's tool only: the app is told to act and the reply carries on at once, with no answer awaited. |
| `data[].filler` | string or null | The line said before the call; `null` for none. |
| `data[].header_names` | array of string | The names of the headers sent with each call; their values are never shown. |
| `data[].secret_hint` | string or null | The signing secret's last four characters; `null` for an app's tool. |
| `data[].agent_ids` | array of string | The agents it is turned on for. |
| `data[].version` | integer | Moves by one on every save. Send it back as `expected_version` on an update to have the save refused (409) if somebody else changed the tool since you read it. |
| `data[].created_at` | string (date-time) |  |
| `data[].updated_at` | string (date-time) |  |
| `data[].recent_failures` | integer | Its failed calls in your agents' conversations within the platform's `agents.tools.failure_window_minutes` — a deadline missed, an error answered, a server out of reach — counted from the first; 0 once the window passes with none. Each conversation reports its failures at most once in the platform's `agents.tools.report_every_minutes`, so the count can lag by that much. At the platform's `agents.tools.failures_to_mail` in the window, your organisation's admins are mailed, at most once in its `agents.tools.mail_every_hours` per tool. Each is on `GET /v1/platform/limits`. |
| `data[].last_failed_at` | string (date-time) or null | When a call last failed; `null` if none has. |
| `data[].last_error` | string or null | What the model was told of the last failure; `null` if none has. |
| `next_cursor` | string or null | Pass as `cursor` for the next page; `null` when this is the last. |

## Set which tools an agent can call

`PUT /v1/agents/{agent_id}/tools`

Needs `agent:write`.

Replaces this agent's tools with exactly the ones named, at most the platform's `limits.agent.tools` (`GET /v1/platform/limits`). **Declarative**: the list is the complete set, and an empty one turns every tool off. Ids that are not yours come back in `skipped`. Conversations already open pick the change up within about a minute.

**Every tool rides on every model call**: each costs ~100–250 input tokens a call, whether it is used or not.

```bash title="curl"
curl -X PUT "https://api.lumavox.ai/v1/agents/$AGENT_ID/tools" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tool_ids":[]}'
```

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `agent_id` | string | yes |  |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `tool_ids` | array of string | yes | The complete set, at most the platform's `limits.agent.tools` (`GET /v1/platform/limits`). An empty list turns every tool off. At most 200 items. |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — How many tools were turned on and off |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | Unknown agent id |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `added` | integer |  |
| `removed` | integer |  |
| `skipped` | array of string | Ids sent that are not this organisation's, ignored. |
