Agents hold no secrets. Model API keys live in
ProviderAuth and are merged in at call time. Sending a secret field inside config.models is rejected by validation.What an agent is
An agent is a document in theAgents collection, scoped to one organisation. It stores:
- a name, unique within the organisation
- an
agent_categorythat decides how calls reach it - prompts, behaviour settings, language, and non-secret model configuration
- an optional knowledge-base attachment
- for telephony agents, a provisioned provider application and an optional linked phone number
created_by is taken from the JWT email) and deleted only by admin or super_admin. See Multi-tenancy and roles.
telephony vs websocket
agent_category is Literal["telephony", "websocket"], defined in apps/api/app/models/schemas.py. It is the single field that decides everything about how the agent is reached.
The runtime resolves the category with
agent_category() in apps/runtime/services/agent_routing.py, which defaults to websocket when the field is missing and raises AgentRoutingError for any other value.
- telephony
- websocket
Set
agent_category: "telephony" and a telephony_provider that is registered in apps/telephony — check GET /configuration/telephony for the current list. Organisation credentials for that provider must already exist via POST /auth, and VOICE_SERVER_BASE_URL must be configured, or creation fails.The agent document
AgentResponse is the full shape returned by every agent route:
AgentTelephonyAttachment carries provider, application_id, answer_url, and an optional hangup_url — optional only because agents provisioned before hangup URLs were always set may lack it.
AgentConfigPayload holds schema_version (default 1), prompts, behaviour, language, models, knowledge_base, and custom_variables. Every field is documented in Agent configuration; how the runtime consumes it is in Voice pipeline.
Lifecycle and side effects
Creating or changing an agent can call out to your telephony provider. This diagram is the full state machine implemented byagent_service.py and agent_telephony_service.py:
Validation runs first, always. validate_agent_config() in agent_config_validation.py requires prompts.greeting_message and language.primary to be non-empty, rejects any secret or auth field inside a model config, and validates each of stt_config, tts_config, and llm_config against the registered provider’s config class. When the knowledge base is enabled, it also requires non-empty document_ids and checks that those documents are ready.
One validation rule catches people out: knowledge_base.mode: "tool" is accepted only for LLM providers that support function calling — openai, groq, azure_openai, and anthropic, listed as KB_TOOL_LLM_PROVIDERS. Any other provider is rejected outright. Use mode: "context" instead.
Telephony provisioning on create
For atelephony agent, create_agent() calls provision_application(org_id, provider, agent_id) before inserting the document. That function:
- Requires
VOICE_SERVER_BASE_URL, and builds{VOICE_SERVER_BASE_URL}/answer?agent_id={agent_id}&org_id={org_id}. The answer URL and the hangup URL are the same URL — the runtime dispatches on the webhook event. - Loads the organisation’s provider credentials from
ProviderAuth. Missing or incomplete credentials (auth_idandauth_tokenare both required) raiseAgentTelephonyError. - Calls
client.create_application(agent_id, answer_url)through the provider registry. The application is named by theagent_idUUID, deliberately — providers commonly reject spaces and punctuation in application names. - Returns the attachment
{provider, application_id, answer_url, hangup_url}and stores it on the agent.
Changing provider on PATCH
PATCH /agents/{agent_id} accepts name, agent_category, telephony_provider, and config, all optional. config is merged, not replaced: nested objects are updated key by key against the stored config, then the merged result is re-validated in full.
Telephony is reconciled only when telephony_changed — that is, when the effective category changes, or when the category stays telephony and the provider changes. When it does, in this order:
phone_number_service.detach_from_agent()unlinks any attached number at the provider and clears the association.delete_application()removes the old provider application.- If the new category is
telephony, a new application is provisioned with the new provider. linked_phone_numberis set back tonullon the document.
name does not rename the provider application. rename_application() exists in agent_telephony_service.py but is not called by agent_service.py, so the application keeps its agent_id name for life. Since applications are named by UUID rather than by the display name, this has no practical effect.
Linked phone numbers
Phone numbers live in a separate org inventory and are attached to agents through/api/v1/phone-numbers:
Omit
agent_id on attach to import a number into the inventory without any provider link. An agent holds at most one linked number; attaching a number already owned by another agent unlinks it from the previous owner first.
How an agent is resolved at call time
The runtime never reads the database. It resolves agents over the API, and it does so differently depending on which end of the call you are on:org_id and agent_id are always passed per request. There is no global default organisation.
For a telephony agent the runtime then reads telephony.provider off the document to pick the Stream XML dialect and the frame serializer — it does not assume a single vendor. That dispatch is described in Telephony model.
Related
- Agent configuration — every config field, with defaults
- Voice pipeline — what the runtime does with the config
- Telephony model — applications, numbers, and Stream XML
- Calls and call artifacts — what a call produces
- REST API — the full route list