Skip to content

Jornada do usuario ponta-a-ponta

Este guia narra o caminho completo de uma mensagem atravessando todos os serviços — do primeiro contato do usuário até a resolução do atendimento. Onde os outros guias de fluxo dissecam um salto (inbound, outbound, sessão, atendimento humano, agentes de IA), aqui costuramos tudo numa linha do tempo única: quando cada serviço atua, quando a sessão muda de modo (flowagenthuman) e quando um ticket abre ou fecha.

Para o detalhe de cada etapa, cruze com os guias específicos: Inbound completo, Outbound completo, Ciclo de vida da sessão, Atendimento humano e Comunicação e ciclo de vida dos agentes de IA.


Visão geral — os serviços que a mensagem atravessa

O sistema é orientado a eventos: os serviços internos nunca se chamam por HTTP, só por Redis Streams. HTTP e Socket.io existem apenas nas bordas (gateways ↔ Meta/IG/browser). A mensagem de um usuário passa, na ordem, por:

usuário (WhatsApp / Instagram / WebChat)
     │  webhook HTTP (WA/IG) ou Socket.io (WebChat)

gateway do canal        flow-ai-meta-api | flow-ai-ig-api | flow-ai-webchat-gateway
     │  stream:incoming

flow-ai-orchestrator    ← persiste, resolve/atualiza a SessionState, roteia por session.mode
     │  stream:flow (flow) | stream:agent (agent) | stream:helpdesk (human)
     ├──────────────────────┬───────────────────────┐
     ▼                      ▼                        ▼
flow-ai-engine        flow-ai-agent            flow-ai-core (helpdesk)
executa o flow        loop LLM + tools         entrega ao agente humano via Socket.io
     │                      │                        │
     └──────────────────────┴────────────────────────┘
                            │  stream:outgoing-meta | -chat | -ig

gateway do canal        ← envia a resposta ao usuário

O campo session.mode ("flow" | "human" | "agent") é a única fonte de verdade para o roteamento no orchestrator. Toda troca de modo é persistida no Redis antes de qualquer publicação em stream — assim o próximo consumer nunca lê uma sessão desatualizada.

Os fluxos abaixo usam o canal WhatsApp como referência (o contrato inbound é WhatsApp-shaped em todos os canais). Para as diferenças de Instagram e WebChat, veja Inbound completo.


Quem faz o que em cada etapa

Cada serviço tem uma responsabilidade estreita. A tabela abaixo é o mapa que os dois cenários instanciam:

ServiçoConsomeProduzPapel na jornada
flow-ai-meta-api (gateway, porta 4444)webhook HTTP da Meta; stream:outgoing-meta (grupo send)stream:incoming, stream:statusValida webhook (verify token + HMAC-SHA256), normaliza inbound e publica; consome outbound e envia à Meta Cloud API
flow-ai-orchestrator (sem HTTP)stream:incoming; stream:outgoing-* (grupo persist); stream:status (grupo status)stream:flow / stream:agent / stream:helpdeskPersiste Contact/Chat/Message, cria/atualiza SessionState, roteia por session.mode; persiste outbound e aplica status
flow-ai-engine (sem HTTP)stream:flow (grupo engine)stream:outgoing-*, stream:helpdesk, stream:agentExecuta a máquina de estados do flow; entra em HumanAttendanceBlock (→ human) e AgentBlock (→ agent); retoma o flow no humanSessionEnded/agentSessionEnded
flow-ai-agent (sem HTTP)stream:agent (grupo agent, consumer único serial)stream:outgoing-meta/-chat, stream:agent (self-loop), stream:helpdesk, stream:flow, telemetriaRoda o loop LLM de tool-calling; transfere entre agentes; abre/fecha o ticket de IA; encerra devolvendo ao flow
flow-ai-core (porta 3333)stream:helpdesk, stream:status (grupos core-*), stream:outgoing-*stream:flow (humanSessionEnded/agentSessionEnded), stream:agent (manualHandoff), Socket.io ao deskCria/atribui/fecha tickets humanos, resolve fila, entrega ao agente humano em tempo real; única fronteira de cripto e escritora de caches

Detalhe da fronteira de saída (persist + send + status em stream:status) em Outbound completo. Detalhe de criação/atualização de sessão em Ciclo de vida da sessão.


Cenário A — WhatsApp: flow determinístico → handoff humano → resume

Um contato novo escreve pela primeira vez, percorre um flow, é encaminhado a um atendente humano e, ao fim do atendimento, volta ao flow de onde parou.

