Files
B2BCall-dialer/docs/AGENTS.md
Matheus a05104a05f feat(agents): agentes, tiers e pausas — call center completo
Fecha agente.md secao 45-49/52. Depois desta fase, um usuario autenticado
consegue logar como agente, entrar numa fila real, se pausar e voltar,
tudo refletido de verdade no FreeSWITCH.

Schema (migration 20260828124245_agents):
- agents (tenant-scoped, RLS): User -> Extension -> identidade de agente,
  state (enum AgentState de 8 valores) espelhando o estado real, so
  alterado via login/logout/pause/resume, nunca escrito direto pela API.
- tiers: Queue<->Agent (level/position 1:1 com mod_callcenter).
- agent_sessions: um ciclo login->logout por linha.
- agent_state_events: historico de transicoes de estado.
- pause_reasons / agent_pause_events (secao 48).

Dois bugs reais corrigidos em FreeSwitchTelephonyProvider, presentes desde
a fase de Event Socket original:
- "queue add/del member" nao existe no mod_callcenter — membership de
  fila usa tier add/tier del. So foi pego agora ao confirmar de novo a
  sintaxe via `help callcenter_config` antes de codar esta fase.
- addAgent/removeAgent nao existiam ainda (agent add/del).

Mecanismo de sync: agentes e tiers nao tem representacao em XML, so
comando ESL direto — diferente do padrao "regenera todos os arquivos"
usado em Trunks/Queues. apps/api publica uma mensagem por acao com
payload (b2bcall:agents:sync, b2bcall:tiers:sync); b2bcall-fs-config
aplica o comando correspondente (agent-sync.ts).

Achados confirmados manualmente contra o FreeSWITCH real antes de codar:
- `agent add`/`tier add` nao sao idempotentes (erro em duplicata) — sync
  ignora esse erro (.catch), condicao esperada em resync.
- `agent del`/`tier del` em algo inexistente nao da erro — seguro chamar
  sem checar existencia antes.
- `agent set status` so aceita 3 valores exatos (Available/On Break/
  Logged Out) — testado deliberadamente com valor invalido.
- Corrida real: atribuir tier antes do primeiro login do agente falha
  silenciosamente do lado do FreeSWITCH (agente so existe la a partir do
  `agent add` no login). Login sempre re-sincroniza todos os tiers do
  agente depois de garantir que ele existe — auto-correcao confirmada no
  teste ponta a ponta.

apps/api: AgentsController (CRUD), AgentsMeController (login/logout/
pause/resume — sempre resolve o agente via JWT, nunca um agentId
arbitrario do client), PauseReasonsController (CRUD), QueueAgentsController
(POST/DELETE de tier em /queues/:id/agents).

Verificado ponta a ponta via curl + fs_cli contra o FreeSWITCH real:
login -> Available, pause -> On Break, resume -> Available, logout ->
Logged Out, todos batendo entre Agent.state (banco) e `agent list`
(FreeSWITCH). typecheck do workspace inteiro limpo. ~350MB de memoria
total (docker stats).

Documentado em docs/AGENTS.md, incluindo lacuna conhecida: estados
derivados de chamada (RINGING/IN_CALL/WRAP_UP/RESERVED) dependem do
evento CUSTOM callcenter::info, ainda nao comprovado chegando em
fs-events nesta sessao (mesma lacuna de sofia::gateway_state ja
documentada em docs/TRUNKS.md) — precisa de uma chamada real passando
pela fila pra investigar.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1HxY46WGU4G1zmVDNKcWw
2026-08-28 09:58:45 -03:00

5.1 KiB

Agentes, Tiers e Pausas

Agente.md secao 45-49 e 52. Completa o Call Center: depois desta fase, um usuário autenticado consegue logar como agente, entrar numa fila real, se pausar e voltar — tudo refletido de verdade no FreeSWITCH.

Modelo

  • agents (tenant-scoped, RLS): liga User (login) → Extension (ramal SIP) → identidade de agente (User/Agent/Extension separados, agente.md secao 45). state espelha o estado real (secao 46), nunca escrito direto pela API — só via login/logout/pause/resume.
  • tiers: Queue↔Agent (secao 52) — level/position espelham 1:1 o mod_callcenter.
  • agent_sessions: uma linha por ciclo login→logout.
  • agent_state_events: histórico de transições de estado (auditoria).
  • pause_reasons/agent_pause_events (secao 48).

