Call & Fax Agent
SIP-backed calling and fax module with native RTP/live-audio bridge for realtime phone conversations on a shared line.
What it does
Call & Fax Agent is a bundled React module for SIP-based telephone and fax workflows on one shared SIP line.
Outbound calls are bridged into an OpenAI Realtime or Google Gemini Live audio session through a native RTP
worker, so the live phone agent can hold low-latency duplex conversations, execute Clapilot tools during the
call, and write an automatic post-call summary. The same line also sends outbound faxes (text or
document-backed) and routes externally received inbound faxes into Dokumente.
Current scope:
- store one global SIP/router configuration and test SIP registration via the native
pjsuaruntime path - queue outbound calls from the module UI
- queue outbound text faxes from the same module and launch fax-send popups directly from Dokumente, Word, Excel, and Notizen for document-backed sends
- select a target customer from the Mandanten database and prefill the phone number from customer master data
- route externally supplied inbound fax files into
Dokumenteviamandanten/_inbox/...and keep a dedicated fax audit log - capture caller speech, agent speech, and tool outcomes from live calls so summaries are based on the real conversation flow
- keep a registered inbound SIP listener alive on the configured local SIP port when incoming calls are enabled, and hand inbound calls into the same live-audio worker
- restrict inbound live-tool access by caller number so only approved callers get the full internal tool surface while all other callers stay in a public-information-only mode
- enforce a mutual-exclusion line lock so one active voice or fax activity blocks the other
- persist worker/runtime state, call history, fax history, and fax audit events in PostgreSQL
Current non-goal: multiple provider profiles at once (one shared line/configuration).
How to open / enable it
- Module:
/modules/call-agent— slugcall-agent, manifestbundled-modules/call-agent/module.json, React renderersrc/components/modules/call-agent-module.tsx. The module view follows the current Clapilot styleguide (display-type module title, status badge, standard navy segmented controls,clapilot-fieldinputs, borderless quiet tiles and hairline outlined rows). - Settings:
/settings/call-agent(redirects to the admin-gated/admin/call-agentpage), panelsrc/components/call-agent-settings-panel.tsx. - Feature toggles in the settings decide what the line does: incoming calls, outgoing calls, fax (inbound/outbound), plus default calendar/notes access for the live agent.
Key workflows
Start an outbound live call
- Open the module, optionally pick a customer from the Mandanten database (prefills the phone number from customer master data and biases the live agent toward that customer context).
- Choose the per-call permissions (see below) and start the call. The worker dials through the configured router account, attaches the RTP audio to the native bridge process, and streams the audio into the configured live provider.
- During the call the live agent can execute Clapilot tool calls through the existing agent tool proxy
(
/api/agent-runtime/tool-proxy) with the initiating user as tool context. - After the call, an automatic post-call summary is written into the persisted call log, based on captured caller speech, agent speech, and tool outcomes.
Per-call permissions:
Kalenderzugriffexposes calendar tools to the live phone agent for that callNotizenzugriffexposes Notizen tools to the live phone agent for that callKundenzugriffexposes read access to Mandanten tools during the callDokumentzugriffexposes read access to Clapilot document tools during the call- when a customer is selected in the module UI, the live phone agent is biased toward that customer context and document queries default to that Mandant where possible
- without those toggles, the live phone agent is explicitly instructed not to access those domains
Receive inbound calls
When incoming calls are enabled, the worker keeps a registered inbound SIP listener alive on the configured
local SIP port and hands inbound calls into the same live-audio worker (mode: incoming_live_audio). Inbound
live-tool access is restricted by caller number: only numbers in approved_incoming_numbers (newline- or
comma-separated in the settings) get the full internal tool surface; all other callers stay in a
public-information-only mode.
Send a fax
- Queue outbound text faxes from the module UI, or launch fax-send popups directly from Dokumente, Word,
Excel, and Notizen for document-backed sends (
src/components/fax-send-dialog.tsx). fax_transport_mode=g711starts a real outbound SIP fax session through the nativecall-agent-fax-bridge, renders text-only or cover-page input into a PDF, converts the final transmission into TIFF, and only marks the faxsentafter the native fax engine reports success.disabledrejects outbound fax enqueue requests immediately.- Inbound fax handling currently lands in the shared document inbox/audit flow only after an external handoff
to
POST /internal/call-agent/faxes/receive; the native SIP line work in this codebase is currently focused on inbound voice plus outbound fax delivery.
Shared line rule
Voice and fax use the same stored telephony line:
- one active voice call blocks fax send/receive
- one active fax blocks starting a new voice call
- blocked inbound fax attempts are still logged in
call_agent_faxeswith statusblocked - the runtime state persists
active_kindplusactive_call_id/active_fax_id
This mutual-exclusion rule is enforced in the native worker and surfaced in the module UI and agent tool contracts.
How the agent can drive it
Chat/live agents get a native ClapilotAICore tool family for the module (see
Agent Tool Contracts):
call_agent_get_status— worker/SIP/shared-line statecall_agent_start_call— queue an outbound call on the shared linecall_agent_end_call— end the active callcall_agent_list_calls— call historycall_agent_search_customers— Mandanten lookup for call targetsfaxes_list/faxes_get/faxes_send/faxes_retry/faxes_cancel— fax queue management
Separately, the live phone agent inside a call executes Realtime function calls through
/api/agent-runtime/tool-proxy, scoped by the per-call permission toggles described above.
Configuration & limits
Settings fields
The settings page persists SIP fields such as:
router_ip,sip_domain,published_ip,outbound_published_ipsip_caller_id,sip_username,sip_password, optionalsip_auth_user,sip_realmsip_local_port,sip_port,sip_transportrealtime_provider,realtime_model,realtime_voice,system_promptapproved_incoming_numbers— newline- or comma-separated approved inbound caller numbers that may use the full Call Agent toolset during incoming callsfax_enabled,fax_station_id,fax_header_text,fax_transport_mode- feature toggles for incoming/outgoing/fax/calendar/notes and
auto_answer
The settings UI renders realtime_provider and realtime_model as paired dropdowns. Model choices are derived
from the configured ClapilotAICore provider rows for OpenAI Realtime or Google Gemini Live, with the current
persisted model kept visible as a fallback option.
Runtime architecture
Native worker source:
services/clapilot-agent/src/call-agent/index.mjsservices/clapilot-agent/src/call-agent/live-audio-session.mjsservices/clapilot-agent/src/call-agent/sip-proxy-runtime.mjsplus the proxy scriptsscripts/sip-udp-proxy.mjs(SIP) andscripts/udp-port-proxy.mjs(RTP)services/clapilot-agent/native/call_agent_live_bridge.cppservices/clapilot-agent/native/call_agent_fax_bridge.cppservices/clapilot-agent/src/call-agent/render_text_fax.py
Current runtime mode:
- worker version:
pjsua-live-audio-v2 - SIP registration uses the native
pjsuastack - active calls use a native RTP bridge process (
call-agent-live-bridge) plus a server-side provider session selected byrealtime_provider; supported live providers areopenaiandgoogle_gemini - the worker keeps the SIP password out of process arguments on every active path: live, fax, and
pjsuabridge processes receive it through a one-shot private inherited pipe that is closed immediately after the credential is written; the password must never be included in process arguments or diagnostic output - an internal SIP/RTP proxy layer inside the existing
clapilot-agentcontainer fronts the native bridges; Docker publishes the UDP/TCP port range5060-5090for it (override viaCLAPILOT_AGENT_HOST_PORT_START/CLAPILOT_AGENT_HOST_PORT_END) - OpenAI sessions use G.711 u-law over the Realtime WebSocket; Gemini sessions use raw PCM over Gemini Live and are downsampled back into the RTP leg; caller-side STT is explicitly biased to German so short German answers are not mis-transcribed as English
- status/call rows persist
mediaMode: realtime_live_audiowhile a live bridge is active - fax sending/receiving runs in the same worker under a dedicated fax path that reuses the stored SIP line
settings; fax transport mode can be
g711ordisabled
SIP credential rotation after argv exposure
Deploy the fixed web and clapilot-agent images before rotating the credential. Then change the SIP password at
the router/provider, enter the new value under Einstellungen → Call Agent, save, and restart the active Call
Agent session. Verify registration, place a test call, and exercise fax if it is enabled. Finally, inspect the
running pjsua, call-agent-live-bridge, and call-agent-fax-bridge command lines via /proc/<pid>/cmdline (or
ps auxww). The native bridges should contain --password-fd 5; pjsua should reference /proc/self/fd/5;
none may contain either the old or new password. Treat any password previously passed with --password as
compromised and revoke it.
Inbound SIP routing notes:
- default local SIP listener:
5060/udp - expected forwarded UDP range for inbound voice on one line: local SIP port plus RTP
local_port + 2throughlocal_port + 9 published_ipmust match the IP address that the router or upstream PBX reaches after forwarding- the native bridges always register and dial against the real router/registrar URI and attach the internal
SIP proxy as a loose-route outbound proxy (
--proxy, pjsua2sipConfig.proxies), so digest authentication stays valid on strict registrars (for example Speedport Smart 4R firmware) that reject rewritten Request-URIs - the SIP proxy derives its top-Via branch deterministically from Call-ID + CSeq + inner Via branch (RFC 3261 §16.6), so UDP retransmissions of one request keep their transaction identity instead of replaying the same digest nonce as distinct transactions — strict registrar firmware punishes that with 401/403 and extended 404 lockouts
- when the router reports a different source address than the configured
published_ip(SIPreceived=annotation), the worker surfaces a runtime warning naming both addresses; this typically means the host got a new DHCP lease after a router swap, so updatepublished_ip/outbound_published_ip(and ideally reserve a fixed LAN address for the host in the router UI)
Runtime toggles
CLAPILOT_CALL_AGENT_SIP_LOCAL_PORT— optional env override for the effective inbound SIP listener port; when set, it overrides the storedsip_local_portfrom the settings UI at runtime without changing the saved DB valueCLAPILOT_CALL_AGENT_LIVE_AUDIO_ENABLED— default enabled; set tofalseto fall back to prompt-file playback instead of live duplex audioCLAPILOT_CALL_AGENT_TRANSCRIPTION_LANGUAGE— defaultde; pins caller-side speech transcription to German for phone calls unless intentionally overridden
Endpoints
Internal native endpoints:
GET /internal/call-agent/statusGET /internal/call-agent/callsPOST /internal/call-agent/calls/startPOST /internal/call-agent/calls/:id/endGET /internal/call-agent/faxesGET /internal/call-agent/faxes/:idPOST /internal/call-agent/faxes/sendPOST /internal/call-agent/faxes/:id/retryPOST /internal/call-agent/faxes/:id/cancelPOST /internal/call-agent/faxes/receivePOST /internal/call-agent/test-connection
Web endpoints:
GET/POST /api/call-agent/configGET /api/call-agent/statusGET /api/call-agent/callsPOST /api/call-agent/calls/startPOST /api/call-agent/calls/:id/endGET /api/call-agent/customers(Mandanten picker for the module UI)GET /api/call-agent/faxesGET /api/call-agent/faxes/:idPOST /api/call-agent/faxes/sendPOST /api/call-agent/faxes/:id/retryPOST /api/call-agent/faxes/:id/cancelPOST /api/call-agent/test-connection
Data model
Tables introduced for the module:
call_agent_configscall_agent_runtime_statecall_agent_callscall_agent_faxescall_agent_fax_events
Migrations: db/migrations/041_call_agent.sql (base tables) plus the follow-ups 042 (customer/document
access), 043 (published IPs), 054 (realtime provider), 059_call_agent_fax.sql (fax tables), 060, 061,
063 (fax status/transport refinements), 064 (default SIP local port), and 068 (inbound tool allowlist).
Troubleshooting
- Registration suddenly answers 401/403, then 404 to everything — strict registrar firmware (Speedport Smart 4R) rejects digest mismatches and rate-limits aggressively. Two worker-side causes are already fixed: the bridges now register against the real router URI with the internal proxy as a loose-route hop (so the digest URI matches), and the proxy's Via branch is deterministic per SIP transaction (so UDP retransmissions are not treated as nonce replays). If a lockout was triggered, the router may keep answering 404 for an extended period before recovering.
- Silent calls / no inbound audio after a router swap — the router now sees a different source address than
published_ip(surfaced as a runtime warning naming both addresses, from the SIPreceived=annotation). Updatepublished_ip/outbound_published_ipand reserve a fixed LAN address for the host. - Fax enqueue rejected immediately —
fax_transport_modeisdisabled, or the shared line is busy with a voice call (blocked inbound attempts are logged with statusblocked). - Inbound callers cannot use internal tools — only numbers listed in
approved_incoming_numbersget the full tool surface; everyone else is intentionally kept in public-information-only mode.