A.1 Primeiro contato e criação de sessão

  1. O usuário envia uma mensagem. A Meta faz POST /webhook no flow-ai-meta-api.
  2. O gateway extrai phoneNumberId, busca wa:webhook:{phoneNumberId} no Redis, valida o HMAC-SHA256 do corpo, normaliza cada mensagem para WhatsAppIncomingMessage e publica um IncomingStreamEvent em stream:incoming (sessionId = wa-{phoneNumberId}-{from}).
  3. O flow-ai-orchestrator consome, deduplica por whatsappMessageId, resolve o Contact/Chat no Postgres. Como não há sessão no Redis, resolve o roteamento inicial via cache wa:router:{phoneNumberId} ({ routerId, initialFlowId }) e cria a SessionState com mode: "flow", gravando em session:{sessionId}.
  4. Como mode === "flow", publica um FlowStreamUserInputEvent em stream:flow e insere o sessionId no índice sessions:active:{routerId}.

A.2 Execução do flow no engine

  1. O flow-ai-engine consome stream:flow, carrega SessionState do Redis, o FlowDefinition do cache agregado flows e as variáveis (vars:router:{routerId}, vars:flow:{flowId}).
  2. Executa walkBlocks(): avalia inputActionscontentoutputConditions. Para cada mensagem gerada, publica no stream de saída resolvido por resolveContentChannel(sessionId) — aqui stream:outgoing-meta. Regrava a SessionState com o currentBlockId do bloco que aguarda input.
  3. Fan-out de saída: o orchestrator (grupo persist) grava a Message com status: "pending" e o meta-api (grupo send) resolve wa:credentials:{phoneNumberId}, envia à Meta e publica o resultado (sent/send_failed) em stream:status. O usuário recebe a resposta do bot.

A.3 Handoff para atendimento humano

  1. Em algum ponto o walkBlocks() entra num HumanAttendanceBlock. O engine muda session.mode = "human", estende o TTL para max(janela Meta restante, 1h) e publica um HelpdeskHandoffEvent em stream:helpdesk.
  2. O flow-ai-core consome o handoff, resolve a fila (event.presetQueueId ?? resolveQueue via QueueDistributionRule por routerId), cria o Ticket e grava session.helpdesk = { ticketId, queueId }.

Ticket humano (abertura): kind: "human", openedByKind: "flow", openReason: "human_handoff". Sem presetUserId, nasce status: "waiting" e os agentes da fila são notificados via helpdesk:waiting; a atribuição vem por pickup ou auto-assign (least_loaded). Com presetUserId, já nasce assigned.

A.4 Conversa durante o atendimento

  1. Enquanto mode === "human", cada mensagem do contato faz o orchestrator publicar um HelpdeskMessageEvent em stream:helpdesk (não em stream:flow). O core busca o ticket e emite ticket:message via Socket.io ao agente atribuído. Cada mensagem renova o TTL da sessão no Redis.

A.5 Fechamento e resume ao flow

  1. O agente fecha o ticket (agent:close via Socket.io ou POST /tickets/:id/close). O core fecha com closeKind: "resolved" e publica um humanSessionEnded em stream:flow.
  2. O flow-ai-engine consome, executa runFlowFromHumanSessionEnded(): session.mode = "flow", restaura o expiryTimeout para o sessionExpiryMs do flow, deleta session.helpdesk, roda os outputActions/outputConditions do HumanAttendanceBlock e continua o walkBlocks(). O contato volta a conversar com o bot de onde parou.

Se a janela de 24h da Meta expirar antes do fechamento (erro 131026 em send_failed), o próprio core fecha o ticket com closeKind: "meta_window_expired" e publica o mesmo humanSessionEnded — ver Atendimento humano.

Diagrama de sequência — Cenário A

Cada seta anota [stream] e o {cache} consultado/gravado naquele salto.