Mecanismo: 100% dinâmico, sem arquivo

Confirmado na fase anterior (Queues) via help callcenter_config: agentes e tiers não têm representação em XML — só comandos ESL diretos (agent add/del/set status/set contact, tier add/del). Isso muda o padrão de sincronização: em vez de "regenerar todos os arquivos" (Trunks/Queues), apps/api publica uma mensagem por ação, com payload ({tenantId, agentId, action} em b2bcall:agents:sync; {tenantId, queueId, agentId, level, position, action} em b2bcall:tiers:sync) — b2bcall-fs-config recebe e aplica o comando correspondente.

Achados reais confirmados manualmente antes de codar

Rodei cada comando contra o FreeSWITCH real antes de escrever qualquer lógica de sync (mesmo método usado nas fases anteriores):

  • agent add em cima de um agente que já existe: erro ("Agent already exist!") — não é idempotente. tier add duplicado: mesmo erro ("Tier already exist!"). Ambos capturados e ignorados no sync (.catch(() => undefined)), já que essa condição é esperada em qualquer resync.
  • tier del/agent del em algo que não existe: não dá erro (+OK) — seguro chamar sem checar existência antes.
  • agent set status só aceita 3 valores exatos: Available, On Break, Logged Out — qualquer outro string dá -ERR Invalid Agent Status! (testado deliberadamente). Os estados derivados de chamada do nosso enum (RESERVED/RINGING/IN_CALL/WRAP_UP) não têm status próprio no mod_callcenter — mapeiam pra "Available" (só o state, campo separado, muda sozinho conforme a chamada progride — não escrevemos isso).
  • Achado de corrida real, visto ao testar o fluxo completo: como um agente só passa a existir no FreeSWITCH no login (agent add roda ali, não na criação do registro Agent), atribuir um tier (POST /queues/:id/agents) antes do primeiro login do agente falha silenciosamente do lado do FreeSWITCH (-ERR Agent not found!, logado mas não propagado como erro HTTP). Por isso o login sempre re-sincroniza todos os tiers do agente depois de garantir que ele existe — a auto-correção aconteceu exatamente assim no teste real.

Fluxo de login (agente.md secao 47)

POST /agents/me/login
  → valida usuário (JWT) e ramal (Agent.extensionId precisa existir)
  → cria agent_session
  → agent add (idempotente via catch) + set contact (dial-string do ramal)
  → set status Available
  → re-sincroniza todos os tiers do agente
  → Agent.state = AVAILABLE

POST /agents/me/logout|pause|resume seguem o mesmo padrão — sempre sobre o agente do próprio usuário autenticado (nunca um agentId arbitrário do client, mesmo princípio de nunca confiar em tenant_id/ids sensíveis vindo do frontend sem checar contra o JWT).

Verificado ponta a ponta

POST /extensions {"number":"2000",...}
POST /agents {userId, extensionId, name}
POST /queues {"name":"Suporte"}
POST /queues/:queueId/agents {agentId}          → tier add falha (agente ainda nao existe)
POST /agents/me/login                            → agent add + contact + status Available
                                                  → tier re-sync (dessa vez funciona)

callcenter_config agent list
→ status=Available, contact={ignore_early_media=true}user/2000@b2bcall.local ✓
callcenter_config tier list
→ queue|agent com state=Ready ✓

POST /agents/me/pause {pauseReasonId}  → status=On Break ✓
POST /agents/me/resume                 → status=Available ✓
POST /agents/me/logout                 → status=Logged Out ✓

Todos os 4 estados confirmados batendo entre o banco (Agent.state) e o FreeSWITCH (agent list).

O que falta

  • Estados derivados de chamada (RINGING, IN_CALL, WRAP_UP, RESERVED) — dependem de callcenter::info (CUSTOM event), que ainda não foi provado funcionando nesta sessão (mesma lacuna de sofia::gateway_state documentada em docs/TRUNKS.md). Não implementado; precisa de uma chamada real passando pela fila pra testar.
  • Tela do agente (secao 49) — fase Frontend.
  • Monitoramento de filas/ramais em tempo real (secao 54-55) — depende de WebSocket multi-tenant.
  • Quota de agentes (max_agents) — depende de Plans/Entitlements.
  • PauseReason.maxDuration existe no modelo mas não é aplicado automaticamente ainda (ninguém força o fim da pausa ao expirar).