usuário        meta-api        orchestrator        engine        core (helpdesk)        agente humano
  │  webhook      │                 │                │                 │                      │
  │──────────────>│                 │                │                 │                      │
  │   {wa:webhook:{phoneNumberId}} valida HMAC        │                 │                      │
  │               │ [stream:incoming]                 │                 │                      │
  │               │────────────────>│                 │                 │                      │
  │               │  {wa:router:{phoneNumberId}} → cria SessionState mode:flow {session:{id}}  │
  │               │                 │ [stream:flow] userInput            │                      │
  │               │                 │───────────────>│                 │                      │
  │               │                 │  {session:{id}} + {flows} + {vars:*} → walkBlocks         │
  │               │ [stream:outgoing-meta] persist(orch)+send(meta-api)  │                      │
  │               │<────────────────────────────────┤ {wa:credentials:{phoneNumberId}}         │
  │<──────────────│  resposta do bot                 │                 │                      │
  │               │                 │                │                 │                      │
  │  (walkBlocks entra em HumanAttendanceBlock) mode=human {session:{id}}│                      │
  │               │                 │                │ [stream:helpdesk] handoff                │
  │               │                 │                │────────────────>│                      │
  │               │                 │  resolveQueue(routerId) → cria Ticket(human, waiting)     │
  │               │                 │  grava session.helpdesk {session:{id}}                    │
  │               │                 │                │                 │  Socket.io ticket:*  │
  │               │                 │                │                 │─────────────────────>│
  │  contato manda msg              │                │                 │                      │
  │──────────────>│ [stream:incoming]                │                 │                      │
  │               │────────────────>│  mode=human → [stream:helpdesk] message                  │
  │               │                 │───────────────────────────────>│  Socket.io message   │
  │               │                 │                │                 │─────────────────────>│
  │               │                 │                │                 │  agente fecha ticket │
  │               │                 │                │                 │<─────────────────────│
  │               │                 │  Ticket → closed (resolved)      │                      │
  │               │                 │                │ [stream:flow] humanSessionEnded          │
  │               │                 │                │<────────────────┤                      │
  │  runFlowFromHumanSessionEnded → mode=flow, delete helpdesk, walkBlocks continua            │
  │               │<─── [stream:outgoing-meta] retoma o flow ──────────┤                      │
  │<──────────────│                 │                │                 │                      │

Cenário B — Instagram/WhatsApp: agente de IA, transferência a especialista, retorno ao flow

O contato entra num AgentBlock, conversa com um agente de IA orquestrador, é transferido a um especialista, e ao encerrar o controle retorna ao flow determinístico. Diferente do Cenário A, o loop entre agentes acontece dentro do flow-ai-agent — sem passar pelo orchestrator.

B.1 Entrada no AgentBlock

  1. Sessão em mode: "flow", o flow-ai-engine roda walkBlocks() e entra num AgentBlock. Ele muda session.mode = "agent", semeia um agentState esqueleto (activeAgentId, agentChain: [{ agentId, returnToParent: false }], configSnapshot com strings vazias, turnCount: 0) e retorna o agentEvent no RunResult.
  2. O handler flow.ts grava a sessão (setSession) antes de publicar o AgentStreamEvent em stream:agent — assim o agente sempre encontra agentState no Redis.

A partir daqui, enquanto mode === "agent", o orchestrator roteia toda mensagem do contato direto para stream:agent (não stream:flow) e remove o sessionId do índice sessions:active.

B.2 Executor do agente e ticket de IA

  1. O flow-ai-agent (consumer único e serial de stream:agent) consome o AgentStreamEvent, carrega a sessão, e resolve a config do agente via cache agent:{agentId} (AiAgentCacheConfig) e a credencial LLM via llm:credential:{credentialId}. Preenche o configSnapshot real (systemPrompt, model, guardrails, tools).
  2. Antes do loop, se agentState.ticketId estiver ausente, cria o ticket de IA e emite HelpdeskAITicketEvent (action: "created") em stream:helpdesk.

Ticket de IA (abertura): kind: "ai", openedByKind: "ai_agent", openReason: "ai_session", status: "assigned". É único por sessão e sobrevive às transferências entre agentestransferToAgent não abre nem fecha ticket.

  1. O executor roda o loop de tool-calling (Responses API stateful, previous_response_id). Tools de resposta (sendTextMessage, sendButtonMessage, ...) publicam em stream:outgoing-meta/-chat. Cada iteração emite telemetria em stream:agent-turns e stream:llm-usage.

B.3 Transferência ao especialista (self-loop no flow-ai-agent)

  1. O orquestrador chama a tool transferToAgent(agentId, handoffContext, lastUserInput, returnToOrchestrator: true). O executor valida (profundidade ≤10, agente existe no cache, sem ciclo), empilha { agentId, returnToParent: true } no agentChain, zera o previousResponseId e publica um novo AgentStreamEvent em stream:agent — que ele mesmo consome no próximo turno. O orchestrator não participa dessa transição.

B.4 Especialista encerra → volta ao orquestrador

  1. O especialista resolve e chama endAgentSession(completionContext). Como o entry atual tem returnToParent: true e agentChain.length > 1, o executor faz pop() da cadeia, volta activeAgentId ao orquestrador e republica um AgentStreamEvent em stream:agent (com handoffContext = completionContext). Não publica agentSessionEnded — o controle não volta ao flow ainda. O ticket de IA continua aberto.

B.5 Orquestrador encerra → retorno ao flow

  1. O orquestrador (agora agente raiz, agentChain.length === 1, returnToParent: false) chama endAgentSession(reason). Agora sim: grava session.variables.agentEndReason = reason, faz session.mode = "flow", deleta agentState, fecha o ticket de IA (closeKind: "resolved"), emite SummaryRequestedEvent em stream:summary + HelpdeskAITicketEvent (action: "closed") em stream:helpdesk, e publica um AgentSessionEndedEvent em stream:flow.
  2. O flow-ai-engine consome, executa runFlowFromAgentSessionEnded(): session.mode = "flow", restaura o expiryTimeout do flow, avalia as outputConditions do AgentBlock (usando agentEndReason das session.variables) e continua o walkBlocks(). O contato volta ao flow determinístico.

Além do endAgentSession, o agente pode transferir direto a um humano via transferToHuman (tool) ou o desk pode forçar via AgentManualHandoffEvent em stream:agent. Ambos reusam performHumanHandoff: fecham o ticket de IA, publicam HelpdeskHandoffEvent em stream:helpdesk e mudam session.mode = "human" — daí em diante segue o Cenário A. Ver Agentes de IA.

Diagrama de sequência — Cenário B

usuário       orchestrator            engine                 flow-ai-agent               core
  │               │                     │                         │                        │
  │  (sessão em mode:flow → walkBlocks entra em AgentBlock)        │                        │
  │               │                     │ mode=agent + agentState semeado {session:{id}}    │
  │               │                     │ [stream:agent] AgentStreamEvent (após setSession)  │
  │               │                     │────────────────────────>│                        │
  │               │  {agent:{agentId}} + {llm:credential:{id}} → preenche configSnapshot     │
  │               │                     │  cria Ticket(ai, assigned) [stream:helpdesk]       │
  │               │                     │                         │───────────────────────>│
  │               │                     │  loop LLM → [stream:outgoing-meta] resposta        │
  │<──────────────────────────────────────────────────────────────┤ (persist+send)        │
  │  contato responde                   │                         │                        │
  │──> [stream:incoming] → orchestrator │  mode=agent → [stream:agent] {session:{id}}        │
  │               │────────────────────────────────────────────>│                        │
  │               │  orquestrador chama transferToAgent(returnToOrchestrator:true)          │
  │               │  push agentChain, [stream:agent] SELF-LOOP ──┐ (mesmo serviço)          │
  │               │                     │                        └>│ (especialista roda)   │
  │               │                     │  especialista chama endAgentSession(returnToParent)│
  │               │                     │  pop agentChain → [stream:agent] SELF-LOOP ──┐     │
  │               │                     │                        (volta ao orquestrador)└>│  │
  │               │                     │  orquestrador (raiz) chama endAgentSession        │
  │               │                     │  mode=flow, delete agentState, fecha Ticket(ai)    │
  │               │                     │                         │ [stream:helpdesk] closed │
  │               │                     │                         │───────────────────────>│
  │               │                     │ [stream:flow] agentSessionEnded                    │
  │               │                     │<────────────────────────┤                        │
  │  runFlowFromAgentSessionEnded → avalia outputConditions(agentEndReason) → walkBlocks     │
  │<── [stream:outgoing-meta] retoma o flow ─────────────────────────────────────────────── │

Nota (KNOWN DRIFT — canal de saída do agente): o flow-ai-agent escolhe o stream de saída por prefixo do sessionId: wc-*stream:outgoing-chat, caso contrário stream:outgoing-meta. Não há branch para ig-* — um agente respondendo numa sessão Instagram publica em stream:outgoing-meta (canal errado). WhatsApp e WebChat funcionam corretamente. Ver Agentes de IA.


Ticket: quando abre e quando fecha

Os dois cenários abrem tickets diferentes. Esta é a linha do tempo consolidada de cada kind:

MomentoCenário A (humano)Cenário B (IA)
Quem abreflow-ai-core, ao consumir o handoffflow-ai-agent, antes do loop LLM
kindhumanai
openedByKindflowai_agent
openReasonhuman_handoffai_session
status inicialwaiting (ou assigned com presetUserId)assigned
Durantemensagens via ticket:message (Socket.io)mensagens via loop LLM; sobrevive a transferToAgent
Quem fechaagente humano (ou sistema)agente raiz em endAgentSession (ou performHumanHandoff)
closeKind típicoresolved (ou meta_window_expired, abandoned)resolved (ou system no handoff a humano)
EncadeamentopreviousTicketId liga tickets sucessivos do mesmo chatidem — mantido entre resets/expirações

transferToAgent (B.3) e o retorno-ao-pai (B.4) não abrem nem fecham ticket algum — o ticket de IA é único e persiste por toda a cadeia de agentes. Detalhes dos enums em Atendimento humano.


Transições de modo — mapa consolidado

Toda a jornada é regida por session.mode. As transições que os dois cenários exercitam:

De → ParaGatilhoQuem executaStream do salto
(inexistente) → flowprimeira mensagem do usuárioorchestratorcria session:{id}
flowhumanwalkBlocks entra em HumanAttendanceBlockenginestream:helpdesk (handoff)
humanflowagente fecha ticket (ou janela Meta expira)core → enginestream:flow (humanSessionEnded)
flowagentwalkBlocks entra em AgentBlockenginestream:agent
agentagenttransferToAgent / retorno ao paiflow-ai-agent (self-loop)stream:agent
agentflowagente raiz chama endAgentSessionflow-ai-agent → enginestream:flow (agentSessionEnded)
agenthumantransferToHuman ou handoff manualflow-ai-agent → corestream:helpdesk (handoff)
flowhuman (frozen)erro de runtime irrecuperávelenginestream:helpdesk + sessions:frozen

O diagrama de estados completo (incluindo frozen e expiração por TTL) está em Ciclo de vida da sessão.


Por que a sessão nunca lê estado desatualizado

Um invariante atravessa os dois cenários: a SessionState é persistida no Redis antes de qualquer publicação em stream que dependa dela.

  • No engine, flow.ts faz setSession() e só então publica o agentEvent em stream:agent — o flow-ai-agent sempre encontra agentState (B.1/B.2).
  • No engine, o handoff (flow → human) grava session.mode = "human" antes do publishHandoff (A.3).
  • No flow-ai-agent, transferToAgent/endAgentSession fazem setSession() antes do xadd (B.3–B.5).
  • Como o flow-ai-agent é um consumer único e serial de stream:agent, os self-loops de transferência e o handoff manual são serializados por sessão — sem corrida sobre o agentState.

O custo é que Redis vira ponto de falha crítico e depurar exige inspecionar streams, pending lists e session:{sessionId} — não apenas logs HTTP. Ver Redis consumers e a política de ACK at-least-once em Inbound completo.


Arquivos relevantes

ArquivoPapel na jornada
services/flow-ai-meta-api/src/http/routes/webhook.tsGateway WhatsApp: valida HMAC, normaliza inbound, publica em stream:incoming
services/flow-ai-meta-api/src/consumers/outgoing.tsConsome stream:outgoing-meta (grupo send), envia à Meta, publica status
services/flow-ai-orchestrator/src/handlers/incoming.tsCria/atualiza SessionState, roteia por session.mode para flow/agent/helpdesk
services/flow-ai-orchestrator/src/consumers/persist.tsPersiste outbound (Message pending) no fan-out de saída
services/flow-ai-engine/src/handlers/flow.tsHandler de stream:flow; setSessionxadd(AGENT); dispatch de humanSessionEnded/agentSessionEnded
services/flow-ai-engine/src/runtime/execute.tswalkBlocks, transições flow→human/flow→agent, runFlowFromHumanSessionEnded, runFlowFromAgentSessionEnded
services/flow-ai-agent/src/executor/index.tsLoop de tool-calling, criação do ticket de IA, handleManualHandoff
services/flow-ai-agent/src/tools/actions.tstransferToAgent, endAgentSession (retorno ao pai vs. raiz), performHumanHandoff
services/flow-ai-core/src/helpdesk/consumer.tsConsome stream:helpdesk, cria ticket humano, resolve fila
services/flow-ai-core/src/helpdesk/publishHumanSessionEnded.tsPublica humanSessionEnded ao fechar ticket humano
services/flow-ai-core/src/helpdesk/publishAgentSessionEnded.tsPublica agentSessionEnded no fechamento manual do ticket de IA pelo desk
packages/flow-ai-types/src/session.tsSessionState, SessionMode, AgentState, AgentChainEntry
packages/flow-ai-types/src/stream-events.tsIncomingStreamEvent, FlowStreamEvent, HelpdeskHandoffEvent, OutgoingStreamEvent
packages/flow-ai-types/src/ai-agents.tsAgentStreamEvent, AgentSessionEndedEvent, AgentManualHandoffEvent, HelpdeskAITicketEvent
packages/flow-ai-redis/src/constants.tsSTREAMS / GROUPS — mapa canônico de streams e consumer groups
packages/flow-ai-database/prisma/schema.prismaModelo Ticket e enums TicketStatus/TicketOpenedByKind/TicketOpenReason/TicketCloseKind

flow-ai — plataforma proprietária de atendimento via WhatsApp